ci: read TEND_BOT_TOKEN from an environment in every job - #3748
Conversation
The `tend` environment gate closes on a repo only when the repo-level copy
of the secret is gone, since a job naming an environment still reads
repo-level secrets. worktrunk kept one because these hand-maintained
workflows read the token outside tend's generated set.
Each job now names an environment whose deployment branch policy admits the
ref it runs from:
- The two `create-issue-on-*-failure` jobs are already gated to `schedule`,
which runs on `main`, so they take `tend` directly.
- The benchmark gist append moves into its own `append-gist` job. Putting
`tend` on the whole `benchmarks` job would refuse a `workflow_dispatch`
against a chosen branch, which is what that trigger is for; a job GitHub
skips never requests its environment, so the existing cron gate keeps the
policy off the dispatch path.
- `publish-winget` and `publish-homebrew` take the existing `release`
environment, which admits `v*` tags. Tag creation and update are already
restricted to admins by the "Tag operations" ruleset, so that policy is a
real boundary; the tag cannot go on `tend`, whose policy `tend check` pins
to exactly the protected branches.
Jobs naming `tend` use `{name: tend, deployment: false}` so GitHub files no
deployment record. The release jobs keep theirs, which land in no PR
timeline.
Seeding `TEND_BOT_TOKEN` into `release` and deleting the repo-level copy are
separate steps, in that order — until then both paths keep working.
worktrunk-bot
left a comment
There was a problem hiding this comment.
Reviewing as a draft — flagging anything that looks worth a quick fix. Mark ready for a full review.
The workflow side holds up: I confirmed deployment is a real environment: key (actionlint 1.7.12 accepts it and rejects a sibling bogus key, so it isn't being silently ignored), and every job that references secrets.TEND_BOT_TOKEN across all workflows now names an environment — the only remaining match is the prose comment above append-gist, not a real read. Both release jobs are if:-gated on needs.plan.outputs.publishing == 'true', so the pull_request trigger short-circuits them the same way publish-cargo already demonstrates.
Three drifts in the new .github/CLAUDE.md tables (suggestions inline). Separately, one consequence the description doesn't state: seeding TEND_BOT_TOKEN into release means publish-aur can now read the PAT and publish-winget/publish-homebrew can read AUR_SSH_PRIVATE_KEY — the section's own "each environment holds what one phase needs" line is doing more work than before. Same tag-push phase, so probably the right call over a fourth environment, but worth a sentence there since that line is now the only thing bounding it.
worktrunk-bot
left a comment
There was a problem hiding this comment.
Full pass now that this is out of draft — including the two steps the draft review skipped. No other open PR touches these files, and grep -rn TEND_BOT_TOKEN .github/ leaves no reader outside an environment-naming job (the two remaining matches are prose comments).
Two claims the draft pass took on trust, now checked against the source: deployment: false is tend 0.1.14's own generated form, and its changelog states the deployment-branch policy is unaffected by it — so both the gate premise and the tend ≥ 0.1.14 note in .github/CLAUDE.md hold, and 0.1.14 shipped today, so the follow-up regen is unblocked (tend CHANGELOG 0.1.14).
One new drift inline, in the nightly.yaml comment. Commenting rather than approving because that plus the three .github/CLAUDE.md threads from the draft pass are still open — the only new commit is the origin/main merge.
Four fixes from review, all documentation accuracy: - `CODECOV_TOKEN` is read by `ci.yaml` as well as `coverage.yaml`. - `relay` is the one `tend-*.yaml` job that names no environment; it uses `secrets.GITHUB_TOKEN` and holds no secret. - The job that deploys to `github-pages` is `deploy-docs`, not the `publish-docs` workflow file. - `nightly.yaml`'s push trigger is `branches: [main]`, a ref the `tend` policy admits — so push was never what the cron gate keeps this job off. `pull_request` and `workflow_dispatch` are. Also drop the claim that `TEND_BOT_TOKEN` lives only in `tend`; it is now in `release` too, for the publish jobs.
Closes worktrunk's half of the
tendenvironment migration (tend'sTODO.md,"Finish moving the operational secrets into the
tendenvironment", item 2).The environment is a secret scope, not a deploy target: its deployment branch
policy is what stops a workflow pushed to a feature branch from reading the
bot's PAT. That gate closes only when the repo-level copy of the secret is
gone, since a job naming an environment still reads repo-level secrets.
worktrunk kept one because these hand-maintained workflows read
TEND_BOT_TOKENoutside tend's generated set.What changed
benchmarks.yamlappend-gist(new)tendschedule-gated, so it runs onmainbenchmarks.yamlcreate-issue-on-benchmark-failuretendschedule-gatednightly.yamlcreate-issue-on-nightly-failuretendschedule-gatedrelease.yamlpublish-wingetreleasev*tag pushrelease.yamlpublish-homebrewreleasev*tag pushEvery job reading
TEND_BOT_TOKENnow names an environment, so deleting therepo-level copy breaks nothing.
The gist append moved into its own job
The
benchmarksjob has noifgate, so puttingtendon it would refuse aworkflow_dispatchagainst a non-mainref — on-demand runs against a chosenbranch are what that trigger is documented for. A job GitHub skips never
requests its environment, so moving the append into a
schedule-gatedappend-gistjob keeps the policy off the dispatch path entirely. It readstarget/criterionback from the artifact thebenchmarksjob alreadyuploads.
create-issue-on-benchmark-failurenowneedsboth jobs, so a failed appendstill files an issue — previously it failed the
benchmarksjob directly.Why the release jobs get
release, nottendA tag push is not bot-steerable and tag creation and update are already
restricted to admins by the "Tag operations" ruleset, so a tag policy is a
real boundary. The tag entry cannot go on
tend:tend check'scheck_environmentpins that policy to exactly the protected branches and its--fixdeletes anything else.releasealready exists with av*tag policyand already holds
AUR_SSH_PRIVATE_KEY, so no new environment and no newcredential —
TEND_BOT_TOKENis seeded into it as a second copy.deployment: falseJobs naming
tenduse the mapping form. GitHub files a deployment record forevery job that names an environment, against whatever ref the run belongs to;
under
pull_request_targetthat is the PR's own head, which is why PRtimelines grew a "worktrunk-bot deployed to tend" line on every push.
deployment: falsedrops the record and keeps the gate. The release jobs keeptheir records, which land in no PR timeline.
Follow-up: one step, after merge
TEND_BOT_TOKENis already seeded into thereleaseenvironment (readfrom the local
worktrunk-botgh config dir at~/.config/gh-bots/worktrunk-bot, so no new credential was minted and nothingwas pasted). Verified: the token resolves to
worktrunk-botand has push onboth
max-sixty/winget-pkgsandmax-sixty/homebrew-worktrunk.That leaves one step, and it must come after this PR merges:
Not before. On
maintoday the gist append, bothcreate-issue-on-*-failurejobs, and the two publish jobs still read the token with no environment named,
so deleting the repo-level copy first would break the next benchmarks cron
(03:47 UTC daily). Merging this PR is what makes the deletion safe.
What this does not fix
repo-secret-allowliststill fails onCLAUDE_CODE_OAUTH_TOKEN, also atrepo level. It cannot be read back either; separate item.
environment-deploymentsstill fails on the generatedtend-*.yaml, whichpin tend 0.1.13 and carry the bare
environment: tend. Those areregenerated by the published tend, and a regen also carries an unrelated
gh api --paginatefix intend-mention.yaml, so it belongs in its own PR.Verification
append-gistscript was run end-to-end against a realbenchmark-results-*artifact from run 30976221483. It emits 40 rows whosebenchnames match the live gist's existing rows exactly, confirming theartifact round-trip preserves paths relative to
target/criterion.ifshort-circuits the environment gate is confirmed by run31066000517:
publish-cargo, which namesenvironment: release, completedas skipped on a
pull_requestfrom a non-tag ref rather than failing onthe policy.
actionlintreports the same eight pre-existing shellcheck notes asmain;no new findings.
pre-commitpasses.