feat(workflows): release-notify + medal-bumper reusable workflows - #1
feat(workflows): release-notify + medal-bumper reusable workflows#1alioftech wants to merge 2 commits into
Conversation
Two reusable workflows that other Medal-Social repos consume via `uses:`:
- release-notify.yml — Triggered on push:prod from a calling repo. Diffs
HEAD~1..HEAD, extracts changeset/CHANGELOG content, builds a canonical
payload, and dispatches it to up to three destinations:
* Slack via the "Medal Releases" Slack app + chat + @chat-adapter/slack
* Google Chat via "Medal Releases" GChat app + chat + @chat-adapter/gchat
* Sanity via @sanity/client → NextMedal's collection.changelog
Per-channel adapter steps gate on (a) channel name in inputs.channels AND
(b) the relevant secret being non-empty. Failures log + continue (a paused
destination must not block the release pipeline).
- medal-bumper.yml — Pattern B agentic upgrade workflow modeled on the
pilot/MedalSocial-SDK auto-changeset flow. Daily mode bumps infra deps
(Node toolchain — vitest/biome/typescript/tsx/eslint/prettier/@types/node),
weekly mode bumps everything else. Per candidate: bump → install →
pnpm test && typecheck && build. Green bumps bundle into ONE PR per run;
red bumps get isolated single-dep PRs with the failure log embedded.
Claude (via ANTHROPIC_API_KEY) writes the PR description.
- example-consumer.yml — Reference-only docs file showing the consumer-side
opt-in pattern.
Adapter scripts live in scripts/ and run under Node 24 with
--experimental-strip-types (no transpile step). The Slack and GChat scripts
share lib/release-card.ts as the single source of truth for the Card JSX,
serialized to Block Kit and Cards v2 by the chat package's adapters.
README.md documents:
- Workflow usage and inputs
- Required org-level secrets and how to set them
- Manual app-creation steps for Slack and Google Chat
- Source-of-truth pointer to medal-social-best-practices/bots/
Required org secrets (set manually before consumers can succeed):
SLACK_RELEASES_BOT_TOKEN, GCHAT_RELEASES_BOT_TOKEN, SANITY_RELEASES_TOKEN,
SANITY_PROJECT_ID, SANITY_DATASET, ANTHROPIC_API_KEY.
Review Summary by QodoAdd release-notify and medal-bumper reusable workflows with adapter scripts
WalkthroughsDescription• Adds two reusable GitHub Actions workflows for org-wide dependency management - release-notify.yml posts release notifications to Slack, Google Chat, and Sanity - medal-bumper.yml performs agentic dependency upgrades with AI-generated PR descriptions • Implements TypeScript adapter scripts that run under Node 24 with native type stripping - Shared release-card.ts JSX component serialized to Block Kit and Cards v2 - Supports three notification destinations with independent failure handling • Includes comprehensive documentation and setup instructions for org secrets and manual app creation • Provides reference example consumer workflow showing opt-in pattern for downstream repos Diagramflowchart LR
A["Prod Release Push"] -->|triggers| B["release-notify.yml"]
B -->|builds payload| C["Canonical Payload"]
C -->|dispatches to| D["Slack Adapter"]
C -->|dispatches to| E["GChat Adapter"]
C -->|dispatches to| F["Sanity Adapter"]
D -->|posts via chat| G["Slack #releases"]
E -->|posts via chat| H["GChat Releases Space"]
F -->|writes via client| I["Sanity changelog"]
J["Daily/Weekly Schedule"] -->|triggers| K["medal-bumper.yml"]
K -->|enumerates outdated| L["Dependency Candidates"]
L -->|filters by mode| M["Infra or App Deps"]
M -->|bump + test each| N["Green vs Red Results"]
N -->|bundles greens| O["Single Batch PR"]
N -->|isolates reds| P["Individual Failure PRs"]
O -->|AI description| Q["Claude via Anthropic API"]
P -->|AI description| Q
File Changes1. .github/workflows/release-notify.yml
|
Code Review by Qodo
1. Wrong release diff base
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0217e8c48e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const blockContentType = defaultSchema | ||
| .get('blockContent') | ||
| .fields.find((f: { name: string }) => f.name === 'blockContent').type |
There was a problem hiding this comment.
Derive Sanity block type from a document field
defaultSchema.get('blockContent') returns the array type itself, so accessing .fields.find(...) on it will throw at runtime before htmlToBlocks runs. In practice this makes the Sanity adapter fail whenever the Sanity secrets are present, so no changelog document is written. The block content type needs to be resolved from a containing object/document field instead of from the array type directly.
Useful? React with 👍 / 👎.
| prev=$(git rev-parse HEAD~1 2>/dev/null || echo "") | ||
| curr=$(git rev-parse HEAD) |
There was a problem hiding this comment.
Diff against push base SHA, not HEAD~1
The payload builder uses HEAD~1 as the previous revision, which only covers the last commit in the push. For pushes containing multiple commits, the workflow can miss earlier user-facing changes, produce an incomplete compare URL, and even skip notifications if the last commit is docs/CI-only. Using github.event.before as the lower bound is needed to represent the full pushed release range.
Useful? React with 👍 / 👎.
| const branch = `medal-bumper/${MODE}-${todayStamp()}` | ||
| execSync(`git switch -c ${branch}`, { stdio: 'inherit' }) |
There was a problem hiding this comment.
Make bumper branch names unique per workflow run
The batch PR branch name is deterministic per mode/day (medal-bumper/<mode>-YYYY-MM-DD), so rerunning the workflow the same day can hit an existing remote branch and fail to push new commits. Because later spawnSync calls do not gate on failure, reruns can silently fail to create/update the expected PR. Add a unique suffix (for example, run ID) or explicitly handle existing branches.
Useful? React with 👍 / 👎.
| # First push to a branch — github.event.before is all-zeroes; bail. | ||
| if [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]]; then | ||
| echo 'First push to this branch — no previous commit to diff against; skipping notify.' | ||
| echo 'skip=true' >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| prev=$(git rev-parse HEAD~1 2>/dev/null || echo "") | ||
| curr=$(git rev-parse HEAD) | ||
| if [[ -z "$prev" ]]; then | ||
| echo 'No previous commit found; skipping notify.' | ||
| echo 'skip=true' >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
1. Wrong release diff base 🐞 Bug ≡ Correctness
release-notify.yml computes prev as HEAD~1 instead of using github.event.before, so multi-commit pushes/merges to prod will generate payloads/compare links from the wrong base and can incorrectly skip or misreport releases.
Agent Prompt
### Issue description
`release-notify.yml` diffs against `HEAD~1`, which is not the correct previous branch HEAD when a push contains multiple commits (common for merges). This can produce incorrect `changed` detection, incorrect summaries, and incorrect `compareUrl`.
### Issue Context
For `push` events, GitHub provides the previous branch head as `github.event.before`. That value should be used as the diff base.
### Fix Focus Areas
- .github/workflows/release-notify.yml[87-143]
### Suggested change
- Set `prev="${{ github.event.before }}"` and `curr="${{ github.sha }}"` (or `curr=$(git rev-parse HEAD)`), and ensure the commits are available (you already use `fetch-depth: 0`).
- Keep the all-zeroes guard for first-push and consider forced-push behavior explicitly if desired.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for (const g of greens) { | ||
| spawnSync('pnpm', ['up', `${g.name}@${g.to}`], { stdio: 'inherit' }) | ||
| } | ||
| spawnSync('git', ['add', 'package.json', 'pnpm-lock.yaml'], { stdio: 'inherit' }) | ||
|
|
||
| const commitMsg = `chore(deps): ${MODE} bump batch (${greens.length} packages)` | ||
| spawnSync('git', ['commit', '-m', commitMsg], { stdio: 'inherit' }) | ||
| spawnSync('git', ['push', '-u', 'origin', branch], { stdio: 'inherit' }) | ||
|
|
||
| const body = await writePrBodyForBatch(greens, MODE, REPO) | ||
| const bodyFile = path.join(mkdtempSync(path.join(tmpdir(), 'bumper-')), 'body.md') | ||
| writeFileSync(bodyFile, body, 'utf8') | ||
|
|
||
| spawnSync( | ||
| 'gh', | ||
| [ | ||
| 'pr', | ||
| 'create', | ||
| '--repo', | ||
| REPO, | ||
| '--base', | ||
| BASE_BRANCH, | ||
| '--head', | ||
| branch, | ||
| '--title', | ||
| commitMsg, | ||
| '--body-file', | ||
| bodyFile, | ||
| '--label', | ||
| 'dependencies', | ||
| '--label', | ||
| 'medal-bumper', | ||
| ], | ||
| { stdio: 'inherit' }, | ||
| ) | ||
| } | ||
|
|
||
| // ── 6. Isolate red bumps into individual PRs ─────────────────────────────── | ||
|
|
||
| for (const r of reds) { | ||
| console.log(`\n[bumper] Opening isolated PR for failing bump ${r.name}…`) | ||
| execSync(`git reset --hard ${baselineSha}`, { stdio: 'inherit' }) | ||
|
|
||
| const branch = `medal-bumper/needs-attention-${r.name.replace(/[@\/]/g, '-')}-${todayStamp()}` | ||
| execSync(`git switch -c ${branch}`, { stdio: 'inherit' }) | ||
|
|
||
| // Bump (don't run tests — the PR exists to surface the failure to humans). | ||
| spawnSync('pnpm', ['up', `${r.name}@${r.to}`], { stdio: 'inherit' }) | ||
| spawnSync('git', ['add', 'package.json', 'pnpm-lock.yaml'], { stdio: 'inherit' }) | ||
| spawnSync('git', ['commit', '-m', `chore(deps): bump ${r.name} to ${r.to} [needs attention]`], { stdio: 'inherit' }) | ||
| spawnSync('git', ['push', '-u', 'origin', branch], { stdio: 'inherit' }) | ||
|
|
There was a problem hiding this comment.
2. Command failures ignored 🐞 Bug ☼ Reliability
medal-bumper.ts uses spawnSync for critical commands (pnpm up, git commit, git push, `gh pr create`) but does not check exit codes, so it can print "Done" while failing to push branches or create PRs (especially for red bumps where pnpm up may fail again).
Agent Prompt
### Issue description
Critical steps can fail silently because `spawnSync` return values aren’t checked. This can lead to skipped PRs, unpushed branches, or failed commits with no hard failure.
### Issue Context
This is especially problematic in the red-bump PR path: the script attempts `pnpm up` again and proceeds to `git commit/push` even if the bump didn’t apply.
### Fix Focus Areas
- scripts/medal-bumper.ts[152-187]
- scripts/medal-bumper.ts[191-231]
### Suggested change
- Wrap `spawnSync` in a helper like `runOrThrow(cmd, args, opts)` that throws on `status !== 0` or `error`.
- For red bumps, if the bump command fails, skip PR creation (or create a PR/issue that only contains logs) rather than attempting to commit empty/no-op changes.
- Consider switching the critical `spawnSync` calls to `execSync` when you want hard-fail semantics.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…config - notify-sanity: bump apiVersion 2024-10-01 → 2025-12-23 to match NextMedal/src/sanity/lib/project.ts (where the schema is authored). - notify-gchat: use the real @chat-adapter/gchat config shape (`credentials: <SA JSON>`). The previous `serviceAccount` / `serviceAccountToken` keys do not exist in GoogleChatAdapterConfig and the adapter has no raw-token mode — fail fast with a clear error if the secret isn't valid JSON. - README: clarify GCHAT_RELEASES_BOT_TOKEN must be the full SA JSON blob.
|
Addressed open questions from the implementer (commit 72792b4): 1. Sanity 3.
Updated the secrets table in (auto-cs uses Slack only — no gchat reference implementation in customers/, so the API contract was verified directly from the published adapter.) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72792b4197
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| summary=$(git diff "$prev" "$curr" -- '**/CHANGELOG.md' \ | ||
| | grep -E '^\+' | grep -vE '^\+\+\+' | sed 's/^\+//' | head -40) |
There was a problem hiding this comment.
Match root CHANGELOG when extracting release summary
The CHANGELOG extraction uses git diff ... -- '**/CHANGELOG.md' while the step is running with set -euo pipefail. That pathspec does not match a top-level CHANGELOG.md, so in repos where the changelog lives at the root the pipeline on the next line receives no + lines and grep exits non-zero, which aborts payload generation and fails the notify job instead of sending notifications.
Useful? React with 👍 / 👎.
| import { createClient } from '@sanity/client' | ||
| import { JSDOM } from 'jsdom' | ||
| import { htmlToBlocks } from '@portabletext/block-tools' | ||
| import { Schema } from '@sanity/schema' |
There was a problem hiding this comment.
Declare @sanity/schema as a direct script dependency
notify-sanity.ts imports Schema from @sanity/schema, but scripts/package.json does not declare that package. On a clean runner this can fail with ERR_MODULE_NOT_FOUND before the adapter runs, which means Sanity changelog writes are skipped behind the non-fatal wrapper even when Sanity is enabled.
Useful? React with 👍 / 👎.
Summary
Two reusable workflows that other Medal-Social repos consume via
uses::release-notify.yml— POSTs release notifications onpush: prodto Slack(
#releasesvia the Medal Releases Slack app +chat/@chat-adapter/slack),Google Chat (the Releases space via the Medal Releases GChat app +
chat/@chat-adapter/gchat), and Sanity (NextMedal'scollection.changelogvia
@sanity/client). Per-channel adapter steps gate on channel name + secretpresence; failures log + continue (a paused destination must not block release).
medal-bumper.yml— Pattern B agentic upgrade workflow. Daily mode bumpsinfra deps (Node toolchain —
vitest,biome,typescript,tsx,eslint,prettier,@types/node); weekly mode bumps everything else. Each candidateis bumped + tested (
pnpm test && typecheck && build); green bumps bundleinto one PR, red bumps get isolated single-dep PRs with the failure log.
Claude (via
ANTHROPIC_API_KEY) writes the PR description.example-consumer.yml— reference-only file documenting the consumer-sideopt-in pattern.
Adapter scripts live in
scripts/and run under Node 24 with--experimental-strip-types(no transpile). The Slack and GChat scripts sharelib/release-card.tsas a single Card JSX source — thechatpackage serializesthe same Card to Block Kit and Cards v2.
Required org secrets (manual setup before consumers can succeed)
SLACK_RELEASES_BOT_TOKEN— bot token from the Medal Releases Slack appGCHAT_RELEASES_BOT_TOKEN— service-account JSON from the Medal Releases GChat appSANITY_RELEASES_TOKEN— write-scoped token from NextMedal's Sanity projectSANITY_PROJECT_ID— NextMedal Sanity project IDSANITY_DATASET— production dataset name (typicallyproduction)ANTHROPIC_API_KEY— for Medal Bumper PR descriptionsSee
README.mdfor the full setup walkthrough including the manual Slack + GChatapp-creation steps.
Source-of-truth docs
medal-social-best-practices/bots/(in the hacks repo) — was updated in thepreceding hacks-main commit to reflect the final v1 design (three destinations +
hybrid Renovate-narrow + Medal Bumper). This PR is the implementation of that spec.
Validation plan
After this PR merges and the org secrets are set:
pilot-talkadding the consumer-side opt-in workflows(release-notify, medal-bumper) + the security-only
renovate.json.pilot-talk; verify a message lands in Slack#releases, the Releases GChat space, and a newcollection.changelogdocument in NextMedal Sanity.
if any are outdated.
meda,MedalSocial-SDK,pilot,pencil-canvas,Picasso,medal-monorepo,NextMedal,auto-cs)per
medal-social-best-practices/bots/README.md.Test plan
README.mdand confirms the setup flow is unambiguous.chat-packageusage matches
auto-cs/lib/notifications/release.ts(the in-prod pattern).medal-bumper.tsand confirms the bundling logic(greens → 1 PR, reds → N PRs) matches the spec.
Don't merge until
GChat) within ~24h of merge, otherwise the workflows will silently no-op
(which is intentional —
if: secret-presentguards — but still defeats thepoint of merging the implementation).