From 261fe6fa52a08dd831e4f17db2fba78f7136ffcb Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Thu, 1 Oct 2026 14:44:26 -0700 Subject: [PATCH 1/3] feat(digest): authenticate as a GitHub App Mint per-owner app tokens, restore the daily cron, rewrite the runbook. Advances #149. --- .github/workflows/dependabot-digest.yml | 70 +++-- CLAUDE.md | 2 +- .../runbooks/dependabot-digest-credentials.md | 104 ++++++++ docs/runbooks/dependabot-digest-tokens.md | 251 ------------------ scripts/dependabot-digest/collect.sh | 7 +- .../dependabot-digest/tests/test-classify.sh | 4 +- 6 files changed, 157 insertions(+), 281 deletions(-) create mode 100644 docs/runbooks/dependabot-digest-credentials.md delete mode 100644 docs/runbooks/dependabot-digest-tokens.md diff --git a/.github/workflows/dependabot-digest.yml b/.github/workflows/dependabot-digest.yml index 4bb8b0a..e177b98 100644 --- a/.github/workflows/dependabot-digest.yml +++ b/.github/workflows/dependabot-digest.yml @@ -14,8 +14,10 @@ name: Dependabot Digest # scheduled job should route around. on: - # Daily schedule (cron '0 14 * * *') removed 2026-10-01: every run failed - # on token scope. Restore it once dev-env#149 lands the GitHub App. + schedule: + # 14:00 UTC daily — morning in US Pacific, so the queue is current when + # the day starts. + - cron: '0 14 * * *' # A scheduled workflow runs only on the default branch, so a pull request # cannot exercise it. This is how the first run gets triggered and how the # job is tested after a change. @@ -36,7 +38,7 @@ jobs: permissions: contents: read # Writes the digest issue in this repository. The PR data itself is read - # with the per-owner tokens below, which carry no write access anywhere. + # with the per-owner app tokens, which carry no write access anywhere. issues: write steps: - name: Check out @@ -44,29 +46,49 @@ jobs: with: persist-credentials: false + - name: Mint a token for smartwatermelon + id: token-smartwatermelon + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.DIGEST_APP_CLIENT_ID }} + private-key: ${{ secrets.DIGEST_APP_PRIVATE_KEY }} + owner: smartwatermelon # zizmor: ignore[github-app] whole-fleet survey; no repo subset exists + permission-checks: read + permission-contents: read + permission-pull-requests: read + permission-statuses: read + + - name: Mint a token for nightowlstudiollc + id: token-nightowlstudiollc + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.DIGEST_APP_CLIENT_ID }} + private-key: ${{ secrets.DIGEST_APP_PRIVATE_KEY }} + owner: nightowlstudiollc # zizmor: ignore[github-app] whole-fleet survey; no repo subset exists + permission-checks: read + permission-contents: read + permission-pull-requests: read + permission-statuses: read + + - name: Mint a token for twistedmelonman + id: token-twistedmelonman + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.DIGEST_APP_CLIENT_ID }} + private-key: ${{ secrets.DIGEST_APP_PRIVATE_KEY }} + owner: twistedmelonman # zizmor: ignore[github-app] whole-fleet survey; no repo subset exists + permission-checks: read + permission-contents: read + permission-pull-requests: read + permission-statuses: read + - name: Build and publish the digest env: - # One token per owner: a fine-grained PAT is "limited to access - # resources owned by a single user or organization" (GitHub docs), so - # three owners need three tokens. Each needs Pull requests, Contents, - # Commit statuses and Metadata — all read-only, on all repositories - # for that owner. - # - # Those four are the most a fine-grained token can be given, and they - # are NOT enough to survey private repos: a fine-grained PAT has no - # Checks permission at all - # (github.com/orgs/community/discussions/129512), so it cannot read - # GitHub Actions results there. GitHub does not error — it returns - # statusCheckRollup with HTTP 200 and nulls every CheckRun, which - # would turn failing builds into "nothing failing". collect.sh - # refuses such a response rather than under-report. - # - # Resolving that needs a classic PAT (write access fleet-wide), a - # GitHub App (which does have Checks), or accepting public-repo-only - # coverage. See docs/runbooks/dependabot-digest-tokens.md. - DIGEST_TOKEN_SMARTWATERMELON: ${{ secrets.DIGEST_TOKEN_SMARTWATERMELON }} - DIGEST_TOKEN_NIGHTOWLSTUDIOLLC: ${{ secrets.DIGEST_TOKEN_NIGHTOWLSTUDIOLLC }} - DIGEST_TOKEN_TWISTEDMELONMAN: ${{ secrets.DIGEST_TOKEN_TWISTEDMELONMAN }} + # Per-owner app tokens minted above. Why an app, not PATs: + # docs/runbooks/dependabot-digest-credentials.md + DIGEST_TOKEN_SMARTWATERMELON: ${{ steps.token-smartwatermelon.outputs.token }} + DIGEST_TOKEN_NIGHTOWLSTUDIOLLC: ${{ steps.token-nightowlstudiollc.outputs.token }} + DIGEST_TOKEN_TWISTEDMELONMAN: ${{ steps.token-twistedmelonman.outputs.token }} # A token that has lost private-repo access still answers searches # successfully, returning only public results. Naming one private # repo per owner lets the collector prove it can still see private diff --git a/CLAUDE.md b/CLAUDE.md index 92a073d..fbf57b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ This repo is the **dev-env infrastructure repository** — it contains documenta - `docs/token-rotation.md` — Where each `CLAUDE_CODE_OAUTH_TOKEN` lives and when it expires; never contains a token - `docs/runbooks/fleet-probe-token-scopes.md` — The two fine-grained-PAT properties a fleet probe needs (`Administration: Read-only` + All-repositories), and why an under-scoped token returns wrong numbers instead of errors - `scripts/org-migration/` — Snapshot/transfer/verify tooling for the 2026-09 org migration; tests in `scripts/org-migration/tests/run-tests.sh` -- `scripts/dependabot-digest/` — Collects open Dependabot PRs across all three owners and upserts one digest issue describing the queue; run by `.github/workflows/dependabot-digest.yml`. Tokens are installed by hand: see `docs/runbooks/dependabot-digest-tokens.md` +- `scripts/dependabot-digest/` — Collects open Dependabot PRs across all three owners and upserts one digest issue describing the queue; run by `.github/workflows/dependabot-digest.yml`. Credentials (a GitHub App) are installed by hand: see `docs/runbooks/dependabot-digest-credentials.md` - `.claude/` — Project-specific Claude Code configuration templates - `.claude/config.sh.template` — Template for project configuration (Node version, required tools, deployment secrets, build/deploy hooks) - `.project-hooks/pre-commit` and `.project-hooks/pre-push` — Project-specific git hook extensions, run by the global hooks at `~/.config/git/hooks/` when executable diff --git a/docs/runbooks/dependabot-digest-credentials.md b/docs/runbooks/dependabot-digest-credentials.md new file mode 100644 index 0000000..4b02c44 --- /dev/null +++ b/docs/runbooks/dependabot-digest-credentials.md @@ -0,0 +1,104 @@ +# Dependabot digest: GitHub App credentials + +The `Dependabot Digest` workflow (`.github/workflows/dependabot-digest.yml`) +reads open Dependabot pull requests across all three owners and rewrites one +issue in `smartwatermelon/dev-env` describing the queue. It authenticates as a +GitHub App, `dependabot-digest-swm`, and mints one short-lived installation +token per owner on each run with `actions/create-github-app-token`. + +**These steps are yours, not an agent's.** Creating an app, generating its key +and installing a secret are human operations. + +## Why a GitHub App and not a fine-grained PAT + +A fine-grained PAT has no Checks permission at all +(), so it cannot read +GitHub Actions results on a private repository however it is scoped. GitHub +does not error: it answers `statusCheckRollup` with HTTP 200 and the correct +`totalCount`, then nulls every CheckRun. Measured 2026-09-11 on +`nightowlstudiollc/kebab-tax-netlify#280`, 11 of 12 contexts came back null, so +seven failing builds read as "1 failing check". An app does have Checks, and +one installation per owner covers all three owners, which a PAT cannot. +`collect.sh` still refuses any rollup with a nulled context, which is the +proof that this path works: a published digest means every check was read. + +## The app + +- **Name**: `dependabot-digest-swm`, installed on `smartwatermelon`, + `nightowlstudiollc` and `twistedmelonman`, **All repositories** each. +- **Repository permissions**, all read-only: Checks, Contents, Metadata, + Pull requests, Commit statuses. Nothing else. Webhook inactive. +- The private key lives in 1Password vault `Automation`, item `DIGEST_APP` + (fields `client_id`, `private_key`). + +## Install the secrets + +Both go on `smartwatermelon/dev-env` as repository secrets, because that is +where the workflow runs: + +```bash +gh secret set DIGEST_APP_CLIENT_ID --repo smartwatermelon/dev-env +gh secret set DIGEST_APP_PRIVATE_KEY --repo smartwatermelon/dev-env +``` + +The private key is multi-line: paste the whole `.pem` and press Ctrl-D. + +Repository secrets rather than org secrets, deliberately: `dev-env` is the only +repo that runs this, and org-level secrets do not reach private repos on the +free plan (see `docs/token-rotation.md`). The key does not expire, so there is +no rotation row to add; rotate it by generating a new key on the app's page. + +## Verify + +Trigger a run by hand — a scheduled workflow only runs on the default branch, +so this is also how the very first digest gets created: + +First capture what the answer should be, using your own credentials as the +reference. Your local `gh` login can read all three owners, so this is the +known-good result the workflow must reproduce: + +```bash +bash scripts/dependabot-digest/run-digest.sh --dry-run > /tmp/digest-local.md +grep -c '^| ' /tmp/digest-local.md # rows in the queue table +``` + +Then trigger a run by hand — a scheduled workflow only runs on the default +branch, so this is also how the very first digest gets created: + +```bash +gh workflow run dependabot-digest.yml --repo smartwatermelon/dev-env +sleep 30 +gh run list --workflow dependabot-digest.yml --repo smartwatermelon/dev-env --limit 1 +``` + +**Do not trigger a second run within a few minutes of the first.** The digest +finds its issue partly through GitHub's body-search index, which lags creation +by an unbounded amount. A second run landing before the index catches up is +covered by an unindexed issue listing, but there is no reason to lean on the +fallback while verifying. + +Then compare the published issue against the local reference: + +```bash +gh issue list --repo smartwatermelon/dev-env --search 'dependabot-digest in:body' --state open +gh issue view --repo smartwatermelon/dev-env --json body --jq '.body' > /tmp/digest-ci.md +diff /tmp/digest-local.md /tmp/digest-ci.md +``` + +Differences in counts and timestamps are expected — the queue moves between the +two runs. What must **not** differ is which owners appear. An installation that +is missing from one owner, or limited to selected repositories, can still +return an empty result set rather than an error. + +The comparison, not the green run, is the evidence. A green run means the +scripts did not crash; only the diff shows the workflow saw the same fleet you +can see. The collector's private-repo probe catches a credential that lost private +access, but it cannot catch one that was scoped to the wrong owner. + +## If the digest goes stale + +GitHub disables scheduled workflows in public repositories after 60 days with +no repository activity. The digest issue carries its own generation timestamp +for this reason: a date more than a day or two old means the schedule stopped, +not that the queue is quiet. Re-enable it with `gh workflow enable +dependabot-digest.yml --repo smartwatermelon/dev-env`. diff --git a/docs/runbooks/dependabot-digest-tokens.md b/docs/runbooks/dependabot-digest-tokens.md deleted file mode 100644 index d78f319..0000000 --- a/docs/runbooks/dependabot-digest-tokens.md +++ /dev/null @@ -1,251 +0,0 @@ -# Dependabot digest: minting and installing the read tokens - -The `Dependabot Digest` workflow (`.github/workflows/dependabot-digest.yml`) -reads open Dependabot pull requests across all three owners and rewrites one -issue in `smartwatermelon/dev-env` describing the queue. It cannot run until -the tokens below exist — until then the workflow fails loudly rather than -publishing a digest that silently omits an owner. - -**These steps are yours, not an agent's.** Minting a credential and installing -a secret are human operations. - -## Why three tokens - -A fine-grained personal access token is, in GitHub's words, "limited to access -resources owned by a single user or organization." The fleet spans three -owners, so it needs three tokens: - -| Owner | Kind | Token secret name | -| --- | --- | --- | -| `smartwatermelon` | org | `DIGEST_TOKEN_SMARTWATERMELON` | -| `nightowlstudiollc` | org | `DIGEST_TOKEN_NIGHTOWLSTUDIOLLC` | -| `twistedmelonman` | personal account | `DIGEST_TOKEN_TWISTEDMELONMAN` | - -The one-token alternative is a classic PAT, which "will grant access to all -repositories within the organizations that you have access to, as well as all -personal repositories" — write access included. That is far more authority than -a read-only digest should hold, and the `gh` wrapper already warns against -widening the CCCLI PAT for the same reason. Three narrow tokens are the safer -shape even though it is three times the paperwork. - -## Mint each token - -For each of the three owners, at -: - -1. **Token name**: `dependabot-digest-` -2. **Resource owner**: the owner from the table above. If an org does not - appear, it has fine-grained tokens disabled; enable them under the org's - Settings → Personal access tokens first. -3. **Expiration**: one year is reasonable. Record the date — see *After - installing* below. -4. **Repository access**: **All repositories**. The digest must see every repo - under the owner, including ones created after the token was minted. -5. **Repository permissions** — all four, all read-only: - - **Pull requests**: Read-only - - **Contents**: Read-only - - **Commit statuses**: Read-only - - **Metadata**: Read-only (mandatory; GitHub selects it automatically) - - Grant nothing else. The digest never writes to any surveyed repository. - - **These four are the most a fine-grained token can be given, and they are - not enough to run the digest on private repositories.** Read the next - section before minting anything. - - The collector reads each PR's checks through - `pullRequest.commits(last:1).commit.statusCheckRollup`. Each hop needs its - own permission, and `statusCheckRollup` merges two REST resources: - `commits//status` (**Commit statuses**), which these four grants do - cover, and `commits//check-runs` (**Checks**), which they cannot — - see below. -6. For the two org tokens, the request may need org approval before it works. - Approval can be **per repository**: a token minted for "All repositories" - can still answer for some repos and return 403 for others, which looks - exactly like a permissions error and is not one. If one repo fails while - its neighbours succeed, check the org's Settings → Personal access tokens → - Active tokens for that token's actual repository list before re-minting. - -## What a fine-grained token cannot do - -**A fine-grained PAT has no Checks permission.** It is not a grant that was -overlooked; the permission does not exist on the fine-grained list at all -(). Since GitHub Actions -results are check runs, a fine-grained token cannot read them on a private -repository no matter how it is scoped. - -The failure is silent, which is what makes it dangerous. GitHub does not -return an error for the unreadable part. It answers `statusCheckRollup` with -HTTP 200 and the correct `totalCount`, then sets every CheckRun's fields to -null. Measured 2026-09-11 on `nightowlstudiollc/kebab-tax-netlify#280`, with -all four permissions above granted: - -| Endpoint | Result | -| --- | --- | -| `pulls/280` | ok | -| `commits/` | ok | -| `commits//status` | ok — this is why one Netlify status survived | -| `commits//check-runs` | **DENIED — and ungrantable** | - -That PR has 12 contexts. Eleven came back null and one Netlify StatusContext -survived. Mapped naively, seven failing builds and one green required check -read as "1 failing check, 0 required checks". A PR with no Netlify status at -all — most of the fleet — reads as entirely green and lands in -`ready-to-merge` with a paste-ready merge-lock line under it. - -`collect.sh` therefore refuses any rollup containing a nulled context rather -than publishing an under-reporting digest, `classify.sh` buckets such a PR as -`checks-unreadable`, and `render.sh` says so in the body. The digest fails -loudly instead of quietly telling you to merge broken code. - -Public repositories are unaffected: check results there are readable without -Checks or Commit statuses at all. That is why an under-scoped token appears to -work until it meets a private repo — and why testing against a public repo -proves nothing. - -**This is an open design decision, not a step to follow.** Three ways forward: - -| Option | Trade-off | -| --- | --- | -| Classic PAT | One token reads everything, including check runs. But it carries write access across every repo in both orgs for a read-only report. | -| GitHub App | Apps *do* have a Checks permission. Correct scoping and the right long-term shape; more setup than a PAT. | -| Public-only coverage | Keep the fine-grained tokens and have the digest state plainly that private repos are unsurveyed. Honest, and incomplete. | - -Until that is decided, the workflow will keep failing on the private repos -rather than publishing a partial digest. - -## Install the secrets - -All three go on `smartwatermelon/dev-env` as repository secrets, because that -is where the workflow runs: - -```bash -gh secret set DIGEST_TOKEN_SMARTWATERMELON --repo smartwatermelon/dev-env -gh secret set DIGEST_TOKEN_NIGHTOWLSTUDIOLLC --repo smartwatermelon/dev-env -gh secret set DIGEST_TOKEN_TWISTEDMELONMAN --repo smartwatermelon/dev-env -``` - -Each command prompts for the value. Paste the token and press Enter; it is not -echoed and does not enter shell history. - -Repository secrets rather than org secrets, deliberately: `dev-env` is the only -repo that runs this, and org-level secrets do not reach private repos on the -free plan (see `docs/token-rotation.md`). - -## Verify - -Trigger a run by hand — a scheduled workflow only runs on the default branch, -so this is also how the very first digest gets created: - -First capture what the answer should be, using your own credentials as the -reference. Your local `gh` login can read all three owners, so this is the -known-good result the workflow must reproduce: - -```bash -bash scripts/dependabot-digest/run-digest.sh --dry-run > /tmp/digest-local.md -grep -c '^| ' /tmp/digest-local.md # rows in the queue table -``` - -Then trigger a run by hand — a scheduled workflow only runs on the default -branch, so this is also how the very first digest gets created: - -```bash -gh workflow run dependabot-digest.yml --repo smartwatermelon/dev-env -sleep 30 -gh run list --workflow dependabot-digest.yml --repo smartwatermelon/dev-env --limit 1 -``` - -**Do not trigger a second run within a few minutes of the first.** The digest -finds its issue partly through GitHub's body-search index, which lags creation -by an unbounded amount. A second run landing before the index catches up is -covered by an unindexed issue listing, but there is no reason to lean on the -fallback while verifying. - -Then compare the published issue against the local reference: - -```bash -gh issue list --repo smartwatermelon/dev-env --search 'dependabot-digest in:body' --state open -gh issue view --repo smartwatermelon/dev-env --json body --jq '.body' > /tmp/digest-ci.md -diff /tmp/digest-local.md /tmp/digest-ci.md -``` - -Differences in counts and timestamps are expected — the queue moves between the -two runs. What must **not** differ is which owners appear. A fine-grained token -is restricted to a single resource owner, and an owner whose token is missing a -permission can still return an empty result set rather than an error. - -The comparison, not the green run, is the evidence. A green run means the -scripts did not crash; only the diff shows the workflow saw the same fleet you -can see. The collector's private-repo probe catches a token that lost private -access, but it cannot catch a token that was scoped to the wrong owner. - -## If a run fails with FORBIDDEN - -The run log names the repo, the PR, and GitHub's own reason: - -``` -collect.sh: nightowlstudiollc/kebab-tax-netlify#280: could not read PR detail (exit 1) -collect.sh: stderr: gh: Resource not accessible by personal access token -collect.sh: graphql: FORBIDDEN: Resource not accessible by personal access token -``` - -The collector stops at the first failure and publishes nothing, so a later repo -succeeding is **not** evidence its access is fine — it was never attempted. -Diagnose by probing the token directly rather than reasoning from which repos -appear to work. - -The tokens live in 1Password (vault `Automation`, item -`DIGEST_TOKEN_`, field `token`), so a probe can read one directly -instead of pasting it. Run against the **private** repo that failed: - -```bash -t="$(op read "op://Automation/DIGEST_TOKEN_NIGHTOWLSTUDIOLLC/token")" -sha="$(GH_TOKEN="$t" gh api repos/OWNER/REPO/pulls/NNN --jq '.head.sha')" -for path in \ - "pulls/NNN" \ - "commits/${sha}" \ - "commits/${sha}/check-runs" \ - "commits/${sha}/status" -do - if GH_TOKEN="$t" gh api "repos/OWNER/REPO/${path}" >/dev/null 2>&1; then - echo " ${path##*/}: ok" - else - echo " ${path##*/}: DENIED" - fi -done -unset t -``` - -Each line maps to exactly one permission: - -| Denied endpoint | Missing permission | -| --- | --- | -| `pulls/NNN` | **Pull requests: Read-only** | -| `commits/` | **Contents: Read-only** | -| `commits//status` | **Commit statuses: Read-only** | -| `commits//check-runs` | **Checks: Read-only** — which a fine-grained token cannot be given. On one, this line reads DENIED on every private repo and no grant changes it. See *What a fine-grained token cannot do*. | - -Two traps, both of which cost time on 2026-09-11: - -- **Test a private repo.** Check results on public repositories read without - Checks or Commit statuses, so a probe against a public repo passes with an - under-scoped token and proves nothing. -- **Editing a token's permissions does not change its value**, so the stored - secret stays valid and needs no reinstall. If a run still fails after a - grant change, re-run the probe before suspecting the secret — the token - string is almost certainly fine. - -## After installing - -Add a row per token to `docs/token-rotation.md` with its expiry, and set one -calendar reminder two weeks before the earliest. An expired digest token does -not fail quietly — the workflow exits non-zero rather than publishing a partial -digest — but a failing scheduled workflow is easy not to notice. - -## If the digest goes stale - -GitHub disables scheduled workflows in public repositories after 60 days with -no repository activity. The digest issue carries its own generation timestamp -for this reason: a date more than a day or two old means the schedule stopped, -not that the queue is quiet. Re-enable it with `gh workflow enable -dependabot-digest.yml --repo smartwatermelon/dev-env`. diff --git a/scripts/dependabot-digest/collect.sh b/scripts/dependabot-digest/collect.sh index ce6fe23..196fea7 100755 --- a/scripts/dependabot-digest/collect.sh +++ b/scripts/dependabot-digest/collect.sh @@ -205,9 +205,10 @@ while IFS=$'\t' read -r nwo number; do if [[ -n "${nulled}" && "${nulled}" != "null" && "${nulled}" -gt 0 ]]; then echo "collect.sh: ${nwo}#${number}: ${nulled} check(s) returned null —" \ "the token can see that checks exist but not what they say." \ - "A fine-grained token cannot grant Checks: read" \ - "(github.com/orgs/community/discussions/129512), so this survey would" \ - "under-report failures rather than fail. Refusing to continue." >&2 + "The credential cannot read check runs: a fine-grained token never can" \ + "(github.com/orgs/community/discussions/129512), and an app installation" \ + "cannot if it lacks Checks: read. This survey would under-report failures" \ + "rather than fail. Refusing to continue." >&2 exit 1 fi jq -c --arg nwo "${nwo}" --argjson base_red "${base_red}" ' diff --git a/scripts/dependabot-digest/tests/test-classify.sh b/scripts/dependabot-digest/tests/test-classify.sh index bb22c5c..e2bbea9 100755 --- a/scripts/dependabot-digest/tests/test-classify.sh +++ b/scripts/dependabot-digest/tests/test-classify.sh @@ -168,8 +168,8 @@ fi # A check whose fields came back null is an access failure, not a passing # check. GitHub returns statusCheckRollup with HTTP 200 and the correct -# totalCount, then nulls every CheckRun a fine-grained token may not read — -# Checks: read cannot be granted to one at all +# totalCount, then nulls every CheckRun the credential may not read — +# a fine-grained token, or an app installation lacking Checks: read # (github.com/orgs/community/discussions/129512). Measured 2026-09-11: 11 of 12 # contexts null, and the one survivor was a Netlify StatusContext. # From 7e7e21877877ed3638bdb1291d7dff2946846dfa Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Thu, 1 Oct 2026 14:44:55 -0700 Subject: [PATCH 2/3] docs(digest): drop duplicate runbook lead-in Reviewer finding on the runbook rewrite. Advances #149. --- docs/runbooks/dependabot-digest-credentials.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/runbooks/dependabot-digest-credentials.md b/docs/runbooks/dependabot-digest-credentials.md index 4b02c44..b7ff1f3 100644 --- a/docs/runbooks/dependabot-digest-credentials.md +++ b/docs/runbooks/dependabot-digest-credentials.md @@ -50,9 +50,6 @@ no rotation row to add; rotate it by generating a new key on the app's page. ## Verify -Trigger a run by hand — a scheduled workflow only runs on the default branch, -so this is also how the very first digest gets created: - First capture what the answer should be, using your own credentials as the reference. Your local `gh` login can read all three owners, so this is the known-good result the workflow must reproduce: From 10c650bf70ab6c2d3e01f8baa4afe9add072edae Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Thu, 1 Oct 2026 14:57:01 -0700 Subject: [PATCH 3/3] docs(digest): pipe the key from 1Password The pasted field is flattened to one line. Advances #149. --- docs/runbooks/dependabot-digest-credentials.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/runbooks/dependabot-digest-credentials.md b/docs/runbooks/dependabot-digest-credentials.md index b7ff1f3..6e00bd2 100644 --- a/docs/runbooks/dependabot-digest-credentials.md +++ b/docs/runbooks/dependabot-digest-credentials.md @@ -29,7 +29,9 @@ proof that this path works: a published digest means every check was read. - **Repository permissions**, all read-only: Checks, Contents, Metadata, Pull requests, Commit statuses. Nothing else. Webhook inactive. - The private key lives in 1Password vault `Automation`, item `DIGEST_APP` - (fields `client_id`, `private_key`). + (fields `client_id`, `private_key`). Use the attached + `dependabot-digest-swm.2026-10-01.private-key.pem`, not the `private_key` + field: 1Password flattens a pasted PEM to one line, which is unusable. ## Install the secrets @@ -37,11 +39,13 @@ Both go on `smartwatermelon/dev-env` as repository secrets, because that is where the workflow runs: ```bash -gh secret set DIGEST_APP_CLIENT_ID --repo smartwatermelon/dev-env -gh secret set DIGEST_APP_PRIVATE_KEY --repo smartwatermelon/dev-env +gh secret set DIGEST_APP_CLIENT_ID --repo smartwatermelon/dev-env +op read "op://Automation/DIGEST_APP/dependabot-digest-swm.2026-10-01.private-key.pem" \ + | gh secret set DIGEST_APP_PRIVATE_KEY --repo smartwatermelon/dev-env ``` -The private key is multi-line: paste the whole `.pem` and press Ctrl-D. +The first command prompts for the client ID. The second pipes the key file +straight from 1Password, so its line breaks survive. Repository secrets rather than org secrets, deliberately: `dev-env` is the only repo that runs this, and org-level secrets do not reach private repos on the