diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c6649..29b0424 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ jobs: CHECK_BASE_REF: origin/main GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: fetch-depth: 0 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: ${{ matrix.node-version }} cache: 'npm' @@ -47,7 +47,7 @@ jobs: - run: npm run check:links - name: Upload tested npm package if: matrix.node-version == '24.x' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1, Node.js 24 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: npm-package path: artifacts/*.tgz @@ -64,10 +64,10 @@ jobs: env: GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: fetch-depth: 0 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24.x registry-url: 'https://registry.npmjs.org' @@ -77,7 +77,7 @@ jobs: - name: Verify package version matches release tag run: test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME" - name: Download tested npm package - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1, Node.js 24 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: npm-package path: artifacts diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..54dc2c9 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,22 @@ +name: Dependabot auto-merge + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - id: metadata + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 + - name: Auto-merge non-major Dependabot update + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/test/unit/docs.test.js b/test/unit/docs.test.js index c5e2ed8..09520d4 100644 --- a/test/unit/docs.test.js +++ b/test/unit/docs.test.js @@ -1,6 +1,6 @@ const { it } = require('node:test'); const assert = require('node:assert/strict'); -const { readFileSync } = require('node:fs'); +const { readdirSync, readFileSync } = require('node:fs'); it('uses preflight wording in user-facing docs', () => { for (const file of ['README.md', 'SKILL.md']) { @@ -53,8 +53,6 @@ it('keeps CI and release safety guarantees documented and wired', () => { assert.match(workflow, /actions\/download-artifact@/); assert.match(workflow, /npm publish \.\/artifacts\/\*\.tgz/); assert.match(workflow, /matrix:/); - assert.match(workflow, /upload-artifact@043fb46d/); - assert.match(workflow, /download-artifact@3e5f45b2/); assert.match(release, /DRY_RUN=1/); assert.match(release, /--generate-notes/); assert.match(releaseNotes, /categories:/); @@ -62,6 +60,30 @@ it('keeps CI and release safety guarantees documented and wired', () => { assert.doesNotMatch(release, /git commit -m "sync skill metadata/); }); +it('pins every GitHub Action to a full immutable commit SHA', () => { + // Supply-chain invariant: assert the property (full 40-char SHA, no @v6/@main + // mutable refs) not a specific value, so dependabot bumps keep CI green. + const files = readdirSync('.github/workflows').filter((f) => f.endsWith('.yml')); + const refs = []; + for (const file of files) { + const workflow = readFileSync(`.github/workflows/${file}`, 'utf8'); + refs.push( + ...[...workflow.matchAll(/(?:uses|with:\s*using):\s+([^\s#@]+)@([^\s#]+)/g)].map( + (m) => [file, m[1], m[2]], + ), + ); + } + assert.ok(refs.length > 0, 'workflows should reference at least one GitHub Action'); + + for (const [file, name, ref] of refs) { + assert.match( + ref, + /^[0-9a-f]{40}$/, + `${file}: ${name} must be pinned to a full 40-character immutable commit SHA, got ${JSON.stringify(ref)}`, + ); + } +}); + it('documents and configures dependency freshness checks', () => { const dependabot = readFileSync('.github/dependabot.yml', 'utf8'); const readme = readFileSync('README.md', 'utf8');