Skip to content

feat(release): draft per-channel announcement workflow#725

Merged
kojiromike merged 8 commits into
openemr:masterfrom
kojiromike:draft-announcements
May 14, 2026
Merged

feat(release): draft per-channel announcement workflow#725
kojiromike merged 8 commits into
openemr:masterfrom
kojiromike:draft-announcements

Conversation

@kojiromike
Copy link
Copy Markdown
Member

Closes #719.

Summary

A workflow that consumes openemr-tag (and workflow_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

  • Per-channel Twig templates under 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 existing DockerHubOverviewRenderer pattern (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, uploads release-announcements artifact (8 files: 5 short-copy + mail.html + mail.subject.txt + mail.eml preview).
  • PHPUnit coverage for the renderer.

Maintainer flow

  1. Tag fires → workflow runs → step summary shows ready-to-paste copy for forum / chat / X / Facebook / LinkedIn.
  2. Maintainer creates the Discourse thread, then re-runs the workflow via workflow_dispatch with forum_url set (or find/replaces {{FORUM_URL}} in the rendered output).
  3. Maintainer downloads the release-announcements artifact and runs the production sender from openemr-registration:
    node oe-sender.js -s "$(cat mail.subject.txt)" -f mail.html
    
    oe-sender.js hard-codes Source: no-reply@open-emr.org and pulls recipients from the DynamoDB oe_registration scan. The .eml is 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)

  • Actual posting to any channel (Discourse, X, Facebook, LinkedIn, chat).
  • SMTP / SES integration (lives in openemr-registration, not here).
  • Discourse thread auto-creation.

Test plan

  • composer -d tools/release check (phpcs / phpstan / rector / require-checker / phpunit) — all pass.
  • actionlint — clean.
  • Local CLI smoke test:
    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-810
    Inspected each output file; {{FORUM_URL}} placeholder appears where expected; X copy is well under 280 chars; .eml opens in a mail client.
  • gh workflow run release-announcements.yml -F version=8.1.0 -F tag=v8_1_0 -F branch=rel-810 against this branch on the upstream repo (after merge it triggers off real tags).

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
Copilot AI review requested due to automatic review settings May 14, 2026 14:10
@kojiromike kojiromike added the github_actions Pull requests that update GitHub Actions code label May 14, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .eml preview).
  • Added per-channel announcement templates under tools/release/templates/announcements/.
  • Added a release-announcements.yml workflow to render drafts from openemr-tag dispatch (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.

Comment thread tools/release/src/AnnouncementRenderer.php Outdated
Comment thread tools/release/src/AnnouncementRenderer.php Outdated
Comment thread tools/release/tests/AnnouncementRendererTest.php
Comment thread tools/release/tests/AnnouncementRendererTest.php
Comment thread tools/release/templates/announcements/mail.html.twig Outdated
Comment thread tools/release/templates/announcements/mail.html.twig Outdated
Comment thread tools/release/templates/announcements/mail.html.twig Outdated
Comment thread .github/workflows/release-announcements.yml
kojiromike added a commit to kojiromike/openemr that referenced this pull request May 14, 2026
…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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/release-announcements.yml Outdated
`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
Comment thread tools/release/templates/announcements/forum.md.twig Outdated
Comment thread tools/release/templates/announcements/linkedin.txt.twig Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 use forum_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']);
    }

Comment thread tools/release/tests/AnnouncementRendererTest.php Outdated

<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>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@margarethaywood is working on an update to this verbiage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@kojiromike kojiromike requested a review from stephenwaite May 14, 2026 15:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated no new comments.

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 19 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/release-announcements.yml Outdated
Comment thread tools/release/tests/AnnouncementRendererTest.php Outdated
- 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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 19 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/release-announcements.yml
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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

@kojiromike kojiromike merged commit 2979b8d into openemr:master May 14, 2026
11 checks passed
@kojiromike kojiromike deleted the draft-announcements branch May 14, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(release): draft per-channel announcements as workflow outputs (no posting)

3 participants