feat(release): draft per-channel announcement workflow#725
Conversation
A workflow consuming `openemr-tag` (and exposed via `workflow_dispatch`) that renders per-channel release-announcement drafts for forum, chat, X, Facebook, LinkedIn, and the registered-users mailing list. The short-copy channels are surfaced inline in `$GITHUB_STEP_SUMMARY`; mailing list ships as `mail.html` + `mail.subject.txt` (the inputs to openemr-registration's `oe-sender.js`) plus a `mail.eml` preview the maintainer can drop into a mail client to eyeball the rendered HTML. This is the drafts-only first phase of openemr#711 — no posting, no SMTP, no OAuth. The maintainer copies the rendered output onto each channel by hand, and runs `oe-sender.js` against `mail.html` for the mailing list. Per-channel API integration follows once credentials are in place. The forum URL is rendered as a `{{FORUM_URL}}` placeholder by default; the maintainer find/replaces it once the Discourse thread exists. The workflow accepts an optional `forum_url` input on `workflow_dispatch` for a polished single-trigger render. Mirrors the existing `DockerHubOverviewRenderer` pattern: PHP renderer + Twig templates + Symfony Console binary + PHPUnit coverage. Assisted-by: Claude Code
There was a problem hiding this comment.
Pull request overview
Adds release-announcement drafting to the release tooling, producing per-channel copy (forum/chat/social + mailing list) from Twig templates and exposing outputs via a GitHub Actions workflow artifact + step summary.
Changes:
- Added
AnnouncementRenderer(Twig-based) and a CLI wrapper to render all announcement channels and write outputs (plus a.emlpreview). - Added per-channel announcement templates under
tools/release/templates/announcements/. - Added a
release-announcements.ymlworkflow to render drafts fromopenemr-tagdispatch (or manual inputs) and upload them as an artifact, with PHPUnit coverage for the renderer.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
tools/release/src/AnnouncementRenderer.php |
New Twig renderer to render all announcement channels from templates. |
tools/release/bin/render-announcements.php |
New CLI to render templates into an output directory and synthesize a preview mail.eml. |
tools/release/tests/AnnouncementRendererTest.php |
PHPUnit coverage for channel rendering, placeholders, subject formatting, and determinism. |
tools/release/templates/announcements/forum.md.twig |
Discourse/forum draft template. |
tools/release/templates/announcements/chat.md.twig |
Chat draft template. |
tools/release/templates/announcements/x.txt.twig |
X draft template. |
tools/release/templates/announcements/facebook.txt.twig |
Facebook draft template. |
tools/release/templates/announcements/linkedin.txt.twig |
LinkedIn draft template. |
tools/release/templates/announcements/mail.subject.txt.twig |
Mailing-list subject template. |
tools/release/templates/announcements/mail.html.twig |
Mailing-list HTML body template. |
.github/workflows/release-announcements.yml |
Workflow to derive inputs, render drafts, write step summary, and upload artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…2117) Step 15 of the runbook stays [Manual] because the actual posting is still copy/paste, but the drafting half landed in openemr/openemr-devops#725 (closes openemr/openemr-devops#719). Add a pointer so a release manager reading the runbook knows the drafts exist before they start writing announcements by hand. The Automation gaps row continues to point at openemr/openemr-devops#711 because the fan-out itself is unchanged — drafts-only is an intermediate phase, not closure. Assisted-by: Claude Code
- AnnouncementRenderer: enable filename-based autoescape so .html.twig
escapes interpolated values; .md/.txt remain unescaped. Defense in
depth — current inputs come from CI/maintainer, but autoescape is
free to enable.
- AnnouncementRenderer: fix doc comment ("mail.subject" → "mail.subject.txt").
- mail.html.twig: "Thanks goes" → "Thanks go", "github" → "GitHub",
reword the unsubscribe footer to match the mailto: link target.
- AnnouncementRendererTest: assert mail subject contains neither LF nor
CR (CR also enables header injection); approximate X's t.co URL
shortening (23 chars) before measuring tweet length.
- release-announcements.yml: bump actions/upload-artifact v4 → v7 to
match the rest of the repo.
Assisted-by: Claude Code
`jq -r` emits the literal string "null" for missing fields, so the prior `-z`-only check would let a malformed `openemr-tag` payload through with version/tag/branch set to "null" and produce artifacts referring to OpenEMR null. Switch to `jq -re '... // empty'` (raw output of empty when missing, exits non-zero) and add an explicit loop that fails the step with a clear error message if any field is empty or "null". Assisted-by: Claude Code
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
tools/release/tests/AnnouncementRendererTest.php:76
testForumUrlSubstitutesWhenProvided()only verifies substitution/removal of{{FORUM_URL}}in chat.md and mail.html, but facebook.txt and linkedin.txt also useforum_url. Add assertions for those outputs too so the substitution behavior is consistently covered across all affected channels.
self::assertStringContainsString($url, $rendered['chat.md']);
self::assertStringContainsString($url, $rendered['mail.html']);
self::assertStringNotContainsString('{{FORUM_URL}}', $rendered['chat.md']);
self::assertStringNotContainsString('{{FORUM_URL}}', $rendered['mail.html']);
}
|
|
||
| <tr><td align="left" style="font-size:0px;padding:0px 25px;word-break:break-word;"> | ||
| <div style="font-family:Arial, sans-serif;font-size:13px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;"> | ||
| <p style="text-align: left; margin: 10px 0;"><span style="text-align:left;font-size:13px;color:#55575d;font-family:Arial;line-height:15px;">OpenEMR needs donations for funding of critical features like maintaining ONC 2015 certification, API, FHIR, and a huge amount of other cool stuff. So please donate to the OpenEMR project: <a href="https://www.open-emr.org/donate/">https://www.open-emr.org/donate/</a></span></p> |
There was a problem hiding this comment.
@margarethaywood is working on an update to this verbiage.
There was a problem hiding this comment.
Leaving this paragraph as-is for now and waiting on @margarethaywood's revised copy. Happy to swap it in here or as a follow-up PR — whichever's easier.
Mirrors the release-rotation.yml pattern: a Symfony Console binary (bin/derive-announcement-inputs.php) wraps an AnnouncementDispatchPayload value object that validates each field against the canonical patterns from tools/release/contracts/dispatch.schema.json (version, tag, branch). The workflow's Derive step now pipes CLIENT_PAYLOAD into `task release:derive-announcement-inputs` instead of doing inline jq + shell-side null/empty checks. Trades a few lines of YAML for a tested PHP class — the validation rules now live next to the schema they enforce, missing/null/ malformed envelopes fail at parse time with a structured message, and the test suite covers each rejection path. Assisted-by: Claude Code
…L test - forum.md.twig + linkedin.txt.twig: drop the secondary "sponsor on GitHub" link per @stephenwaite's review — open-emr.org/donate already lists every donation route, so the second URL is noise. - AnnouncementRendererTest: extend the placeholder round-trip / substitute tests to cover facebook.txt and linkedin.txt as @copilot-pull-request-reviewer flagged. Pulled the channel list into a constant so adding the next forum-linking channel is a single edit. mail.html still has the longer donations paragraph; @margarethaywood is working on revised copy and will replace it in a follow-up. Assisted-by: Claude Code
Three workflow steps were doing real work in shell — input derivation, artifact rendering, step-summary emission. Pulled all of it into PHP: - AnnouncementDispatchPayload gains fromPayloadFile() so the bin script is just CLI plumbing. - AnnouncementRenderer exposes the FORUM_URL_PLACEHOLDER constant for reuse across the pipeline. - AnnouncementStepSummaryRenderer + step-summary.md.twig render the GHA step summary from the per-channel files; replaces ~25 lines of bash with two `summary`/`channel` shell functions. - bin/derive-announcement-inputs.php now accepts either --payload-file (repository_dispatch) OR --release-* flags (workflow_dispatch), so one task call covers both triggers — no more shell branching. - bin/render-announcement-step-summary.php (new) wraps the renderer. - Taskfile entries: release:render-announcements, release:render-announcement-step-summary; existing release:derive-announcement-inputs extended to accept release flags. - Bin scripts default --template-dir to dirname(__DIR__)/templates so they're robust to working-directory. Workflow steps are now either single `uses:` actions or single `task release:…` invocations. Remaining shell is the pipe + redirect plumbing at the workflow boundary — pure ≤3-line glue, no logic. PHPUnit grew from 121 to 136 tests; covers the dispatch/flag mode split, the step-summary placeholder hint, and missing-channel error handling. Assisted-by: Claude Code
- release-announcements.yml: replace `permissions: {}` with explicit
`contents: read` so checkout's required scope is documented at the
workflow level rather than relying on the runner's default token
behavior.
- AnnouncementRendererTest: switch the context() default from the
literal '{{FORUM_URL}}' string to AnnouncementRenderer::FORUM_URL_PLACEHOLDER
so the test follows the constant if the placeholder ever changes.
Assisted-by: Claude Code
derive-announcement-inputs / render-announcements / render-announcement-step-summary all routed `<error>...</error>` writes through OutputInterface, which points at stdout. The release-announcements workflow appends derive's stdout directly to $GITHUB_OUTPUT via `>>`; if derive fails, the error tag lines would land in the runner's output file and trigger an "Invalid format" step failure that obscures the real cause. Switched all three bin scripts to write errors via ConsoleOutputInterface::getErrorOutput() (stderr) and stdout stays strictly key=value lines (or empty on failure). Added a CLI integration test (Symfony Process) that exercises both the success and four failure paths and asserts stdout stays empty when the script fails. Assisted-by: Claude Code
Closes #719.
Summary
A workflow that consumes
openemr-tag(andworkflow_dispatch) and renders per-channel release-announcement drafts to a workflow artifact + step summary. The drafts-only first phase of the #711 fan-out — no posting, no SMTP, no OAuth.What ships
tools/release/templates/announcements/for forum (Discourse), chat, X, Facebook, LinkedIn, and the mailing list (HTML body + subject). Mailing list HTML is adapted from the 7.0.4 MJML-rendered template @bradymiller archived in feat(release): draft per-channel announcements as workflow outputs (no posting) #719.AnnouncementRenderer+bin/render-announcements.php— mirrors the existingDockerHubOverviewRendererpattern (PHP + Twig + Symfony Console)..github/workflows/release-announcements.yml— derives version/tag/branch from the dispatch payload (or workflow_dispatch inputs), runs the renderer, writes a per-channel step summary, uploadsrelease-announcementsartifact (8 files: 5 short-copy +mail.html+mail.subject.txt+mail.emlpreview).Maintainer flow
workflow_dispatchwithforum_urlset (or find/replaces{{FORUM_URL}}in the rendered output).release-announcementsartifact and runs the production sender from openemr-registration:oe-sender.jshard-codesSource: no-reply@open-emr.organd pulls recipients from the DynamoDBoe_registrationscan. The.emlis preview-only — drag it into a mail client to confirm the HTML renders correctly.Out of scope (deferred to per-channel follow-ons under #711)
openemr-registration, not here).Test plan
composer -d tools/release check(phpcs / phpstan / rector / require-checker / phpunit) — all pass.actionlint— clean.php tools/release/bin/render-announcements.php \ --output-dir=/tmp/announce-810 \ --release-version=8.1.0 --release-tag=v8_1_0 --release-branch=rel-810Inspected each output file;
{{FORUM_URL}}placeholder appears where expected; X copy is well under 280 chars;.emlopens in a mail client.gh workflow run release-announcements.yml -F version=8.1.0 -F tag=v8_1_0 -F branch=rel-810against this branch on the upstream repo (after merge it triggers off real tags).