diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 473f420a..5b15db40 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,3 +24,11 @@ updates: update-types: - minor - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + # gives a hijacked action release time to be caught before it is proposed + default-days: 7 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8c26b954..8a385ed2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,9 +13,9 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm i @@ -28,9 +28,9 @@ jobs: node-version: [26, 24, 22, 20] fail-fast: false steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm runtime set node ${{ matrix.node-version }} @@ -40,7 +40,7 @@ jobs: - run: pnpm run test:coverage - - uses: codecov/codecov-action@v7 + - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 # TODO: use v26, there're tests skipped on v26 if: matrix.node-version == 24 with: @@ -49,9 +49,9 @@ jobs: publish_preview: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm i diff --git a/.github/workflows/codspeed.yaml b/.github/workflows/codspeed.yaml index de63dc42..42a4dabe 100644 --- a/.github/workflows/codspeed.yaml +++ b/.github/workflows/codspeed.yaml @@ -17,13 +17,13 @@ jobs: benchmarks: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm i - - uses: CodSpeedHQ/action@v4 + - uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 with: mode: simulation run: pnpm run bench diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index c84c88c8..3b196fe8 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -22,11 +22,11 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 1 - name: Run agent - uses: pullfrog/pullfrog@v0 + uses: pullfrog/pullfrog@ad4283a862b8377cb525a0f5bf30f17c023b18ea # v0.1.98 with: prompt: ${{ inputs.prompt }} env: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b79e9b76..09e24c25 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,9 @@ concurrency: jobs: release: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: npm-release permissions: contents: write id-token: write @@ -62,11 +64,11 @@ jobs: fi echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm i diff --git a/.github/workflows/sponsors-sync.yaml b/.github/workflows/sponsors-sync.yaml index 68a2225c..56ac241e 100644 --- a/.github/workflows/sponsors-sync.yaml +++ b/.github/workflows/sponsors-sync.yaml @@ -11,18 +11,20 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - run: pnpm i - run: pnpm run sponsors:sync - - uses: EndBug/add-and-commit@v10 - with: - author_name: GitHub Actions - author_email: 41898282+github-actions[bot]@users.noreply.github.com - message: 'chore: sync sponsors' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and push changes + run: | + git add . + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git -c user.name="GitHub Actions" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" commit -m "chore: sync sponsors" + git push origin HEAD diff --git a/tests/github-workflows.test.ts b/tests/github-workflows.test.ts new file mode 100644 index 00000000..b5d59318 --- /dev/null +++ b/tests/github-workflows.test.ts @@ -0,0 +1,60 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const githubDir = fileURLToPath(new URL('../.github/', import.meta.url)) +const workflowsDir = join(githubDir, 'workflows') + +const workflows = readdirSync(workflowsDir) + .filter((name) => /\.ya?ml$/.test(name)) + .map((name) => ({ name, content: readFileSync(join(workflowsDir, name), 'utf8') })) + +/** + * Collects every `uses:` reference across all workflows, with its version comment if any. + */ +function collectUses(): { location: string; ref: string; comment: string | undefined }[] { + return workflows.flatMap(({ name, content }) => + content.split('\n').flatMap((line, index) => { + const match = /^\s*(?:-\s+)?uses:\s*(['"]?)([^\s'"]+)\1(?:\s+#\s*(.*?))?\s*$/.exec(line) + return match ? [{ location: `${name}:${index + 1}`, ref: match[2]!, comment: match[3] }] : [] + }), + ) +} + +describe('github workflows', () => { + it('pins every third-party action to a full commit SHA with a version comment', () => { + const uses = collectUses().filter(({ ref }) => !ref.startsWith('./')) + expect(uses.length).toBeGreaterThan(0) + + const unpinned = uses + .filter( + ({ ref, comment }) => + !/@[0-9a-f]{40}$/.test(ref) || !/^v\d+\.\d+\.\d+$/.test(comment ?? ''), + ) + .map( + ({ location, ref, comment }) => + `${location}: ${ref}${comment === undefined ? '' : ` # ${comment}`}`, + ) + expect(unpinned).toEqual([]) + }) + + it('keeps action pins updated through dependabot', () => { + expect(readFileSync(join(githubDir, 'dependabot.yml'), 'utf8')).toMatch( + /^\s*-\s+package-ecosystem:\s*github-actions\s*$/m, + ) + }) + + it('gates the release job behind an environment and the main branch', () => { + const release = readFileSync(join(workflowsDir, 'release.yaml'), 'utf8') + expect(release).toMatch(/^\s+environment:\s*npm-release\s*$/m) + expect(release).toMatch(/^\s+if:\s*github\.ref\s*==\s*'refs\/heads\/main'\s*$/m) + }) + + it('does not hand a write token to EndBug/add-and-commit', () => { + expect( + workflows + .filter(({ content }) => content.includes('EndBug/add-and-commit')) + .map(({ name }) => name), + ).toEqual([]) + }) +})