diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index cb64826..b3a45fd 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -4,6 +4,7 @@ on: pull_request: branches: ["main"] types: [opened, edited, synchronize, reopened] + workflow_call: permissions: pull-requests: read @@ -11,9 +12,9 @@ permissions: jobs: pr-title: name: PR Title - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -31,11 +32,12 @@ jobs: commitlint: name: Commit Messages - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 + persist-credentials: false - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6 with: configFile: .commitlintrc.yml diff --git a/.github/workflows/issues-add-labels.yaml b/.github/workflows/issues-add-labels.yaml index 7cf9551..11703b0 100644 --- a/.github/workflows/issues-add-labels.yaml +++ b/.github/workflows/issues-add-labels.yaml @@ -7,9 +7,10 @@ on: types: - reopened - opened + workflow_call: jobs: label_issues: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write steps: diff --git a/.github/workflows/issues-add-to-project.yml b/.github/workflows/issues-add-to-project.yml index 7509dd5..4feb0e8 100644 --- a/.github/workflows/issues-add-to-project.yml +++ b/.github/workflows/issues-add-to-project.yml @@ -10,15 +10,19 @@ on: pull_request: types: - opened + workflow_call: + secrets: + ADD_TO_PROJECT_PAT: + required: true jobs: add-to-project: name: Add issue to project - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Authenticates with ADD_TO_PROJECT_PAT, so the workflow GITHUB_TOKEN needs no scopes. permissions: {} steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0 with: project-url: https://github.com/orgs/opendefensecloud/projects/3 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml new file mode 100644 index 0000000..cc5f552 --- /dev/null +++ b/.github/workflows/osv-scanner.yml @@ -0,0 +1,49 @@ +name: OSV-Scanner + +# Reusable only. The caller's stub carries the triggers (pull_request, push to +# main, a weekly schedule); its event decides which job runs. See "Governance +# workflows" in the README. +# +# No merge_group: the upstream PR scan diffs against $GITHUB_BASE_REF, which is +# empty in a merge queue, so it would pass without comparing anything. No repo +# uses a merge queue; pick a working scan for it when one does. + +on: + workflow_call: + inputs: + scan-args: + description: Arguments passed to osv-scanner, one per line, e.g. add `--config ./.osv-scanner.toml`. + type: string + default: |- + --include-git-root + -r + ./ + +# Least privilege by default. The write permission needed to upload the SARIF +# report is granted per job, not here -- a top-level write permission is what +# OpenSSF Scorecard's Token-Permissions check flags. +permissions: + contents: read + +jobs: + scan-scheduled: + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} + permissions: + # Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117 + actions: read + security-events: write + contents: read + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc # v2.6.0 + with: + scan-args: ${{ inputs.scan-args }} + runs-on: ubuntu-24.04 + scan-pr: + if: ${{ github.event_name == 'pull_request' }} + permissions: + actions: read + security-events: write + contents: read + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc # v2.6.0 + with: + scan-args: ${{ inputs.scan-args }} + runs-on: ubuntu-24.04 diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index dd7567d..27adb89 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -25,7 +25,7 @@ jobs: # write permission is required for autolabeler # otherwise, read permission is required at least pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set #- name: Set GHE_HOST diff --git a/.github/workflows/renovate-auto-approve.yml b/.github/workflows/renovate-auto-approve.yml index 7078d44..f449cfc 100644 --- a/.github/workflows/renovate-auto-approve.yml +++ b/.github/workflows/renovate-auto-approve.yml @@ -47,7 +47,7 @@ concurrency: jobs: auto-approve: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: >- github.event_name == 'pull_request' && github.event.pull_request.user.login == inputs.renovate-actor && diff --git a/.github/workflows/renovate-dev-kit-lock.yml b/.github/workflows/renovate-dev-kit-lock.yml new file mode 100644 index 0000000..1715f96 --- /dev/null +++ b/.github/workflows/renovate-dev-kit-lock.yml @@ -0,0 +1,96 @@ +name: renovate-dev-kit-lock + +# Reusable only. Commits `nix flake lock` to Renovate's dev-kit PR. It relocks +# only inputs whose flake.nix reference changed, here the bumped dev-kit tag, +# and is a no-op otherwise, so a re-run after its own commit changes nothing. +# Renovate bumps the flake input tag with a regex manager, which can't update +# flake.lock; without this, `nix develop` rewrites the lock in CI and any +# diff-check fails. See "Governance workflows" in the README for the stub. +# +# Commits through the GitHub App's installation token, via the API: GitHub +# signs the commit (required_signatures), and unlike GITHUB_TOKEN it starts CI +# on the new head. renovate-config lists the app's bot in gitIgnoredAuthors, so +# Renovate keeps rebasing the branch; each rebase re-runs this and relocks. + +on: + workflow_call: + inputs: + branch: + description: Renovate's branch for the dev-kit group. + type: string + default: renovate/dev-kit + renovate-actor: + description: Login of the Renovate app that authors the pull requests. + type: string + default: renovate[bot] + secrets: + DEV_KIT_BOT_APP_CLIENT_ID: + required: true + DEV_KIT_BOT_APP_PRIVATE_KEY: + required: true + +permissions: + contents: read + +jobs: + relock: + name: Relock dev-kit + # Only Renovate's own branch in this repo: fork PRs get no secrets, and a + # same-named branch from someone else doesn't get a bot commit. + if: >- + github.event.pull_request.head.ref == inputs.branch && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == inputs.renovate-actor + runs-on: ubuntu-24.04 + concurrency: + group: renovate-dev-kit-lock-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Install nix + uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + with: + # Upstream Nix and no telemetry, as in setup-nix. + determinate: false + diagnostic-endpoint: '' + - name: Relock dev-kit + id: relock + run: | + nix flake lock + if git diff --quiet -- flake.lock; then + echo "flake.lock already matches flake.nix" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + if: steps.relock.outputs.changed == 'true' + id: app + with: + client-id: ${{ secrets.DEV_KIT_BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.DEV_KIT_BOT_APP_PRIVATE_KEY }} + permission-contents: write + - name: Commit flake.lock + if: steps.relock.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.event.pull_request.head.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # createCommitOnBranch, not git push: GitHub signs API commits. + # expectedHeadOid makes it fail instead of clobbering a newer push; + # that push re-runs this workflow anyway. + jq -n --arg repo "$REPO" --arg branch "$BRANCH" --arg head "$HEAD_SHA" \ + --arg lock "$(base64 -w0 flake.lock)" '{ + query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }", + variables: { input: { + branch: { repositoryNameWithOwner: $repo, branchName: $branch }, + message: { headline: "chore(deps): relock dev-kit in flake.lock" }, + expectedHeadOid: $head, + fileChanges: { additions: [{ path: "flake.lock", contents: $lock }] } + } } + }' | gh api graphql --input - --jq '.data.createCommitOnBranch.commit.oid' diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index 936629a..023a2fe 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -5,11 +5,13 @@ on: paths: - '.github/actions/**/*.sh' - 'scripts/*.sh' + - '.github/workflows/update-action-pins.yml' push: branches: [main] paths: - '.github/actions/**/*.sh' - 'scripts/*.sh' + - '.github/workflows/update-action-pins.yml' permissions: contents: read @@ -31,3 +33,5 @@ jobs: run: .github/actions/diff-check/test-verify-clean.sh - name: Test extract-go-version.sh run: .github/actions/get-go-version/test-extract-go-version.sh + - name: Test the update-action-pins check + run: scripts/test-update-action-pins.sh diff --git a/.github/workflows/update-action-pins.yml b/.github/workflows/update-action-pins.yml index dcf48e2..f999b70 100644 --- a/.github/workflows/update-action-pins.yml +++ b/.github/workflows/update-action-pins.yml @@ -1,31 +1,43 @@ name: Update Action Pins +# Reusable, and also run on dev-kit's own PRs. Consumers call it from a stub; +# see "Governance workflows" in the README. +# +# No `paths` filter on purpose: a required check gated by paths never reports on +# PRs outside them, so the merge stays blocked. + permissions: contents: read on: pull_request: - paths: - - ".github/workflows/**" - - ".github/actions/**" + workflow_call: jobs: check-pins: name: Check action pins - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Verify all actions are pinned to a SHA run: | - unpinned=$(grep -rhE '^\s+(- )?uses: ' .github/ \ - | grep -vE "^\s+(- )?uses: ['\"]?\./" \ - | grep -vE '@[0-9a-f]{40}($|\s)' || true) + # Scan all of .github/ so composite actions are covered. Strip quotes + # before matching so single- and double-quoted values are both handled, + # and allow any YAML sequence spacing: `- uses:` is valid YAML, and a + # matcher expecting exactly one space would let an unpinned action + # through unseen. Drop trailing comments, or `@main # @` would + # pass on the SHA in the comment. + unpinned=$(grep -rhE '^[[:space:]]*(-[[:space:]]+)?uses:' .github/ \ + | tr -d "\"'" \ + | sed -E 's/[[:space:]]+#.*$//' \ + | grep -vE 'uses:[[:space:]]*\./' \ + | grep -vE '@[0-9a-f]{40}([[:space:]]|$)' || true) if [[ -n "$unpinned" ]]; then echo "::error::Found unpinned GitHub Actions (must use SHA digest, not tag):" echo "$unpinned" echo "" - echo "Run 'GITHUB_TOKEN=\$(gh auth token) update-action-pins .github/workflows/ .github/actions/' to fix." + echo "Run 'GITHUB_TOKEN=\$(gh auth token) update-action-pins .github/' to fix." exit 1 fi diff --git a/README.md b/README.md index b54e637..ad6884c 100644 --- a/README.md +++ b/README.md @@ -187,18 +187,18 @@ It configures: The repository settings are configurable via make variables (set them in your `Makefile` or pass them on the command line, e.g. `make repo-settings REPO_STATUS_CHECKS='["CI","lint"]' REPO_RULESET_BRANCHES='["release/*"]'`): -| Variable | Default | Description | -| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | -| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | -| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | -| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | -| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | -| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | -| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | -| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | -| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | -| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | +| Variable | Default | Description | +| -------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | +| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | +| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | +| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | +| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | +| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | +| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | +| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | +| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","update-action-pins / Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | +| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | ### GitHub Actions @@ -239,6 +239,149 @@ jobs: | `cachix-auth-token` | `''` | Auth token; empty falls back to a read-only cache, as on fork PRs | | `cachix-signing-key` | `''` | Signing key; empty disables pushing to the cache | +### Governance workflows + +The org-wide checks and automations are reusable workflows. A consuming repo +keeps one small stub per workflow, which sets the triggers and the permissions +and calls dev-kit. Pin each stub to a SHA like any other action; Renovate bumps +them together with `DEV_KIT_VERSION`. + +| Workflow | Stub triggers | Needs | +| --------------------------- | ------------------------------------------------------ | ------------------------------- | +| `update-action-pins.yml` | `pull_request` | | +| `conventional-commits.yml` | `pull_request` (opened, edited, synchronize, reopened) | `.commitlintrc.yml` in the repo | +| `osv-scanner.yml` | `pull_request`, `push` to main, weekly | optional `scan-args` input | +| `issues-add-labels.yaml` | `issues` (opened, reopened) | | +| `issues-add-to-project.yml` | `issues`, `pull_request` (opened) | `ADD_TO_PROJECT_PAT` secret | +| `renovate-auto-approve.yml` | `pull_request` | see the header of the workflow | +| `renovate-dev-kit-lock.yml` | `pull_request` (opened, synchronize, reopened) | `DEV_KIT_BOT_APP_*` org secrets | + +No stub triggers on `merge_group`, as no repo uses a merge queue. A repo that +turns one on must add the trigger to every stub whose check it requires, and +must not require `osv-scanner / scan-pr / osv-scan`: that PR scan diffs +against `$GITHUB_BASE_REF`, which is empty in a merge queue (see +`osv-scanner.yml`). + +`.github/workflows/update-action-pins.yml`: + +```yaml +name: Update Action Pins +on: + pull_request: +permissions: + contents: read +jobs: + update-action-pins: + uses: opendefensecloud/dev-kit/.github/workflows/update-action-pins.yml@<40-char-sha> # +``` + +`.github/workflows/conventional-commits.yml`: + +```yaml +name: Conventional Commits +on: + pull_request: + branches: ["main"] + types: [opened, edited, synchronize, reopened] +permissions: + pull-requests: read +jobs: + conventional-commits: + uses: opendefensecloud/dev-kit/.github/workflows/conventional-commits.yml@<40-char-sha> # +``` + +`.github/workflows/osv-scanner.yml`: + +```yaml +name: OSV-Scanner +on: + pull_request: + branches: ["main"] + schedule: + - cron: "12 12 * * 1" + push: + branches: ["main"] +permissions: + contents: read +jobs: + osv-scanner: + permissions: + actions: read + security-events: write + contents: read + uses: opendefensecloud/dev-kit/.github/workflows/osv-scanner.yml@<40-char-sha> # + # with: + # scan-args: |- + # --include-git-root + # --config ./.osv-scanner.toml + # -r + # ./ +``` + +`.github/workflows/issues-add-labels.yaml`: + +```yaml +name: Label issues +on: + issues: + types: [opened, reopened] +permissions: + contents: read +jobs: + label: + permissions: + issues: write + uses: opendefensecloud/dev-kit/.github/workflows/issues-add-labels.yaml@<40-char-sha> # +``` + +`.github/workflows/issues-add-to-project.yml`: + +```yaml +name: Add issues to project +on: + issues: + types: [opened] + pull_request: + types: [opened] +permissions: + contents: read +jobs: + add-to-project: + permissions: {} + uses: opendefensecloud/dev-kit/.github/workflows/issues-add-to-project.yml@<40-char-sha> # + secrets: + ADD_TO_PROJECT_PAT: ${{ secrets.ADD_TO_PROJECT_PAT }} +``` + +`.github/workflows/renovate-dev-kit-lock.yml` commits `nix flake lock` to +Renovate's `renovate/dev-kit` PR, which bumps the flake input tag but can't +update `flake.lock`. It commits as the org's dev-kit GitHub App, so the commit +is signed and starts CI; any other PR skips it: + +```yaml +name: Relock dev-kit +on: + pull_request: + branches: ["main"] + types: [opened, synchronize, reopened] +permissions: + contents: read +jobs: + relock: + uses: opendefensecloud/dev-kit/.github/workflows/renovate-dev-kit-lock.yml@<40-char-sha> # + secrets: + DEV_KIT_BOT_APP_CLIENT_ID: ${{ secrets.DEV_KIT_BOT_APP_CLIENT_ID }} + DEV_KIT_BOT_APP_PRIVATE_KEY: ${{ secrets.DEV_KIT_BOT_APP_PRIVATE_KEY }} +``` + +A job from a reusable workflow reports as ` / `, so +`REPO_STATUS_CHECKS` needs the new names after switching to a stub, e.g. +`update-action-pins / Check action pins`, `conventional-commits / PR Title` and +`conventional-commits / Commit Messages`. osv-scanner nests Google's reusable +workflow one level deeper, so its scan reports as +`osv-scanner / scan-pr / osv-scan`. Update the list and rerun `make repo-settings` in the same change, or the old +names stay required and never report. + ### Default git hooks The dev shell installs the following git hooks automatically: diff --git a/common.mk b/common.mk index ed28f46..35ee683 100644 --- a/common.mk +++ b/common.mk @@ -96,7 +96,6 @@ repo-settings: ## Reconcile GitHub repository settings (labels, merge strategy, REPO_REQUIRE_LAST_PUSH_APPROVAL='$(REPO_REQUIRE_LAST_PUSH_APPROVAL)' \ REPO_RULESET_BRANCHES='$(REPO_RULESET_BRANCHES)' \ REPO_STATUS_CHECKS='$(REPO_STATUS_CHECKS)' \ - DEV_KIT_VERSION='$(DEV_KIT_VERSION)' \ GH='$(GH)' JQ='$(JQ)' \ bash diff --git a/scripts/repo-settings.sh b/scripts/repo-settings.sh index 681f8f5..c608cb6 100755 --- a/scripts/repo-settings.sh +++ b/scripts/repo-settings.sh @@ -3,7 +3,7 @@ set -euo pipefail # Reconcile a repository's GitHub settings: labels, merge strategy, secret -# scanning, branch protection ruleset, and the update-action-pins workflow. +# scanning and branch protection ruleset. # # All configuration is passed through environment variables. Defaults are the # single source of truth in common.mk and are always passed by the make target; @@ -18,7 +18,6 @@ set -euo pipefail # REPO_ALLOW_SQUASH_MERGE allow squash merging # REPO_ALLOW_REBASE_MERGE allow rebase merging # REPO_REQUIRE_LAST_PUSH_APPROVAL require the most recent push to be approved before merging -# DEV_KIT_VERSION dev-kit version to fetch the workflow from # GH, JQ commands used to talk to GitHub and build JSON "$GH" auth status > /dev/null 2>&1 || { @@ -158,11 +157,4 @@ else echo " Created new ruleset" fi -echo " Installing update-action-pins workflow..." -mkdir -p .github/workflows -curl --fail -sSL \ - "https://raw.githubusercontent.com/opendefensecloud/dev-kit/$DEV_KIT_VERSION/.github/workflows/update-action-pins.yml" \ - -o .github/workflows/update-action-pins.yml -echo " Wrote .github/workflows/update-action-pins.yml" - echo "Done." diff --git a/scripts/test-update-action-pins.sh b/scripts/test-update-action-pins.sh new file mode 100755 index 0000000..7fd8fa4 --- /dev/null +++ b/scripts/test-update-action-pins.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runs the pin check from update-action-pins.yml itself (extracted with yq), so +# the test cannot drift from what CI runs. + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CHECK="$(yq '.jobs.check-pins.steps[] | select(.name == "Verify all actions are pinned to a SHA") | .run' \ + "$ROOT/.github/workflows/update-action-pins.yml")" +[[ -n "$CHECK" ]] || { + echo "FAIL: pin check step not found" + exit 1 +} + +SHA=0123456789abcdef0123456789abcdef01234567 +fail=0 + +# expect +expect() { + local want=$1 desc=$2 file=$3 content=$4 dir got + dir="$(mktemp -d)" + mkdir -p "$dir/.github/$(dirname "$file")" + printf '%s\n' "$content" > "$dir/.github/$file" + if (cd "$dir" && bash -c "$CHECK") > /dev/null 2>&1; then got=pass; else got=fail; fi + rm -rf "$dir" + if [[ "$got" == "$want" ]]; then + echo "ok $desc" + else + echo "FAIL $desc: expected $want, got $got" + fail=1 + fi +} + +expect pass "pinned action" workflows/a.yml " - uses: actions/checkout@$SHA # v7" +expect pass "pinned reusable workflow" workflows/a.yml " uses: org/repo/.github/workflows/x.yml@$SHA # v1" +expect pass "local action" workflows/a.yml " - uses: ./.github/actions/foo" +expect pass "quoted local action" workflows/a.yml " - uses: './.github/actions/foo'" +expect pass "double-quoted pinned action" workflows/a.yml " - uses: \"actions/checkout@$SHA\" # v7" +expect fail "tag ref" workflows/a.yml " - uses: actions/checkout@v7" +expect fail "double-quoted tag ref" workflows/a.yml " - uses: \"actions/checkout@v7\"" +expect fail "single-quoted tag ref" workflows/a.yml " - uses: 'actions/checkout@v7'" +expect fail "extra sequence spacing" workflows/a.yml " - uses: actions/checkout@v7" +expect fail "unpinned reusable workflow" workflows/a.yml " uses: org/repo/.github/workflows/x.yml@main" +expect fail "unpinned composite action" actions/foo/action.yml " - uses: actions/checkout@v7" +expect fail "SHA only in the comment" workflows/a.yml " - uses: actions/checkout@main # @$SHA" +expect fail "short SHA" workflows/a.yml " - uses: actions/checkout@${SHA:0:7}" + +exit "$fail"