From 0278df458cedd57b18bf6f40bea75e32b294b006 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 29 May 2026 09:10:05 +0200 Subject: [PATCH] ci: upgrade actions to Node 24, SHA-pin, harden, consolidate workflows Ahead of GitHub's 2026-06-02 forced Node20->Node24 action migration (and Node20 removal from runners in fall 2026), upgrade every action to a Node24 release and pin to full commit SHAs. Actions (all SHA-pinned, Node24-verified): - actions/checkout v4 -> v6.0.2 - actions/setup-node v4 -> v6.4.0 - oven-sh/setup-bun v1 -> v2.2.0 (bun-version pinned 1.3.14, was 'latest') - actions/attest-build-provenance v2 -> v4.1.0 - actions/github-script v6 -> v9.0.0 - jwalton/gh-find-current-pr v1 -> v1.3.5 - rtCamp/action-slack-notify v2 -> v2.3.3 Build toolchain Node 20 -> 24 (Node 20 reached EOL 2026-04-30; @types/node is already 24). Dropped the single-value node matrix. Security hardening: - Replace Ilshidur/action-discord@master (moving-branch = supply-chain RCE, DISCORD_WEBHOOK is live) with a plain curl webhook call (jq-encoded). - Least-privilege permissions: top-level contents:read everywhere; release job keeps its annotated write scopes; notify job pinned to contents:read. - Remove dead NPM_TOKEN from the Release step (@semantic-release/npm has npmPublish:false; the plugin is kept because it drives the version bump). - Env-bind github-script inputs and use `node -p` for version extraction to remove script-injection surface. - harden-runner (egress audit) on release + CI build/test jobs. Modernization: - Consolidate test.yml + build-with-lint.yml into ci.yml (push/PR to master, paths-ignore docs/md, cancel-in-progress concurrency, persist-credentials false, --frozen-lockfile) + dependency-review on PRs. - Add CodeQL code scanning (codeql.yml). - Add Dependabot for github-actions + bun (root + docs) so actions never silently drift again. - Serialized concurrency on release.yml; drop the no-op `build --if-present`. Local validation: `bun install --frozen-lockfile` clean, `bun run build` OK, 1083 tests pass. --- .github/dependabot.yml | 53 +++++++++++++ .github/workflows/build-with-lint.yml | 31 -------- .github/workflows/ci.yml | 90 +++++++++++++++++++++ .github/workflows/codeql.yml | 47 +++++++++++ .github/workflows/release.yml | 108 +++++++++++++++----------- .github/workflows/test.yml | 29 ------- 6 files changed, 252 insertions(+), 106 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/build-with-lint.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c49d6928 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,53 @@ +version: 2 +updates: + # GitHub Actions across all workflows. Keeps the SHA pins from rotting — the + # Node 20 deprecation slipped in precisely because nothing watched these. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + groups: + actions: + patterns: ["*"] + commit-message: + prefix: ci + labels: + - dependencies + - github-actions + + # Plugin dependencies (root package.json + bun.lock). + - package-ecosystem: bun + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 10 + groups: + dev-dependencies: + dependency-type: development + update-types: ["minor", "patch"] + prod-dependencies: + dependency-type: production + update-types: ["minor", "patch"] + commit-message: + prefix: build + labels: + - dependencies + + # Docs site (docs/package.json + docs/bun.lock). + - package-ecosystem: bun + directory: /docs + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + groups: + docs-dependencies: + patterns: ["*"] + update-types: ["minor", "patch"] + commit-message: + prefix: build + labels: + - dependencies + - documentation diff --git a/.github/workflows/build-with-lint.yml b/.github/workflows/build-with-lint.yml deleted file mode 100644 index f399855a..00000000 --- a/.github/workflows/build-with-lint.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build With Lint - -on: - pull_request: - branches: - - master - workflow_dispatch: - -jobs: - build-with-lint: - name: Build With Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - run: | - bun install - - name: Run build with lint - run: | - bun run build-with-lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b7303b54 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [master] + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + branches: [master] + paths-ignore: + - 'docs/**' + - '**/*.md' + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 + - name: Use Node.js 24 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Run tests + run: bun run test + + build-lint: + name: Build + Lint + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 + - name: Use Node.js 24 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Run build with lint + run: bun run build-with-lint + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Dependency Review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + comment-summary-in-pr: on-failure + fail-on-severity: high diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..1579978b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: CodeQL + +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: '0 6 * * 1' + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + strategy: + fail-fast: false + matrix: + language: [javascript-typescript] + steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + with: + languages: ${{ matrix.language }} + queries: security-extended + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index faccd865..2c0c308f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,38 +24,53 @@ on: type: string repository_dispatch: types: [release-trigger] + +# Serialize releases so two triggers can never race on tags/commits. +# Do NOT cancel-in-progress: an interrupted semantic-release is dangerous. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +# Least-privilege default; the release job elevates exactly what it needs. +permissions: + contents: read + jobs: release: name: Release runs-on: ubuntu-latest permissions: - contents: write - issues: write - pull-requests: write - id-token: write - attestations: write - strategy: - matrix: - node-version: [20] + contents: write # semantic-release/git pushes the release commit+tag; semantic-release/github creates the Release + issues: write # semantic-release/github comments on released issues + pull-requests: write # semantic-release/github comments on released PRs + id-token: write # attest-build-provenance: OIDC token for Sigstore (NOT npm — npmPublish is false) + attestations: write # attest-build-provenance: persist the attestation outputs: version: ${{ steps.extract-version.outputs.version }} released: ${{ steps.release.outputs.released }} steps: + - name: Harden Runner + uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.4 + with: + egress-policy: audit + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + # semantic-release needs full history + tags and the persisted + # credential to push the release commit. Do not change these. fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + bun-version: 1.3.14 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Use Node.js 24 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: ${{ matrix.node-version }} + node-version: 24 - name: Install dependencies run: | @@ -76,31 +91,28 @@ jobs: - name: Build if: steps.validate.outputs.valid == 'true' - run: | - bun run build --if-present + run: bun run build - name: Run tests if: steps.validate.outputs.valid == 'true' - run: | - bun run test + run: bun run test - name: Release id: release if: steps.validate.outputs.valid == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_TYPE: ${{ github.event.inputs.releaseType || 'auto' }} DRY_RUN: ${{ github.event.inputs.dryRun || 'false' }} PRE_RELEASE: ${{ github.event.inputs.preRelease || '' }} run: | EXTRA_ARGS="" - + if [ "$DRY_RUN" == "true" ]; then echo "Running in dry-run mode" EXTRA_ARGS="--dry-run" fi - + if [ -n "$PRE_RELEASE" ]; then echo "Running pre-release: $PRE_RELEASE" bunx semantic-release --prerelease $PRE_RELEASE $EXTRA_ARGS @@ -114,82 +126,86 @@ jobs: bunx semantic-release --release-as $RELEASE_TYPE $EXTRA_ARGS RELEASE_EXIT_CODE=$? fi - + # Set release status based on exit code if [ "$DRY_RUN" != "true" ] && [ $RELEASE_EXIT_CODE -eq 0 ]; then echo "released=true" >> $GITHUB_OUTPUT else echo "released=false" >> $GITHUB_OUTPUT fi - + exit $RELEASE_EXIT_CODE - name: Attest release assets if: steps.release.outputs.released == 'true' - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: | main.js manifest.json styles.css - # Extract version for notifications - more resilient approach + # Extract version for notifications - name: Extract version id: extract-version if: steps.release.outputs.released == 'true' run: | - # Save the version to a temporary file to avoid issues with special characters - node -e "console.log('VERSION=' + require('./package.json').version)" > version.txt - source version.txt - echo "version=$VERSION" >> $GITHUB_OUTPUT - rm version.txt + VERSION="$(node -p "require('./package.json').version")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" # Find related PR to comment on - name: Find PR id: find-pr if: steps.release.outputs.released == 'true' - uses: jwalton/gh-find-current-pr@v1 + uses: jwalton/gh-find-current-pr@f3d61b485d2801773f7a07b2aaa3306bd8f8e653 # v1.3.5 with: state: closed # Comment on the PR that triggered this release - name: Comment on PR if: steps.release.outputs.released == 'true' && steps.find-pr.outputs.pr - uses: actions/github-script@v6 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_NUMBER: ${{ steps.find-pr.outputs.pr }} + RELEASE_VERSION: ${{ steps.extract-version.outputs.version }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - github.rest.issues.createComment({ - issue_number: ${{ steps.find-pr.outputs.pr }}, + await github.rest.issues.createComment({ + issue_number: Number(process.env.PR_NUMBER), owner: context.repo.owner, repo: context.repo.repo, - body: '🚀 Release has been published: v${{ steps.extract-version.outputs.version }}' + body: `🚀 Release has been published: v${process.env.RELEASE_VERSION}` }) - + # Notification job that runs after release notify: name: Send notifications needs: release if: needs.release.outputs.released == 'true' runs-on: ubuntu-latest + permissions: + contents: read + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + VERSION: ${{ needs.release.outputs.version }} steps: # Optional: Slack notifications - name: Notify Slack - uses: rtCamp/action-slack-notify@v2 if: env.SLACK_WEBHOOK != '' + uses: rtCamp/action-slack-notify@e31e87e03dd19038e411e38ae27cbad084a90661 # v2.3.3 env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_TITLE: "🚀 New Release" - SLACK_MESSAGE: "Version v${{ needs.release.outputs.version }} of QuickAdd has been released!" + SLACK_MESSAGE: "Version v${{ env.VERSION }} of QuickAdd has been released!" SLACK_COLOR: "good" SLACK_ICON: "https://github.com/chhoumann.png" MSG_MINIMAL: true - - # Optional: Discord notifications + + # Optional: Discord notifications — plain webhook call, no third-party action. + # jq safely JSON-encodes the message; the step is skipped when the secret is unset. - name: Notify Discord if: env.DISCORD_WEBHOOK != '' - uses: Ilshidur/action-discord@master - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - with: - args: '🚀 **New Release**: Version v${{ needs.release.outputs.version }} of QuickAdd has been released!' + run: | + payload="$(jq -n --arg content "🚀 **New Release**: Version v${VERSION} of QuickAdd has been released!" '{content: $content}')" + curl -fsSL -X POST -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index a788519f..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Test -on: push - -jobs: - test: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20] - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: | - bun install - - name: Run tests - run: | - bun run test