Skip to Content

← All archived runs

Run: ship-note-run-detection

run.md

Run: ship-note-run-detection

  • lane: chore
  • branch: claude/ship-note-run-detection-k6m371
  • pr: #800

lane/output/notes.md

Chore: ship-note-run-detection

  • invariant: nothing user-facing changes — no app, package, route or persona is touched. What differs is CI: ship-note.yaml now resolves the shipped run from the PR's file list instead of a local git diff against a commit the squash-merge destroyed, and goes red instead of green when it cannot resolve or cannot send. The note's content, the 60-word cap and the merge-gated authorisation model are untouched.
  • change: .github/workflows/ship-note.yaml: three fixes and one record correction (below).
  • rollback: revert the commit. The workflow returns to its previous behaviour — which is to say, to sending nothing. No migration, dependency, env var or index is involved.

1. Run detection (the defect)

git diff --name-only "$BASE_SHA" "$HEAD_SHA" cannot work on a merged PR. After a squash-merge the PR head commit is not reachable from main, and once the branch is deleted the object is gone, so fetch-depth: 0 never helped. On PR #798 it printed fatal: bad object a497cc98…, || true swallowed it, slugs came out empty and the job reported success having sent nothing.

Not specific to #798 — it has never worked. All six runs of this workflow since it landed on 2026-08-10 show Send: skipped, including the very first (run 31384388833, the metric-dictionary-and-claim-surface merge). No ship note has ever been delivered by it; the Resend account shows no ship-note mail at all.

Now: gh api repos/{repo}/pulls/{n}/files. Chosen over diffing merge_commit_sha against its first parent because the PR file list is correct for every merge strategy — a rebase-merge sets merge_commit_sha to the last rebased commit, so a first-parent diff would silently miss a ship note introduced in any earlier commit. That is the same class of silent miss being fixed here. It also needs no git history at all, so fetch-depth: 0 (a full clone of every branch in the repo) is gone; the checkout now pins merge_commit_sha, the exact tree the merge produced. Verified against #798's real file list: resolves demo-scope-visuals.

The path pattern matches the current 05_ship/ layout plus every layout still on disk — 06_ship/ from the six-stage runs, and the pre-expansion 03_release/ and 03_ship/ — the same fallbacks send-investor-update.sh already resolves. (Rebased onto the pipeline/.icm/ restructure of #803, which renumbered Ship from 06 to 05 and moved this run record; the pattern is anchored at .icm/runs/.)

2. Failing loudly

  • The || true is gone and the step runs under set -euo pipefail. An unresolvable file list is now ::error:: + exit 1 rather than a green "nothing to send".
  • Match extraction uses sed -n …p, not grep, so a genuine no-match (exit 0) stays distinguishable from a failure — the distinction the || true erased.
  • A determinate no-match is not failed, but it is no longer silent: it logs a ::warning:: naming every investor-update.md path the PR did touch, so the log explains itself. This case only arises if a PR deletes or renames a note.
  • The send-config guard now checks all three of RESEND_API_KEY, INVESTOR_RECIPIENTS and EMAIL_FROM, and names each missing one. Previously only RESEND_API_KEY was checked in the workflow; the rest surfaced as a one-line die from inside the script.

The sender is one variable now

INVESTOR_EMAIL_FROM is gone. It existed only as an optional override of EMAIL_FROM for ship-note mail, and nothing anywhere set or read it — the whole monorepo had exactly two mentions, both inside the fallback expression itself. EMAIL_FROM is the load-bearing name: the platform's own mail reads process.env.EMAIL_FROM (packages/services/src/email/send-html-email.ts) and it is catalogued in turbo.json's globalEnv. Removed from the script's from resolution, its config docs, the workflow's env: block and the guard; turbo.json's catalogue note is corrected to say the ship note has no sender of its own. One address, one variable to set, one thing to diagnose.

Unchanged: the github.event.pull_request.merged == true guard, so a closed-without-merge PR still sends nothing. permissions: gains pull-requests: read for the API call.

3. The send config is unset — an ops task, not a code fix

The failing run's step env printed all four blank. GitHub renders a set secret as ***, so blank means absent: RESEND_API_KEY, INVESTOR_RECIPIENTS, EMAIL_FROM and INVESTOR_EMAIL_FROM do not exist at repo level. No values are invented here. After the convergence above there are three to set, not four — RESEND_API_KEY, INVESTOR_RECIPIENTS, EMAIL_FROM — under Settings → Secrets and variables → Actions. Until they exist the job now fails loudly and posts to #alerts instead of passing quietly.

4. The demo-scope-visuals ship record

Both files were already on main and stale:

  • investor-update.mdDig deeper links filled: PR #798 and https://help.sustentus.com/changelog/2026-08-13-demo-scope-visuals. Body untouched (56 words, and the links line sits outside the cap by contract).
  • release.md — records the squash-merge (dd84b1e, 2026-08-13) and records the send honestly: it did not go out, with both reasons and the manual re-send instructions.

The help-centre host is help.sustentus.com — the custom domain on the Vercel help-centre project, confirmed serving the entry with a 200. Note for whoever reads the older notes: the three most recent runs used the help.sustentus.com alias, which sits behind Vercel deployment protection and 302s to SSO. Those links are not publicly openable. Not corrected here — out of this chore's scope, but worth a follow-up.