diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d09fabf..94db785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -114,7 +114,7 @@ jobs: head-ref: ${{ needs.source.outputs.head }} - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -146,7 +146,7 @@ jobs: - name: Upload browser results and failure diagnostics if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: browser-diagnostics-${{ github.run_id }} if-no-files-found: ignore @@ -183,7 +183,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -219,7 +219,7 @@ jobs: - name: Upload screenshot candidates and comparison gallery if: ${{ always() && steps.eligibility.outputs.eligible == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: screenshot-candidate-${{ github.run_id }}-${{ github.run_attempt }} path: .cache/screenshots/candidate/ diff --git a/.github/workflows/deploy-examples.yml b/.github/workflows/deploy-examples.yml index 2ae6bc5..af408c4 100644 --- a/.github/workflows/deploy-examples.yml +++ b/.github/workflows/deploy-examples.yml @@ -131,7 +131,7 @@ jobs: - name: Set up exact Node if: ${{ steps.identity.outputs.eligible == 'true' }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -168,7 +168,7 @@ jobs: - name: Upload the raw verified channel without deployment authority if: ${{ steps.identity.outputs.eligible == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.artifact.outputs.name }} if-no-files-found: error @@ -200,7 +200,7 @@ jobs: ref: ${{ needs.build.outputs.source-commit }} - name: Set up exact Node for canonical Pages assembly - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -214,7 +214,7 @@ jobs: run: npm ci --ignore-scripts - name: Download the raw verified channel - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.build.outputs.channel-artifact }} path: ${{ runner.temp }}/pages-channel diff --git a/.github/workflows/publish-nightly.yml b/.github/workflows/publish-nightly.yml index 997431b..06cee05 100644 --- a/.github/workflows/publish-nightly.yml +++ b/.github/workflows/publish-nightly.yml @@ -123,7 +123,7 @@ jobs: ref: ${{ github.sha }} - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -257,7 +257,7 @@ jobs: - name: Upload browser diagnostics on failure if: failure() && steps.registry-state.outputs.eligible == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release-browser-diagnostics-${{ github.run_id }} if-no-files-found: ignore @@ -331,7 +331,7 @@ jobs: - name: Upload the exact npm release bundle if: ${{ steps.registry-state.outputs.eligible == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.identity.outputs.bundle-artifact }} if-no-files-found: error @@ -341,7 +341,7 @@ jobs: - name: Upload normalized release notes if: ${{ steps.registry-state.outputs.eligible == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.identity.outputs.notes-artifact }} if-no-files-found: error @@ -365,13 +365,13 @@ jobs: steps: - name: Download the verified bundle without checking out source - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.verify.outputs.bundle-artifact }} path: ${{ runner.temp }}/release-bundle - name: Download normalized release notes - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.verify.outputs.notes-artifact }} path: ${{ runner.temp }}/release-notes @@ -490,7 +490,7 @@ jobs: steps: - name: Set up exact Node for npm trusted publishing - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -635,7 +635,7 @@ jobs: ' <<< "${comparison}" > /dev/null - name: Download the verified bundle without checking out source - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.verify.outputs.bundle-artifact }} path: ${{ runner.temp }}/release-bundle @@ -847,7 +847,7 @@ jobs: steps: - name: Set up exact Node for registry verification - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -857,7 +857,7 @@ jobs: run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts - name: Download the retained bundle for provenance verification - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.verify.outputs.bundle-artifact }} path: ${{ runner.temp }}/release-bundle diff --git a/.github/workflows/refresh-screenshot-evidence.yml b/.github/workflows/refresh-screenshot-evidence.yml index 01980d6..15691d5 100644 --- a/.github/workflows/refresh-screenshot-evidence.yml +++ b/.github/workflows/refresh-screenshot-evidence.yml @@ -40,7 +40,6 @@ jobs: LFC_UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} LFC_UPSTREAM_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} LFC_UPSTREAM_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - LFC_CANDIDATE_DIRECTORY: ${{ runner.temp }}/screenshot-candidate steps: - name: Check out trusted promotion code without persisted credentials @@ -50,7 +49,7 @@ jobs: ref: ${{ github.sha }} - name: Set up exact Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false @@ -61,7 +60,7 @@ jobs: - name: Download the validated candidate artifact if: ${{ steps.context.outputs.eligible == 'true' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: github-token: ${{ github.token }} repository: ${{ github.repository }} @@ -72,4 +71,6 @@ jobs: - name: Refresh identical evidence and verify the new exact head if: ${{ steps.context.outputs.eligible == 'true' }} + env: + LFC_CANDIDATE_DIRECTORY: ${{ runner.temp }}/screenshot-candidate run: node scripts/screenshot-automation.mjs promote diff --git a/.github/workflows/rollback-examples.yml b/.github/workflows/rollback-examples.yml index 810a9b6..c702310 100644 --- a/.github/workflows/rollback-examples.yml +++ b/.github/workflows/rollback-examples.yml @@ -47,7 +47,7 @@ jobs: ref: ${{ github.sha }} - name: Set up exact Node for canonical Pages assembly - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ env.LFC_NODE_VERSION }} package-manager-cache: false diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs index 0db9a64..77ca201 100644 --- a/scripts/tests/workflow-contracts.test.mjs +++ b/scripts/tests/workflow-contracts.test.mjs @@ -556,7 +556,11 @@ void test("screenshot promotion consumes a validated artifact without executing assert.match(promote, /LFC_UPSTREAM_RUN_ATTEMPT: \$\{\{ github\.event\.workflow_run\.run_attempt \}\}/u); assert.match(promote, /LFC_UPSTREAM_HEAD_SHA: \$\{\{ github\.event\.workflow_run\.head_sha \}\}/u); assert.match(promote, /screenshot-automation\.mjs promotion-context[\s\S]*?steps\.context\.outputs\.eligible == 'true'[\s\S]*?actions\/download-artifact@[0-9a-f]{40}[\s\S]*?run-id: \$\{\{ github\.event\.workflow_run\.id \}\}[\s\S]*?artifact-ids: \$\{\{ steps\.context\.outputs\.artifact-id \}\}[\s\S]*?screenshot-automation\.mjs promote/u); - assert.match(promote, /LFC_CANDIDATE_DIRECTORY: \$\{\{ runner\.temp \}\}\/screenshot-candidate/u); + const stepBoundary = promote.indexOf("\n steps:"); + assert.ok(stepBoundary > 0, "Promotion must declare its steps."); + assert.doesNotMatch(promote.slice(0, stepBoundary), /\$\{\{\s*runner\./u, + "The runner context is unavailable in job-level environment variables."); + assert.match(promote, /Refresh identical evidence and verify the new exact head[\s\S]*?\n {8}env:\s*\n {10}LFC_CANDIDATE_DIRECTORY: \$\{\{ runner\.temp \}\}\/screenshot-candidate\s*\n {8}run: node scripts\/screenshot-automation\.mjs promote/u); }); void test("hosted gates run Chromium and WebKit while local Playwright retains Firefox", async () => { @@ -664,24 +668,25 @@ void test("workflow artifacts use bounded purpose-specific retention", async () } }); -void test("artifact downloads use the official Node 24 action", async () => { +void test("artifact downloads consistently pin the official v8 action", async () => { const workflows = [ "deploy-examples.yml", "publish-nightly.yml", "refresh-screenshot-evidence.yml", "rollback-examples.yml" ]; - const expected = - "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1"; + const pins = new Set(); let downloads = 0; for (const name of workflows) { const source = await workflow(name); for (const match of source.matchAll(/^\s*uses:\s*(actions\/download-artifact@[^\r\n]+)$/gmu)) { downloads += 1; - assert.equal(match[1], expected, name); + assert.match(match[1], /^actions\/download-artifact@[0-9a-f]{40} # v8\.\d+\.\d+$/u, name); + pins.add(match[1]); } } assert.equal(downloads, 6); + assert.equal(pins.size, 1, "All artifact downloads must use the same reviewed action pin."); }); void test("workflow dependency caches stay disabled", async () => {