From f7abbe6abd020a328ff4b6bfe2970e2870922a2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:52:31 +0900 Subject: [PATCH 1/7] ci: cancel superseded pull request runs --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df092755..472b7900 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: rust: runs-on: ubuntu-latest From 89176e2cc57088e4d772de9b1686ab89a3e69aeb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:12:21 +0900 Subject: [PATCH 2/7] fix(ci): preserve every main push run --- .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 472b7900..d2b36870 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: From fd95005ba0c929a407be0eee1ed5df5cabcf3be5 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 21:20:17 +0900 Subject: [PATCH 3/7] fix(ci): scope concurrency groups by event --- .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 d2b36870..133e38b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: From 53001f111769c9e2f0f7cbde2358026055da0c3d Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 21:53:46 +0900 Subject: [PATCH 4/7] ci: coalesce superseded Wardnet validation --- .github/workflows/ci.yml | 2 +- .github/workflows/fuzz.yml | 43 ++++++++++++++------------------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 133e38b0..ced0e196 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + group: wardnet-ci-${{ github.repository }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ebcb3ce9..486f6f0d 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -18,20 +18,14 @@ permissions: contents: read concurrency: - group: fuzz-${{ github.ref }} - cancel-in-progress: true + group: wardnet-fuzz-${{ github.repository }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'schedule' && format('schedule-{0}', github.ref) || format('run-{0}', github.run_id) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: fuzz: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - target: - - fuzz_score_request - - fuzz_appdata_json - - fuzz_parse_admin_tokens - - fuzz_dnsbl_zone + env: + FUZZ_SECONDS: ${{ github.event_name == 'pull_request' && '60' || '300' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -46,30 +40,25 @@ jobs: path: | ~/.cargo/bin/cargo-fuzz fuzz/target - key: cargo-fuzz-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('fuzz/Cargo.toml') }} + key: cargo-fuzz-0.13.2-${{ runner.os }}-${{ hashFiles('fuzz/Cargo.toml') }} - name: Install cargo-fuzz - run: cargo +nightly install cargo-fuzz --version 0.13.2 --locked || true + run: command -v cargo-fuzz >/dev/null || cargo +nightly install cargo-fuzz --version 0.13.2 --locked - # PRs: 60s per target. Nightly/manual: 300s per target. - - name: Set fuzz duration + - name: Fuzz bounded targets run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "FUZZ_SECONDS=60" >> "$GITHUB_ENV" - else - echo "FUZZ_SECONDS=300" >> "$GITHUB_ENV" - fi - - - name: Fuzz ${{ matrix.target }} - run: | - cargo +nightly fuzz run ${{ matrix.target }} -- \ - -max_total_time="$FUZZ_SECONDS" \ - -rss_limit_mb=2048 + exit_code=0 + for target in fuzz_score_request fuzz_appdata_json fuzz_parse_admin_tokens fuzz_dnsbl_zone; do + cargo +nightly fuzz run "$target" -- \ + -max_total_time="$FUZZ_SECONDS" \ + -rss_limit_mb=2048 || exit_code=$? + done + exit "$exit_code" - name: Upload crash artifacts if: failure() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: fuzz-artifacts-${{ matrix.target }} - path: fuzz/artifacts/${{ matrix.target }}/ + name: fuzz-artifacts + path: fuzz/artifacts/ if-no-files-found: ignore From 03acae5b7012121ca0ca2e192ffcd4817a6ac329 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 21:54:50 +0900 Subject: [PATCH 5/7] test(actions): lock central workflow ownership --- .github/workflows/fuzz.yml | 1 + AGENTS.md | 6 +++ tests/workflow_queue_contract.rs | 68 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 tests/workflow_queue_contract.rs diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 486f6f0d..cfcd79da 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -23,6 +23,7 @@ concurrency: jobs: fuzz: + # One runner covers every bounded target; a matrix would consume four org slots. runs-on: ubuntu-latest env: FUZZ_SECONDS: ${{ github.event_name == 'pull_request' && '60' || '300' }} diff --git a/AGENTS.md b/AGENTS.md index 2a32694b..ae81252e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,12 @@ Cross-agent conventions for any agent (Claude, Codex, Cursor, opencode, …) wor - Do **not** weaken or disable the gate. A local scan with a stale DB misses findings: run `trivy --download-db-only` first, then scan the **merge ref**, not just the PR head (e.g. `trivy fs --scanners vuln,misconfig --severity CRITICAL,HIGH --ignore-unfixed .`). - Gating is by the Security Scan **job result**, not the `code_scanning` rule. That org ruleset is intentionally **CodeQL-only** (multiple code-scanning tools can't converge on one PR ref) — do **not** add tools to it. +### GitHub Actions ownership + +- Organization ruleset `18156473` owns Wardnet PR review, governance, security, and CodeQL through the seven required workflows in `ContextualWisdomLab/.github@769691526f8c73cf714de8fe8ba51ae6cfa2901a`. Do not add local PR Governance, Dependency Review, Close Empty PR, CodeQL, Security Scan, SAST, Strix, OpenCode, or Noema copies. +- Keep repository-local `CI` and path-filtered `Fuzz` because they validate Wardnet's Rust code. Their PR concurrency keys must include a fixed workflow name, `github.repository`, and the PR number; only superseded heads of that PR may be cancelled. +- Keep the local Scorecard default-branch, weekly, and branch-protection workflow until the central reusable owner matches Wardnet's pinned Scorecard version. The compared central revision uses Scorecard v2.4.3 while Wardnet uses v2.4.4, so delegating now would weaken deployed security evidence. + ### Code exploration - There is no `.codegraph/` index in this repo, so use normal search (grep/ripgrep, `cargo` tooling, editor navigation). If a `.codegraph/` index is added later, prefer CodeGraph (`codegraph explore ""` or the code-review-graph MCP tools) before grep/find — it surfaces callers/callees/impact that text search misses. diff --git a/tests/workflow_queue_contract.rs b/tests/workflow_queue_contract.rs new file mode 100644 index 00000000..41a3489c --- /dev/null +++ b/tests/workflow_queue_contract.rs @@ -0,0 +1,68 @@ +use std::{fs, path::PathBuf}; + +fn workflow_text(name: &str) -> String { + fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(".github/workflows") + .join(name), + ) + .unwrap() +} + +#[test] +fn local_pr_workflows_cancel_only_superseded_heads_of_the_same_pull_request() { + for (name, fixed_group) in [("ci.yml", "wardnet-ci"), ("fuzz.yml", "wardnet-fuzz")] { + let workflow = workflow_text(name); + assert!(workflow.contains(&format!( + "group: {fixed_group}-${{{{ github.repository }}}}-${{{{ github.event_name == 'pull_request'" + ))); + assert!(workflow.contains("format('pr-{0}', github.event.pull_request.number)")); + assert!(workflow.contains( + "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" + )); + } +} + +#[test] +fn fuzz_uses_one_runner_for_all_bounded_targets() { + let workflow = workflow_text("fuzz.yml"); + assert!(!workflow.contains("\n strategy:\n")); + assert!(!workflow.contains("Set fuzz duration")); + for target in [ + "fuzz_score_request", + "fuzz_appdata_json", + "fuzz_parse_admin_tokens", + "fuzz_dnsbl_zone", + ] { + assert!(workflow.contains(target)); + } +} + +#[test] +fn central_required_workflows_are_not_copied_locally() { + let workflow_directory = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".github/workflows"); + for name in [ + "pr-governance.yml", + "dependency-review.yml", + "close-empty-pr.yml", + "codeql-pr.yml", + "security-scan.yml", + "sast-semgrep.yml", + "strix.yml", + "opencode-review.yml", + "noema-review.yml", + ] { + assert!(!workflow_directory.join(name).exists()); + } +} + +#[test] +fn local_scorecard_preserves_non_pr_security_evidence() { + let workflow = workflow_text("scorecard-analysis.yml"); + assert!(workflow.contains("branch_protection_rule:")); + assert!(workflow.contains("schedule:")); + assert!(workflow.contains("push:")); + assert!(!workflow.contains("pull_request:")); + assert!(workflow.contains("# v2.4.4")); +} From 04787d9bcbe03244630ac0960b326903ad57e042 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 4 Sep 2026 21:55:09 +0900 Subject: [PATCH 6/7] style(actions): format workflow contract test --- tests/workflow_queue_contract.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/workflow_queue_contract.rs b/tests/workflow_queue_contract.rs index 41a3489c..0e1c4659 100644 --- a/tests/workflow_queue_contract.rs +++ b/tests/workflow_queue_contract.rs @@ -17,9 +17,9 @@ fn local_pr_workflows_cancel_only_superseded_heads_of_the_same_pull_request() { "group: {fixed_group}-${{{{ github.repository }}}}-${{{{ github.event_name == 'pull_request'" ))); assert!(workflow.contains("format('pr-{0}', github.event.pull_request.number)")); - assert!(workflow.contains( - "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" - )); + assert!( + workflow.contains("cancel-in-progress: ${{ github.event_name == 'pull_request' }}") + ); } } @@ -40,8 +40,7 @@ fn fuzz_uses_one_runner_for_all_bounded_targets() { #[test] fn central_required_workflows_are_not_copied_locally() { - let workflow_directory = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".github/workflows"); + let workflow_directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".github/workflows"); for name in [ "pr-governance.yml", "dependency-review.yml", From 8dc374ed597292a9d97a25e7cdea832b5990b6dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:02:35 +0900 Subject: [PATCH 7/7] fix(actions): adopt pinned hosted runner prerequisite --- .github/workflows/ci.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/scorecard-analysis.yml | 2 +- tests/workflow_runner_contract.rs | 49 ++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tests/workflow_runner_contract.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ced0e196..e3096352 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: jobs: rust: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index cfcd79da..135966fd 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -24,7 +24,7 @@ concurrency: jobs: fuzz: # One runner covers every bounded target; a matrix would consume four org slots. - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 env: FUZZ_SECONDS: ${{ github.event_name == 'pull_request' && '60' || '300' }} steps: diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index dfa64206..2d147bee 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -13,7 +13,7 @@ permissions: jobs: analysis: name: Scorecard Analysis - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read security-events: write diff --git a/tests/workflow_runner_contract.rs b/tests/workflow_runner_contract.rs new file mode 100644 index 00000000..ff42bad4 --- /dev/null +++ b/tests/workflow_runner_contract.rs @@ -0,0 +1,49 @@ +//! Repository contract for deterministic GitHub-hosted runner selection. +//! +//! Wardnet's required pull-request workflows must not depend on GitHub's floating +//! `ubuntu-latest` alias. A floating image can change independently of the +//! repository and, during hosted-runner transitions, can leave exact-head jobs +//! queued before checkout. Pinning the Ubuntu image makes runner acquisition a +//! reviewed repository change while preserving GitHub-hosted execution. + +use std::fs; +use std::path::Path; + +const PINNED_UBUNTU_RUNNER: &str = "ubuntu-24.04"; +const FLOATING_UBUNTU_RUNNER: &str = "ubuntu-latest"; + +const RUNNER_BACKED_WORKFLOWS: &[&str] = &[ + ".github/workflows/ci.yml", + ".github/workflows/fuzz.yml", + ".github/workflows/scorecard-analysis.yml", +]; + +#[test] +fn runner_backed_workflows_pin_the_hosted_ubuntu_image() { + let repository = Path::new(env!("CARGO_MANIFEST_DIR")); + + for relative in RUNNER_BACKED_WORKFLOWS { + let path = repository.join(relative); + let workflow = fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + + assert!( + !workflow.contains(FLOATING_UBUNTU_RUNNER), + "{relative} must not use the floating {FLOATING_UBUNTU_RUNNER} runner alias" + ); + + let runners = workflow + .lines() + .filter_map(|line| line.trim().strip_prefix("runs-on:")) + .map(str::trim) + .collect::>(); + assert!( + !runners.is_empty(), + "{relative} must define at least one runs-on value" + ); + assert!( + runners.iter().all(|runner| *runner == PINNED_UBUNTU_RUNNER), + "{relative} must use {PINNED_UBUNTU_RUNNER} for every runs-on value; found {runners:?}" + ); + } +}