Skip to content

ci: read TEND_BOT_TOKEN from an environment in every job - #3748

Merged
max-sixty merged 3 commits into
mainfrom
tend-environment-gate
Aug 6, 2026
Merged

ci: read TEND_BOT_TOKEN from an environment in every job#3748
max-sixty merged 3 commits into
mainfrom
tend-environment-gate

Conversation

@max-sixty

@max-sixty max-sixty commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes worktrunk's half of the tend environment migration (tend's TODO.md,
"Finish moving the operational secrets into the tend environment", 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_TOKEN outside tend's generated set.

What changed

Job Environment Why that one
benchmarks.yaml append-gist (new) tend schedule-gated, so it runs on main
benchmarks.yaml create-issue-on-benchmark-failure tend already schedule-gated
nightly.yaml create-issue-on-nightly-failure tend already schedule-gated
release.yaml publish-winget release runs on a v* tag push
release.yaml publish-homebrew release runs on a v* tag push

Every job reading TEND_BOT_TOKEN now names an environment, so deleting the
repo-level copy breaks nothing.

The gist append moved into its own job

The benchmarks job has no if gate, so putting tend on it would refuse a
workflow_dispatch against a non-main ref — on-demand runs against a chosen
branch are what that trigger is documented for. A job GitHub skips never
requests its environment, so moving the append into a schedule-gated
append-gist job keeps the policy off the dispatch path entirely. It reads
target/criterion back from the artifact the benchmarks job already
uploads.

create-issue-on-benchmark-failure now needs both jobs, so a failed append
still files an issue — previously it failed the benchmarks job directly.

Why the release jobs get release, not tend

A 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's
check_environment pins that policy to exactly the protected branches and its
--fix deletes anything else. release already exists with a v* tag policy
and already holds AUR_SSH_PRIVATE_KEY, so no new environment and no new
credential — TEND_BOT_TOKEN is seeded into it as a second copy.

deployment: false

Jobs naming tend use the mapping form. GitHub files a deployment record for
every job that names an environment, against whatever ref the run belongs to;
under pull_request_target that is the PR's own head, which is why PR
timelines grew a "worktrunk-bot deployed to tend" line on every push.
deployment: false drops the record and keeps the gate. The release jobs keep
their records, which land in no PR timeline.

Follow-up: one step, after merge

TEND_BOT_TOKEN is already seeded into the release environment (read
from the local worktrunk-bot gh config dir at
~/.config/gh-bots/worktrunk-bot, so no new credential was minted and nothing
was pasted). Verified: the token resolves to worktrunk-bot and has push on
both max-sixty/winget-pkgs and max-sixty/homebrew-worktrunk.

That leaves one step, and it must come after this PR merges:

gh secret delete TEND_BOT_TOKEN --repo max-sixty/worktrunk

Not before. On main today the gist append, both create-issue-on-*-failure
jobs, 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-allowlist still fails on CLAUDE_CODE_OAUTH_TOKEN, also at
    repo level. It cannot be read back either; separate item.
  • environment-deployments still fails on the generated tend-*.yaml, which
    pin tend 0.1.13 and carry the bare environment: tend. Those are
    regenerated by the published tend, and a regen also carries an unrelated
    gh api --paginate fix in tend-mention.yaml, so it belongs in its own PR.

Verification

  • The append-gist script was run end-to-end against a real
    benchmark-results-* artifact from run 30976221483. It emits 40 rows whose
    bench names match the live gist's existing rows exactly, confirming the
    artifact round-trip preserves paths relative to target/criterion.
  • That a skipped if short-circuits the environment gate is confirmed by run
    31066000517: publish-cargo, which names environment: release, completed
    as skipped on a pull_request from a non-tag ref rather than failing on
    the policy.
  • actionlint reports the same eight pre-existing shellcheck notes as main;
    no new findings. pre-commit passes.

This was written by Claude Code on behalf of max-sixty

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 worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/CLAUDE.md Outdated
Comment thread .github/CLAUDE.md Outdated
Comment thread .github/CLAUDE.md Outdated

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/nightly.yaml Outdated
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.
@max-sixty
max-sixty merged commit 59c7320 into main Aug 6, 2026
44 checks passed
@max-sixty
max-sixty deleted the tend-environment-gate branch August 6, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants