Files here go into a new repo themegrill/.github:
.github/workflows/pr-build-zip.yml— the reusable workflow (the bot's logic).github/workflows/pr-build-zip.caller.yml— template to copy into each consuming repo
Steps 1–4 below require account and cloud access, so they are yours to do.
Create a GitHub user tg-autopilot with its own mailbox and 2FA; store the
recovery codes in the team vault. A machine user (not a GitHub App) is what the
pirate-bot reference uses, and it keeps one identity across both orgs.
Invite it to wpeverest (and later themegrill/Masteriyo) with write access to target repos.
Generate a fine-grained PAT, scoped to the target repos:
| Permission | Level |
|---|---|
| Contents | Read |
| Pull requests | Read and write |
| Issues | Read and write |
| Metadata | Read |
Set it as the org secret BOT_TOKEN.
The
comment-author: tg-autopilotline inpr-build-zip.ymlmust match this login exactly. If you pick a different name, update it there.
Create the artifact bucket. Cloudflare R2 is recommended over S3 — build ZIPs are large and R2 charges no egress.
Public read. Do not rely on --acl public-read; the workflow deliberately
does not send an ACL. Modern S3 buckets ship with Object Ownership set to
"Bucket owner enforced", which disables ACLs outright, and R2 has no ACLs at all.
Grant public read once, at the bucket level:
- R2 — enable public access (r2.dev subdomain, or attach a custom domain).
- S3 — a bucket policy allowing
s3:GetObjectto*onarn:aws:s3:::BUCKET/*.
Lifecycle rule: expire objects after 30 days. Without it this bucket grows
forever. Keep the number in step with the retention-days input, which is only
the text shown in the PR comment.
Then set the org secrets ARTIFACTS_KEY and ARTIFACTS_SECRET. Do not
make the bucket name itself a secret — pass it as the plain artifacts-bucket
input in each caller workflow instead, and point public-base-url at whatever
domain fronts the bucket.
A bucket name isn't sensitive, and treating it as one backfires: GitHub Actions masks any string matching any registered secret's value, wherever that string shows up. If the bucket name is a secret, it gets masked to
***insidepublic-base-urltoo — even though that's a plain input — which breaks the download link in the PR comment. Learned this the hard way during Phase 1 testing; keep only the actual credentials (access key ID and secret access key) as secrets.
A private reusable workflow cannot be called from a different organization.
Access is limited to the same repository, the same org/user, or the same
enterprise account. So Masteriyo/* repos cannot call a private
themegrill/.github. Options:
- Make
themegrill/.githubpublic (recommended). The workflow YAML holds no secrets — secrets stay per-org and are passed by the caller viasecrets: inherit. One copy for both orgs. - If both orgs sit under one GitHub Enterprise account, internal visibility works.
- Otherwise duplicate the workflow into a
Masteriyo/.github— which reintroduces exactly the drift this design exists to avoid.
Note that secrets: inherit only passes secrets within the same org or enterprise.
Cross-org callers must pass each secret explicitly instead:
secrets:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
ARTIFACTS_KEY: ${{ secrets.ARTIFACTS_KEY }}
ARTIFACTS_SECRET: ${{ secrets.ARTIFACTS_SECRET }}Copy pr-build-zip.caller.yml to the repo as .github/workflows/pr-build-zip.yml
and set build-command, zip-glob, and the target branches. Reuse the repo's
existing release target (gulp release, yarn dist, ...) so packaging rules such
as .distignore stay owned by the repo rather than the bot.
gh api users/tg-autopilotreturns the account;gh secret list --org wpeverestlistsBOT_TOKENand the three bucket secrets.- Open a draft PR → no build. Mark ready → build runs, one comment appears
authored by
tg-autopilot. - No-login download (the whole point of not using GitHub artifacts) —
with no credentials present:
Then confirm in a logged-out browser that it downloads, and that the ZIP installs via Plugins → Add New → Upload Plugin without unzipping first.
curl -sIL "<url>" | head -1 # expect 200 - Updated, not duplicated — push a second commit: still exactly one bot comment, new SHA and timestamp. Push a third immediately: the earlier run is cancelled by the concurrency group.
- Confirm the lifecycle rule (
aws s3api get-bucket-lifecycle-configuration, or the R2 equivalent). - Onboard a second repo with a different toolchain to prove the input contract.
Two failure modes this workflow avoids on purpose:
- Identifying its own comment by
comment.user.type == 'Bot'. A machine user is typeUser, so that test never matches and every push posts a new comment. We match on author login + the<!-- tg-autopilot: pr-build-zip -->marker. - Gating builds on a magic commit-message token such as
#build. It relies on people remembering it and gives reviewers nothing by default. We gate on draft status instead.