From d06a4b686af1f78ddda697d0b17ede7eec5add46 Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 8 Apr 2026 11:30:42 -0400 Subject: [PATCH 1/2] feat: split CI and release into separate workflows Closes #710 - Move docker-release job to dedicated release.yml, triggered only on release published events without re-running the test suite - Remove release trigger from ci.yml (push/PR only) - Pin all GitHub Actions to full commit SHAs for supply chain safety - Fix SBOM tag mismatch (v2.3.4 vs 2.3.4) by using metadata output Signed-off-by: Will --- .github/workflows/ci.yml | 75 ++++++----------------------------- .github/workflows/release.yml | 53 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 891f1d46..30d6ab53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,12 +19,10 @@ on: - '.github/workflows/docs.yml' - '.github/workflows/release.yml' - '.github/workflows/dependabot.yml' - release: - types: [published] concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'release' }} + cancel-in-progress: true jobs: # ─── LINT + SECURITY ───────────────────────────────────── @@ -34,14 +32,14 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1 with: ruby-version: '.ruby-version' bundler-cache: true - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -62,10 +60,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -73,7 +71,7 @@ jobs: - name: Vitest with coverage run: yarn test:unit --coverage - name: Upload frontend coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: coverage-frontend @@ -140,14 +138,14 @@ jobs: VULCAN_LDAP_ADMIN_PASS: GoodNewsEveryone steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1 with: ruby-version: '.ruby-version' bundler-cache: true - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -156,7 +154,7 @@ jobs: run: sudo apt-get -yqq install libpq-dev - name: Cache JS build - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 id: js-cache with: path: app/assets/builds @@ -178,7 +176,7 @@ jobs: spec/ - name: Upload backend coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: coverage-shard-${{ matrix.ci_node_index }} @@ -202,52 +200,3 @@ jobs: exit 1 fi echo "All CI jobs passed" - - # ─── DOCKER RELEASE (release only, multi-arch via Build Cloud) ─── - docker-release: - needs: [lint, frontend, backend] - if: github.event_name == 'release' - runs-on: ubuntu-24.04 - timeout-minutes: 20 - permissions: - contents: write # Required for anchore/sbom-action dependency-snapshot submission - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKER_USER }} - password: ${{ secrets.DOCKER_PAT }} - - - name: Set up Docker Buildx (Build Cloud) - uses: docker/setup-buildx-action@v3 - with: - driver: cloud - endpoint: "mitre/mitre-builder" - - - name: Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: mitre/vulcan - tags: | - type=semver,pattern={{version}} - type=raw,value=latest - - - name: Build and push multi-arch image - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: SBOM scan and dependency submission - uses: anchore/sbom-action@v0 - with: - image: mitre/vulcan:${{ github.event.release.tag_name }} - artifact-name: image.spdx.json - dependency-snapshot: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..aeeb6b6f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + release: + types: [published] + +jobs: + # ─── DOCKER RELEASE (multi-arch via Build Cloud) ─── + docker-release: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: write # Required for anchore/sbom-action dependency-snapshot submission + packages: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Login to DockerHub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + username: ${{ vars.DOCKER_USER }} + password: ${{ secrets.DOCKER_PAT }} + + - name: Set up Docker Buildx (Build Cloud) + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + driver: cloud + endpoint: "mitre/mitre-builder" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: mitre/vulcan + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Build and push multi-arch image + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: SBOM scan and dependency submission + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: mitre/vulcan:${{ steps.meta.outputs.version }} + artifact-name: image.spdx.json + dependency-snapshot: true From 82e51e9641b0ed3759a8ccd24fb3c310e49c9991 Mon Sep 17 00:00:00 2001 From: Will Date: Wed, 8 Apr 2026 13:15:02 -0400 Subject: [PATCH 2/2] chore: pin remaining workflow actions to SHAs and fix release spec - Pin docs.yml and dependabot.yml actions to full commit SHAs - Update release_infrastructure_spec to check release.yml instead of ci.yml for the docker-release trigger Signed-off-by: Will --- .github/workflows/dependabot.yml | 2 +- .github/workflows/docs.yml | 10 +++++----- spec/config/release_infrastructure_spec.rb | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 3dd0e051..1d1764dd 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write contents: write steps: - - uses: hmarr/auto-approve-action@v4 + - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4 - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --squash "$PR_URL" env: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fcb87554..522429cb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,12 +22,12 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 # Required: VitePress lastUpdated uses git log timestamps - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: # Docs use Node 24 (same as app). The docs/ subdirectory has its own # package.json with VitePress/Vue 3 — isolated from the main app's Vue 2. @@ -36,7 +36,7 @@ jobs: cache-dependency-path: docs/yarn.lock - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4 - name: Install and build docs env: @@ -48,7 +48,7 @@ jobs: yarn build - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: docs/.vitepress/dist @@ -66,4 +66,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/spec/config/release_infrastructure_spec.rb b/spec/config/release_infrastructure_spec.rb index a2a057b3..7d73b959 100644 --- a/spec/config/release_infrastructure_spec.rb +++ b/spec/config/release_infrastructure_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'release infrastructure' do # Releases are created manually via the GitHub UI. # Version must be consistent across all sources. Docker builds trigger - # on published GitHub releases (handled by ci.yml). + # on published GitHub releases (handled by release.yml). describe 'changelog configuration' do it 'cliff.toml exists with Keep a Changelog sections' do @@ -29,10 +29,10 @@ end describe 'Docker release trigger' do - it 'ci.yml triggers docker-release on published releases' do - ci = Rails.root.join('.github/workflows/ci.yml').read - expect(ci).to match(/release:.*\n.*types:.*published/m), - 'ci.yml must trigger on release published events for Docker builds' + it 'release.yml triggers docker-release on published releases' do + release = Rails.root.join('.github/workflows/release.yml').read + expect(release).to match(/release:.*\n.*types:.*published/m), + 'release.yml must trigger on release published events for Docker builds' end end