From d0e8070b528a56c65215217af6a33a45ba6717b3 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 14:44:36 +0300 Subject: [PATCH 01/10] docs: Refine patch development workflow Distinguish temporary low-risk instrumentation from reviewable patches. Keep the complete lint gate mandatory and tighten task contract semantics. --- .agents/skills/develop-patch/SKILL.md | 200 ++++++++++++++---- .../skills/develop-patch/agents/openai.yaml | 6 +- 2 files changed, 157 insertions(+), 49 deletions(-) diff --git a/.agents/skills/develop-patch/SKILL.md b/.agents/skills/develop-patch/SKILL.md index aea21ac2a..746377bce 100644 --- a/.agents/skills/develop-patch/SKILL.md +++ b/.agents/skills/develop-patch/SKILL.md @@ -1,19 +1,21 @@ --- name: develop-patch description: >- - Develop minimal, reviewable SCST patches from task contract through - committed lint and independent review. Use for bug fixes, new behavior, and - refactoring that require file changes. Do not use for standalone review, - lint-only requests, or read-only investigation. + Develop minimal SCST patches from task contract through committed lint and + independent review. Allow a separate experimental path with mandatory lint + and deferred review for explicitly temporary, low-risk diagnostics. Use for + bug fixes, new behavior, refactoring, and temporary instrumentation that + require file changes. Do not use for standalone review, lint-only requests, + or read-only investigation. --- # Develop an SCST patch Execute this workflow in the main agent. -## Record the contract and design +## Record the task contract -Before implementation, record the user's requirements without inventing +Before investigation, record the user's requirements without inventing exceptions: ```text @@ -25,11 +27,63 @@ ALLOWED_EXCEPTIONS: FORBIDDEN_CHANGES: ``` -Use `None` for an unspecified value. Apply exceptions literally and do not -extend them by analogy. Ask for clarification only when ambiguity would -materially change scope or outcome. +`GOAL` and `ACCEPTANCE` must describe the required result and its minimal +verifiable conditions and cannot be `None`. Use `None` for any other +unspecified field. -Before the first reviewable snapshot, record: +`NON_GOALS` describes scenarios and behavior outside the current scope, while +`FORBIDDEN_CHANGES` describes prohibited mechanisms, paths, and change +boundaries. Do not duplicate an item in both fields. Apply exceptions +literally and do not extend them by analogy. Ask for clarification only when +ambiguity would materially change scope or outcome. + +After recording `TASK_CONTRACT`, change it only in response to a new user +decision. Do not change it during investigation or implementation, and never +adjust it merely to pass lint or review. + +## Select the patch phase + +After `TASK_CONTRACT` and before investigation, record: + +```text +PATCH_PHASE: +REVIEW_POLICY: +``` + +Only `EXPERIMENTAL/DEFERRED` and `REVIEWABLE/REQUIRED` are valid pairs. +`PATCH_PHASE` determines reviewer policy, not lint policy: the complete +`$lint-patch` gate is mandatory for every committed snapshot in both phases. + +Use `EXPERIMENTAL` only when the user explicitly requests temporary diagnostic +or test instrumentation, or a short-lived hypothesis check, and all of these +conditions hold: + +- the patch is not intended for merge, handoff, or another permanent use; +- changes are limited to observability or easily removable test scaffolding + and do not alter supported behavior; +- the patch does not change an external or internal interface or contract, + ABI, UAPI, sysfs, configuration, a parser or machine-readable output, + persistent or in-memory state, build, CI, generation, delivery, or release + configuration; +- the patch does not affect data integrity, a security boundary, memory or + resource lifetime, reference counting, locking, concurrency, teardown, + rollback, cancellation, error semantics, or destructive operations. + +Use `REVIEW_POLICY: DEFERRED` for `EXPERIMENTAL`. This defers review until the +result becomes a permanent patch; it does not waive final review. An explicit +user request for review promotes the patch to `REVIEWABLE/REQUIRED`. When +uncertain, use `REVIEWABLE`; a small diff alone does not make a patch +experimental. + +Classify independent commit ranges separately and do not combine experimental +and reviewable commits in one gate range. If experimental scope expands beyond +the boundaries above, announce promotion to `REVIEWABLE` before making further +changes. Never downgrade the phase after a lint or reviewer finding to bypass +a gate. + +## Record the minimal design + +For `REVIEWABLE`, record this block before the first reviewable snapshot: ```text SUPPORTED_SCENARIO: @@ -37,14 +91,15 @@ DESIGN: KNOWN_LIMITATIONS: ``` -Derive `SUPPORTED_SCENARIO` from the task contract. Keep `DESIGN` to the -smallest complete mechanism that satisfies acceptance. Do not relabel a -patch-introduced correctness or regression defect as a known limitation. +Derive `SUPPORTED_SCENARIO` from the task contract. Choose the smallest +complete `DESIGN` that satisfies `ACCEPTANCE`. Do not narrow the supported +scenario after a finding or relabel a correctness or regression defect +introduced by the patch as a known limitation. -## Develop the patch +## Common preparation -1. Inspect `git status`, including ignored state, and preserve every - pre-existing tracked, untracked, ignored, generated, and secret path. +1. Inspect `git status`, including ignored state, and preserve every existing + tracked, untracked, ignored, generated, and secret path. 2. Record the starting commit as exact `base` unless the user supplied a different base. 3. Read every applicable `AGENTS.md` from the repository root to each file @@ -56,31 +111,72 @@ patch-introduced correctness or regression defect as a known limitation. 6. Implement the smallest complete patch satisfying the task contract. Avoid unrelated cleanup, speculative hardening, and premature architectural expansion. -7. Inspect the complete diff and run `git diff --check`. -8. Create a reviewable commit or logical commit series unless the user - explicitly requested an uncommitted patch. Keep each commit a coherent, - independently reviewable semantic unit and exclude unrelated worktree - state. -9. Record the committed snapshot as exact `head`. -10. Invoke `$lint-patch` directly in the main agent for the complete - `base..head` range. -11. Do not start review until the current head has `LINT_STATUS: PASS`. -12. Invoke `$review-patch` for the same `base..head`, repository path, and - unchanged `TASK_CONTRACT`. -13. Wait for the terminal reviewer result before modifying the patch. -14. If a required change modifies `head`, amend or fix up the appropriate - logical commit, update `head`, rerun the complete lint gate, and start a - new independent review only after lint passes. Run builds or runtime tests only when the user explicitly requests them and the applicable `AGENTS.md` permits the exact operation. They remain outside this base workflow. Otherwise report both as `NOT_REQUESTED`. +## Experimental path + +For `PATCH_PHASE: EXPERIMENTAL`: + +1. Implement the shortest patch for the specified check without adding + production guarantees beyond `TASK_CONTRACT`. +2. Create a separate commit so the patch can be removed or rewritten + unambiguously. Do not combine it with a permanent change. Record exact + `base` and `head` commits. +3. Read the complete `git diff ..` and run + `git diff --check ..`. +4. Invoke `$lint-patch` directly in the main agent for the complete exact + range. On `FAIL`, fix every diagnostic without a waiver, update `head`, and + repeat the complete lint gate. Do not finish without `LINT_STATUS: PASS` + for the current `head`. +5. Do not start a reviewer while the patch remains within the experimental + boundaries and the user has not explicitly requested review. On an + explicit review request, first promote the patch to `REVIEWABLE/REQUIRED` + and use the reviewable path. +6. Record: + +```text +LINT_STATUS: +REVIEW_STATUS: DEFERRED_EXPERIMENTAL +REVIEW_ATTEMPTS: 0 +``` + +Any change to `head` invalidates lint and the results of requested optional +gates for the previous snapshot. Repeat the applicable checks for the new +exact range. Any diagnostic that is neither fixed nor covered by a justified +waiver, or any unreliable validation, requires `BLOCKED`, not deferred +review. + +## Reviewable path + +For `PATCH_PHASE: REVIEWABLE`: + +1. Inspect the complete diff and run `git diff --check`. +2. Create a reviewable commit or logical commit series unless the user + explicitly requested an uncommitted patch. Keep each commit a coherent, + independently reviewable semantic unit and exclude unrelated worktree + state. +3. Record the committed snapshot as exact `head`. For an explicitly requested + uncommitted patch, stop before the committed-only gates and report them as + `BLOCKED` unless the user separately changes acceptance. +4. Invoke `$lint-patch` directly in the main agent for the complete + `base..head` range. +5. Do not start review until the current `head` has `LINT_STATUS: PASS`. +6. Invoke `$review-patch` for the same `base..head`, repository path, and + unchanged `TASK_CONTRACT`. +7. Wait for the terminal reviewer result before modifying the patch. +8. If a required change modifies `head`, amend or fix up the appropriate + logical commit, update `head`, rerun the complete lint gate, and start a + new independent review only after lint passes. + ## Bound the review cycle -Allow no more than three reviewer attempts for one `TASK_CONTRACT`. Count an -attempt only when `scst_reviewer` actually starts for a specific `head`. Lint -reruns do not consume attempts. +Use this cycle only for `REVIEW_POLICY: REQUIRED`. Allow no more than three +reviewer attempts for one `TASK_CONTRACT`. Count an attempt only when +`scst_reviewer` actually starts for a specific `head`. Lint reruns do not +consume attempts. ### Attempt 1 @@ -97,22 +193,23 @@ reruns do not consume attempts. - Review the complete updated range. - On `FAIL`, do not immediately apply another sequence of isolated fixes. -- Re-read `TASK_CONTRACT`, `SUPPORTED_SCENARIO`, `DESIGN`, - `KNOWN_LIMITATIONS`, the full diff, and findings from both attempts. +- Reread `TASK_CONTRACT`, `SUPPORTED_SCENARIO`, `DESIGN`, + `KNOWN_LIMITATIONS`, the complete diff, and findings from both attempts. - Group findings by root cause and determine whether the design has grown beyond the task. - Prefer simplifying the patch to the smallest correct design. -- Stop with `BLOCKED` if a new user decision is required. +- Finish with `BLOCKED` if a new user decision is required. - Otherwise make at most one final coherent snapshot update, rerun the full lint gate, and start attempt 3 only after lint passes. ### Attempt 3 -- Treat this as the final independent review. Never start a fourth automatic - reviewer. +- Treat this as the final independent review. Never automatically start a + fourth reviewer. - `PASS` permits successful completion. - On `FAIL`, classify every remaining finding. Finish with `BLOCKED` if any - finding violates `GOAL`, `ACCEPTANCE`, or `SUPPORTED_SCENARIO` correctness. + finding violates `GOAL`, `ACCEPTANCE`, or correctness of + `SUPPORTED_SCENARIO`. - Record a finding outside the supported scenario as a known limitation only when it does not contradict the task contract. The reviewer result remains `FAIL`; accept it only under this final-attempt rule. @@ -130,18 +227,29 @@ Use only: DEVELOP_STATUS: ``` -Require all of the following for `SUCCESS`: +For `SUCCESS` in both phases, require: + +- `GOAL` and `ACCEPTANCE` are satisfied; +- the current `head` has `LINT_STATUS: PASS`; +- every explicitly requested optional gate reports `PASS` or a justified + `NOT_APPLICABLE`. + +For `EXPERIMENTAL`, also require the patch to remain within every experimental +boundary and the result to contain `REVIEW_STATUS: DEFERRED_EXPERIMENTAL` and +`REVIEW_ATTEMPTS: 0`. + +For `REVIEWABLE`, also require: -- `GOAL`, `ACCEPTANCE`, and `SUPPORTED_SCENARIO` are satisfied. -- The current `head` has `LINT_STATUS: PASS`. -- The final reviewer result is acceptable under the bounded-cycle rules. -- Every remaining limitation is explicitly documented. +- `SUPPORTED_SCENARIO` is satisfied; +- the final reviewer result is acceptable under the bounded-cycle rules; +- every remaining limitation is explicitly documented. Include the following in the final report: +- `PATCH_PHASE` and `REVIEW_POLICY`; - exact `base` and final `head`; - `DEVELOP_STATUS`, lint status, reviewer status, and reviewer attempt count; - task-specific exceptions and lint waivers; - checks actually executed; - build and test status, normally `NOT_REQUESTED`; -- all `KNOWN_LIMITATIONS`. +- all `KNOWN_LIMITATIONS`, or `None` for an experimental patch. diff --git a/.agents/skills/develop-patch/agents/openai.yaml b/.agents/skills/develop-patch/agents/openai.yaml index 73bb18faa..725b5eea9 100644 --- a/.agents/skills/develop-patch/agents/openai.yaml +++ b/.agents/skills/develop-patch/agents/openai.yaml @@ -1,8 +1,8 @@ interface: display_name: "Develop Patch" - short_description: "Develop focused SCST patches through review" - default_prompt: "Use $develop-patch to implement a focused SCST change - through committed lint and independent review." + short_description: "Develop focused SCST patches with mandatory lint" + default_prompt: "Use $develop-patch to implement a focused SCST change with + committed lint and either required or explicitly deferred independent review." policy: allow_implicit_invocation: true From 85e1c95c38cda68e6f942ef791aa369a275f0c0b Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 18:01:18 +0300 Subject: [PATCH 02/10] scripts: Generate CI kernel matrices Keep exact kernel versions only in ABT_KERNELS. Resolve the bounded GitHub regression and RPM subsets from stable selectors, and reject missing or ambiguous matches. --- .github/kernel-matrix.conf | 68 ++++++++++++ scripts/kernel-matrix | 208 +++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 .github/kernel-matrix.conf create mode 100644 scripts/kernel-matrix diff --git a/.github/kernel-matrix.conf b/.github/kernel-matrix.conf new file mode 100644 index 000000000..d2655c7ba --- /dev/null +++ b/.github/kernel-matrix.conf @@ -0,0 +1,68 @@ +# Kernel selectors for the GitHub Actions regression subset. Upstream kernels +# are selected by major.minor. Distribution kernels are selected by +# distribution and release; add major.minor only when that pair is ambiguous. +GITHUB_REGRESSION_KERNEL_SELECTORS="\ +7.2 \ +7.1 \ +7.0 \ +6.19 \ +6.18 \ +6.17 \ +6.16 \ +6.15 \ +6.14 \ +6.13 \ +6.12 \ +6.11 \ +6.10 \ +6.9 \ +6.8 \ +6.7 \ +6.6 \ +6.1 \ +5.15 \ +5.10 \ +5.4 \ +4.19 \ +4.14 \ +4.9 \ +3.18 \ +3.10 \ +Rocky^10.2 \ +Rocky^10.1 \ +Rocky^9.8 \ +Rocky^9.7 \ +Rocky^8.10 \ +AlmaLinux^10.0 \ +AlmaLinux^9.6 \ +AlmaLinux^9.5 \ +AlmaLinux^9.4 \ +AlmaLinux^9.3 \ +AlmaLinux^9.2 \ +AlmaLinux^9.1 \ +AlmaLinux^9.0 \ +AlmaLinux^8.9 \ +AlmaLinux^8.8 \ +AlmaLinux^8.7 \ +AlmaLinux^8.6 \ +AlmaLinux^8.5 \ +AlmaLinux^8.4 \ +AlmaLinux^8.3 \ +CentOS^7.9.2009 \ +CentOS^7.5.1804 \ +UEK^10 \ +UEK^9 \ +UEK^8 \ +UEK^7^4.14 \ +UEK^7^4.1 \ +" + +# target|selector|display name|base image|kernel package|kernel repository +GITHUB_RPM_TARGETS=" +rocky-10.2|Rocky^10.2|Rocky Linux 10.2|rockylinux/rockylinux:10.2|kernel| +rocky-9.8|Rocky^9.8|Rocky Linux 9.8|rockylinux/rockylinux:9.8|kernel| +rocky-8.10|Rocky^8.10|Rocky Linux 8.10|rockylinux/rockylinux:8.10|kernel| +uek-10|UEK^10|Oracle Linux 10 UEK|oraclelinux:10|kernel-uek|ol10_UEKR8 +uek-9|UEK^9|Oracle Linux 9 UEK|oraclelinux:9|kernel-uek|ol9_UEKR7 +uek-8|UEK^8|Oracle Linux 8 UEK|oraclelinux:8|kernel-uek|ol8_UEKR6 +" diff --git a/scripts/kernel-matrix b/scripts/kernel-matrix new file mode 100644 index 000000000..d7be7c240 --- /dev/null +++ b/scripts/kernel-matrix @@ -0,0 +1,208 @@ +#!/bin/bash + +set -euo pipefail + +root_dir="$(readlink -f "$(dirname "$0")/..")" +readonly root_dir +readonly nightly_config="${SCST_NIGHTLY_CONFIG:-${root_dir}/nightly/conf/nightly.conf}" +readonly github_config="${SCST_GITHUB_MATRIX_CONFIG:-${root_dir}/.github/kernel-matrix.conf}" + +# shellcheck source=../nightly/conf/nightly.conf +source "${nightly_config}" +# shellcheck source=../.github/kernel-matrix.conf +source "${github_config}" + +: "${ABT_KERNELS:?ABT_KERNELS is not defined in ${nightly_config}}" +: "${GITHUB_REGRESSION_KERNEL_SELECTORS:?GITHUB_REGRESSION_KERNEL_SELECTORS is not defined in ${github_config}}" +: "${GITHUB_RPM_TARGETS:?GITHUB_RPM_TARGETS is not defined in ${github_config}}" +command -v jq >/dev/null 2>&1 || { + echo "Error: jq is required." >&2 + exit 1 +} + +read -r -a raw_kernel_entries <<<"${ABT_KERNELS}" + +strip_kernel_options() { + stripped_kernel="$1" + + while true; do + case "${stripped_kernel}" in + *-nc) stripped_kernel="${stripped_kernel%-nc}" ;; + *-ns) stripped_kernel="${stripped_kernel%-ns}" ;; + *-nm) stripped_kernel="${stripped_kernel%-nm}" ;; + *-4) stripped_kernel="${stripped_kernel%-4}" ;; + *-f) stripped_kernel="${stripped_kernel%-f}" ;; + *-i) stripped_kernel="${stripped_kernel%-i}" ;; + *-u) stripped_kernel="${stripped_kernel%-u}" ;; + *-p) stripped_kernel="${stripped_kernel%-p}" ;; + *) return ;; + esac + done +} + +canonical_kernel_entries=() +for raw_kernel in "${raw_kernel_entries[@]}"; do + strip_kernel_options "${raw_kernel}" + IFS='^' read -r -a kernel_parts <<<"${stripped_kernel}" + if [[ ${#kernel_parts[@]} -ne 1 && ${#kernel_parts[@]} -ne 3 ]]; then + echo "Error: invalid ABT_KERNELS entry: ${raw_kernel}" >&2 + exit 1 + fi + canonical_kernel_entries+=("${stripped_kernel}") +done +readonly -a canonical_kernel_entries + +version_matches_series() { + local version="$1" + local series="$2" + + [[ "${version}" == "${series}" || + "${version}" == "${series}."* || + "${version}" == "${series}-"* ]] +} + +resolve_kernel() { + local selector="$1" + local entry version distro release + local -a selector_parts entry_parts matches=() + + IFS='^' read -r -a selector_parts <<<"${selector}" + if [[ ${#selector_parts[@]} -lt 1 || ${#selector_parts[@]} -gt 3 ]]; then + echo "Error: invalid kernel selector: ${selector}" >&2 + return 1 + fi + + for entry in "${canonical_kernel_entries[@]}"; do + IFS='^' read -r -a entry_parts <<<"${entry}" + version="${entry_parts[0]}" + distro="${entry_parts[1]:-}" + release="${entry_parts[2]:-}" + + if [[ ${#selector_parts[@]} -eq 1 ]]; then + if [[ -z "${distro}" ]] && + version_matches_series "${version}" "${selector_parts[0]}"; then + matches+=("${entry}") + fi + elif [[ "${distro}" == "${selector_parts[0]}" && + "${release}" == "${selector_parts[1]}" ]]; then + if [[ ${#selector_parts[@]} -eq 2 ]] || + version_matches_series "${version}" "${selector_parts[2]}"; then + matches+=("${entry}") + fi + fi + done + + if [[ ${#matches[@]} -ne 1 ]]; then + echo "Error: selector ${selector} matched ${#matches[@]} ABT_KERNELS entries." >&2 + return 1 + fi + resolved_kernel="${matches[0]}" +} + +emit_regression_matrix() { + local selector kernel + local -a versions=() + local -A seen_selectors=() + + for selector in ${GITHUB_REGRESSION_KERNEL_SELECTORS}; do + if [[ -n "${seen_selectors[${selector}]+present}" ]]; then + echo "Error: duplicate regression selector: ${selector}" >&2 + return 1 + fi + seen_selectors["${selector}"]=1 + resolve_kernel "${selector}" || return 1 + kernel="${resolved_kernel}" + versions+=("${kernel}") + done + + printf '%s\n' "${versions[@]}" | + jq -Rsc 'split("\n") | map(select(length > 0)) | {version: .}' +} + +emit_rpm_matrix() { + local line target selector name base_image kernel_package kernel_repository extra + local kernel kernel_version row + local -a rows=() + local -A seen_targets=() seen_selectors=() + + while IFS= read -r line; do + [[ -n "${line}" ]] || continue + IFS='|' read -r target selector name base_image kernel_package \ + kernel_repository extra <<<"${line}" + if [[ -z "${target}" || -z "${selector}" || -z "${name}" || + -z "${base_image}" || -z "${kernel_package}" || -n "${extra:-}" ]]; then + echo "Error: invalid RPM target: ${line}" >&2 + return 1 + fi + if [[ -n "${seen_targets[${target}]+present}" ]]; then + echo "Error: duplicate RPM target: ${target}" >&2 + return 1 + fi + if [[ -n "${seen_selectors[${selector}]+present}" ]]; then + echo "Error: duplicate RPM selector: ${selector}" >&2 + return 1 + fi + seen_targets["${target}"]=1 + seen_selectors["${selector}"]=1 + + resolve_kernel "${selector}" || return 1 + kernel="${resolved_kernel}" + kernel_version="${kernel%%^*}" + row="$( + jq -cn \ + --arg name "${name}" \ + --arg target "${target}" \ + --arg base_image "${base_image}" \ + --arg kernel_package "${kernel_package}" \ + --arg kernel_version "${kernel_version}" \ + --arg kernel_repository "${kernel_repository}" \ + '{name: $name, target: $target, base_image: $base_image, + kernel_package: $kernel_package, + kernel_version: $kernel_version, + kernel_repository: $kernel_repository}' + )" || return 1 + rows+=("${row}") + done <<<"${GITHUB_RPM_TARGETS}" + + printf '%s\n' "${rows[@]}" | jq -cs '{include: .}' +} + +emit_rpm_field() { + local target="$1" + local field="$2" + local matrix + + case "${field}" in + name|base_image|kernel_package|kernel_version|kernel_repository) + ;; + *) + echo "Error: unsupported RPM matrix field: ${field}" >&2 + return 1 + ;; + esac + + matrix="$(emit_rpm_matrix)" || return 1 + jq -er \ + --arg target "${target}" \ + --arg field "${field}" ' + [.include[] | select(.target == $target)] | + if length == 1 then .[0][$field] + else error("RPM target \($target) matched \(length) entries") end + ' <<<"${matrix}" +} + +case "${1:-}" in + regression) emit_regression_matrix ;; + rpm) emit_rpm_matrix ;; + rpm-field) + if [[ $# -ne 3 ]]; then + echo "Usage: $0 rpm-field " >&2 + exit 2 + fi + emit_rpm_field "$2" "$3" + ;; + *) + echo "Usage: $0 {regression|rpm|rpm-field}" >&2 + exit 2 + ;; +esac From 3e0d69d9d547f9cae95f47f0262931c75d960b47 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 18:01:18 +0300 Subject: [PATCH 03/10] .github/workflows: Generate regression kernel matrix Generate the existing 53-kernel GitHub regression subset from ABT_KERNELS instead of duplicating exact versions in the workflow. --- .github/workflows/run_regression_tests.yaml | 74 +++++---------------- 1 file changed, 18 insertions(+), 56 deletions(-) diff --git a/.github/workflows/run_regression_tests.yaml b/.github/workflows/run_regression_tests.yaml index e2ebcb52b..75e9edc54 100644 --- a/.github/workflows/run_regression_tests.yaml +++ b/.github/workflows/run_regression_tests.yaml @@ -9,67 +9,29 @@ on: - master jobs: + prepare_matrix: + name: Prepare kernel matrix + runs-on: ubuntu-latest + outputs: + regression: ${{ steps.matrix.outputs.regression }} + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Resolve kernel matrix + id: matrix + shell: bash + run: | + matrix="$(bash scripts/kernel-matrix regression)" + printf 'regression=%s\n' "${matrix}" >>"${GITHUB_OUTPUT}" + regression_tests: name: ${{matrix.version}} + needs: prepare_matrix runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - version: [ - '7.2', - '7.1.8', - '7.0.14', - '6.19.14', - '6.18.44', - '6.17.13', - '6.16.12', - '6.15.11', - '6.14.11', - '6.13.12', - '6.12.103', - '6.11.11', - '6.10.14', - '6.9.12', - '6.8.12', - '6.7.12', - '6.6.151', - '6.1.182', - '5.15.215', - '5.10.264', - '5.4.302', - '4.19.325', - '4.14.336', - '4.9.337', - '3.18.140', - '3.10.108', - '6.12.0-211.47.1.el10_2^Rocky^10.2', - '6.12.0-124.56.1.el10_1^Rocky^10.1', - '5.14.0-687.39.1.el9_8^Rocky^9.8', - '5.14.0-611.55.1.el9_7^Rocky^9.7', - '4.18.0-553.155.1.el8_10^Rocky^8.10', - '6.12.0-55.43.1.el10_0^AlmaLinux^10.0', - '5.14.0-570.62.1.el9_6^AlmaLinux^9.6', - '5.14.0-503.40.1.el9_5^AlmaLinux^9.5', - '5.14.0-427.42.1.el9_4^AlmaLinux^9.4', - '5.14.0-362.24.1.el9_3^AlmaLinux^9.3', - '5.14.0-284.30.1.el9_2^AlmaLinux^9.2', - '5.14.0-162.23.1.el9_1^AlmaLinux^9.1', - '5.14.0-70.30.1.el9_0^AlmaLinux^9.0', - '4.18.0-513.24.1.el8_9^AlmaLinux^8.9', - '4.18.0-477.13.1.el8_8^AlmaLinux^8.8', - '4.18.0-425.19.2.el8_7^AlmaLinux^8.7', - '4.18.0-372.32.1.el8_6^AlmaLinux^8.6', - '4.18.0-348.23.1.el8_5^AlmaLinux^8.5', - '4.18.0-305.25.1.el8_4^AlmaLinux^8.4', - '4.18.0-240.22.1.el8_3^AlmaLinux^8.3', - '3.10.0-1160.118.1.el7^CentOS^7.9.2009', - '3.10.0-862.14.4.el7^CentOS^7.5.1804', - '6.12.0-205.92.4.2.el10uek^UEK^10', - '5.15.0-323.211.3.4.el9uek^UEK^9', - '5.4.17-2136.358.2.1.el8uek^UEK^8', - '4.14.35-2047.543.3.1.el7uek^UEK^7', - '4.1.12-124.93.1.el7uek^UEK^7' - ] + matrix: ${{ fromJSON(needs.prepare_matrix.outputs.regression) }} steps: - name: Checkout code uses: actions/checkout@main From 4575ce758835711c7a01712f8ff9730eb74b5bc1 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 18:01:18 +0300 Subject: [PATCH 04/10] build: Add containerized RPM artifact target Expose verified RPM artifact production through a repository Make target. Use Docker only as the Rocky Linux and Oracle Linux toolchain, keep the source worktree read-only and support the selected UEK package layouts. --- .gitignore | 2 + Makefile | 58 +++++++++++++++++++++- docker/rpm/Dockerfile | 43 +++++++++++++++++ scripts/build-rpm-artifacts | 96 +++++++++++++++++++++++++++++++++++++ scst.spec.in | 5 ++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 docker/rpm/Dockerfile create mode 100644 scripts/build-rpm-artifacts diff --git a/.gitignore b/.gitignore index 9565728d7..9f4a4b40d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ debian/scst.dkms debian/scst/ debian/scstadmin/ debian/tmp/ +docker-rpmbuilddir/ dpkg/ fcst/build_mode iscsi-scst/conftest/*/result-*.txt @@ -58,6 +59,7 @@ nightly/old.verbose nightly/sendmail.log qla2x00t/in-tree-patches/ qla2x00t/qla2xxx-orig/ +rpm-artifacts/ rpmbuilddir/ scst.spec scst/build_mode diff --git a/Makefile b/Makefile index 707ca1e02..08bcfc688 100644 --- a/Makefile +++ b/Makefile @@ -93,6 +93,23 @@ VERSION := $(RELEASE_VERSION).$(REVISION) DEBIAN_REVISION := 1.1 RPMTOPDIR ?= $(shell if [ $$(id -u) = 0 ]; then echo /usr/src/packages;\ else echo $$PWD/rpmbuilddir; fi) +DOCKER ?= docker +DOCKER_RPM_TARGET ?= rocky-10.2 +DOCKER_RPM_CONTEXT ?= docker/rpm +DOCKER_RPM_DOCKERFILE ?= $(DOCKER_RPM_CONTEXT)/Dockerfile +DOCKER_RPM_IMAGE ?= scst-rpm-builder:$(DOCKER_RPM_TARGET) +DOCKER_RPM_OUTPUT ?= $(CURDIR)/docker-rpmbuilddir/$(DOCKER_RPM_TARGET) +DOCKER_RPM_BASE_IMAGE ?= $(shell bash scripts/kernel-matrix rpm-field \ + "$(DOCKER_RPM_TARGET)" base_image) +DOCKER_RPM_KERNEL_PACKAGE ?= $(shell bash scripts/kernel-matrix rpm-field \ + "$(DOCKER_RPM_TARGET)" kernel_package) +DOCKER_RPM_KERNEL_VERSION ?= $(shell bash scripts/kernel-matrix rpm-field \ + "$(DOCKER_RPM_TARGET)" kernel_version) +DOCKER_RPM_KERNEL_REPOSITORY ?= $(shell bash scripts/kernel-matrix rpm-field \ + "$(DOCKER_RPM_TARGET)" kernel_repository) +RPM_ARTIFACT_SOURCE_DIR ?= $(CURDIR) +RPM_ARTIFACT_OUTPUT_DIR ?= $(CURDIR)/rpm-artifacts +RPM_ARTIFACT_KERNEL_DEVEL_PACKAGE ?= kernel-devel SCST_SOURCE_FILES = $(shell if [ -e scripts/list-source-files ]; then \ scripts/list-source-files; \ else \ @@ -173,6 +190,9 @@ help: @echo " scstadm-rpm : make scstadmin RPM packages" @echo " rpm : make both SCST and scstadmin RPM packages" @echo " rpm-dkms : make both SCST DKMS and scstadmin RPM packages" + @echo " rpm-artifacts : build verified RPM artifacts" + @echo " docker-rpm-image : build the RPM builder Docker image" + @echo " docker-rpm : build RPM packages in Docker" @echo "" @echo " dpkg : make SCST dpkg packages" @echo "" @@ -432,6 +452,41 @@ rpm-dkms: find -name '*.rpm'; \ fi +rpm-artifacts: + mkdir -p "$(RPM_ARTIFACT_OUTPUT_DIR)" + RPM_ARTIFACT_SOURCE_DIR="$(RPM_ARTIFACT_SOURCE_DIR)" \ + RPM_ARTIFACT_OUTPUT_DIR="$(RPM_ARTIFACT_OUTPUT_DIR)" \ + RPM_ARTIFACT_KERNEL_DEVEL_PACKAGE="$(RPM_ARTIFACT_KERNEL_DEVEL_PACKAGE)" \ + bash "$(CURDIR)/scripts/build-rpm-artifacts" + +docker-rpm-image: + test -n "$(DOCKER_RPM_BASE_IMAGE)" + test -n "$(DOCKER_RPM_KERNEL_PACKAGE)" + test -n "$(DOCKER_RPM_KERNEL_VERSION)" + $(DOCKER) build --file "$(DOCKER_RPM_DOCKERFILE)" \ + --build-arg "BASE_IMAGE=$(DOCKER_RPM_BASE_IMAGE)" \ + --build-arg "KERNEL_PACKAGE=$(DOCKER_RPM_KERNEL_PACKAGE)" \ + --build-arg "KERNEL_VERSION=$(DOCKER_RPM_KERNEL_VERSION)" \ + --build-arg \ + "KERNEL_REPOSITORY=$(DOCKER_RPM_KERNEL_REPOSITORY)" \ + --tag "$(DOCKER_RPM_IMAGE)" "$(DOCKER_RPM_CONTEXT)" + +docker-rpm: docker-rpm-image + mkdir -p "$(DOCKER_RPM_OUTPUT)" + git_common_dir="$$(git rev-parse --path-format=absolute \ + --git-common-dir)" && \ + $(DOCKER) run --rm \ + --user "$$(id -u):$$(id -g)" \ + --env HOME=/tmp \ + --mount "type=bind,source=$(CURDIR),target=/source,readonly" \ + --mount "type=bind,source=$${git_common_dir},target=$${git_common_dir},readonly" \ + --mount "type=bind,source=$(DOCKER_RPM_OUTPUT),target=/output" \ + "$(DOCKER_RPM_IMAGE)" \ + make -C /source rpm-artifacts \ + RPM_ARTIFACT_SOURCE_DIR=/source \ + RPM_ARTIFACT_OUTPUT_DIR=/output \ + RPM_ARTIFACT_KERNEL_DEVEL_PACKAGE="$(DOCKER_RPM_KERNEL_PACKAGE)-devel" + debian/changelog: debian/changelog.in sed 's/%{scst_version}/$(VERSION)-$(DEBIAN_REVISION)/' \ debian/changelog @@ -528,5 +583,6 @@ multiple-release-archives: fcst fcst_clean fcst_extraclean fcst_install fcst_uninstall \ scst_local scst_local_clean scst_local_extraclean scst_local_install scst_local_uninstall \ usr usr_clean usr_extraclean usr_install usr_uninstall \ - scst-rpm scst-dkms-rpm scstadm-rpm rpm rpm-dkms dpkg \ + scst-rpm scst-dkms-rpm scstadm-rpm rpm rpm-dkms rpm-artifacts \ + docker-rpm-image docker-rpm dpkg \ 2perf 2release 2debug diff --git a/docker/rpm/Dockerfile b/docker/rpm/Dockerfile new file mode 100644 index 000000000..91988a2a5 --- /dev/null +++ b/docker/rpm/Dockerfile @@ -0,0 +1,43 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +SHELL ["/bin/bash", "-euxo", "pipefail", "-c"] + +ARG KERNEL_PACKAGE=kernel +ARG KERNEL_VERSION +ARG KERNEL_REPOSITORY= + +ENV LANG=C \ + LC_ALL=C + +RUN : "${KERNEL_PACKAGE:?KERNEL_PACKAGE is required}" \ + && : "${KERNEL_VERSION:?KERNEL_VERSION is required}" \ + && repo_args=() \ + && if [[ -n "${KERNEL_REPOSITORY}" ]]; then \ + repo_args+=(--enablerepo="${KERNEL_REPOSITORY}"); \ + fi \ + && dnf -y install \ + bzip2 \ + elfutils-libelf-devel \ + findutils \ + gcc \ + git \ + kmod \ + make \ + openssl \ + perl \ + perl-Data-Dumper \ + perl-devel \ + perl-ExtUtils-MakeMaker \ + redhat-rpm-config \ + rpm-build \ + systemd-rpm-macros \ + tar \ + which \ + && dnf -y "${repo_args[@]}" install \ + "${KERNEL_PACKAGE}-${KERNEL_VERSION}" \ + "${KERNEL_PACKAGE}-devel-${KERNEL_VERSION}" \ + && dnf clean all \ + && rm -rf /var/cache/dnf + +CMD ["/bin/bash"] diff --git a/scripts/build-rpm-artifacts b/scripts/build-rpm-artifacts new file mode 100644 index 000000000..0b4a9152c --- /dev/null +++ b/scripts/build-rpm-artifacts @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euo pipefail + +readonly source_dir="${RPM_ARTIFACT_SOURCE_DIR:-$PWD}" +readonly output_dir="${RPM_ARTIFACT_OUTPUT_DIR:-$PWD/rpm-artifacts}" +readonly kernel_devel_package="${RPM_ARTIFACT_KERNEL_DEVEL_PACKAGE:-kernel-devel}" + +if ! git -C "${source_dir}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Error: ${source_dir} is not an SCST Git worktree." >&2 + exit 1 +fi + +if [[ ! -d "${output_dir}" || ! -w "${output_dir}" ]]; then + echo "Error: ${output_dir} is not a writable output directory." >&2 + exit 1 +fi + +mapfile -t kernel_releases < <( + rpm -q --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' \ + "${kernel_devel_package}" 2>/dev/null | sort -V +) +if [[ ${#kernel_releases[@]} -ne 1 ]]; then + echo "Error: expected exactly one ${kernel_devel_package} package." >&2 + printf 'Found kernel releases: %s\n' "${kernel_releases[*]:-none}" >&2 + exit 1 +fi + +readonly kernel_release="${kernel_releases[0]}" +readonly kernel_dir="/usr/src/kernels/${kernel_release}" +if [[ ! -d "${kernel_dir}" ]]; then + echo "Error: kernel build directory ${kernel_dir} does not exist." >&2 + exit 1 +fi + +tmp_dir="$(mktemp -d)" +readonly tmp_dir +artifact_stage="" +cleanup() { + rm -rf -- "${tmp_dir}" + if [[ -n "${artifact_stage}" ]]; then + rm -rf -- "${artifact_stage}" + fi +} +trap cleanup EXIT + +readonly build_dir="${tmp_dir}/scst" +readonly rpm_topdir="${tmp_dir}/rpmbuild" +mkdir -p "${build_dir}" "${rpm_topdir}" + +git -C "${source_dir}" ls-files -z -- | + tar --directory="${source_dir}" --null --files-from=- --create --file=- | + tar --directory="${build_dir}" --extract --file=- + +git_commit="${GIT_COMMIT:-$(git -C "${source_dir}" rev-parse --short=12 HEAD)}" +build_number="${BUILD_NUMBER:-$(git -C "${source_dir}" rev-list --count HEAD)}" +revision="${REVISION:-${build_number}.${git_commit}}" +readonly git_commit build_number revision + +make -C "${build_dir}" \ + BUILD_NUMBER="${build_number}" \ + GIT_COMMIT="${git_commit}" \ + REVISION="${revision}" \ + KDIR="${kernel_dir}" \ + KVER="${kernel_release}" \ + RPMTOPDIR="${rpm_topdir}" \ + rpm + +mapfile -d '' -t rpm_files < <( + find "${rpm_topdir}/RPMS" "${rpm_topdir}/SRPMS" \ + -type f -name '*.rpm' -print0 | sort -z +) +if [[ ${#rpm_files[@]} -eq 0 ]]; then + echo "Error: the RPM build produced no packages." >&2 + exit 1 +fi +rpm --checksig --nosignature "${rpm_files[@]}" + +artifact_stage="$(mktemp -d "${output_dir}/.scst-rpm.XXXXXX")" +cp -a "${rpm_topdir}/RPMS" "${rpm_topdir}/SRPMS" "${artifact_stage}/" +( + cd "${artifact_stage}" + find RPMS SRPMS -type f -name '*.rpm' -print0 | + sort -z | xargs -0 sha256sum >SHA256SUMS +) + +rm -rf -- "${output_dir}/RPMS" "${output_dir}/SRPMS" +rm -f -- "${output_dir}/SHA256SUMS" +mv "${artifact_stage}/RPMS" "${artifact_stage}/SRPMS" \ + "${artifact_stage}/SHA256SUMS" "${output_dir}/" +rmdir "${artifact_stage}" +artifact_stage="" + +echo "Built SCST RPM packages for kernel ${kernel_release}:" +find "${output_dir}/RPMS" "${output_dir}/SRPMS" \ + -type f -name '*.rpm' -print | sort diff --git a/scst.spec.in b/scst.spec.in index e59ddceab..080dea8de 100644 --- a/scst.spec.in +++ b/scst.spec.in @@ -59,6 +59,10 @@ # UEK 7 %define kernel_devel_rpm kernel-uek-devel %else +%if %([ %{kernel_rpm} = kernel-uek-modules-core ]; echo $((1-$?))) +# UEK 8 +%define kernel_devel_rpm kernel-uek-devel +%else # Other Linux distros %define kernel_devel_rpm %{kernel_rpm}-devel %endif @@ -66,6 +70,7 @@ %endif %endif %endif +%endif %{echo:kernel_devel_rpm=%{kernel_devel_rpm} } %endif From 1ec9e42d64b0a90a2b27603278183e06b3293bbb Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 18:01:18 +0300 Subject: [PATCH 05/10] .github/workflows: Build package matrix Build the six RPM targets resolved from ABT_KERNELS and portable Ubuntu DKMS and user-space packages. Upload commit-specific GitHub Actions artifacts for every target. --- .github/workflows/rpm.yml | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/rpm.yml diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml new file mode 100644 index 000000000..c6e3dad5e --- /dev/null +++ b/.github/workflows/rpm.yml @@ -0,0 +1,114 @@ +name: Packages + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +jobs: + prepare_matrix: + name: Prepare RPM matrix + runs-on: ubuntu-latest + outputs: + rpm: ${{ steps.matrix.outputs.rpm }} + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Resolve RPM matrix + id: matrix + shell: bash + run: | + matrix="$(bash scripts/kernel-matrix rpm)" + printf 'rpm=%s\n' "${matrix}" >>"${GITHUB_OUTPUT}" + + build_rpm: + name: ${{ matrix.name }} x86_64 + needs: prepare_matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare_matrix.outputs.rpm) }} + steps: + - name: Checkout code + uses: actions/checkout@main + with: + fetch-depth: 0 + + - name: Build RPM packages + run: | + make docker-rpm \ + DOCKER_RPM_TARGET=${{ matrix.target }} \ + DOCKER_RPM_BASE_IMAGE=${{ matrix.base_image }} \ + DOCKER_RPM_KERNEL_PACKAGE=${{ matrix.kernel_package }} \ + DOCKER_RPM_KERNEL_VERSION=${{ matrix.kernel_version }} \ + DOCKER_RPM_KERNEL_REPOSITORY=${{ matrix.kernel_repository }} + + - name: Upload RPM packages + uses: actions/upload-artifact@main + with: + name: scst-rpm-${{ matrix.target }}-x86_64-${{ github.sha }} + path: | + docker-rpmbuilddir/${{ matrix.target }}/RPMS/**/*.rpm + docker-rpmbuilddir/${{ matrix.target }}/SRPMS/*.rpm + docker-rpmbuilddir/${{ matrix.target }}/SHA256SUMS + if-no-files-found: error + + build_deb: + name: Ubuntu 24.04 DKMS and user space x86_64 + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@main + with: + fetch-depth: 0 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + debhelper \ + devscripts \ + dpkg-dev \ + lintian \ + "linux-headers-$(uname -r)" \ + quilt + + - name: Build DEB packages + run: make dpkg + + - name: Verify published DEB set + shell: bash + run: | + set -euo pipefail + + patterns=( + 'dpkg/scst-dkms_*.deb' + 'dpkg/scstadmin_*.deb' + 'dpkg/iscsi-scst_*.deb' + 'dpkg/*.dsc' + 'dpkg/*.debian.tar.*' + 'dpkg/*.orig.tar.*' + ) + for pattern in "${patterns[@]}"; do + mapfile -t matches < <(compgen -G "${pattern}" || true) + if [[ ${#matches[@]} -ne 1 ]]; then + echo "Expected one package matching ${pattern}, found ${#matches[@]}." >&2 + exit 1 + fi + done + + - name: Upload DEB packages + uses: actions/upload-artifact@main + with: + name: scst-deb-ubuntu-24.04-x86_64-${{ github.sha }} + path: | + dpkg/scst-dkms_*.deb + dpkg/scstadmin_*.deb + dpkg/iscsi-scst_*.deb + dpkg/*.dsc + dpkg/*.debian.tar.* + dpkg/*.orig.tar.* + if-no-files-found: error + From 4d60d08325dd82b07911189fe9440b5af9f627db Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 19:54:02 +0300 Subject: [PATCH 06/10] .github/workflows: Publish master package snapshot Publish a rolling prerelease after the complete master package matrix succeeds. Validate and bundle every downloaded artifact before replacing the previous master-rpm snapshot. --- .github/workflows/rpm.yml | 136 ++++++++++++++++++++++++++++++++ README.md | 14 ++++ scripts/prepare-package-release | 116 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100755 scripts/prepare-package-release diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml index c6e3dad5e..c8bd8b9e0 100644 --- a/.github/workflows/rpm.yml +++ b/.github/workflows/rpm.yml @@ -112,3 +112,139 @@ jobs: dpkg/*.orig.tar.* if-no-files-found: error + publish_master: + name: Publish master package snapshot + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + github.ref == 'refs/heads/master' && + github.repository == 'SCST-project/scst' + needs: [prepare_matrix, build_rpm, build_deb] + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + concurrency: + group: packages-master-release + cancel-in-progress: false + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Download RPM packages + uses: actions/download-artifact@main + with: + pattern: scst-rpm-*-x86_64-${{ github.sha }} + path: package-artifacts/rpm + + - name: Download DEB packages + uses: actions/download-artifact@main + with: + name: scst-deb-ubuntu-24.04-x86_64-${{ github.sha }} + path: package-artifacts/deb + + - name: Prepare release assets + env: + RPM_MATRIX: ${{ needs.prepare_matrix.outputs.rpm }} + RELEASE_OUTPUT_DIR: ${{ runner.temp }}/master-package-release + run: bash scripts/prepare-package-release + + - name: Publish rolling master snapshot + env: + GH_TOKEN: ${{ github.token }} + RELEASE_OUTPUT_DIR: ${{ runner.temp }}/master-package-release + shell: bash + run: | + set -euo pipefail + + readonly tag=master-rpm + get_master_sha() { + gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/master" \ + --jq '.object.sha' + } + + delete_tag_releases() { + local release_ids release_id + + release_ids="$( + gh api --paginate \ + "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ + --jq '.[] | select(.tag_name == "master-rpm") | .id' + )" + while IFS= read -r release_id; do + [[ -n "${release_id}" ]] || continue + gh api --method DELETE \ + "repos/${GITHUB_REPOSITORY}/releases/${release_id}" + done <<<"${release_ids}" + } + + delete_release_tag() { + local tag_status + + tag_status="$( + curl --silent --show-error --output /dev/null \ + --write-out '%{http_code}' \ + --header 'Accept: application/vnd.github+json' \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" + )" + case "${tag_status}" in + 200) + gh api --method DELETE \ + "repos/${GITHUB_REPOSITORY}/git/refs/tags/${tag}" + ;; + 404) ;; + *) + echo "Unable to inspect ${tag} (HTTP ${tag_status})." >&2 + return 1 + ;; + esac + } + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Skipping stale package snapshot for ${GITHUB_SHA}." + exit 0 + fi + + delete_tag_releases + delete_release_tag + + shopt -s nullglob + assets=("${RELEASE_OUTPUT_DIR}"/scst-master-*) + [[ ${#assets[@]} -gt 0 ]] || { + echo 'No release assets were prepared.' >&2 + exit 1 + } + + notes="$(printf '%s\n\n%s\n%s\n' \ + 'Automated development package snapshot from the master branch.' \ + "Commit: ${GITHUB_SHA}" \ + "Workflow: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")" + gh release create "${tag}" "${assets[@]}" \ + --target "${GITHUB_SHA}" \ + --title 'Latest master package snapshot' \ + --notes "${notes}" \ + --prerelease \ + --latest=false \ + --draft + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Discarding stale package snapshot for ${GITHUB_SHA}." + delete_tag_releases + delete_release_tag + exit 0 + fi + + gh release edit "${tag}" \ + --draft=false \ + --prerelease \ + --latest=false + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Removing stale package snapshot for ${GITHUB_SHA}." + delete_tag_releases + delete_release_tag + fi diff --git a/README.md b/README.md index 373dad033..56df0a072 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,20 @@ project includes: Instructions for building and installing SCST are available in the INSTALL.md file. +## Current master packages + +Prebuilt x86_64 packages from the latest successfully published `master` +build are available in the rolling +[`master-rpm` prerelease](https://github.com/SCST-project/scst/releases/tag/master-rpm). +These are development snapshots, not SCST releases. + +The snapshot contains RPM bundles for Rocky Linux 10.2, 9.8 and 8.10 and +Oracle Linux UEK 10, 9 and 8. It also contains an Ubuntu 24.04 bundle with +`scst-dkms`, `scstadmin` and `iscsi-scst` binary packages and their Debian +source package files. Every `.tar.gz` bundle has a matching `.sha256` file. +The complete snapshot is replaced after each successful package workflow for +the current `master` commit. + ## QLogic target driver Two QLogic target drivers are included in the SCST project. diff --git a/scripts/prepare-package-release b/scripts/prepare-package-release new file mode 100755 index 000000000..48896c73a --- /dev/null +++ b/scripts/prepare-package-release @@ -0,0 +1,116 @@ +#!/bin/bash + +set -euo pipefail + +die() { + echo "Error: $*" >&2 + exit 1 +} + +: "${RPM_MATRIX:?RPM_MATRIX is not set}" +: "${GITHUB_SHA:?GITHUB_SHA is not set}" +: "${RELEASE_OUTPUT_DIR:?RELEASE_OUTPUT_DIR is not set}" + +readonly rpm_artifact_root="${RPM_ARTIFACT_ROOT:-package-artifacts/rpm}" +readonly deb_artifact_dir="${DEB_ARTIFACT_DIR:-package-artifacts/deb}" +readonly release_output_dir="${RELEASE_OUTPUT_DIR}" + +[[ "${GITHUB_SHA}" =~ ^[0-9a-fA-F]{40,64}$ ]] || + die "GITHUB_SHA is not a full Git object ID." +jq -e '.include | type == "array" and length > 0' \ + <<<"${RPM_MATRIX}" >/dev/null || die "RPM_MATRIX is invalid." + +mapfile -t rpm_targets < <(jq -r '.include[].target' <<<"${RPM_MATRIX}") +declare -A seen_targets=() +for target in "${rpm_targets[@]}"; do + [[ "${target}" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || + die "Invalid RPM target: ${target}" + [[ -z "${seen_targets[${target}]+present}" ]] || + die "Duplicate RPM target: ${target}" + seen_targets["${target}"]=1 +done + +if [[ -e "${release_output_dir}" && ! -d "${release_output_dir}" ]]; then + die "Release output path is not a directory: ${release_output_dir}" +fi +mkdir -p -- "${release_output_dir}" +if [[ -n "$(find "${release_output_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + die "Release output directory is not empty: ${release_output_dir}" +fi + +create_bundle() { + local source_dir="$1" + local bundle_name="$2" + shift 2 + + tar --sort=name --mtime='UTC 1970-01-01' \ + --owner=0 --group=0 --numeric-owner \ + -C "${source_dir}" -cf - "$@" | + gzip -n >"${release_output_dir}/${bundle_name}" + ( + cd "${release_output_dir}" + sha256sum "${bundle_name}" >"${bundle_name}.sha256" + ) +} + +for target in "${rpm_targets[@]}"; do + artifact_name="scst-rpm-${target}-x86_64-${GITHUB_SHA}" + artifact_dir="${rpm_artifact_root}/${artifact_name}" + [[ -d "${artifact_dir}/RPMS" ]] || + die "Missing RPMS directory in ${artifact_name}." + [[ -d "${artifact_dir}/SRPMS" ]] || + die "Missing SRPMS directory in ${artifact_name}." + [[ -f "${artifact_dir}/SHA256SUMS" ]] || + die "Missing SHA256SUMS in ${artifact_name}." + [[ -n "$(find "${artifact_dir}/RPMS" -type f -name '*.rpm' -print -quit)" ]] || + die "No binary RPM packages found in ${artifact_name}." + [[ -n "$(find "${artifact_dir}/SRPMS" -type f -name '*.rpm' -print -quit)" ]] || + die "No source RPM packages found in ${artifact_name}." + ( + cd "${artifact_dir}" + sha256sum --check SHA256SUMS + ) || die "RPM checksum verification failed for ${artifact_name}." + + create_bundle "${artifact_dir}" \ + "scst-master-${target}-x86_64.tar.gz" \ + RPMS SRPMS SHA256SUMS +done + +[[ -d "${deb_artifact_dir}" ]] || + die "Missing DEB artifact directory: ${deb_artifact_dir}" + +find_one_deb_file() { + local pattern="$1" + local -a matches=() + + mapfile -d '' -t matches < <( + find "${deb_artifact_dir}" -maxdepth 1 -type f \ + -name "${pattern}" -print0 + ) + [[ ${#matches[@]} -eq 1 ]] || + die "Expected one DEB artifact matching ${pattern}, found ${#matches[@]}." + deb_files+=("${matches[0]##*/}") +} + +declare -a deb_files=() +find_one_deb_file 'scst-dkms_*.deb' +find_one_deb_file 'scstadmin_*.deb' +find_one_deb_file 'iscsi-scst_*.deb' +find_one_deb_file '*.dsc' +find_one_deb_file '*.debian.tar.*' +find_one_deb_file '*.orig.tar.*' + +mapfile -d '' -t all_deb_packages < <( + find "${deb_artifact_dir}" -maxdepth 1 -type f -name '*.deb' -print0 +) +[[ ${#all_deb_packages[@]} -eq 3 ]] || + die "Expected exactly three binary DEB packages, found ${#all_deb_packages[@]}." + +create_bundle "${deb_artifact_dir}" \ + "scst-master-ubuntu-24.04-x86_64.tar.gz" "${deb_files[@]}" + +expected_files="$(((${#rpm_targets[@]} + 1) * 2))" +actual_files="$(find "${release_output_dir}" -maxdepth 1 -type f | + wc -l)" +[[ "${actual_files}" -eq "${expected_files}" ]] || + die "Expected ${expected_files} release assets, found ${actual_files}." From 5fa1defa73b9ff5533c430a071231ed49c01a311 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 21:05:55 +0300 Subject: [PATCH 07/10] build: Add warnings-as-errors mode Add a WERROR=y|n build setting that keeps the local default unchanged and exports -Werror for user-space and kernel compilation. Honor the same setting in direct regression kernel builds. --- Makefile | 29 +++++++++++++++++++++++++++-- debian/rules | 11 +++++++++++ scripts/run-regression-tests | 21 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 08bcfc688..d81740cbd 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,26 @@ endif PKG_BUILD_MODE ?= 2release +WERROR ?= n +ifneq ($(words $(strip $(WERROR))),1) +$(error WERROR must be one of: y n (got '$(WERROR)')) +endif +ifneq ($(filter y n,$(strip $(WERROR))),$(strip $(WERROR))) +$(error WERROR must be one of: y n (got '$(WERROR)')) +endif +override WERROR := $(strip $(WERROR)) +export WERROR + +ifeq ($(WERROR),y) +ifeq ($(filter -Werror,$(CFLAGS)),) +override CFLAGS += -Werror +endif +ifeq ($(filter -Werror,$(KCFLAGS)),) +override KCFLAGS += -Werror +endif +export CFLAGS KCFLAGS +endif + OLD_QLA_INI_DIR = qla2x00t OLD_QLA_DIR = $(OLD_QLA_INI_DIR)/qla2x00-target @@ -117,6 +137,9 @@ SCST_SOURCE_FILES = $(shell if [ -e scripts/list-source-files ]; then \ fi) help: + @echo "Build variables:" + @echo " WERROR=y|n : treat warnings as errors (default: n)" + @echo "" @echo " tags : make tags" @echo " cov-build : make coverity build" @echo " shellcheck : check Bash scripts" @@ -478,6 +501,7 @@ docker-rpm: docker-rpm-image $(DOCKER) run --rm \ --user "$$(id -u):$$(id -g)" \ --env HOME=/tmp \ + --env WERROR="$(WERROR)" \ --mount "type=bind,source=$(CURDIR),target=/source,readonly" \ --mount "type=bind,source=$${git_common_dir},target=$${git_common_dir},readonly" \ --mount "type=bind,source=$(DOCKER_RPM_OUTPUT),target=/output" \ @@ -536,8 +560,9 @@ dpkg: ../scst_$(VERSION).orig.tar.gz else \ buildopts+=(-j4); \ fi && \ - DEB_CC_SET="$(CC)" DEB_KVER_SET=$(KVER) DEB_KDIR_SET=$(KDIR) DEB_QLA_DIR_SET=$(QLA_DIR) \ - DEB_QLA_INI_DIR_SET=$(QLA_INI_DIR) DEB_PKG_BUILD_MODE=$(PKG_BUILD_MODE) \ + DEB_CC_SET="$(CC)" DEB_KVER_SET=$(KVER) DEB_KDIR_SET=$(KDIR) \ + DEB_QLA_DIR_SET=$(QLA_DIR) DEB_QLA_INI_DIR_SET=$(QLA_INI_DIR) \ + DEB_PKG_BUILD_MODE=$(PKG_BUILD_MODE) DEB_WERROR_SET="$(WERROR)" \ debuild "$${buildopts[@]}" --lintian-opts --profile debian && \ mkdir -p dpkg && \ for f in "$${output_files[@]}" ../scst_$(VERSION).orig.tar.[gx]z; do\ diff --git a/debian/rules b/debian/rules index 79ab0bdff..41ec4f33e 100755 --- a/debian/rules +++ b/debian/rules @@ -21,6 +21,17 @@ export CC=$(DEB_CC_SET) export QLA_DIR=$(DEB_QLA_DIR_SET) export QLA_INI_DIR=$(DEB_QLA_INI_DIR_SET) export PKG_BUILD_MODE=$(DEB_PKG_BUILD_MODE) +DEB_WERROR_SET?=n +export WERROR=$(DEB_WERROR_SET) +ifeq ($(WERROR),y) +ifeq ($(filter -Werror,$(CFLAGS)),) +override CFLAGS += -Werror +endif +ifeq ($(filter -Werror,$(KCFLAGS)),) +override KCFLAGS += -Werror +endif +export CFLAGS KCFLAGS +endif # Default to building optional modules (override by changing specific ?=m to =n) CONFIG_SCST_LOCAL?=m diff --git a/scripts/run-regression-tests b/scripts/run-regression-tests index 8600cf2a2..e99c6b12f 100755 --- a/scripts/run-regression-tests +++ b/scripts/run-regression-tests @@ -57,6 +57,26 @@ # shellcheck source=./kernel-functions source "$(dirname "$0")/kernel-functions" +readonly WERROR="${WERROR-n}" +case "${WERROR}" in + y|n) ;; + *) + echo "Error: WERROR must be one of: y n (got '${WERROR}')." >&2 + exit 2 + ;; +esac +if [ "${WERROR}" = y ]; then + case " ${CFLAGS:-} " in + *" -Werror "*) ;; + *) CFLAGS="${CFLAGS:+${CFLAGS} }-Werror" ;; + esac + case " ${KCFLAGS:-} " in + *" -Werror "*) ;; + *) KCFLAGS="${KCFLAGS:+${KCFLAGS} }-Werror" ;; + esac + export CFLAGS KCFLAGS +fi + function usage { echo "Usage: $0 [-c ] [-d ] [-h] [-j ] [-k] [-l]" \ "[-p] [-q] ..." @@ -321,6 +341,7 @@ CONFIG_TRACER_MAX_TRACE \ CONFIG_TRACE_BRANCH_PROFILING \ CONFIG_TRACING \ CONFIG_UNWINDER_ORC \ +CONFIG_WERROR \ CONFIG_X86_32 \ CONFIG_X86_X32 \ CONFIG_X86_KERNEL_IBT \ From aeccd8be959c1bf6cdba154cf775defc4e3c9a21 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Mon, 24 Aug 2026 21:07:25 +0300 Subject: [PATCH 08/10] .github/workflows: Configure warnings-as-errors policy Enable WERROR=y for CI and package builds so warnings in SCST code are fatal. Keep WERROR=n for regression and Coverity because these workflows must tolerate failures outside strict SCST package gates. --- .github/workflows/ci.yml | 5 ++++- .github/workflows/coverity.yml | 3 +++ .github/workflows/rpm.yml | 3 +++ .github/workflows/run_regression_tests.yaml | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02bd99829..f60032945 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,9 @@ name: CI on: [push, pull_request] +env: + WERROR: 'y' + jobs: build: runs-on: ubuntu-latest @@ -22,6 +25,6 @@ jobs: - name: Build env: CC: ${{ matrix.compiler }} - CFLAGS: -Werror ${{ matrix.arch == 'x86' && '-m32' || '' }} + CFLAGS: ${{ matrix.arch == 'x86' && '-m32' || '' }} LDFLAGS: ${{ matrix.arch == 'x86' && '-m32' || '' }} run: make CC=${{matrix.compiler}} dpkg diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 307eb365f..86af57ea3 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -5,6 +5,9 @@ on: branches: - master +env: + WERROR: 'n' + jobs: coverity: if: github.repository == 'SCST-project/scst' diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml index c8bd8b9e0..56f3ad78a 100644 --- a/.github/workflows/rpm.yml +++ b/.github/workflows/rpm.yml @@ -5,6 +5,9 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +env: + WERROR: 'y' + jobs: prepare_matrix: name: Prepare RPM matrix diff --git a/.github/workflows/run_regression_tests.yaml b/.github/workflows/run_regression_tests.yaml index 75e9edc54..ff37db8f5 100644 --- a/.github/workflows/run_regression_tests.yaml +++ b/.github/workflows/run_regression_tests.yaml @@ -8,6 +8,9 @@ on: branches: - master +env: + WERROR: 'n' + jobs: prepare_matrix: name: Prepare kernel matrix From 4584ede647d708642f00adc565683ebbf7e9b1bf Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Tue, 25 Aug 2026 09:53:20 +0300 Subject: [PATCH 09/10] .github/workflows: Avoid duplicate branch builds Run CI and package builds for pull requests and for pushes to master. Avoid running both event types for each same-repository PR update. Keep manual package workflow runs available. --- .github/workflows/ci.yml | 8 +++++++- .github/workflows/rpm.yml | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f60032945..83b6c3496 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: CI -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master env: WERROR: 'y' diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml index 56f3ad78a..5478918f5 100644 --- a/.github/workflows/rpm.yml +++ b/.github/workflows/rpm.yml @@ -1,6 +1,13 @@ name: Packages -on: [push, pull_request, workflow_dispatch] +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: permissions: contents: read From 0a8ba54931e6f14b3420961a6a2edb31ac2d6ade Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Tue, 25 Aug 2026 10:34:35 +0300 Subject: [PATCH 10/10] .github/workflows: Drop redundant GCC build Keep the standalone CI matrix focused on Clang. GCC is already exercised by Ubuntu DEB and RPM package builds with WERROR enabled. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83b6c3496..e9941f6a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - compiler: [gcc, clang] + compiler: [clang] arch: [x86_64] steps: - uses: actions/checkout@main