From fa8c4e754940d55854f473c3ba117065e8fb0c17 Mon Sep 17 00:00:00 2001 From: nerdalytics <97166791+nerdalytics@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:04:37 +0200 Subject: [PATCH 1/3] docs(readme): correct the SHA-pin comment section The previous version conflated SHA-pinned and tag-pinned semantics: it claimed pinning '@ # v1' leaves a security window where a compromised maintainer could move the v1 tag to a malicious commit. That's wrong. With SHA pinning, the 40-char hash is what GitHub resolves; the comment is documentation. Moving the v1 tag has zero effect on a SHA-pinned workflow. Both '# v1' and '# v1.0.2' as comments are equally secure. Rewrite the section to reflect the actual tradeoff (cosmetic only) and highlight that the auto-updater normalizes to exact-release form on first update regardless of what you typed. --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c62e576..62147b2 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,21 @@ gh api repos/nerdalytics/check-action-versions/commits/v1.0.1 --jq .sha gh api repos/nerdalytics/check-action-versions/commits/v1 --jq .sha ``` -### Which tag to pin to — exact release or floating major +### What the comment means — exact release or floating major -- **Exact release** (`# v1.0.1`): the SHA never changes underneath you. You consciously bump when a new release ships — and this action itself will open that PR via its normal scan. -- **Floating major** (`# v1`): the `v1` tag re-points to each new `v1.x.y`. Simpler in theory, but leaves a narrow window where a compromised maintainer could move `v1` to a malicious commit. For repos with a strict SHA-pin policy, prefer the exact release. +When you pin by SHA, the 40-char hash is what GitHub resolves. The comment (`# v1`, `# v1.0.1`) is documentation — it does not affect what runs. Both of these pins execute the same action: -The auto-updater always writes the exact release tag in the comment (never the floating `v1`), so once this action rewrites a pin for you, it stays on exact-release form. +```yaml +- uses: nerdalytics/check-action-versions@0d95f1ed70576169ff3c297057b7e3fcfb909a1a # v1 +- uses: nerdalytics/check-action-versions@0d95f1ed70576169ff3c297057b7e3fcfb909a1a # v1.0.2 +``` + +Moving the `v1` floating tag on GitHub has no effect on any workflow pinned by SHA — only the SHA matters. Both comment forms are equally secure. + +When a new release ships and the SHA changes, this action's auto-update path opens a PR rewriting your pin to the new SHA + the exact release name in the comment (`# v1.0.3`, never `# v1`). So whatever comment you type up front will normalize to exact-release form on first update. The choice is purely cosmetic: + +- `# v1.0.2` — concrete, matches what the auto-updater writes. Best if you want the comment to reflect the exact version you vetted. +- `# v1` — brief, handy when grabbing the SHA via `gh api repos/.../commits/v1 --jq .sha`. Will get refreshed by the auto-updater on the next release. ## Permissions From da270284a909a5ccc67793e8131a0f2646117267 Mon Sep 17 00:00:00 2001 From: nerdalytics <97166791+nerdalytics@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:24:51 +0200 Subject: [PATCH 2/3] docs(readme): batched corrections across 9 findings 1. Tagline: 'SHA-pinned uses:' -> 'uses:' (scanner handles any uses ref, not just already-SHA-pinned ones) 2. Intro bullet: 'signed, CI-triggering PR' -> 'CI-triggering PR (optionally SSH or GPG-signed)' 3. Quickstart now SHA-pins the action itself so the example matches the action's own guidance (previously pinned @v1, which suggested the action excluded itself from its own recommendations) 4. Removed all references to the floating v1 tag: consumers should pin by commit SHA with the exact release name as comment 5. Versioning section: removed 'Consumers typically pin to @v1' and 'despite the irony' phrasings that misrepresented current guidance 6. GPG setup: added complete caller-side YAML example to match SSH section, plus a note that the GPG path is implemented but not yet end-to-end validated in production 7. SSH/GPG table: 'CI agent' -> 'Persistent agent' (clearer) 8. Troubleshooting heading for semver warning now matches the real log output instead of using the internal code variable name 9. Troubleshooting: added the 'Actions must be pinned to a full-length commit SHA' org-policy error entry Also: removed the 'Security design notes' section. The 'attackers reading a public repo gain reconnaissance' framing read as security-through- obscurity (or worse, as implying open source is bad). The useful content (required inputs fail-fast; optional inputs use generic placeholders) is implicit in the Inputs section already. --- README.md | 113 +++++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 62147b2..9033816 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Check Action Versions -A composite GitHub Action that audits the SHA-pinned `uses:` references in your workflow files, resolves each action's latest strict-semver release, and opens a security issue + automated PR when anything is outdated. +A composite GitHub Action that audits the `uses:` references in your workflow files, resolves each action's latest strict-semver release, and opens a security issue + automated PR when anything is outdated. - Runs on your schedule (weekly is typical) - Creates a single tracking issue, updates it on each run -- Produces a signed, CI-triggering PR with the SHA and tag updates applied +- Produces a CI-triggering PR (optionally SSH or GPG-signed) with the SHA and tag updates applied - Auto-closes the issue and PR when every action is back on its latest release ## Quickstart -Add this to any repo you want audited: +Add this to any repo you want audited. The action is pinned by commit SHA — the same form this action will produce for every other entry in your workflow files, and the form required by any repo or org with a strict supply-chain policy. ```yaml # .github/workflows/check-action-versions.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ secrets.YOUR_PAT_SECRET_NAME }} - - uses: nerdalytics/check-action-versions@v1 + - uses: nerdalytics/check-action-versions@0d95f1ed70576169ff3c297057b7e3fcfb909a1a # v1.0.2 with: committer-name: your-bot-username committer-email: your-bot-email@example.com @@ -39,53 +39,34 @@ jobs: That's the minimum. Everything else is optional — see below. -## SHA-pinning (required for strict supply-chain policies) +The SHA shown above pins `v1.0.2`. For every subsequent release, this action itself will open a PR in your repo rewriting the pin to the new SHA + the new exact release name. See [Updating the pin](#updating-the-pin) below if you ever need to do it manually. -The Quickstart above and the GitHub Marketplace "Use latest version" button both give you a tag-based pin (`@v1`). That's fine for most repos. **If your repository or GitHub organization enforces SHA-pinning on every `uses:` reference** (for example, a repo ruleset or branch-protection rule that requires "Actions must be pinned to a full-length commit SHA"), the tag form will be rejected at run time with: +## Updating the pin -``` -The action X is not allowed in / because all actions must be pinned to a full-length commit SHA. -``` +This action auto-maintains the pin in your workflow files — when a new `check-action-versions` release ships, a scheduled run will open a PR bumping the SHA + release comment to the new values. You almost never need to do this by hand. -Replace the tag with a full 40-character commit SHA, keeping the version as a comment: - -```yaml - - uses: nerdalytics/check-action-versions@910163eda124120237a01bd990a5c1f107d29ce3 # v1.0.1 -``` - -### Resolving the commit SHA +If you do (initial install, or you've disabled the scheduled audit), resolve the current latest-release commit SHA: **Via the GitHub UI:** -1. Open the [tags page](https://github.com/nerdalytics/check-action-versions/tags) or [Releases page](https://github.com/nerdalytics/check-action-versions/releases) -2. Click the tag you want (e.g. `v1.0.1` or the floating `v1`) -3. The commit SHA appears in the page header; click it to see the full 40-character hash +1. Open the [Releases page](https://github.com/nerdalytics/check-action-versions/releases) +2. Click the release you want (typically the latest) +3. The commit SHA appears in the header; click it to see the full 40-character hash **Via `gh` CLI:** ```sh # Commit SHA for a specific release tag -gh api repos/nerdalytics/check-action-versions/commits/v1.0.1 --jq .sha - -# Commit SHA for the floating major tag -gh api repos/nerdalytics/check-action-versions/commits/v1 --jq .sha +gh api repos/nerdalytics/check-action-versions/commits/v1.0.2 --jq .sha ``` -### What the comment means — exact release or floating major - -When you pin by SHA, the 40-char hash is what GitHub resolves. The comment (`# v1`, `# v1.0.1`) is documentation — it does not affect what runs. Both of these pins execute the same action: +Paste the SHA into your `uses:` line with the release name as a comment: ```yaml -- uses: nerdalytics/check-action-versions@0d95f1ed70576169ff3c297057b7e3fcfb909a1a # v1 -- uses: nerdalytics/check-action-versions@0d95f1ed70576169ff3c297057b7e3fcfb909a1a # v1.0.2 + - uses: nerdalytics/check-action-versions@<40-char-commit-sha> # v1.0.2 ``` -Moving the `v1` floating tag on GitHub has no effect on any workflow pinned by SHA — only the SHA matters. Both comment forms are equally secure. - -When a new release ships and the SHA changes, this action's auto-update path opens a PR rewriting your pin to the new SHA + the exact release name in the comment (`# v1.0.3`, never `# v1`). So whatever comment you type up front will normalize to exact-release form on first update. The choice is purely cosmetic: - -- `# v1.0.2` — concrete, matches what the auto-updater writes. Best if you want the comment to reflect the exact version you vetted. -- `# v1` — brief, handy when grabbing the SHA via `gh api repos/.../commits/v1 --jq .sha`. Will get refreshed by the auto-updater on the next release. +The comment is documentation only — GitHub resolves the action at the SHA. Both humans reading the file and this action's own update logic use the comment to confirm which release a given SHA corresponds to. ## Permissions @@ -215,7 +196,7 @@ The passphrase protecting `signing-key`. Leave unset if the key is unencrypted. | Aspect | SSH | GPG | |--------|-----|-----| | Key format | One `OPENSSH PRIVATE KEY` file | Keyring + key ID + subkeys | -| CI agent required | No (`ssh-keygen -p` strips passphrase in place) | Yes (`gpg-agent` with preset passphrase) | +| Persistent agent required | No (`ssh-keygen -p` strips passphrase in place) | Yes (`gpg-agent` with preset passphrase) | | Passphrase handling | Single `ssh-keygen -p` call | `--pinentry-mode loopback` or agent preset | | Setup steps | 3 (generate, register, store secret) | 5+ (generate, export, register on GitHub, set up agent, configure git) | | Trust model on GitHub | Public key registered as "Signing Key" on committer's account | Same, plus expiration/subkey complexity | @@ -254,13 +235,36 @@ One-time, per repo or per org: ### GPG setup -1. Generate or identify an existing GPG signing key for the bot account -2. Export the private key: `gpg --armor --export-secret-keys ` -3. Register the **public** key on the bot's GitHub account under "SSH and GPG keys" -4. Store private key + passphrase as secrets -5. Pass `signing-method: gpg`, `signing-key: ${{ secrets.GPG_PRIVATE_KEY }}`, and `signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}` in the `with:` block of the action step +> **Note:** the GPG path in this action is implemented but has not been end-to-end validated against a real GPG workflow in production. The SSH path above is what the maintainers use. If you set up the GPG path and it works (or doesn't), please open an issue so we can confirm or fix it. + +1. **Generate or identify** an existing GPG signing key for the bot account: + ```sh + gpg --full-generate-key # for a new key; choose "RSA and RSA" or "ECC" + gpg --list-secret-keys --keyid-format=long + ``` + +2. **Export the armored private key:** + ```sh + gpg --armor --export-secret-keys > bot-signing.gpg + ``` + +3. **Register the public key** on the bot's GitHub account: + - Settings → SSH and GPG keys → **New GPG key** + - Export with `gpg --armor --export ` and paste + +4. **Store the secrets** in the target repo (or an org-level secret): + - `GPG_PRIVATE_KEY` (or any name) — full armored content of `bot-signing.gpg` + - `GPG_PASSPHRASE` — the passphrase on the key (omit if none) + +5. **Reference the secrets** in your caller workflow: + ```yaml + with: + signing-method: gpg + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + ``` -The action sets up `gpg-agent` with a preset passphrase and configures `git commit.gpgsign true` for the duration of the job. +During the run the action imports the key, extracts its key ID, presets the passphrase into `gpg-agent`, and configures `git` with `gpg.format openpgp` + `commit.gpgsign true` for the duration of the job. ### No signing @@ -278,25 +282,11 @@ You do not pass these — the action reads them from the workflow context: If your repo's default branch is `trunk` or `develop`, the PR targets that — no input needed. -## Security design notes - -This action has no defaults for any value that reveals caller-specific information (identity, secret names, signing conventions, branch names tied to individual repos). The reasoning: - -- The action repo is public -- Attackers reading a public action's source gain reconnaissance for free — "this action defaults to `ACTION_UPDATER_PAT` as the PAT secret name" narrows a spray attack -- Defaults for generic GitHub-universal conventions (e.g., workflow files live at `.github/workflows/*.yml`) are safe — they reveal nothing not already known about every GitHub repo -- Defaults for repo-specific opinion (identity, prefixes, secret names) are not safe — they leak the reference design, which is inevitably copied - -Every required input's absence causes the workflow to refuse to start. Every optional input's absence either disables a feature or falls back to a generic placeholder that tells an attacker nothing. - ## Versioning -- Release tags `v1.0.0`, `v1.0.1`, ... -- Floating major-version tag `v1` moves to the latest `v1.x.y` release -- Consumers typically pin to `@v1` -- Breaking input-contract changes ship as `v2`; `v1` stops advancing so existing consumers are not broken - -If you want full determinism despite the irony, pin to a specific release tag or SHA. +- Release tags `v1.0.0`, `v1.0.1`, `v1.0.2`, … +- Consumers pin by commit SHA — see [Quickstart](#quickstart) and [Updating the pin](#updating-the-pin). The comment next to the SHA is the release name, which this action's own update logic rewrites whenever a newer release ships. +- Breaking input-contract changes ship as `v2.0.0`; the `v1.x.y` line stops advancing at that point so existing consumers are not broken. ## Troubleshooting @@ -312,12 +302,15 @@ Either pass `signing-key` or set `signing-method` to empty. **Commit shows "Unverified" despite `signing-method: ssh`** The public key registered on the bot account is marked as an Authentication Key, not a Signing Key. GitHub distinguishes the two. Re-add it with the correct type. -**Tag `latest_tag` not valid semver — action skipped** -The action requires strict `vX.Y.Z` tags. An upstream action that only tags `v1` or ships non-semver releases will be silently skipped (with a warning in the log). File an issue if this affects an action you depend on. +**"Warning: latest release tag '…' is not strict semver, trying tags API"** +This action compares by strict `vX.Y.Z` release tags. When the upstream you're auditing only ships non-semver releases (for example, a release tagged just `latest` or `v1`), the action falls back to the tags API to find the most recent strict-semver tag, and logs this warning while it does. If the fallback also finds nothing, the upstream is silently skipped for that run. **Action finds zero `uses:` references** Your workflows may be in `.yaml` files, not `.yml`. Override `scan-globs`. +**"The action X is not allowed in / because all actions must be pinned to a full-length commit SHA"** +Your repo or GitHub org has a policy requiring SHA-pinning on every `uses:` reference. The tag form (`@v1.0.2`) is rejected. See [Updating the pin](#updating-the-pin) for how to resolve the commit SHA and pin by hash. Subsequent updates happen automatically. + ## License MIT — see [LICENSE](./LICENSE). From fb696dd56754069c1935af7e8302f0b6cfbbf2d9 Mon Sep 17 00:00:00 2001 From: nerdalytics <97166791+nerdalytics@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:45:24 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(readme):=20brand-voice=20pass=20?= =?UTF-8?q?=E2=80=94=20calmer,=20less=20marketing=20cadence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the brand-writer rubric: fluent, calm, direct syntax. Removes em-dash-driven prose rhythm and the 'it's not X, it's Y' mirroring. Splits long run-on sentences. Drops the slight sell-tone in a few callouts (e.g. 'should be supplied because there is no sensible fallback' is now just 'Required. A silent fallback would surprise ...'). No behavior changes. No factual changes. Only prose edits. - All technical terms preserved (SHA, strict-semver, ssh-keygen, gpg-agent, workflow_dispatch, Conventional Commits, etc.) - All YAML, all commands, all SHAs, all tag names unchanged - Section structure unchanged - 21 remaining em dashes are all list-item value separators ('- both — create or update a tracking issue and open a PR'). Zero em dashes in running prose. --- README.md | 214 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 9033816..d859f98 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Check Action Versions -A composite GitHub Action that audits the `uses:` references in your workflow files, resolves each action's latest strict-semver release, and opens a security issue + automated PR when anything is outdated. +A composite GitHub Action. It scans the `uses:` references in your workflow files, resolves each action's latest strict-semver release, and opens a tracking issue and automated PR when anything is outdated. -- Runs on your schedule (weekly is typical) -- Creates a single tracking issue, updates it on each run -- Produces a CI-triggering PR (optionally SSH or GPG-signed) with the SHA and tag updates applied -- Auto-closes the issue and PR when every action is back on its latest release +- Runs on your schedule. Weekly is typical. +- Maintains a single tracking issue, updating it on each run. +- Produces a CI-triggering PR with the SHA and tag updates applied. Optional SSH or GPG signing. +- Closes the issue and PR automatically when every action is back on its latest release. ## Quickstart -Add this to any repo you want audited. The action is pinned by commit SHA — the same form this action will produce for every other entry in your workflow files, and the form required by any repo or org with a strict supply-chain policy. +Add this to any repo you want audited. The action is pinned by commit SHA. That is the same form it will write for every other entry in your workflow files, and the form required by any repo or organization enforcing SHA-pinning on `uses:` references. ```yaml # .github/workflows/check-action-versions.yml @@ -37,21 +37,21 @@ jobs: gh-pat: ${{ secrets.YOUR_PAT_SECRET_NAME }} ``` -That's the minimum. Everything else is optional — see below. +That is the minimum configuration. Everything else is optional. -The SHA shown above pins `v1.0.2`. For every subsequent release, this action itself will open a PR in your repo rewriting the pin to the new SHA + the new exact release name. See [Updating the pin](#updating-the-pin) below if you ever need to do it manually. +The SHA above pins `v1.0.2`. When a newer release ships, this action opens a PR in your repo rewriting the pin to the new SHA and release name. See [Updating the pin](#updating-the-pin) if you need to do it by hand. ## Updating the pin -This action auto-maintains the pin in your workflow files — when a new `check-action-versions` release ships, a scheduled run will open a PR bumping the SHA + release comment to the new values. You almost never need to do this by hand. +This action maintains its own pin in your workflow files. When a new `check-action-versions` release ships, a scheduled run opens a PR bumping the SHA and the release comment. You rarely need to do this by hand. -If you do (initial install, or you've disabled the scheduled audit), resolve the current latest-release commit SHA: +For the cases where you do (initial install, or after disabling the scheduled audit), resolve the latest-release commit SHA as follows. **Via the GitHub UI:** -1. Open the [Releases page](https://github.com/nerdalytics/check-action-versions/releases) -2. Click the release you want (typically the latest) -3. The commit SHA appears in the header; click it to see the full 40-character hash +1. Open the [Releases page](https://github.com/nerdalytics/check-action-versions/releases). +2. Click the release you want (typically the latest). +3. The commit SHA appears in the header. Click it to see the full 40-character hash. **Via `gh` CLI:** @@ -66,11 +66,11 @@ Paste the SHA into your `uses:` line with the release name as a comment: - uses: nerdalytics/check-action-versions@<40-char-commit-sha> # v1.0.2 ``` -The comment is documentation only — GitHub resolves the action at the SHA. Both humans reading the file and this action's own update logic use the comment to confirm which release a given SHA corresponds to. +The comment is documentation. GitHub resolves the action at the SHA. Humans reading the file and this action's own update logic use the comment to confirm which release a given SHA corresponds to. ## Permissions -Your caller workflow needs `contents: write`, `issues: write`, `pull-requests: write`. Declare at workflow or job level. +Your caller workflow needs `contents: write`, `issues: write`, and `pull-requests: write`. Declare them at workflow or job level. ```yaml permissions: @@ -84,106 +84,123 @@ permissions: ### Required #### `committer-name` -Git `user.name` on the automated commit. Must be supplied because there is no sensible fallback — using your personal identity for bot commits is the wrong behavior, and using `github-actions[bot]` silently would look wrong in your repo's history if you expect a specific author. + +Git `user.name` on the automated commit. Required. A silent `github-actions[bot]` fallback would surprise anyone scanning your repo's history, and your personal identity is the wrong owner for a bot commit. Set it explicitly. #### `committer-email` -Git `user.email` on the automated commit. Use a `noreply.github.com` address tied to the bot account that owns the PAT, so the commit's author is visually consistent with the PR author and the "Verified" badge (if signed) resolves to the correct account. + +Git `user.email` on the automated commit. Use a `noreply.github.com` address tied to the bot account that owns the PAT. This keeps the commit author visually consistent with the PR author, and lets the "Verified" badge (when signing is enabled) resolve to the correct account. ### Optional #### `commit-prefix` -Default: empty + +Default: empty. Prepended to both the commit message and the PR title. Examples: -- `chore(core):` — Conventional Commits with a `core` scope -- `build(deps):` — Conventional Commits, what Dependabot emits -- `ci:` — short form -- empty — plain prose (commit reads `Update N GitHub Action(s) to latest versions`) -Reason for no default: Conventional Commits is a convention, not a rule. Repos that use plain prose would see every automated commit polluted with a prefix they don't use. If you use Conventional Commits, supply the prefix you want; otherwise leave empty. +- `chore(core):` — Conventional Commits with a `core` scope. +- `build(deps):` — Conventional Commits, what Dependabot emits. +- `ci:` — short form. +- empty — plain prose (commit reads `Update N GitHub Action(s) to latest versions`). + +Why there is no default: Conventional Commits is a convention, and many repos don't use it. A baked-in prefix would pollute commits in those repos. Supply the prefix you want, or leave it empty for plain prose. #### `scan-globs` -Default: `.github/workflows/*.yml` + +Default: `.github/workflows/*.yml`. Newline-separated list of globs telling the action which files to scan for `uses:` references. Override if: -- Your repo has `.yaml` (not `.yml`) workflow files: add `.github/workflows/*.yaml` -- Your repo ships a composite action: add `action.yml` or `**/action.yml` -Reason for default: this glob matches GitHub's own universal workflow location — defaulting to it leaks nothing about any specific repo's setup. +- Your repo has `.yaml` (not `.yml`) workflow files. Add `.github/workflows/*.yaml`. +- Your repo ships a composite action. Add `action.yml` or `**/action.yml`. + +Why this default: it matches GitHub's universal workflow location, so it leaks nothing about a specific repo's setup. #### `branch-name` -Default: `update-github-actions` + +Default: `update-github-actions`. Branch name for the automated PR. The branch is deleted and recreated on each run that has changes, so a stable name is fine. Override if: -- You're running two parallel callers (e.g., during migration — use a `-v2` suffix for the pilot) -- Your repo has a branch-naming convention you want enforced + +- You are running two parallel callers (during migration, use a `-v2` suffix for the pilot). +- Your repo has a branch-naming convention you want enforced. #### `outdated-behavior` -Default: `both` + +Default: `both`. What happens when the scan finds outdated actions: -- `both` — create/update a tracking issue **and** open a PR with the fixes (recommended default; most visible, most actionable) -- `pr-only` — open a PR, no issue -- `issue-only` — create/update the issue, no PR (humans apply the fixes manually — safer for repos without a bot PAT) -- `dry-run` — log findings, do nothing else (observation phase, first few weeks after adopting) + +- `both` — create or update a tracking issue **and** open a PR with the fixes. The recommended default: most visible, most actionable. +- `pr-only` — open a PR, no issue. +- `issue-only` — create or update the issue, no PR. Humans apply the fixes manually. Safer for repos without a bot PAT. +- `dry-run` — log findings, do nothing else. Useful for the first few weeks after adopting. #### `up-to-date-behavior` -Default: `close` + +Default: `close`. What happens when every action is current: -- `close` — close the tracking issue and any open automated PR (recommended; signals cleanly that the alert is resolved) -- `keep` — leave them open (useful if humans are mid-review) -- `silent` — no-op + +- `close` — close the tracking issue and any open automated PR. Recommended; signals cleanly that the alert is resolved. +- `keep` — leave them open. Useful if humans are mid-review. +- `silent` — no-op. #### `pr-labels` -Default: empty -Comma-separated labels applied to the automated PR. Example: `security,dependencies`. Labels must already exist in your repo — the action does **not** create them (explicit is better than magic). +Default: empty. + +Comma-separated labels applied to the automated PR. Example: `security,dependencies`. Labels must already exist in your repo. The action does not create them. Explicit is better than magic. #### `issue-labels` -Default: empty + +Default: empty. Same as above, for the tracking issue. #### `issue-title` -Default: `Security: Outdated GitHub Actions detected` -The fixed title the action uses to find its own issue across runs (so it can update instead of creating a duplicate). Framed as "security" because SHA-pinning is supply-chain defense. +Default: `Security: Outdated GitHub Actions detected`. + +The fixed title the action uses to find its own issue across runs, so it updates an existing issue instead of creating a duplicate. Framed as "security" because SHA-pinning is supply-chain defense. -Override if your repo has a different naming convention for security tracking issues. Must be stable across runs — changing it after the first run creates an orphan. +Override if your repo has a different naming convention for security tracking issues. The title must be stable across runs; changing it after the first run leaves an orphan. #### `signing-method` -Default: empty (unsigned) + +Default: empty (unsigned). How automated commits are cryptographically signed: -- empty — unsigned. Simplest. Commit author is set correctly; commit appears as "Unverified" in the GitHub UI. -- `ssh` — SSH signing. Recommended when you want verified commits. See [Signing](#signing) below. -- `gpg` — GPG signing. Supported for repos with existing GPG infrastructure, but SSH is simpler to set up and maintain. + +- empty — unsigned. Author is set correctly; GitHub renders the commit as "Unverified." +- `ssh` — SSH signing. See [Signing](#signing). +- `gpg` — GPG signing. Supported for repos with existing GPG infrastructure. New setups should prefer SSH. ## Secret-bearing inputs -Composite actions don't have a separate `secrets:` block — secret values are passed as regular inputs. Always reference them via `${{ secrets.NAME }}` at the call site so GitHub masks the value in logs. +Composite actions do not have a separate `secrets:` block. Secret values ride in as regular inputs. Always reference them via `${{ secrets.NAME }}` at the call site so GitHub masks the value in logs. ### `gh-pat` — required A Personal Access Token (classic) or fine-grained PAT with `repo` and `workflow` scopes on the target repo. -**Why not `GITHUB_TOKEN`?** PRs opened by `GITHUB_TOKEN` do not trigger workflows. Your CI (lint, test, build) will not run on the automated PR, so you won't know if the update broke anything. A PAT from a bot account works around this. +Why not `GITHUB_TOKEN`: PRs opened by `GITHUB_TOKEN` do not trigger workflows. Your CI (lint, test, build) will not run on the automated PR, so you won't know if the update broke anything. A PAT from a bot account works around this. -Store as a repo or org secret and pass via `gh-pat: ${{ secrets.YOUR_PAT_SECRET_NAME }}`. +Store as a repo or organization secret and pass via `gh-pat: ${{ secrets.YOUR_PAT_SECRET_NAME }}`. ### `signing-key` — optional -Required iff `signing-method` is non-empty. Validated at runtime; the action fails fast with a clear error if set to `ssh` or `gpg` without a key. +Required when `signing-method` is non-empty. Validated at runtime; the action fails fast if set to `ssh` or `gpg` without a key. -- For `ssh`: the full private key including the `-----BEGIN OPENSSH PRIVATE KEY-----` header and footer -- For `gpg`: an armored private key block (`gpg --armor --export-secret-keys `) +- For `ssh`: the full private key, including the `-----BEGIN OPENSSH PRIVATE KEY-----` header and footer. +- For `gpg`: an armored private key block (`gpg --armor --export-secret-keys `). ### `signing-passphrase` — optional @@ -191,39 +208,42 @@ The passphrase protecting `signing-key`. Leave unset if the key is unencrypted. ## Signing -### Why SSH over GPG (when you want signatures at all) +### SSH or GPG | Aspect | SSH | GPG | |--------|-----|-----| | Key format | One `OPENSSH PRIVATE KEY` file | Keyring + key ID + subkeys | -| Persistent agent required | No (`ssh-keygen -p` strips passphrase in place) | Yes (`gpg-agent` with preset passphrase) | +| Persistent agent required | No (`ssh-keygen -p` strips the passphrase in place) | Yes (`gpg-agent` with a preset passphrase) | | Passphrase handling | Single `ssh-keygen -p` call | `--pinentry-mode loopback` or agent preset | | Setup steps | 3 (generate, register, store secret) | 5+ (generate, export, register on GitHub, set up agent, configure git) | -| Trust model on GitHub | Public key registered as "Signing Key" on committer's account | Same, plus expiration/subkey complexity | +| Trust model on GitHub | Public key registered as "Signing Key" on the committer's account | Same, plus expiration and subkey complexity | | Debugging | `ssh-keygen -Y verify` | `gpg --verify` + keyring state | -SSH signing gives you the same "Verified" badge as GPG with substantially less moving machinery in CI. GPG exists in this action for repos that already have GPG infrastructure they don't want to change; new setups should prefer SSH. +SSH signing produces the same "Verified" badge as GPG with substantially less machinery in CI. The GPG path exists for repos that already have GPG infrastructure they don't want to change. New setups should prefer SSH. ### SSH setup (recommended) -One-time, per repo or per org: +One-time, per repo or per organization. 1. **Generate a dedicated signing key** on your workstation. Do not reuse your GitHub auth key. + ```sh ssh-keygen -t ed25519 -f ./bot-signing-key -C "bot-signing-key" ``` - Use a passphrase if you want the secret-at-rest protected; the CI workflow will strip it via `ssh-keygen -p` before use. -2. **Register the public key** on the bot account that owns the PAT: - - GitHub → Settings → SSH and GPG keys → **New SSH key** - - **Key type: Signing Key** (not Authentication Key — this is the critical distinction) - - Paste `bot-signing-key.pub` + Use a passphrase if you want the secret at rest protected. The CI workflow strips it via `ssh-keygen -p` before use. + +2. **Register the public key** on the bot account that owns the PAT. + - GitHub → Settings → SSH and GPG keys → **New SSH key**. + - **Key type: Signing Key**, not Authentication Key. This distinction matters. + - Paste `bot-signing-key.pub`. -3. **Store the secrets** in the target repo (or an org-level secret): - - `SSH_SIGNING_KEY` (or whatever name you prefer) — full content of `bot-signing-key` - - `SSH_SIGNING_KEY_PASSPHRASE` — the passphrase you chose (omit if none) +3. **Store the secrets** in the target repo (or as an organization secret). + - `SSH_SIGNING_KEY` (or any name) — full content of `bot-signing-key`. + - `SSH_SIGNING_KEY_PASSPHRASE` — the passphrase you chose. Omit if none. + +4. **Reference the secrets** in your caller workflow. -4. **Reference the secrets** in your caller workflow: ```yaml with: signing-method: ssh @@ -235,28 +255,31 @@ One-time, per repo or per org: ### GPG setup -> **Note:** the GPG path in this action is implemented but has not been end-to-end validated against a real GPG workflow in production. The SSH path above is what the maintainers use. If you set up the GPG path and it works (or doesn't), please open an issue so we can confirm or fix it. +> **Note:** the GPG path is implemented but not yet validated end-to-end against a real GPG workflow in production. The SSH path is what the maintainers use. If you set up the GPG path and it works (or does not), open an issue so we can confirm or fix it. + +1. **Generate or identify** an existing GPG signing key for the bot account. -1. **Generate or identify** an existing GPG signing key for the bot account: ```sh gpg --full-generate-key # for a new key; choose "RSA and RSA" or "ECC" gpg --list-secret-keys --keyid-format=long ``` -2. **Export the armored private key:** +2. **Export the armored private key.** + ```sh gpg --armor --export-secret-keys > bot-signing.gpg ``` -3. **Register the public key** on the bot's GitHub account: - - Settings → SSH and GPG keys → **New GPG key** - - Export with `gpg --armor --export ` and paste +3. **Register the public key** on the bot's GitHub account. + - Settings → SSH and GPG keys → **New GPG key**. + - Export with `gpg --armor --export ` and paste. + +4. **Store the secrets** in the target repo (or as an organization secret). + - `GPG_PRIVATE_KEY` (or any name) — full armored content of `bot-signing.gpg`. + - `GPG_PASSPHRASE` — the passphrase on the key. Omit if none. -4. **Store the secrets** in the target repo (or an org-level secret): - - `GPG_PRIVATE_KEY` (or any name) — full armored content of `bot-signing.gpg` - - `GPG_PASSPHRASE` — the passphrase on the key (omit if none) +5. **Reference the secrets** in your caller workflow. -5. **Reference the secrets** in your caller workflow: ```yaml with: signing-method: gpg @@ -264,53 +287,60 @@ One-time, per repo or per org: signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} ``` -During the run the action imports the key, extracts its key ID, presets the passphrase into `gpg-agent`, and configures `git` with `gpg.format openpgp` + `commit.gpgsign true` for the duration of the job. +During the run the action imports the key, extracts its key ID, presets the passphrase into `gpg-agent`, and configures `git` with `gpg.format openpgp` and `commit.gpgsign true` for the duration of the job. ### No signing -Leave `signing-method` empty (or omit). Commits will show as "Unverified" in the GitHub UI but the author metadata is still correct. Fine for internal repos where the PR itself (not the commit signature) is the reviewed artifact. +Leave `signing-method` empty (or omit it). Commits display as "Unverified" in the GitHub UI but the author metadata is still correct. Fine for internal repos where the PR itself, not the commit signature, is the reviewed artifact. ## What's autodetected -You do not pass these — the action reads them from the workflow context: +You do not pass these. The action reads them from the workflow context. | Value | Source | |-------|--------| | PR base branch | `github.event.repository.default_branch` | -| Repo owner/name | `github.repository` | +| Repo owner and name | `github.repository` | | Runner OS | fixed to `ubuntu-latest` | -If your repo's default branch is `trunk` or `develop`, the PR targets that — no input needed. +If your repo's default branch is `trunk` or `develop`, the PR targets that. No input needed. ## Versioning -- Release tags `v1.0.0`, `v1.0.1`, `v1.0.2`, … -- Consumers pin by commit SHA — see [Quickstart](#quickstart) and [Updating the pin](#updating-the-pin). The comment next to the SHA is the release name, which this action's own update logic rewrites whenever a newer release ships. -- Breaking input-contract changes ship as `v2.0.0`; the `v1.x.y` line stops advancing at that point so existing consumers are not broken. +- Release tags follow strict semver: `v1.0.0`, `v1.0.1`, `v1.0.2`, and so on. +- Consumers pin by commit SHA. See [Quickstart](#quickstart) and [Updating the pin](#updating-the-pin). The comment next to the SHA is the release name. This action rewrites that comment whenever a newer release ships. +- Breaking input-contract changes ship as `v2.0.0`. The `v1.x.y` line stops advancing at that point, so existing consumers are not broken. ## Troubleshooting **"Input required and not supplied: gh-pat"** + Your caller workflow is missing `gh-pat: ${{ secrets.YOUR_PAT_SECRET_NAME }}` in the `with:` block. Add it. **"signing-method is 'ssh' but signing-key input is empty"** + Either pass `signing-key` or set `signing-method` to empty. -**PR opens but CI doesn't run on it** -`gh-pat` is actually `GITHUB_TOKEN`. Use a PAT from a bot account — see the `gh-pat` section above. +**PR opens but CI does not run on it** + +Your `gh-pat` is actually `GITHUB_TOKEN`. Use a PAT from a bot account. See the `gh-pat` section above. **Commit shows "Unverified" despite `signing-method: ssh`** + The public key registered on the bot account is marked as an Authentication Key, not a Signing Key. GitHub distinguishes the two. Re-add it with the correct type. **"Warning: latest release tag '…' is not strict semver, trying tags API"** -This action compares by strict `vX.Y.Z` release tags. When the upstream you're auditing only ships non-semver releases (for example, a release tagged just `latest` or `v1`), the action falls back to the tags API to find the most recent strict-semver tag, and logs this warning while it does. If the fallback also finds nothing, the upstream is silently skipped for that run. + +This action compares by strict `vX.Y.Z` release tags. When the upstream you are auditing only ships non-semver releases (for example, a release tagged just `latest` or `v1`), the action falls back to the tags API to find the most recent strict-semver tag, and logs this warning while it does. If the fallback also finds nothing, the upstream is silently skipped for that run. **Action finds zero `uses:` references** + Your workflows may be in `.yaml` files, not `.yml`. Override `scan-globs`. **"The action X is not allowed in / because all actions must be pinned to a full-length commit SHA"** -Your repo or GitHub org has a policy requiring SHA-pinning on every `uses:` reference. The tag form (`@v1.0.2`) is rejected. See [Updating the pin](#updating-the-pin) for how to resolve the commit SHA and pin by hash. Subsequent updates happen automatically. + +Your repo or GitHub organization has a policy requiring SHA-pinning on every `uses:` reference. The tag form (`@v1.0.2`) is rejected. See [Updating the pin](#updating-the-pin) for how to resolve the commit SHA and pin by hash. Subsequent updates happen automatically. ## License -MIT — see [LICENSE](./LICENSE). +MIT. See [LICENSE](./LICENSE).