Skip to content

ci: notify CODE to redeploy travisbreaks.org when a transmission publishes - #8

Draft
travisbreaks wants to merge 2 commits into
mainfrom
ci/notify-code-on-publish
Draft

travisbreaks wants to merge 2 commits into
mainfrom
ci/notify-code-on-publish

Conversation

@travisbreaks

@travisbreaks travisbreaks commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

What this does

Adds one file, .github/workflows/notify-code-deploy.yml. On a push to main that touches a build input, it sends a repository_dispatch to 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.yml checks this repo out, runs npm ci && npm run build, copies dist/ over travisbreaks-site/transmissions/, then ships travisbreaks-site to Netlify and Cloudflare Workers. Until now that refresh was opportunistic: it only ran when something else pushed to CODE's main, or when a human clicked workflow_dispatch over 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:

on:
  repository_dispatch:
    types: [transmissions-published]

This PR is the sender for that receiver. It POSTs to https://api.github.com/repos/travisbreaks/CODE/dispatches with event_type: transmissions-published and a client_payload carrying sha and ref. Plain curl plus jq, 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:

Field Value
Name CODE_DISPATCH_TOKEN
Resource owner travisbreaks
Repository access Only select repositories: travisbreaks/CODE
Repository permissions Contents: Read and write (nothing else)

Contents: Read and write is what the repository_dispatch REST endpoint is gated on. Grant no other permission. The built-in GITHUB_TOKEN cannot be used here: it is scoped to this repository and cannot dispatch into travisbreaks/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 curl without ever appearing in a process command line.

  • It is read through env: at the step level, never interpolated into the run: body, so it is not baked into the script by workflow expression expansion.
  • The Authorization header is passed via a curl config file (curl --config), not a -H argument. Anything in a process's argv is readable by every other process on the runner through ps or /proc/*/cmdline; a file written under RUNNER_TEMP with umask 077 is not. The heredoc that writes that file is deliberately unquoted, because the shell must expand $CODE_DISPATCH_TOKEN while writing it: curl performs no variable expansion of its own when reading a config file.

The job declares permissions: {}. Nothing in it uses GITHUB_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

push to main limited to paths that are inputs to astro build, plus workflow_dispatch for manual runs:

  • src/content/** (the transmissions and projects markdown itself)
  • src/content.config.ts (collection schemas; a sibling of src/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.ts decides which entries ship in a production build)
  • src/styles/** (global.css)
  • public/** (copied verbatim into dist/: timing sidecars, _headers, robots.txt, llms.txt, images)
  • astro.config.mjs (site, base: '/transmissions', sitemap)
  • package.json and package-lock.json (what npm ci on 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 outside src/content/, never picked up by a collection
  • scripts/**: manual tooling (OG images, narration, split). package.json has no pre/post build hook, build is bare astro build, so these never run during a build
  • assets/**: source art consumed by scripts/, not an Astro asset dir and not public/
  • tsconfig.json: types only
  • root *.md, .claude/**, .github/**

.github/** excludes this workflow from itself on purpose: editing the sender should not redeploy the apex. Use workflow_dispatch to exercise it.

Scope note on drafts

The drafts/** exclusion is about the top-level drafts/ directory only. It is not a claim that the workflow skips draft content in general.

src/content.config.ts gives the transmissions collection a draft field (z.boolean().default(false)), and src/lib/published.ts filters draft: true entries out of production enumeration. But such an entry is still a file under src/content/, so editing it does match src/content/** and does dispatch, even though the resulting dist/ 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

  1. Add CODE_DISPATCH_TOKEN per the table above.
  2. Merge this PR (or run from the branch, since workflow_dispatch is available on any branch once the file is on main; the first run must come from main).
  3. Actions tab on travisbreaks/transmissions > Notify CODE to redeploy travisbreaks.org > Run workflow on main.
  4. The step should log Dispatched transmissions-published to travisbreaks/CODE for <sha> on refs/heads/main.
  5. Watch CODE's Deploy travisbreaks.org runs. A new run should appear within seconds, triggered by repository_dispatch. repository_dispatch is not workflow_dispatch, so the github.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/.
  6. Confirm the apex serves the new content, and that the post-deploy health check including https://travisbreaks.org/transmissions/ passes.

Verification done here

  • ruby -ryaml parses the file cleanly. permissions reads back as an empty mapping and concurrency as a top-level key.
  • bash -n on the run block 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 runs shellcheck over run blocks): exit 0, no findings.
  • The extracted run block was executed against a curl shim that dumps its own argv, with a placeholder token. The token does not appear in argv, the config file is written mode 0600, and it contains the correctly expanded Authorization header. curl --config was separately confirmed to apply the header (a placeholder bearer token turns an anonymous 200 on /rate_limit into a 401).

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.

travisbreaks and others added 2 commits September 6, 2026 00:20
…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>
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.

1 participant