ci: notify CODE to redeploy travisbreaks.org when a transmission publishes - #8
Draft
travisbreaks wants to merge 2 commits into
Draft
travisbreaks wants to merge 2 commits into
travisbreaks wants to merge 2 commits into
Conversation
…ishes
travisbreaks.org/transmissions/ is served by the CODE monorepo, not by this
repo. CODE's deploy-netlify.yml checks this repo out, builds it, and copies
dist/ over travisbreaks-site/transmissions/ on every run. Until now that
refresh was opportunistic: it happened only when something else pushed to
CODE's main or a human clicked workflow_dispatch there, so a transmission
published here could sit off the apex indefinitely.
CODE PR #334 added the receiver:
on:
repository_dispatch:
types: [transmissions-published]
This is the sender. On a push to main that touches a build input, it POSTs
repository_dispatch (event_type transmissions-published, client_payload with
sha and ref) to travisbreaks/CODE via the GitHub REST API, using plain curl
and jq so no third-party action sees the token.
Path triggers cover only inputs to `astro build`: src/content/**,
src/content.config.ts, src/pages/**, src/layouts/**, src/components/**,
src/lib/**, src/styles/**, public/**, astro.config.mjs, package.json,
package-lock.json. Excluded because they cannot change dist/: drafts/**,
scripts/**, assets/**, tsconfig.json, root docs, .claude/**, .github/**
(including this file, so editing the sender does not redeploy the apex).
Requires a repo secret CODE_DISPATCH_TOKEN: a fine-grained PAT owned by
travisbreaks, scoped to travisbreaks/CODE only, with Contents: Read and
write. The built-in GITHUB_TOKEN cannot dispatch cross-repo. The workflow
fails fast with a clear message while the secret is absent.
Validated with ruby -ryaml, bash -n on the run block, and actionlint (which
runs shellcheck over run blocks); all clean. Not yet exercised: the secret
does not exist, so no live dispatch has been sent.
Co-Authored-By: Tadao <tadao@travisfixes.com>
…currency)
Four reviewer findings on .github/workflows/notify-code-deploy.yml:
1. permissions: {} instead of contents: read. The job never uses
GITHUB_TOKEN: it does no checkout and makes no API call against this
repo. The dispatch authenticates with CODE_DISPATCH_TOKEN.
2. The env-indirection comment claimed the token was kept "out of the
command line", which was false: it was expanded into curl's argv by
-H "Authorization: Bearer $CODE_DISPATCH_TOKEN", where any other
process on the runner could read it via ps or /proc/*/cmdline. Made
the claim true rather than deleting it. The Authorization header now
goes through a curl config file written to RUNNER_TEMP under
umask 077 and passed as --config. The heredoc is unquoted on purpose
so the shell expands the token while writing the file, because curl
does no variable expansion of its own. The comment now states only
what is true: env keeps the secret out of expression expansion, and
the config file keeps it out of argv.
3. Narrowed the draft-exclusion claim. The drafts/** exclusion covers
the top-level drafts/ directory only. src/content.config.ts gives
the transmissions collection a draft field, and an entry under
src/content/ with draft: true still matches src/content/**, so it
still dispatches even though src/lib/published.ts filters it out of
a production build. Path filters cannot read frontmatter.
4. Added top-level concurrency (group notify-code-deploy,
cancel-in-progress: true) so a newer publish supersedes an older
pending dispatch instead of queueing a redundant apex rebuild.
Verified: ruby -ryaml parses the file; actionlint exit 0; bash -n clean
on the run block extracted from the YAML (confirming the heredoc EOF
dedents to column 0 inside the block scalar); and the extracted block
run against a curl shim shows the token absent from argv with the
config file written mode 0600 and the header correctly expanded.
Co-Authored-By: Tadao <tadao@travisfixes.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds one file,
.github/workflows/notify-code-deploy.yml. On a push tomainthat touches a build input, it sends arepository_dispatchto the CODE monorepo so the apex site rebuilds and republishes the new transmission immediately.travisbreaks.org/transmissions/is not served from this repo. CODE's.github/workflows/deploy-netlify.ymlchecks this repo out, runsnpm ci && npm run build, copiesdist/overtravisbreaks-site/transmissions/, then shipstravisbreaks-siteto Netlify and Cloudflare Workers. Until now that refresh was opportunistic: it only ran when something else pushed to CODE'smain, or when a human clickedworkflow_dispatchover there. A transmission published here could sit off the apex indefinitely.The receiver it targets
CODE PR #334 added the receiving trigger to
.github/workflows/deploy-netlify.yml:This PR is the sender for that receiver. It POSTs to
https://api.github.com/repos/travisbreaks/CODE/dispatcheswithevent_type: transmissions-publishedand aclient_payloadcarryingshaandref. Plaincurlplusjq, no third-party actions, so nothing but GitHub's own API sees the token. Success is HTTP 204; anything else fails the job and prints the response body.Secret the Boss must create
The workflow reads a repository secret named
CODE_DISPATCH_TOKEN. It does not exist yet, and the workflow cannot run correctly until it does.Create it at Settings > Secrets and variables > Actions > New repository secret on
travisbreaks/transmissions, holding a fine-grained personal access token with exactly this scope:CODE_DISPATCH_TOKENtravisbreakstravisbreaks/CODEContents: Read and writeis what therepository_dispatchREST endpoint is gated on. Grant no other permission. The built-inGITHUB_TOKENcannot be used here: it is scoped to this repository and cannot dispatch intotravisbreaks/CODE.While the secret is missing, the job fails on the first line with a pointer to the header comment rather than firing an unauthenticated request.
How the token is handled
The secret reaches
curlwithout ever appearing in a process command line.env:at the step level, never interpolated into therun:body, so it is not baked into the script by workflow expression expansion.Authorizationheader is passed via a curl config file (curl --config), not a-Hargument. Anything in a process's argv is readable by every other process on the runner throughpsor/proc/*/cmdline; a file written underRUNNER_TEMPwithumask 077is not. The heredoc that writes that file is deliberately unquoted, because the shell must expand$CODE_DISPATCH_TOKENwhile writing it:curlperforms no variable expansion of its own when reading a config file.The job declares
permissions: {}. Nothing in it usesGITHUB_TOKEN: there is no checkout and no API call against this repository.Top-level
concurrency(group: notify-code-deploy,cancel-in-progress: true) means a newer publish supersedes an older pending dispatch rather than queueing a second, redundant apex rebuild.What triggers it
pushtomainlimited to paths that are inputs toastro build, plusworkflow_dispatchfor manual runs:src/content/**(the transmissions and projects markdown itself)src/content.config.ts(collection schemas; a sibling ofsrc/content/, so the glob above does not cover it)src/pages/**([slug].astro,index.astro,terminal.astro,rss.xml.ts)src/layouts/**(BaseLayout.astro)src/components/**(TransmissionCard,FloatingPlayer,ReadAlong,SEOHead,EmailCapture)src/lib/**(published.tsdecides which entries ship in a production build)src/styles/**(global.css)public/**(copied verbatim intodist/: timing sidecars,_headers,robots.txt,llms.txt, images)astro.config.mjs(site,base: '/transmissions', sitemap)package.jsonandpackage-lock.json(whatnpm cion the CODE runner installs)Deliberately excluded, because none of them can change the bytes under
dist/, and a dispatch costs a full apex rebuild plus two health-check passes:drafts/**: unpublished markdown outsidesrc/content/, never picked up by a collectionscripts/**: manual tooling (OG images, narration, split).package.jsonhas no pre/post build hook,buildis bareastro build, so these never run during a buildassets/**: source art consumed byscripts/, not an Astro asset dir and notpublic/tsconfig.json: types only*.md,.claude/**,.github/**.github/**excludes this workflow from itself on purpose: editing the sender should not redeploy the apex. Useworkflow_dispatchto exercise it.Scope note on drafts
The
drafts/**exclusion is about the top-leveldrafts/directory only. It is not a claim that the workflow skips draft content in general.src/content.config.tsgives thetransmissionscollection adraftfield (z.boolean().default(false)), andsrc/lib/published.tsfiltersdraft: trueentries out of production enumeration. But such an entry is still a file undersrc/content/, so editing it does matchsrc/content/**and does dispatch, even though the resultingdist/output is unchanged. Path filters cannot read frontmatter, so this cannot be fixed with a glob. Accepted as a rare and cheap false positive.Manual test, once the secret exists
CODE_DISPATCH_TOKENper the table above.workflow_dispatchis available on any branch once the file is onmain; the first run must come frommain).travisbreaks/transmissions> Notify CODE to redeploy travisbreaks.org > Run workflow onmain.Dispatched transmissions-published to travisbreaks/CODE for <sha> on refs/heads/main.repository_dispatch.repository_dispatchis notworkflow_dispatch, so thegithub.event_name == 'workflow_dispatch'gates on the optional sub-project builds stay false and only the ungated steps run:dj-archive,agency-roundtable, and the transmissions checkout/build/copy. That is exactly the set needed to refresh/transmissions/.https://travisbreaks.org/transmissions/passes.Verification done here
ruby -ryamlparses the file cleanly.permissionsreads back as an empty mapping andconcurrencyas a top-level key.bash -non therunblock extracted from the parsed YAML: no syntax errors. This also confirms the heredoc terminator dedents to column 0 inside the block scalar, which is the failure mode a heredoc in a YAML|block invites.actionlint(which runsshellcheckoverrunblocks): exit 0, no findings.runblock was executed against acurlshim that dumps its own argv, with a placeholder token. The token does not appear in argv, the config file is written mode0600, and it contains the correctly expandedAuthorizationheader.curl --configwas separately confirmed to apply the header (a placeholder bearer token turns an anonymous200on/rate_limitinto a401).Not verified
The secret does not exist yet, so the workflow has never been executed on a runner and no dispatch has been sent. The 204 response, the token scope being sufficient in practice, and the CODE receiver actually firing are all unconfirmed until step 3 above is run. Left as a draft for that reason.