Skip to content

ci(github): tag and publish a GitHub Release when the release PR merge - #473

Merged
halcwb merged 7 commits into
informedica:masterfrom
7sharp9:ci/470-tag-and-publish-release
Aug 19, 2026
Merged

ci(github): tag and publish a GitHub Release when the release PR merge#473
halcwb merged 7 commits into
informedica:masterfrom
7sharp9:ci/470-tag-and-publish-release

Conversation

@7sharp9

@7sharp9 7sharp9 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Overview

Closes #470, the last open part of #234 item 2

ShipIt delivered the version bump and the changelog section; it cannot produce release artifacts.

This adds .github/workflows/tag-release.yml, which fires when a ShipIt release PR merges and:

  1. checks out the merge commit (pull_request.merge_commit_sha);
  2. runs scripts/ReleaseNotes.fsx to get the version, tag and Release body;
  3. creates an annotated v<version> tag on that commit;
  4. publishes a GitHub Release with that version's CHANGELOG.md section, flagged pre-release for a SemVer pre-release version.

Both write steps are idempotent, so a re-run recovers rather than duplicating.

Further information

  • Tag format: v-prefixed, so downstream workflows can filter on v*.
  • Tag target: the merge commit on master, not ShipIt's chore: release commit on the reused release/master branch.
  • Pre-release flag: derived from SemVer version, not from CHANGELOG.md front matter.
  • Build output: none; the tag and changelog body are the release artifact.

Why not use ShipIt's trigger: github.event.head_commit.message is not reliable for merge commits. The workflow instead watches release/master, which works regardless of merge method.

Why not read the pre-release flag from the changelog: pre_release: describes the next release, so it can drift over time. Versioning.isPreRelease derives it from the actual version.

About #459: the tag and release are created with GITHUB_TOKEN, which does not trigger workflows. The Docker publish therefore has to be in the same workflow, or use workflow_dispatch/repository_dispatch, or a PAT/App token.

Why parse in a script instead of YAML: the same version logic is used by CI, backfill, and local dry runs. Keeping it in Versioning.fsx avoids version-resolution drift.

AI usage disclosure

Claude code was used to assemble the documentation and verify some logic for ShipIt's mechanism for release tags etc.

Move the <Version> read out of CheckSolutionVersions.fsx into a loadable scripts/Versioning.fsx, so the release tooling added next resolves the version through the same code path.

Notes:

- More than one <Version> element is now an error rather than silently taking the first, since that would mean the file has stopped being the source of truth.
- The module also gains isPreRelease, deriving SemVer pre-release status from a version string. It belongs with the version semantics rather than with the release script that consumes it.

Refs informedica#470
scripts/Changelog.fsx extracts the section a version's GitHub Release body is built from.
It implements a small changelog grammar rather than parsing Markdown:
any line starting `## ` delimits a section, and both ShipIt's `## 0.1.2-alpha.4 - <date>` and the
hand-written `## [0.1.2-alpha.1] - <date>` headings that predate it are also recognised.

A lookup that fails says how it failed. Missing, empty and duplicated sections are distinguished rather
than collapsing into one message, and duplicates are rejected instead of silently taking the first
-- 7611fe0 removed a duplicate heading from this changelog, so that is not hypothetical.

scripts/ChangelogTests.fsx pins the grammar with Expecto, including a check that the shipped CHANGELOG.md
still has a section for the version in Directory.Build.props, which fails if ShipIt ever changes its heading format.

Refs informedica#470
Add tag-release.yml. When a ShipIt release PR merges it creates an annotated v<version> tag on the merge commit and publishes a GitHub Release carrying that version's CHANGELOG.md section, flagged pre-release for a SemVer pre-release version. Both steps are idempotent, so re-running is safe.

ShipIt 3.0.1 cannot create a tag or a Release, so nothing downstream of a merged release PR produced an immutable ref; this is the missing part of informedica#234 item 2.

The trigger deliberately does not use ShipIt's documented head_commit condition. All three release PRs so far merged as merge commits, so head_commit.message is always "Merge pull request #NNN from ..." and never starts with "chore: release " -- 0 for 3. The head ref is merge-method independent instead.

scripts/ReleaseNotes.fsx supplies the version, tag and Release body, so CI, a local dry run and the backfill of versions that shipped before this workflow existed all run the same code rather than three duplicates. Its argument parsing fails closed: an unknown flag, a repeated flag or a stray positional is an error, $GITHUB_OUTPUT is validated before anything is written.

Refs informedica#470
DEVELOPMENT.md's Release Automation section gains the tag and Release step: what it does, the trigger, why ShipIt's documented head_commit condition is not used, how to preview a Release body locally, and the GITHUB_TOKEN chaining constraint that decides the shape informedica#459 takes.

That constraint was confirmed against this repo rather than taken from the documentation. None of the three ShipIt release PRs, which were opened by github-actions[bot], ran its checks automatically: informedica#455 and informedica#458 had runs created but parked at action_required until a maintainer re-ran them, and informedica#464 got no pull_request runs at all until it was closed and reopened by hand. So, an on: release or on: push: tags: workflow will not fire from a tag this workflow creates.

ADR-0021 is amended with the release-artifact as design choice 7, and 0000-change-log.md gets a matching entry.

Refs informedica#470
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds automation that turns merged ShipIt release PRs into annotated version tags and GitHub Releases, along with release-note extraction tooling and updated release-process documentation.

  • Adds a pull-request-close workflow that tags the merge commit and publishes its changelog section.
  • Centralizes version and changelog handling for release automation.
  • Documents release triggers, pre-release handling, backfills, and downstream workflow constraints.

Confidence Score: 2/5

The PR should not merge until release jobs are restricted to genuine repository-owned ShipIt PRs and existing tags are validated against the intended merge commit.

The workflow can publish official artifacts for a merged non-ShipIt branch sharing the expected name, and its recovery path can publish a Release for an existing tag that points at the wrong revision.

Files Needing Attention: .github/workflows/tag-release.yml

Security Review

The release workflow authorizes publication using only the unqualified source branch name. A merged same-named branch from a fork or another non-ShipIt source can therefore invoke repository-write release automation. How this was verified: The job has contents: write permission and checks head.ref without checking head-repository identity, author, or the documented ShipIt label.

Important Files Changed

Filename Overview
.github/workflows/tag-release.yml Adds the release publication workflow, but its ShipIt identity gate and existing-tag recovery path can publish unintended or incorrectly targeted release artifacts.
DEVELOPMENT.md Documents the new release workflow, its trigger, local preview commands, and downstream GITHUB_TOKEN limitation.
docs/mdr/design-history/0021-build-system-versioning-and-release.md Amends the accepted release architecture with explicit tag-target, pre-release, backfill, and artifact decisions.
docs/mdr/design-history/0000-change-log.md Records the ADR amendment in the design-history change log.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Pull request closed against master] --> B{Merged and head.ref equals release/master?}
  B -- No --> C[Skip workflow]
  B -- Yes --> D[Checkout merge_commit_sha]
  D --> E[Extract version and changelog section]
  E --> F{Tag name already exists?}
  F -- No --> G[Create and push annotated tag]
  F -- Yes --> H[Accept existing tag without target validation]
  G --> I{GitHub Release exists?}
  H --> I
  I -- No --> J[Publish GitHub Release]
  I -- Yes --> K[Leave existing Release unchanged]
Loading

Reviews (1): Last reviewed commit: "docs(config): record the release-artifac..." | Re-trigger Greptile

Comment thread .github/workflows/tag-release.yml
Comment thread .github/workflows/tag-release.yml Outdated
7sharp9 and others added 3 commits August 19, 2026 18:09
0.1.2-alpha.2, .3 and .4 shipped before tag-release.yml existed and stay untagged rather than being tagged retroactively: a backfilled tag carries a tagger date unrelated to when the version shipped, and CHANGELOG.md plus the merge commits it links already reconstruct those three. The tag record therefore starts at the first release after this lands.

ADR-0021's settled-points table, the design-history entry and DEVELOPMENT.md now say so, and the comments claiming backfill as a caller of scripts/ReleaseNotes.fsx are corrected to CI plus local dry runs. The script still accepts an explicit version, which is what a dry run against an older release uses.

Refs informedica#470
Two findings from review of informedica#473.

The job guard tested only merged and head.ref, and head.ref is a bare branch name. A merged pull request from a fork with a branch called release/master would have reached the tag and Release writes. The guard now also requires head.repo.full_name to be this repository, which is always true of a ShipIt release PR.

The existing-tag check matched on name alone, so a tag pointing at some other commit was accepted and the Release published against it, leaving the version's immutable ref naming the wrong revision. It now compares the tag's target with the merge commit: equal means a re-run and the step is skipped, different fails the job. The comparison reads refs/tags/X^{} rather than refs/tags/X, because for an annotated tag the plain ref resolves to the tag object rather than the commit.

Refs informedica#470
@halcwb
halcwb merged commit f48579f into informedica:master Aug 19, 2026
4 of 7 checks passed
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.

Tag and publish a GitHub Release when the ShipIt release PR merges

2 participants