From 01f0da643a410ce438d11fe551b756d4d48c61b6 Mon Sep 17 00:00:00 2001 From: Pavol Caban Date: Tue, 4 Aug 2026 16:33:13 +0200 Subject: [PATCH] ci: publish GitHub Releases for every released package The Releases page stopped updating after @webikon/webentor-core@0.15.3. Those existing releases are named `@scope/pkg@version` -- the changesets convention -- because the changesets action created them. When the release pipeline was revamped (changesets -> check-versions + publish-npm.mjs + release-tags.sh), nothing replaced that: the new flow only pushes git tags. So core-v0.15.4 through core-v0.15.7 exist as tags and are published on npm, but have no Release object. release-tags.sh now ensures three things per released package instead of one: the namespaced tag, a GitHub Release on it carrying that version's CHANGELOG section, and (mirrored packages only) the split-workflow dispatch. Also widens coverage beyond the mirrored packages. `configs` and `codemods` are npm-published but were never tagged at all, so they get `configs-v*` / `codemods-v*` tags with no split dispatch. The theme stays out -- it ships inside webentor-starter and has no independent distribution. Release titles are derived from each package's own manifest `name` rather than a hardcoded template, which reproduces the pre-changesets scheme exactly for the npm packages (@webikon/webentor-core@0.15.7) and yields the natural composer analogue for the PHP-only ones (webikon/webentor-setup@1.1.0). Tags keep the `-v` form the split workflows key off. The tag and Release checks are deliberately independent, so a version whose tag already exists but whose Release is missing gets backfilled on the next run -- which means merging this creates the missing Releases for all five current committed versions. Split dispatch stays gated on a freshly created tag so mirrors are never re-pushed. Changelog slicing tracks code fences, so a future entry containing a fenced `# comment` cannot silently truncate the release notes. Verified with `DRY_RUN=true` and `bash scripts/test-release.sh --tags` against live origin state: correctly reports core/setup/starter tags as existing while proposing their missing Releases, and proposes both tag and Release for configs/codemods. Notes extraction checked for all five changelogs, including the starter's `###` heading level and nested sub-bullets. Claude-Session: https://claude.ai/code/session_015gnVTZywLgQYkM6Epr2uW2 --- AGENTS.md | 29 +++++++++++--- scripts/release-tags.sh | 84 +++++++++++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2ebe751..62e9c49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -160,12 +160,29 @@ what is committed on `main`. 1. `scripts/check-versions.mjs` — aborts if any mirrored version source drifted. 2. `scripts/publish-npm.mjs` — publishes any npm package (`core`, `configs`, `codemods`) whose committed version is not on the registry yet. -3. `scripts/release-tags.sh` — pushes the namespaced tag (`core-v*`, - `setup-v*`, `starter-v*`) for any mirrored package version that has no tag - yet and dispatches the matching split workflow: - - `core-v*` -> `split-webentor-core.yml` (also pings Packagist) - - `setup-v*` -> `split-webentor-setup.yml` - - `starter-v*` -> `split-webentor-starter.yml` +3. `scripts/release-tags.sh` — for every released package (`core`, `configs`, + `codemods`, `setup`, `starter`; the theme ships inside the starter and has no + tag of its own) ensures three things exist for the committed version: + - **the namespaced tag** (`core-v*`, `configs-v*`, `codemods-v*`, `setup-v*`, + `starter-v*`) if it does not exist yet; + - **a GitHub Release** on that tag, titled with the package's canonical + manifest name (`@webikon/webentor-core@0.15.7`, + `webikon/webentor-setup@1.1.0`) and carrying that version's CHANGELOG + section as its notes; + - **a split-workflow dispatch**, for mirrored packages only: + - `core-v*` -> `split-webentor-core.yml` (also pings Packagist) + - `setup-v*` -> `split-webentor-setup.yml` + - `starter-v*` -> `split-webentor-starter.yml` + + The tag and Release checks are independent: a version whose tag exists but + whose Release is missing gets the Release backfilled on the next run. Split + dispatch only fires for a freshly created tag, so mirrors are never + re-pushed. Run with `DRY_RUN=true` to preview. + +The Release title deliberately reproduces the `@webikon/webentor-*@*` scheme the +changesets action used before it was removed, so the Releases page reads +continuously across the migration even though the underlying tags use +`-v`. No manual tag pushes are needed for a normal release. To re-run a mirror split manually, dispatch the split workflow with the tag as input, or push the tag diff --git a/scripts/release-tags.sh b/scripts/release-tags.sh index 1f62f3f..ee70028 100644 --- a/scripts/release-tags.sh +++ b/scripts/release-tags.sh @@ -1,10 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -# Pushes the namespaced release tag for every mirrored package whose -# committed version is not tagged yet, then dispatches the matching split -# workflow. The dispatch is required because tag pushes made with the -# workflow's GITHUB_TOKEN do not trigger `on: push: tags` workflows. +# For every released package, ensures three things exist for the committed +# version: the namespaced git tag, a GitHub Release carrying that version's +# CHANGELOG section, and (for mirrored packages) a split-workflow dispatch. +# +# The tag keeps the "-v" form the split workflows key off, while +# the Release is *titled* with the package's canonical manifest name — +# "@webikon/webentor-core@0.15.7" — restoring the scheme the changesets action +# used before it was removed. Nothing else creates Releases, so without this +# the Releases page silently stops updating while tags keep advancing. +# +# The tag and Release checks are independent on purpose: a package whose tag +# already exists but whose Release is missing gets the Release backfilled on the +# next run. Split dispatch stays gated on a freshly created tag so mirrors are +# never re-pushed. # # Requires: git remote "origin", gh CLI authenticated (GH_TOKEN in CI). # Set DRY_RUN=true to only report what would happen. @@ -14,33 +24,77 @@ cd "$ROOT_DIR" DRY_RUN="${DRY_RUN:-false}" -# namespace | version manifest | split workflow +# The theme is intentionally absent: it ships inside webentor-starter and has no +# independent distribution. +# +# namespace | manifest (name + version) | changelog | heading level | split workflow ("-" = not mirrored) SPECS=" -core|packages/webentor-core/package.json|split-webentor-core.yml -setup|packages/webentor-setup/composer.json|split-webentor-setup.yml -starter|packages/webentor-starter/composer.json|split-webentor-starter.yml +core|packages/webentor-core/package.json|packages/webentor-core/CHANGELOG.md|##|split-webentor-core.yml +configs|packages/webentor-configs/package.json|packages/webentor-configs/CHANGELOG.md|##|- +codemods|packages/webentor-codemods/package.json|packages/webentor-codemods/CHANGELOG.md|##|- +setup|packages/webentor-setup/composer.json|packages/webentor-setup/CHANGELOG.md|##|split-webentor-setup.yml +starter|packages/webentor-starter/composer.json|packages/webentor-starter/CHANGELOG.md|###|split-webentor-starter.yml " -while IFS='|' read -r ns manifest workflow; do +# Prints the CHANGELOG body for version $3 in file $1, whose entries sit at +# heading level $2. Any following heading ends the section — fence tracking keeps +# a "# comment" inside a code block from truncating the notes. +changelog_notes() { + awk -v heading="$2 $3" ' + /^```/ { fence = !fence } + $0 == heading { found = 1; next } + found && !fence && /^#/ { exit } + found { print } + ' "$1" +} + +while IFS='|' read -r ns manifest changelog heading workflow; do [ -z "$ns" ] && continue version="$(node -p "require('./${manifest}').version")" + name="$(node -p "require('./${manifest}').name")" tag="${ns}-v${version}" + title="${name}@${version}" + # --- Tag, plus the split dispatch for mirrored packages --------------------- if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then - echo "${tag} already exists on origin, skipping." + echo "${tag} already exists on origin." + elif [ "$DRY_RUN" = "true" ]; then + if [ "$workflow" = "-" ]; then + echo "[dry-run] Would tag ${tag}." + else + echo "[dry-run] Would tag ${tag} and dispatch ${workflow}." + fi + else + echo "Tagging ${tag}..." + git tag "$tag" 2>/dev/null || echo "Tag ${tag} already exists locally." + git push origin "refs/tags/${tag}" + + if [ "$workflow" != "-" ]; then + echo "Dispatching ${workflow}..." + gh workflow run "$workflow" -f tag="$tag" + fi + fi + + # --- GitHub Release -------------------------------------------------------- + if gh release view "$tag" >/dev/null 2>&1; then + echo "Release ${tag} already exists." continue fi + notes="$(changelog_notes "$changelog" "$heading" "$version")" + if [ -z "${notes//[[:space:]]/}" ]; then + echo "No ${heading} ${version} section in ${changelog}, falling back to a pointer." + notes="See [${changelog}](${changelog}) for details." + fi + if [ "$DRY_RUN" = "true" ]; then - echo "[dry-run] Would tag ${tag} and dispatch ${workflow}." + echo "[dry-run] Would create release ${tag} titled \"${title}\"." continue fi - echo "Tagging ${tag} and dispatching ${workflow}..." - git tag "$tag" 2>/dev/null || echo "Tag ${tag} already exists locally." - git push origin "refs/tags/${tag}" - gh workflow run "$workflow" -f tag="$tag" + echo "Creating release ${tag} (\"${title}\")..." + printf '%s\n' "$notes" | gh release create "$tag" --title "$title" --notes-file - done <