diff --git a/.agents/skills/.gitignore b/.agents/skills/.gitignore index 725b05232..fba79cacc 100644 --- a/.agents/skills/.gitignore +++ b/.agents/skills/.gitignore @@ -17,5 +17,7 @@ !cosmosdb-mongodb/** !documentation/ !documentation/** +!github-actions/ +!github-actions/** !vscode-electron-worker/ !vscode-electron-worker/** \ No newline at end of file diff --git a/.agents/skills/github-actions/SKILL.md b/.agents/skills/github-actions/SKILL.md new file mode 100644 index 000000000..1846bb6db --- /dev/null +++ b/.agents/skills/github-actions/SKILL.md @@ -0,0 +1,26 @@ +--- +name: github-actions +description: Use when creating, editing, reviewing, or troubleshooting GitHub Actions workflows in Scope. +metadata: + version: "1.0.0" +--- + +# GitHub Actions + +Every external action or reusable workflow under `.github/workflows/` must use +a full 40-character commit SHA followed by its release version comment. Local +actions (`./...`) and container actions (`docker://...`) are exempt. + +Use `pinact run` to add or update pins instead of resolving SHAs manually. +Preserve the selected action version unless the task explicitly requests an +upgrade. + +Before completion, run: + +```bash +pinact run --check --verify-comment +pnpm exec vitest run scripts/ci-workflow.test.ts +``` + +Do not hand-edit generated `*.lock.yml` workflows. Update their owning gh-aw +Markdown source and run `gh aw compile`. diff --git a/.github/pinact.yaml b/.github/pinact.yaml new file mode 100644 index 000000000..37898a900 --- /dev/null +++ b/.github/pinact.yaml @@ -0,0 +1,15 @@ +version: 3 + +files: + - pattern: workflows/*.yml + - pattern: workflows/*.yaml + +rules: + # gh-aw owns generated lockfile annotations. Preserve its major-version + # comments while the Vitest invariant still enforces a full SHA and comment. + - ignore: true + conditions: + - expr: ActionName == "github/gh-aw-actions/setup" + - expr: ActionName == "actions/github-script" && VersionComment == "v8" + - expr: ActionName == "actions/upload-artifact" && VersionComment == "v7" + - expr: ActionName == "actions/download-artifact" && VersionComment == "v8" diff --git a/.github/workflows/check-worker-versions.yml b/.github/workflows/check-worker-versions.yml index 4d42d510c..4e49dee2d 100644 --- a/.github/workflows/check-worker-versions.yml +++ b/.github/workflows/check-worker-versions.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -55,7 +55,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceeac9b9e..621537e18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ on: - "tsconfig.base.json" - "vitest.config.ts" - "vitest.integration.config.ts" - - ".github/workflows/ci.yml" - - ".github/workflows/publish-cli.yml" + - ".github/workflows/**" + - ".github/pinact.yaml" - ".dockerignore" - "install-cli.sh" - "website/install-cli.sh" @@ -32,8 +32,8 @@ on: - "tsconfig.base.json" - "vitest.config.ts" - "vitest.integration.config.ts" - - ".github/workflows/ci.yml" - - ".github/workflows/publish-cli.yml" + - ".github/workflows/**" + - ".github/pinact.yaml" - ".dockerignore" - "install-cli.sh" - "website/install-cli.sh" @@ -66,7 +66,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -76,7 +76,7 @@ jobs: pnpm --version - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} registry-url: "https://registry.npmjs.org" @@ -97,7 +97,7 @@ jobs: pnpm exec github-actions-ctrf "ctrf/*-ctrf-report.json" --title "Test Results (Node.js ${{ matrix.node-version }})" - name: Upload coverage reports - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: coverage-reports-node-${{ matrix.node-version }} @@ -138,7 +138,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload CTRF test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: ctrf-test-results-node-${{ matrix.node-version }} @@ -165,7 +165,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -175,7 +175,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -205,7 +205,7 @@ jobs: pnpm exec github-actions-ctrf "ctrf/*-eval-ctrf-report.json" --title "LLM Eval Results" || true - name: Upload CTRF eval results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: ctrf-eval-results @@ -277,7 +277,7 @@ jobs: df -h / - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -287,7 +287,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -300,12 +300,12 @@ jobs: run: pnpm --filter shared --filter github-auth build - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4.0.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: driver: docker-container - name: Cache Docker layers - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: /tmp/.buildx-cache key: docker-${{ matrix.worker.name }}-${{ hashFiles(matrix.worker.dockerfile, matrix.worker.versions_env) }} @@ -313,7 +313,7 @@ jobs: - name: Log in to Docker Hub if: env.TRUSTED_CODE == 'true' && env.DOCKERHUB_LOGIN_ENABLED == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -346,7 +346,7 @@ jobs: pnpm exec github-actions-ctrf "ctrf/*-integration-ctrf-report.json" --title "Integration Test Results (${{ matrix.worker.name }})" || true - name: Upload CTRF integration test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: ctrf-integration-test-results-${{ matrix.worker.name }} @@ -354,7 +354,7 @@ jobs: if-no-files-found: ignore - name: Upload test snapshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: test-snapshots-${{ matrix.worker.name }} @@ -371,7 +371,7 @@ jobs: done - name: Upload test videos - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: test-videos-${{ matrix.worker.name }} @@ -401,7 +401,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -411,7 +411,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -432,7 +432,7 @@ jobs: if: >- env.DOCKERHUB_LOGIN_ENABLED == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -446,7 +446,7 @@ jobs: pnpm exec github-actions-ctrf "ctrf/*-integration-ctrf-report.json" --title "Integration Test Results (queue recovery)" || true - name: Upload CTRF integration test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: ctrf-integration-test-results-queue-recovery @@ -463,7 +463,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -473,7 +473,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -497,7 +497,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -507,7 +507,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -529,7 +529,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Install pnpm run: | @@ -537,7 +537,7 @@ jobs: corepack prepare pnpm@10.29.1 --activate - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" cache: "pnpm" @@ -552,7 +552,7 @@ jobs: run: pnpm vitest run --config vitest.integration.config.ts apps/cli/src/bundle.integration.test.ts apps/cli/src/utils/update-check.integration.test.ts - name: Upload CLI bundle - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: scope-cli-bundle path: apps/cli/dist/scope.mjs @@ -570,7 +570,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -580,7 +580,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -602,16 +602,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.94.1 + uses: dtolnay/rust-toolchain@ee868d7805ade43a1b47dd054727ab76c9108b4f # 1.94.1 with: toolchain: 1.94.1 components: rustfmt, clippy - name: Cache Cargo registry + build - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: workspaces: apps/gateway # Fall back to the most recent partial cache when Cargo.lock changes, @@ -623,7 +623,7 @@ jobs: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" @@ -662,7 +662,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload CTRF test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: ctrf-test-results-gateway @@ -679,7 +679,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # pnpm/action-setup@v4 is not allowed on EMU - name: Install pnpm @@ -689,7 +689,7 @@ jobs: pnpm --version - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" registry-url: "https://registry.npmjs.org" @@ -703,14 +703,14 @@ jobs: run: pnpm install --frozen-lockfile --ignore-scripts - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.94.1 + uses: dtolnay/rust-toolchain@ee868d7805ade43a1b47dd054727ab76c9108b4f # 1.94.1 with: toolchain: 1.94.1 # cargo-about only reads `cargo metadata` + each crate's LICENSE file, so # this caches the crates.io index and downloaded sources — no target build. - name: Cache Cargo registry - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: workspaces: apps/gateway save-if: ${{ github.ref == 'refs/heads/main' }} @@ -718,7 +718,7 @@ jobs: # Cache the prebuilt cargo-about binary so warm runs skip the download. - name: Cache cargo-about id: cache-cargo-about - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.cargo/bin/cargo-about key: cargo-about-0.9.1-${{ runner.os }}-x86_64 @@ -754,7 +754,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Build and smoke-test local Windows images @@ -787,9 +787,9 @@ jobs: typescript: ${{ steps.changes.outputs.typescript }} evals: ${{ steps.changes.outputs.evals }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 if: github.event_name == 'push' || github.event_name == 'pull_request' - - uses: dorny/paths-filter@v3.0.2 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 if: github.event_name == 'push' || github.event_name == 'pull_request' id: changes with: @@ -843,8 +843,8 @@ jobs: - '.dockerignore' - 'packages/test-utils/**' typescript: - - '.github/workflows/ci.yml' - - '.github/workflows/publish-cli.yml' + - '.github/workflows/**' + - '.github/pinact.yaml' - 'scripts/ci-workflow.test.ts' - 'scripts/install-cli.test.ts' - 'scripts/build-windows-worker.test.ts' diff --git a/.github/workflows/community-contribution-labeler.yml b/.github/workflows/community-contribution-labeler.yml index 2d646380d..2f4ba415d 100644 --- a/.github/workflows/community-contribution-labeler.yml +++ b/.github/workflows/community-contribution-labeler.yml @@ -19,7 +19,7 @@ jobs: steps: # This privileged follow-up must never check out or execute pull request code. - name: Update community contribution label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const label = "community-contribution"; diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index e9cb0b97d..3c608c8fd 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (full history) - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: # Full history so both the PR commit range and --log-opts="--all" work. fetch-depth: 0 @@ -85,14 +85,14 @@ jobs: # a SARIF was actually produced, and is skipped for fork PRs — which don't receive # security-events: write (the scan step still gates the check there). if: ${{ !cancelled() && hashFiles('results.sarif') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }} - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 with: sarif_file: results.sarif category: gitleaks - name: Upload gitleaks report if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: gitleaks-sarif-report path: results.sarif diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 545320ece..4a90b17c7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Apply labels - uses: actions/labeler@v5 + uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 with: configuration-path: .github/labeler.yml repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pin-actions.yml b/.github/workflows/pin-actions.yml new file mode 100644 index 000000000..c5c0ea2d6 --- /dev/null +++ b/.github/workflows/pin-actions.yml @@ -0,0 +1,35 @@ +name: Pin GitHub Actions + +on: + push: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/pinact.yaml" + pull_request: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/pinact.yaml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + pin-actions: + name: Validate immutable action references + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Validate action pins + uses: suzuki-shunsuke/pinact-action@b1a1f46bb10f2908950d95f626ca8cc109e2cf7d # v3.1.0 + with: + config: .github/pinact.yaml + fix: "false" + github_token: ${{ github.token }} + verify: "true" diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 290fe35b1..2e983d2d5 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -25,7 +25,7 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 persist-credentials: false @@ -33,7 +33,7 @@ jobs: run: | corepack enable corepack prepare pnpm@10.29.1 --activate - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" cache: pnpm @@ -82,7 +82,7 @@ jobs: test "$(node apps/cli/dist/scope.mjs --version)" = "$VERSION" pnpm exec vitest run --config vitest.integration.config.ts apps/cli/src/bundle.integration.test.ts - name: Upload tested bundle - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: cli-release-bundle path: apps/cli/dist/scope.mjs @@ -96,7 +96,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: cli-release-bundle path: bundle diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e01540020..2e6332a34 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -31,19 +31,19 @@ jobs: working-directory: website steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Install pnpm - uses: pnpm/action-setup@v6.0.8 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 with: package_json_file: website/package.json - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm cache-dependency-path: website/pnpm-lock.yaml - name: Restore Astro build cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: # Only the persistent data store is cached. `website/.astro` holds # generated content-collection modules that import source files by @@ -64,7 +64,7 @@ jobs: - name: Setup Pages id: pages if: github.ref == 'refs/heads/main' - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Build site env: # The Pages API can report an isolated hostname instead of the public URL. @@ -78,7 +78,7 @@ jobs: run: pnpm exec playwright test - name: Upload artifact if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: ./website/dist @@ -92,4 +92,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0115178da..ce13c577d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -263,6 +263,23 @@ workers have no Dockerfiles or integration suites in this repository. Live-model credentials remain necessary for live-auth tests; passing public tool checks does not validate live-model access. +All external GitHub Actions and reusable workflows must use an immutable, +full-length commit SHA followed by the corresponding release version: + +```yaml +- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 +``` + +When updating an action, update the SHA and version comment together. The +dedicated **Pin GitHub Actions** check validates both through +[pinact](https://github.com/suzuki-shunsuke/pinact), while +`scripts/ci-workflow.test.ts` independently rejects mutable references and +missing comments. If pinact is installed locally, run the same validation with: + +```bash +pinact run --check --verify-comment +``` + Windows validation uses GitHub's `windows-2022` runner and its Windows Docker engine. `scripts/build-windows-worker.ps1` builds `Dockerfile.base` from public `servercore:ltsc2022`, builds pinned Copilot dependencies against that local image, diff --git a/scripts/ci-workflow.test.ts b/scripts/ci-workflow.test.ts index 714073c78..0da2bcedc 100644 --- a/scripts/ci-workflow.test.ts +++ b/scripts/ci-workflow.test.ts @@ -48,9 +48,59 @@ const communityContributionWorkflow = parse(communityContributionWorkflowSource) permissions: Record; jobs: Record; }; +const actionPinningWorkflow = parse(readFileSync(".github/workflows/pin-actions.yml", "utf8")) as { + on: { + push: { paths: string[] }; + pull_request: { paths: string[] }; + workflow_dispatch: null; + }; + permissions: Record; + jobs: Record; +}; const jobs = workflow.jobs; const directories: string[] = []; const trusted = "github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository"; +const fullCommitSha = /^[0-9a-f]{40}$/i; + +interface WorkflowActionReference { + action: string; + line: number; + versionComment?: string; +} + +// Extract external, local and container action references without parsing comments away. +function workflowActionReferences(source: string): WorkflowActionReference[] { + return source.split("\n").flatMap((line, index) => { + const match = line.match(/^\s*(?:-\s*)?uses:\s*(?:(["'])([^"'#\s]+)\1|([^\s#]+))(?:\s+#(.*))?\s*$/); + if (!match) return []; + + return [{ + action: match[2] ?? match[3], + line: index + 1, + versionComment: match[4]?.trim(), + }]; + }); +} + +// Report immutable-reference policy violations with source line numbers for CI output. +function workflowActionViolations(source: string): string[] { + return workflowActionReferences(source).flatMap(({ action, line, versionComment }) => { + if (action.startsWith("./") || action.startsWith("docker://")) return []; + + const separator = action.lastIndexOf("@"); + const reference = separator >= 0 ? action.slice(separator + 1) : ""; + const violations: string[] = []; + + if (!fullCommitSha.test(reference)) { + violations.push(`line ${line}: ${action} must use a full 40-character commit SHA`); + } + if (!versionComment) { + violations.push(`line ${line}: ${action} must include a version comment`); + } + + return violations; + }); +} function step(job: string, name: string): Step { const result = jobs[job].steps.find((candidate) => candidate.name === name); @@ -62,12 +112,62 @@ afterEach(() => { for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true }); }); +describe("GitHub Action pinning", () => { + const sha = "a".repeat(40); + + it.each([ + `uses: actions/checkout@${sha} # v4.4.0`, + `uses: "actions/checkout@${sha}" # v4.4.0`, + `uses: dtolnay/rust-toolchain@${sha} # 1.94.1`, + "uses: ./path/to/local-action", + "uses: docker://alpine:3.22", + ])("accepts immutable, local or container reference: %s", (line) => { + expect(workflowActionViolations(line)).toEqual([]); + }); + + it.each([ + ["uses: actions/checkout@v4 # v4", "full 40-character commit SHA"], + ["uses: actions/checkout@abcdef # v4", "full 40-character commit SHA"], + [`uses: actions/checkout@${sha}`, "version comment"], + [`uses: actions/checkout@${sha} #`, "version comment"], + ])("rejects invalid reference: %s", (line, expectedViolation) => { + expect(workflowActionViolations(line).join("\n")).toContain(expectedViolation); + }); + + it("pins every external action and reusable workflow in the repository", () => { + for (const file of readdirSync(".github/workflows").filter((entry) => /\.ya?ml$/.test(entry))) { + const source = readFileSync(join(".github/workflows", file), "utf8"); + expect(workflowActionViolations(source), file).toEqual([]); + } + }); + + it("runs pinact as a least-privilege validation-only check", () => { + const job = actionPinningWorkflow.jobs["pin-actions"]; + const validation = job.steps.find((candidate) => candidate.name === "Validate action pins"); + + expect(actionPinningWorkflow.permissions).toEqual({ contents: "read" }); + expect(actionPinningWorkflow.on.workflow_dispatch).toBeNull(); + for (const event of [actionPinningWorkflow.on.push, actionPinningWorkflow.on.pull_request]) { + expect(event.paths).toEqual([".github/workflows/**", ".github/pinact.yaml"]); + } + expect(validation?.uses).toMatch(/^suzuki-shunsuke\/pinact-action@[0-9a-f]{40}$/); + expect(validation?.with).toEqual({ + config: ".github/pinact.yaml", + fix: "false", + github_token: "${{ github.token }}", + verify: "true", + }); + }); +}); + describe("CI execution prerequisites", () => { it("selects integration checks for workflow changes through a dedicated filter", () => { const filtersStep = jobs["detect-changes"].steps.find((candidate) => candidate.uses?.startsWith("dorny/paths-filter@")); const filters = parse(filtersStep!.with!.filters) as Record; expect(filters.ci).toContain(".github/workflows/ci.yml"); expect(filters.ci).toContain("scripts/ci-workflow.test.ts"); + expect(filters.typescript).toContain(".github/workflows/**"); + expect(filters.typescript).toContain(".github/pinact.yaml"); expect(filters.typescript).toContain("scripts/ci-workflow.test.ts"); expect(filters.shared).not.toContain(".github/workflows/ci.yml"); expect(jobs["detect-changes"].outputs?.ci).toBe("${{ steps.changes.outputs.ci }}");