From 5621069b512a99a952bf0e7cd4f1b93db0c2631e Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Wed, 11 Mar 2026 14:51:01 -0500 Subject: [PATCH 1/4] ci: restructure workflows to match org ruleset contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split test matrix into test-run + test aggregator. Split lint into lint/typecheck/security. Add quality-gate aggregator. CodeQL job key analyze → codeql, drop matrix. Scorecard → thin caller. Release → centralized build. Delete local _build-reusable.yml. Required checks: test, lint, typecheck, security, codeql, semgrep, quality-gate Co-Authored-By: Claude Opus 4.6 --- .github/workflows/_build-reusable.yml | 115 -------------------------- .github/workflows/codeql.yml | 13 ++- .github/workflows/release.yml | 3 +- .github/workflows/scorecard.yml | 73 ++-------------- .github/workflows/tests.yml | 82 +++++++++++++++++- 5 files changed, 90 insertions(+), 196 deletions(-) delete mode 100644 .github/workflows/_build-reusable.yml diff --git a/.github/workflows/_build-reusable.yml b/.github/workflows/_build-reusable.yml deleted file mode 100644 index ad7f8cc..0000000 --- a/.github/workflows/_build-reusable.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Reusable Build (SLSA L3) - -on: - workflow_call: - inputs: - tag_name: - required: true - type: string - - - -permissions: {} - -concurrency: - group: release-${{ inputs.tag_name }} - cancel-in-progress: false - -jobs: - build-and-release: - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - id-token: write - attestations: write - steps: - - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Set up uv - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v5.4.2 - with: - enable-cache: true - - - name: Set up Python - run: uv python install 3.12 - - - name: Install dependencies - run: uv sync - - - name: Install release tools - run: uv pip install cyclonedx-bom==7.2.2 pip-licenses==5.5.1 - - - name: Run test suite - run: uv run pytest tests/ -x -q - - - name: Generate SBOM (CycloneDX) - run: uv run cyclonedx-py environment --output-format json -o sbom.cdx.json - - - name: Generate SBOM (SPDX) - uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0 - with: - format: spdx-json - output-file: sbom.spdx.json - - - name: Generate license report - run: | - uv run pip-licenses --format=csv --with-urls --with-authors --output-file licenses.csv - uv run pip-licenses --format=plain-vertical --with-urls --with-authors --output-file licenses.txt - - - name: Generate release notes - uses: orhun/git-cliff-action@c93ef52f3d0ddcdcc9bd5447d98d458a11cd4f72 # v4.7.1 - with: - config: cliff.toml - args: --latest --strip header - env: - OUTPUT: release-notes.md - GITHUB_REPO: ${{ github.repository }} - - - name: Fallback for empty release notes - env: - TAG_NAME: ${{ inputs.tag_name }} - run: | - if [ ! -s release-notes.md ]; then - echo "Release ${TAG_NAME}" > release-notes.md - fi - - - name: Build package - run: uv build - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ inputs.tag_name }} - run: | - gh release create "$TAG_NAME" \ - --title "$TAG_NAME" \ - --notes-file release-notes.md \ - dist/* \ - sbom.cdx.json \ - sbom.spdx.json \ - licenses.csv \ - licenses.txt - - - name: Attest release artifacts - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-path: | - dist/* - sbom.cdx.json - sbom.spdx.json - licenses.csv - licenses.txt - - - name: Upload dist artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: dist - path: dist/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fc0d33f..c97f6d6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,18 +16,15 @@ permissions: contents: read jobs: - analyze: - name: Analyze (${{ matrix.language }}) + # CONTRACT: org ruleset requires this exact check name 'codeql' + # Do NOT add a 'name:' override or matrix strategy + codeql: runs-on: ubuntu-latest timeout-minutes: 30 permissions: actions: read security-events: write contents: read - strategy: - fail-fast: false - matrix: - language: ["python"] steps: - name: Harden runner @@ -40,7 +37,7 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: - languages: ${{ matrix.language }} + languages: python queries: +security-extended - name: Autobuild @@ -49,4 +46,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: - category: "/language:${{ matrix.language }}" + category: "/language:python" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 494aaba..a5fddbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,10 @@ permissions: {} jobs: build: - uses: ./.github/workflows/_build-reusable.yml + uses: Project-Navi/.github/.github/workflows/_build-reusable.yml@6c4c2d8f200b1b9c3bd651ebc297425e45d5934e with: tag_name: ${{ github.ref_name }} + package_name: navi_bootstrap permissions: contents: write id-token: write diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0720af2..91a81ae 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -7,81 +7,18 @@ on: - cron: "15 6 * * 1" # Monday 06:15 UTC workflow_dispatch: -permissions: read-all +permissions: {} concurrency: group: scorecard cancel-in-progress: false jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - timeout-minutes: 15 + scorecard: + uses: Project-Navi/.github/.github/workflows/scorecard.yml@6c4c2d8f200b1b9c3bd651ebc297425e45d5934e permissions: + contents: read security-events: write id-token: write actions: read - steps: - - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - token: ${{ secrets.SCORECARD_TOKEN || secrets.GITHUB_TOKEN }} - - - name: Run Scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 - with: - repo_token: ${{ secrets.SCORECARD_TOKEN || secrets.GITHUB_TOKEN }} - results_file: results.sarif - results_format: sarif - publish_results: true - - - name: Upload SARIF to Security tab - uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 - with: - sarif_file: results.sarif - - badge: - name: Update badge - needs: analysis - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - token: ${{ secrets.SCORECARD_TOKEN || secrets.GITHUB_TOKEN }} - - - name: Run Scorecard (JSON) - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 - with: - repo_token: ${{ secrets.SCORECARD_TOKEN || secrets.GITHUB_TOKEN }} - results_file: results.json - results_format: json - publish_results: false - - - name: Generate badge - run: | - SCORE=$(jq -r '.score' results.json) - SCORE_INT=${SCORE%.*} - if [ "$SCORE_INT" -ge 8 ]; then COLOR=brightgreen - elif [ "$SCORE_INT" -ge 6 ]; then COLOR=green - elif [ "$SCORE_INT" -ge 4 ]; then COLOR=yellowgreen - else COLOR=red; fi - mkdir -p .github/badges - curl -sf "https://img.shields.io/badge/openssf_scorecard-${SCORE}-${COLOR}" -o .github/badges/scorecard.svg - - - name: Commit badge - run: | - git add .github/badges/scorecard.svg - git diff --cached --quiet && exit 0 - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "ci: update scorecard badge [skip ci]" - git pull --rebase - git push || echo "::warning::Scorecard badge push failed -- will retry on next run" + secrets: inherit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c434efd..7d6c33a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,8 @@ concurrency: permissions: {} jobs: - test: + # ── Test matrix (NOT a required check) ────────────────────────────── + test-run: runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -53,6 +54,23 @@ jobs: files: coverage.xml fail_ci_if_error: false + # ── Required check: test (aggregator) ─────────────────────────────── + # CONTRACT: org ruleset requires this exact check name + test: + needs: test-run + if: always() + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Check matrix results + run: | + if [ "${{ needs.test-run.result }}" != "success" ]; then + echo "::error::Test matrix failed" + exit 1 + fi + + # ── Required check: lint ──────────────────────────────────────────── + # CONTRACT: org ruleset requires this exact check name lint: runs-on: ubuntu-latest timeout-minutes: 10 @@ -85,12 +103,39 @@ jobs: - name: Format check run: uv run ruff format --check src/navi_bootstrap/ tests/ - - name: Security scan - run: uv run bandit -r src/navi_bootstrap -ll + # ── Required check: typecheck ─────────────────────────────────────── + # CONTRACT: org ruleset requires this exact check name + typecheck: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Harden runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v5.4.2 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync - name: Type check run: uv run mypy src/navi_bootstrap/ + # ── Required check: security ──────────────────────────────────────── + # CONTRACT: org ruleset requires this exact check name security: runs-on: ubuntu-latest timeout-minutes: 10 @@ -101,16 +146,45 @@ jobs: uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" + - name: Set up uv uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v5.4.2 with: enable-cache: true + - name: Install dependencies run: uv sync - - name: pip-audit + + - name: Security scan (bandit) + run: uv run bandit -r src/navi_bootstrap -ll + + - name: Audit dependencies (pip-audit) run: uvx pip-audit==2.9.0 + + # ── Required check: quality-gate ──────────────────────────────────── + # CONTRACT: org ruleset requires this exact check name + quality-gate: + needs: [test, lint, typecheck, security] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Check all required jobs passed + run: | + for result in \ + "${{ needs.test.result }}" \ + "${{ needs.lint.result }}" \ + "${{ needs.typecheck.result }}" \ + "${{ needs.security.result }}"; do + if [ "$result" != "success" ]; then + echo "::error::Required job failed: $result" + exit 1 + fi + done From f0453bcd1493f2ed45958c8170a29380df4ba6a4 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Wed, 11 Mar 2026 15:55:18 -0500 Subject: [PATCH 2/4] chore: trigger navi-bot approval From 699a8c10f99308a3a34dd836413b1efab33e0d3f Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Wed, 11 Mar 2026 16:16:23 -0500 Subject: [PATCH 3/4] chore: trigger CI re-evaluation From 3f5237272aab158912524144691d5820ad7c2e6e Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Wed, 11 Mar 2026 16:16:36 -0500 Subject: [PATCH 4/4] chore: trigger CI re-evaluation