From 006752dc21d42028f8674cb79851393e364d2f6f Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sun, 4 Oct 2026 01:20:10 -0700 Subject: [PATCH 1/7] feat(sync-release-notes): run automatically as part of the standard v4 set The sync-release-notes caller becomes a standard companion every v4 repo carries, and it no longer waits for a manual dispatch: - Template triggers: release: published (releases are created with the bot App token, so the event fires), push to the default branch touching docs/changelog/** or docs/changelogs/** (a companion, not a required check, so the paths filter is fine), and workflow_dispatch with all the switches. - Automatic runs apply changes: sync bodies from the changelog files and re-publish draft releases whose tag exists. Creating missing tags and releases stays manual (dispatch switches). - Tag creation refuses to run without the bot GPG key, and flags a tag that came out unsigned because GitHub forced the REST fallback. - Idempotent full sweep: a run with nothing to change says so. Drift that needs a decision is a warning; the job fails only when an attempted repair fails. - Least privilege: GITHUB_TOKEN is contents: read; every write uses the App token. Runs queue per repo. - Docs list it in the standard set (README, examples/README, WORKFLOW-SETUP-GUIDE, AGENT-SCAFFOLDING, release-flow-v4). Refs #362 --- .../steps/sync-release-notes/action.mjs | 46 +++++++++--- .../steps/sync-release-notes/action.yml | 4 +- .../workflows/local-sync-release-notes.yml | 69 +++++++++++------- .../workflows/workflow-sync-release-notes.yml | 19 +++-- README.md | 2 +- docs/conventions/release-flow-v4.md | 2 +- examples/README.md | 16 +++-- examples/guides/AGENT-SCAFFOLDING.md | 1 + examples/guides/WORKFLOW-SETUP-GUIDE.md | 72 ++++++++++--------- .../release-companions/sync-release-notes.yml | 66 +++++++++++------ 10 files changed, 190 insertions(+), 107 deletions(-) diff --git a/.github/actions/github/steps/sync-release-notes/action.mjs b/.github/actions/github/steps/sync-release-notes/action.mjs index 12703f1f..c26ea495 100644 --- a/.github/actions/github/steps/sync-release-notes/action.mjs +++ b/.github/actions/github/steps/sync-release-notes/action.mjs @@ -117,11 +117,12 @@ for (const r of releases) { const rows = []; let changed = 0; let problems = 0; +let failures = 0; for (const version of [...versions].sort(cmpVersion)) { if (versionFilter.length && !versionFilter.includes(version)) continue; const tagName = `v${version}`; - const row = { version, tag: "", release: "", changelog: "", actions: [], issues: [] }; + const row = { version, tag: "", release: "", changelog: "", actions: [], issues: [], failures: [] }; let tagSha = tags.get(version) || ""; const relCommit = releaseCommits.get(version) || ""; const rels = releases.filter((r) => r.tag_name === tagName); @@ -144,6 +145,10 @@ for (const version of [...versions].sort(cmpVersion)) { } else if (!relCommit) { row.tag = "missing"; row.issues.push("no tag and no release commit on the default branch"); + } else if (createTags && !dryRun && !gpg.gpg_private_key) { + // Release tags must be bot-signed; never create an unsigned one. + row.tag = `missing (release commit ${relCommit.slice(0, 7)})`; + row.failures.push("tag not created: no bot GPG key provided (release tags must be signed)"); } else if (createTags && !dryRun) { try { await createTag({ token, repo: repoFull, tag: tagName, sha: relCommit, message: tagName, push: true, ...gpg }); @@ -151,9 +156,15 @@ for (const version of [...versions].sort(cmpVersion)) { row.tag = `created at ${relCommit.slice(0, 7)}`; row.actions.push("created tag"); changed++; + // tag/create falls back to the REST API (annotated, NOT signed) when + // GitHub refuses an App push of a tag on an older commit. Say so loudly. + git(["fetch", "--force", "origin", `+refs/tags/${tagName}:refs/tags/${tagName}`]); + if (!/BEGIN PGP SIGNATURE/.test(git(["cat-file", "-p", `refs/tags/${tagName}`]))) { + row.failures.push("tag was created WITHOUT a signature (GitHub refused the signed push; REST fallback used) โ€” re-sign it by hand"); + } } catch (e) { row.tag = "missing"; - row.issues.push(`tag creation failed: ${e.message}`); + row.failures.push(`tag creation failed: ${e.message}`); } } else { row.tag = `missing (release commit ${relCommit.slice(0, 7)})`; @@ -183,7 +194,7 @@ for (const version of [...versions].sort(cmpVersion)) { row.actions.push("created release"); changed++; } catch (e) { - row.issues.push(`release creation failed: ${e.message}`); + row.failures.push(`release creation failed: ${e.message}`); } } } else { @@ -215,15 +226,16 @@ for (const version of [...versions].sort(cmpVersion)) { row.release = updated.draft ? "draft" : "published"; row.actions.push(`updated ${what}`); changed++; - if (patch.draft === false && updated.draft) row.issues.push("still a draft after publishing"); + if (patch.draft === false && updated.draft) row.failures.push("still a draft after publishing"); } catch (e) { - row.issues.push(`update failed: ${e.message}`); + row.failures.push(`update failed: ${e.message}`); } } } } problems += row.issues.length; + failures += row.failures.length; rows.push(row); } @@ -231,14 +243,28 @@ const esc = escapeTableCell; let md = `## ๐Ÿ“ Release notes sync โ€” ${repoFull}${dryRun ? " (dry run)" : ""}\n\n`; md += "| Version | Tag | Release | Changelog | Actions | Problems |\n|---|---|---|---|---|---|\n"; for (const r of rows) { - md += `| ${r.version} | ${esc(r.tag)} | ${esc(r.release)} | ${esc(r.changelog || "โ€”")} | ${esc(r.actions.join("; ") || "โ€”")} | ${esc(r.issues.join("; ") || "โ€”")} |\n`; + md += `| ${r.version} | ${esc(r.tag)} | ${esc(r.release)} | ${esc(r.changelog || "โ€”")} | ${esc(r.actions.join("; ") || "โ€”")} | ${esc([...r.failures.map((f) => `โŒ ${f}`), ...r.issues].join("; ") || "โ€”")} |\n`; +} +const planned = rows.reduce((n, r) => n + r.actions.filter((a) => a.startsWith("would ")).length, 0); +if (changed === 0 && planned === 0 && failures === 0) { + md += "\nโœ… Nothing to change โ€” every release already matches its changelog file.\n"; +} else if (dryRun) { + md += `\n${planned} change(s) would be made (dry run).\n`; +} else { + md += `\n${changed} change(s) applied.\n`; } -md += `\n${changed} change(s) applied, ${problems} problem(s) left.\n`; +if (problems > 0) + md += `\n${problems} item(s) need a manual decision (see Problems; the dispatch switches can repair missing tags/releases).\n`; +if (failures > 0) md += `\nโŒ ${failures} attempted repair(s) failed.\n`; console.log(md); appendSummary(md); -setOutputs({ "changed-count": String(changed), "problems-count": String(problems) }); +setOutputs({ "changed-count": String(changed), "problems-count": String(problems), "failures-count": String(failures) }); -if (!dryRun && problems > 0) { - console.error(`::error::${problems} release/tag problem(s) remain โ€” see the job summary.`); +// Fail only when something this run attempted did not work. Pre-existing drift +// that needs a decision (old tags with no release, release commits with no tag) +// is reported as a warning, so the automatic runs don't stay red forever. +if (problems > 0) console.warn(`::warning::${problems} release/tag item(s) need a manual decision โ€” see the job summary.`); +if (failures > 0) { + console.error(`::error::${failures} attempted repair(s) failed โ€” see the job summary.`); process.exitCode = 1; } diff --git a/.github/actions/github/steps/sync-release-notes/action.yml b/.github/actions/github/steps/sync-release-notes/action.yml index 793c6f55..78bbdd34 100644 --- a/.github/actions/github/steps/sync-release-notes/action.yml +++ b/.github/actions/github/steps/sync-release-notes/action.yml @@ -59,7 +59,9 @@ outputs: changed-count: description: "Number of tags/releases created or updated." problems-count: - description: "Number of problems left (drafts, missing tags/releases, duplicates, failures)." + description: "Number of items left that need a manual decision (drafts not published, missing tags/releases, duplicates)." + failures-count: + description: "Number of repairs this run attempted that failed. The step fails only when this is non-zero." runs: using: node24 diff --git a/.github/workflows/local-sync-release-notes.yml b/.github/workflows/local-sync-release-notes.yml index 428d0901..b803b717 100644 --- a/.github/workflows/local-sync-release-notes.yml +++ b/.github/workflows/local-sync-release-notes.yml @@ -7,23 +7,42 @@ # @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. # -# Dogfood: runs the local workflow-sync-release-notes.yml against this repo. +# Dogfood: runs the local workflow-sync-release-notes.yml against this repo +# (relative uses:, so a dispatch from a branch tests that branch's version). # -# Manual repair tool. Rewrites this repository's GitHub Release bodies from the -# committed per-version changelog files (docs/changelog[s]/v/v.md) -# โ€” the way to give releases made before a changelog existed (or with a -# backfilled one) their curated notes. Keeps the Contributors and coverage -# blocks, drops the duplicated "release: vX.Y.Z - โ€ฆ" subject line, neutralizes -# accidental @word mentions. +# Part of the STANDARD v4 set โ€” every v4 repo carries this file. # -# Also reports release/tag drift: draft releases, versions with no tag, tags -# with no release, duplicate releases. Dry run by default; each repair is its -# own opt-in. Results land in the job summary. +# Keeps the GitHub Release pages in step with the committed per-version +# changelog files (docs/changelog[s]/v/v.md): rewrites each +# release body from its file, keeping the Contributors and coverage blocks, +# dropping the duplicated "release: vX.Y.Z - โ€ฆ" subject line and neutralizing +# accidental @word mentions. Also reports release/tag drift (draft releases, +# versions with no tag, tags with no release, duplicate releases). # -# Relative uses: so a dispatch from a branch tests that branch's version. +# Runs automatically: +# - when a release is published (the publish workflow creates releases with +# the bot App token, so release:published fires โ€” a GITHUB_TOKEN-created +# release would not trigger workflows); +# - when a changelog file lands on the default branch (backfills and fixes). +# Automatic runs apply changes: they sync bodies and re-publish draft releases +# whose tag exists (CI-created drafts โ€” CLDMV/.github#362). Creating missing +# tags and missing releases stays manual: use the dispatch switches. +# Every run is an idempotent full sweep โ€” with nothing to change it changes +# nothing and says so. Not a required check (companion workflow), so the +# `paths:` filter below is fine here, unlike in ci.yml. +# +# Thin caller: all logic lives in workflow-sync-release-notes.yml@v4, which +# also queues overlapping runs per repo and picks the runner. name: ๐Ÿ“ Sync Release Notes on: + release: + types: [published] + push: + branches: [master, main] + paths: + - "docs/changelog/**" + - "docs/changelogs/**" workflow_dispatch: inputs: dry_run: @@ -36,8 +55,13 @@ on: type: string required: false default: "" + publish_drafts: + description: "Publish draft releases whose tag exists" + type: boolean + required: false + default: true create_missing_tags: - description: "Create missing vX.Y.Z tags at their release commits (signed)" + description: "Create missing vX.Y.Z tags at their release commits (bot-signed)" type: boolean required: false default: false @@ -46,11 +70,6 @@ on: type: boolean required: false default: false - publish_drafts: - description: "Publish draft releases whose tag exists" - type: boolean - required: false - default: false normalize_all: description: "Also tidy bodies of releases with no changelog file" type: boolean @@ -62,16 +81,18 @@ permissions: jobs: sync: + # All writes go through the bot App token; GITHUB_TOKEN only reads. permissions: - contents: write + contents: read uses: ./.github/workflows/workflow-sync-release-notes.yml with: - dry_run: ${{ inputs.dry_run }} - versions: ${{ inputs.versions }} - create_missing_tags: ${{ inputs.create_missing_tags }} - create_missing_releases: ${{ inputs.create_missing_releases }} - publish_drafts: ${{ inputs.publish_drafts }} - normalize_all: ${{ inputs.normalize_all }} + # Automatic runs (release / push) apply changes; manual runs use the switches. + dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + versions: ${{ inputs.versions || '' }} + publish_drafts: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_drafts }} + create_missing_tags: ${{ github.event_name == 'workflow_dispatch' && inputs.create_missing_tags }} + create_missing_releases: ${{ github.event_name == 'workflow_dispatch' && inputs.create_missing_releases }} + normalize_all: ${{ github.event_name == 'workflow_dispatch' && inputs.normalize_all }} secrets: BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/workflow-sync-release-notes.yml b/.github/workflows/workflow-sync-release-notes.yml index 5c4cf98a..0a199920 100644 --- a/.github/workflows/workflow-sync-release-notes.yml +++ b/.github/workflows/workflow-sync-release-notes.yml @@ -5,10 +5,14 @@ # and report โ€” optionally repair โ€” release/tag drift: draft releases, versions # with no tag, tags with no release, duplicate releases. # -# Consumers call it from a workflow_dispatch wrapper -# (examples/individual-repo-workflows/release-companions/sync-release-notes.yml). -# Runs against the CALLING repository with the bot App token. Dry run by -# default; every repair is a separate opt-in. See CLDMV/.github#362. +# Part of the standard v4 set: every repo carries the caller +# (examples/individual-repo-workflows/release-companions/sync-release-notes.yml), +# which runs it automatically when a release is published and when a changelog +# file lands on the default branch, and by manual dispatch. Runs against the +# CALLING repository with the bot App token. Concurrent runs for one repo queue +# (one running + the newest pending); every run is a full, idempotent sweep, so +# a superseded pending run loses nothing. The inputs default to a dry run; the +# caller passes the automatic-run settings. See CLDMV/.github#362. name: ๐Ÿ“ Sync Release Notes (Org-Level) on: @@ -70,13 +74,16 @@ jobs: sync: name: "๐Ÿ“ Sync release notes" runs-on: ${{ inputs.runs_on != '' && inputs.runs_on || (vars.RUNS_ON_DEFAULT != '' && vars.RUNS_ON_DEFAULT || ((github.repository_owner == 'CLDMV' && github.event.repository.private) && 'cldmv-runners' || 'ubuntu-latest')) }} + # Every write (release PATCH/POST, tag push) goes through the bot App + # token, so the workflow GITHUB_TOKEN only needs to read. permissions: - contents: write + contents: read concurrency: group: sync-release-notes-${{ github.repository }} cancel-in-progress: false steps: - # App token: creating a tag on a release commit that touches + # App token: release edits and tag pushes run as the bot. Creating a + # tag on a release commit that touches # .github/workflows/ needs the `workflows` scope the Actions # GITHUB_TOKEN can never hold. - name: Create App token (auto-detect) diff --git a/README.md b/README.md index b24fbfbf..8a0f0a01 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Shared GitHub Actions workflows for the CLDMV organization. These workflows ship a complete CI / release / publish pipeline tuned for the **v4 staging-branch release flow** โ€” feature PRs land on `next`, urgent work on `hotfixes`, and `master` is a clean release-only history. New repos should adopt v4 directly; existing v3 repos have a [migration guide](docs/migration/v3-to-v4.md). 1. **Adopt the v4 release-flow workflows** โ€” copy the set from [`examples/individual-repo-workflows/release-flow-v4/`](examples/individual-repo-workflows/release-flow-v4/) into your repo's `.github/workflows/`. These are adopted as a set (they depend on each other). -2. **Copy the core CI / publish / tag / bundle-size templates** from [`examples/individual-repo-workflows/core-cicd/`](examples/individual-repo-workflows/core-cicd/) (every v4 repo carries all of them except the v3-only `release.yml`), update `package_name` to your NPM package name, and set `bundle-size.yml`'s `build_command` + `dist_paths` to the repo's real build and published files. Add the security / automation templates you want from the other subfolders. +2. **Copy the core CI / publish / tag / bundle-size templates** from [`examples/individual-repo-workflows/core-cicd/`](examples/individual-repo-workflows/core-cicd/) (every v4 repo carries all of them except the v3-only `release.yml`), update `package_name` to your NPM package name, and set `bundle-size.yml`'s `build_command` + `dist_paths` to the repo's real build and published files. Also copy the standard release companions from [`examples/individual-repo-workflows/release-companions/`](examples/individual-repo-workflows/release-companions/): `tag-health.yml`, `master-commit-audit.yml` and `sync-release-notes.yml`. Add the security / automation templates you want from the other subfolders. 3. **Bootstrap the repo** โ€” applies branches + rulesets + security toggles + repo settings in one shot. Two ways: - **Org-wide fanout (recommended for โ‰ฅ3 repos)** โ€” dispatch `org-onboarding.yml` from the org's **private** org-admin repo (template: [`examples/individual-repo-workflows/packaging-docs/org-onboarding.yml`](examples/individual-repo-workflows/packaging-docs/org-onboarding.yml)), with the targets inline or in a batch file kept in that private repo. Never run it from a public repo: its job names and run summary list every target, so an auto-discovery run would publish the names of the org's private repos. Runs against N repos in parallel; idempotent. Repos the baseline would break, such as the CLA signatures ledger, go in `data/onboarding-exclude.txt` in that private repo and are skipped in every mode. - **Per-repo dispatch (one-offs)** โ€” dispatch `v4-bootstrap.yml` from the target repo's Actions tab. Same baseline, scoped to the one repo. diff --git a/docs/conventions/release-flow-v4.md b/docs/conventions/release-flow-v4.md index 1ec6cb72..a41d123d 100644 --- a/docs/conventions/release-flow-v4.md +++ b/docs/conventions/release-flow-v4.md @@ -105,7 +105,7 @@ hotfixes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - **Updates on every push to `next`** โ€” workflow recalculates version, regenerates body, syncs labels via delta (v3.2.4's label fix carries forward). - **A maintainer decision is required to merge โ€” never unattended.** By default that's the manual "Squash and merge" click. Optionally (per repo, via the `release-merge.yml` caller โ†’ `workflow-release-merge.yml`), the maintainer's **approval** instead triggers an API squash-merge once every check on the head is green โ€” required AND non-required (e.g. the coverage badge and this release-PR body refresh), so the body is never captured stale. The commit message is set explicitly to the PR body, so the release commit is exactly the curated body: no mobile Default-path title-only drop, and no GitHub squash-UI `Co-authored-by:` auto-append (which is neither deduped by account nor bot-stripped) โ€” the clean `` block already in the body is the whole credit. It merges via `PUT โ€ฆ/merge` (which succeeds precisely when the PR is mergeable, unlike native auto-merge's "clean status" refusal) and never approves as the bot. Re-evaluation when an approval precedes green CI uses `workflow_run`, not `check_suite` โ€” GitHub does not send `check_suite: completed` for a suite created by GitHub Actions itself (every check the repo's own CI produces), only for one created by a third-party App (#318: an approval given before CI finished left a release PR stuck fully green with nothing re-firing the merge check, because check_suite never fired for that case at all). Because the gate waits on every check on the head, the caller's `workflow_run` trigger must name **every** workflow that puts a check on that commit โ€” CI, CodeQL, dependency review, the release-PR refresh, the `pull_request_target` automations โ€” by literal workflow `name:`, filtered to `branches: [next, hotfixes]`. Only the last one to finish can see a fully green head, and CodeQL routinely outlasts CI: naming only `ci.yml` left PR #322 approved and green with nothing re-firing once CodeQL finished. The template lists the whole standard v4 set; `workflow_dispatch` remains available as a manual fallback. - On merge: master gets one `release: vX.Y.Z - ` commit. Tag + publish flow runs. `next` is **moved onto the release commit** (ยง7): reset to it, with anything merged into `next` after the release PR was cut carried forward on top. -- **GitHub Release notes.** The publish run creates the `vX.Y.Z` tag **signed** (GPG signing is on whenever the bot key secret is mapped) and the GitHub Release. When the release commit carries `docs/changelog[s]/v/v.md`, that file is the release body; otherwise the body is the release commit message. Either way the duplicated `release: vX.Y.Z - โ€ฆ` subject line and the squash trailers are dropped, the Contributors and coverage blocks are kept, and `@word` text outside code that isn't a listed contributor (for example a fix-headers `@Author` tag in prose) is wrapped in a code span, so GitHub doesn't mention that account or add it to the release's Contributors. The job then checks that the release is published and its tag exists, and fails if not. To re-sync existing releases from changelog files written or backfilled later, and to repair drafts or missing tags, dispatch the `sync-release-notes.yml` companion (CLDMV/.github#362). +- **GitHub Release notes.** The publish run creates the `vX.Y.Z` tag **signed** (GPG signing is on whenever the bot key secret is mapped) and the GitHub Release. When the release commit carries `docs/changelog[s]/v/v.md`, that file is the release body; otherwise the body is the release commit message. Either way the duplicated `release: vX.Y.Z - โ€ฆ` subject line and the squash trailers are dropped, the Contributors and coverage blocks are kept, and `@word` text outside code that isn't a listed contributor (for example a fix-headers `@Author` tag in prose) is wrapped in a code span, so GitHub doesn't mention that account or add it to the release's Contributors. The job then checks that the release is published and its tag exists, and fails if not. Every v4 repo also carries the standard `sync-release-notes.yml` companion. It runs on `release: published` and whenever a changelog file lands on the default branch: it re-syncs release bodies from changelog files written or backfilled later, and re-publishes drafts that CI left behind. Missing tags or releases are repaired by dispatching it with the matching switch (CLDMV/.github#362). ### 5.4 Hotfix release PR (`hotfixes โ†’ master`) diff --git a/examples/README.md b/examples/README.md index a481726e..1e5d2236 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,7 @@ Example workflow configurations for consuming the CLDMV org-level workflows. Cop ## Template Catalog -Templates live in [`individual-repo-workflows/`](individual-repo-workflows/), grouped by purpose into six subfolders. Each one references the matching org workflow via `@v4`. Copy what you need; you don't need to adopt all of them โ€” except `release-flow-v4/`, which is adopted as a set, and the standard `core-cicd/` files every v4 repo carries. +Templates live in [`individual-repo-workflows/`](individual-repo-workflows/), grouped by purpose into six subfolders. Each one references the matching org workflow via `@v4`. Copy what you need; you don't need to adopt all of them โ€” except `release-flow-v4/`, which is adopted as a set, and the standard `core-cicd/` and `release-companions/` files every v4 repo carries. ### ๐Ÿงช [`core-cicd/`](individual-repo-workflows/core-cicd/) โ€” Core CI/CD @@ -45,12 +45,14 @@ After installing these, complete the cutover via the [v3โ†’v4 migration guide](. ### ๐Ÿ“‹ [`release-companions/`](individual-repo-workflows/release-companions/) โ€” Release-flow companions -| Template | Triggers | What it does | -| ------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `tag-health.yml` | weekly Sunday cron + dispatch | Validates tags, fixes bot-signature drift, recreates orphaned tags. | -| `release-notify.yml` | `release: published` | Posts to configured Discord/Slack/generic webhook channels. | -| `master-commit-audit.yml` | push to default | Files a GitHub Issue if a master commit doesn't match the expected release-flow subject pattern. | -| `sync-release-notes.yml` | manual dispatch | Rewrites release bodies from `docs/changelog[s]/v/v.md`; reports (and on opt-in repairs) draft releases, missing tags and missing releases. | +Every v4 repo carries `tag-health.yml`, `master-commit-audit.yml`, and `sync-release-notes.yml` (standard set, not optional). `release-notify.yml` and `pr-notify.yml` are opt-in. + +| Template | Triggers | What it does | +| ------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tag-health.yml` | weekly Sunday cron + dispatch | Validates tags, fixes bot-signature drift, recreates orphaned tags. | +| `release-notify.yml` | `release: published` | Posts to configured Discord/Slack/generic webhook channels. | +| `master-commit-audit.yml` | push to default | Files a GitHub Issue if a master commit doesn't match the expected release-flow subject pattern. | +| `sync-release-notes.yml` | `release: published`, push to default touching `docs/changelog[s]/**`, dispatch | **Standard.** Rewrites release bodies from `docs/changelog[s]/v/v.md` and re-publishes CI-drafted releases; reports missing tags/releases (repaired via dispatch switches). | ### ๐Ÿ”’ [`security/`](individual-repo-workflows/security/) โ€” Security baseline (recommended for OSS repos) diff --git a/examples/guides/AGENT-SCAFFOLDING.md b/examples/guides/AGENT-SCAFFOLDING.md index eb66f1e9..94a480d5 100644 --- a/examples/guides/AGENT-SCAFFOLDING.md +++ b/examples/guides/AGENT-SCAFFOLDING.md @@ -85,6 +85,7 @@ Map Phase 1 answers to the template set you'll copy. **Always include** the v4 r | ------------------------- | -------------------------------------------- | -------------------------------------------- | | `master-commit-audit.yml` | `release-companions/master-commit-audit.yml` | No customization needed for default patterns | | `tag-health.yml` | `release-companions/tag-health.yml` | No customization needed | +| `sync-release-notes.yml` | `release-companions/sync-release-notes.yml` | No customization needed (standard v4 set) | ### Conditional (based on Phase 1) diff --git a/examples/guides/WORKFLOW-SETUP-GUIDE.md b/examples/guides/WORKFLOW-SETUP-GUIDE.md index 08bb9134..ad53c3e3 100644 --- a/examples/guides/WORKFLOW-SETUP-GUIDE.md +++ b/examples/guides/WORKFLOW-SETUP-GUIDE.md @@ -6,37 +6,37 @@ Per-template setup reference for every example workflow under [`../individual-re ## Workflows at a Glance -| Category | Workflow | File | Trigger | Purpose | -| ------------------ | ----------------------------------------------------------------- | -------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| Core CI/CD | [CI Tests & Build](#-ci-tests--build) | `core-cicd/ci.yml` | push / fork-PR | Test matrix + build; PR gate. Supports [embedded private tests](#-ci-tests--build) via anonymous gitlinks (opt-in) | -| Core CI/CD | [Create Release PR](#-create-release-pr) | `core-cicd/release.yml` | push to non-default | Opens versioned release PRs | -| Core CI/CD | [Release and Publish](#-release-and-publish) | `core-cicd/publish.yml` | push to master/main | Publishes to NPM / GitHub Packages | -| Core CI/CD | [Update Major Version Tags](#%EF%B8%8F-update-major-version-tags) | `core-cicd/update-major-version-tags.yml` | release published | Maintains `vX` / `vX.Y` floating tags | -| Core CI/CD | [Bundle Size](#-bundle-size) | `core-cicd/bundle-size.yml` | PR to master/main | Comments size delta of the published files (standard in every v4 repo) | -| Release flow v4 | [Next Release](#-next-release-v4) | `release-flow-v4/next-release.yml` | push to `next` | Refreshes persistent `next โ†’ master` release PR | -| Release flow v4 | [Hotfixes Release](#-hotfixes-release-v4) | `release-flow-v4/hotfixes-release.yml` | push to `hotfixes` | Refreshes persistent `hotfixes โ†’ master` release PR | -| Release flow v4 | [Next/Hotfixes Reset](#%EF%B8%8F-nexthotfixes-reset-v4) | `release-flow-v4/next-reset.yml` | push to `master` (release commit) | Re-syncs integration branches after a release | -| Release flow v4 | [Hotfix PR Redirector](#-hotfix-pr-redirector-v4) | `release-flow-v4/hotfix-redirector.yml` | PR opened | Retargets `hotfix/*` / `security/*` PRs **and Dependabot security updates** onto `hotfixes` | -| Release flow v4 | [PR Title Normalizer](#%EF%B8%8F-pr-title-normalizer) | `release-flow-v4/pr-title-normalizer.yml` | PR opened / synchronize | Normalizes PR titles to conventional-commit shape | -| Release flow v4 | [v4 Bootstrap](#-v4-bootstrap) | `release-flow-v4/v4-bootstrap.yml` | manual dispatch | Creates `next` + `hotfixes`; configures repo for v4 | -| Release companions | [Tag Health](#-tag-health) | `release-companions/tag-health.yml` | weekly cron + dispatch | Validates / repairs tags | -| Release companions | [Release Notifier](#-release-notifier) | `release-companions/release-notify.yml` | release published | Notifies Discord / Slack / webhooks | -| Release companions | [Master Commit Audit](#%EF%B8%8F-master-commit-audit) | `release-companions/master-commit-audit.yml` | push to default | Files Issues on subject-line drift | -| Release companions | [Sync Release Notes](#-sync-release-notes) | `release-companions/sync-release-notes.yml` | manual dispatch | Re-syncs release bodies from changelog files; repairs drafts / missing tags on opt-in | -| Security | [CodeQL](#-codeql) | `security/codeql.yml` | push / PR / weekly cron | SAST via CodeQL | -| Security | [Dependency Review](#%EF%B8%8F-dependency-review) | `security/dependency-review.yml` | PR | Blocks PRs with high-severity new deps | -| Security | [OpenSSF Scorecard](#-openssf-scorecard) | `security/scorecard.yml` | weekly + dispatch | Publishes OSSF Scorecard score | -| Security | [CLA Bot](#%EF%B8%8F-cla-bot) | `security/cla.yml` | PR + issue_comment | Per-CLA-version, org-wide signing via central ledger; org members exempt | -| Automation | [Dependabot config](#-dependabot-config) | `automation/dependabot.yml` | (config file) | Routes Dependabot PRs to `next`; security updates auto-promoted to `hotfixes` | -| Automation | [Dependabot Auto-Merge](#-dependabot-auto-merge) | `automation/dependabot-auto-merge.yml` | PR by dependabot[bot] | Auto-merges patch/minor bumps into the PR's target branch (`next` or `hotfixes`) | -| Automation | [Member Auto-Enable Auto-Merge](#-member-auto-enable-auto-merge) | `automation/member-auto-merge.yml` | PR by org member | Auto-enables GitHub's auto-merge flag on member PRs to `next` / `hotfixes`; does NOT approve | -| Automation | [Labeler](#%EF%B8%8F-labeler) | `automation/labeler.yml` | pull_request_target | Path-based PR labels | -| Automation | [Welcome](#-welcome) | `automation/welcome.yml` | first issue / PR | Welcome comments | -| Automation | [Stale](#-stale) | `automation/stale.yml` | daily cron | Marks/closes inactive issues + PRs | -| Automation | [Branch Retention](#-branch-retention) | `automation/branch-retention.yml` | PR merged | Prunes head branches with retention | -| Packaging/docs | [Docker Publish](#-docker-publish) | `packaging-docs/docker-publish.yml` | push to default + dispatch | Builds + pushes image to GHCR | -| Packaging/docs | [Docs Publish](#-docs-publish) | `packaging-docs/docs.yml` | push to default (filtered) | Publishes docs to `gh-pages` | -| Packaging/docs | [Sync Org Labels](#%EF%B8%8F-sync-org-labels) | `packaging-docs/sync-org-labels.yml` | manual / weekly cron | Syncs labels across org repos | +| Category | Workflow | File | Trigger | Purpose | +| ------------------ | ----------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Core CI/CD | [CI Tests & Build](#-ci-tests--build) | `core-cicd/ci.yml` | push / fork-PR | Test matrix + build; PR gate. Supports [embedded private tests](#-ci-tests--build) via anonymous gitlinks (opt-in) | +| Core CI/CD | [Create Release PR](#-create-release-pr) | `core-cicd/release.yml` | push to non-default | Opens versioned release PRs | +| Core CI/CD | [Release and Publish](#-release-and-publish) | `core-cicd/publish.yml` | push to master/main | Publishes to NPM / GitHub Packages | +| Core CI/CD | [Update Major Version Tags](#%EF%B8%8F-update-major-version-tags) | `core-cicd/update-major-version-tags.yml` | release published | Maintains `vX` / `vX.Y` floating tags | +| Core CI/CD | [Bundle Size](#-bundle-size) | `core-cicd/bundle-size.yml` | PR to master/main | Comments size delta of the published files (standard in every v4 repo) | +| Release flow v4 | [Next Release](#-next-release-v4) | `release-flow-v4/next-release.yml` | push to `next` | Refreshes persistent `next โ†’ master` release PR | +| Release flow v4 | [Hotfixes Release](#-hotfixes-release-v4) | `release-flow-v4/hotfixes-release.yml` | push to `hotfixes` | Refreshes persistent `hotfixes โ†’ master` release PR | +| Release flow v4 | [Next/Hotfixes Reset](#%EF%B8%8F-nexthotfixes-reset-v4) | `release-flow-v4/next-reset.yml` | push to `master` (release commit) | Re-syncs integration branches after a release | +| Release flow v4 | [Hotfix PR Redirector](#-hotfix-pr-redirector-v4) | `release-flow-v4/hotfix-redirector.yml` | PR opened | Retargets `hotfix/*` / `security/*` PRs **and Dependabot security updates** onto `hotfixes` | +| Release flow v4 | [PR Title Normalizer](#%EF%B8%8F-pr-title-normalizer) | `release-flow-v4/pr-title-normalizer.yml` | PR opened / synchronize | Normalizes PR titles to conventional-commit shape | +| Release flow v4 | [v4 Bootstrap](#-v4-bootstrap) | `release-flow-v4/v4-bootstrap.yml` | manual dispatch | Creates `next` + `hotfixes`; configures repo for v4 | +| Release companions | [Tag Health](#-tag-health) | `release-companions/tag-health.yml` | weekly cron + dispatch | Validates / repairs tags | +| Release companions | [Release Notifier](#-release-notifier) | `release-companions/release-notify.yml` | release published | Notifies Discord / Slack / webhooks | +| Release companions | [Master Commit Audit](#%EF%B8%8F-master-commit-audit) | `release-companions/master-commit-audit.yml` | push to default | Files Issues on subject-line drift | +| Release companions | [Sync Release Notes](#-sync-release-notes) | `release-companions/sync-release-notes.yml` | release published / changelog push / dispatch | **Standard v4 set.** Syncs release bodies from changelog files; re-publishes CI drafts | +| Security | [CodeQL](#-codeql) | `security/codeql.yml` | push / PR / weekly cron | SAST via CodeQL | +| Security | [Dependency Review](#%EF%B8%8F-dependency-review) | `security/dependency-review.yml` | PR | Blocks PRs with high-severity new deps | +| Security | [OpenSSF Scorecard](#-openssf-scorecard) | `security/scorecard.yml` | weekly + dispatch | Publishes OSSF Scorecard score | +| Security | [CLA Bot](#%EF%B8%8F-cla-bot) | `security/cla.yml` | PR + issue_comment | Per-CLA-version, org-wide signing via central ledger; org members exempt | +| Automation | [Dependabot config](#-dependabot-config) | `automation/dependabot.yml` | (config file) | Routes Dependabot PRs to `next`; security updates auto-promoted to `hotfixes` | +| Automation | [Dependabot Auto-Merge](#-dependabot-auto-merge) | `automation/dependabot-auto-merge.yml` | PR by dependabot[bot] | Auto-merges patch/minor bumps into the PR's target branch (`next` or `hotfixes`) | +| Automation | [Member Auto-Enable Auto-Merge](#-member-auto-enable-auto-merge) | `automation/member-auto-merge.yml` | PR by org member | Auto-enables GitHub's auto-merge flag on member PRs to `next` / `hotfixes`; does NOT approve | +| Automation | [Labeler](#%EF%B8%8F-labeler) | `automation/labeler.yml` | pull_request_target | Path-based PR labels | +| Automation | [Welcome](#-welcome) | `automation/welcome.yml` | first issue / PR | Welcome comments | +| Automation | [Stale](#-stale) | `automation/stale.yml` | daily cron | Marks/closes inactive issues + PRs | +| Automation | [Branch Retention](#-branch-retention) | `automation/branch-retention.yml` | PR merged | Prunes head branches with retention | +| Packaging/docs | [Docker Publish](#-docker-publish) | `packaging-docs/docker-publish.yml` | push to default + dispatch | Builds + pushes image to GHCR | +| Packaging/docs | [Docs Publish](#-docs-publish) | `packaging-docs/docs.yml` | push to default (filtered) | Publishes docs to `gh-pages` | +| Packaging/docs | [Sync Org Labels](#%EF%B8%8F-sync-org-labels) | `packaging-docs/sync-org-labels.yml` | manual / weekly cron | Syncs labels across org repos | --- @@ -279,11 +279,15 @@ Repo secret overrides org secret of the same name (built-in GitHub precedence). **File:** `release-companions/sync-release-notes.yml`  ยท  **Calls:** `workflow-sync-release-notes.yml@v4` -Manual dispatch. For every released version (version tags, GitHub Releases and `release: vX.Y.Z` commits on the default branch) that has a committed changelog file โ€” `docs/changelog[s]/v/v.md`, read at the default-branch tip (which carries later corrections and backfills), else at the release tag โ€” it rewrites the release body from that file. The Contributors and coverage blocks are kept, the duplicated `release: vX.Y.Z - โ€ฆ` subject line is dropped, and accidental `@word` mentions in prose are wrapped in code spans so GitHub doesn't add those accounts to the release's Contributors. +**Part of the standard v4 set โ€” every v4 repo carries it.** It keeps the GitHub Release pages in step with the committed changelog files. For every released version (version tags, GitHub Releases and `release: vX.Y.Z` commits on the default branch) that has `docs/changelog[s]/v/v.md` โ€” read at the default-branch tip (which carries later corrections and backfills), else at the release tag โ€” it rewrites the release body from that file. The Contributors and coverage blocks are kept, the duplicated `release: vX.Y.Z - โ€ฆ` subject line is dropped, and accidental `@word` mentions in prose are wrapped in code spans so GitHub doesn't add those accounts to the release's Contributors. -It also reports drift: draft releases, versions with no tag, tags with no release, and duplicate releases for one tag. `dry_run` is on by default. Repairs are separate opt-ins: `create_missing_tags` (signed tag at the release commit), `create_missing_releases`, `publish_drafts`, and `normalize_all` (tidy bodies that have no changelog file). The results table goes to the job summary. +**Triggers.** It runs on `release: published` (the publish workflow creates releases with the bot App token, so the event fires), on a push to the default branch that touches `docs/changelog/**` or `docs/changelogs/**`, and by manual dispatch. It is a companion, not a required check, so the `paths:` filter is fine here, unlike in `ci.yml`. -**Required secrets** โ€” bot App credentials; GPG signing secrets for signed tags. +**What the automatic runs do.** They apply changes: they sync bodies and re-publish draft releases whose tag exists (drafts that CI left behind, CLDMV/.github#362). Creating missing tags (`create_missing_tags`, bot-signed at the `release: vX.Y.Z` commit) and missing releases (`create_missing_releases`) stays manual, through the dispatch switches. Dispatch defaults to `dry_run`; `normalize_all` also tidies bodies that have no changelog file. Every run is an idempotent full sweep: with nothing to change, it changes nothing and says so in the job summary. Drift that needs a decision is reported as a warning. The job fails only when a repair it attempted fails. Overlapping runs for one repo queue rather than run at the same time. + +**Permissions.** The workflow `GITHUB_TOKEN` is read-only. Every write goes through the bot App token. + +**Required secrets** โ€” `CLDMV_BOT_APP_CLIENT_ID` / `CLDMV_BOT_APP_PRIVATE_KEY`; the `CLDMV_BOT_NAME` / `CLDMV_BOT_EMAIL` / `CLDMV_BOT_GPG_*` secrets so manually created tags are signed (without the key, tag creation is refused). **Prereqs** โ€” none. diff --git a/examples/individual-repo-workflows/release-companions/sync-release-notes.yml b/examples/individual-repo-workflows/release-companions/sync-release-notes.yml index 4ecb3ed7..0cf38224 100644 --- a/examples/individual-repo-workflows/release-companions/sync-release-notes.yml +++ b/examples/individual-repo-workflows/release-companions/sync-release-notes.yml @@ -9,21 +9,39 @@ # Individual repo: .github/workflows/sync-release-notes.yml # -# Manual repair tool. Rewrites this repository's GitHub Release bodies from the -# committed per-version changelog files (docs/changelog[s]/v/v.md) -# โ€” the way to give releases made before a changelog existed (or with a -# backfilled one) their curated notes. Keeps the Contributors and coverage -# blocks, drops the duplicated "release: vX.Y.Z - โ€ฆ" subject line, neutralizes -# accidental @word mentions. +# Part of the STANDARD v4 set โ€” every v4 repo carries this file. # -# Also reports release/tag drift: draft releases, versions with no tag, tags -# with no release, duplicate releases. Dry run by default; each repair is its -# own opt-in. Results land in the job summary. +# Keeps the GitHub Release pages in step with the committed per-version +# changelog files (docs/changelog[s]/v/v.md): rewrites each +# release body from its file, keeping the Contributors and coverage blocks, +# dropping the duplicated "release: vX.Y.Z - โ€ฆ" subject line and neutralizing +# accidental @word mentions. Also reports release/tag drift (draft releases, +# versions with no tag, tags with no release, duplicate releases). # -# Thin caller: all logic lives in workflow-sync-release-notes.yml@v4. +# Runs automatically: +# - when a release is published (the publish workflow creates releases with +# the bot App token, so release:published fires โ€” a GITHUB_TOKEN-created +# release would not trigger workflows); +# - when a changelog file lands on the default branch (backfills and fixes). +# Automatic runs apply changes: they sync bodies and re-publish draft releases +# whose tag exists (CI-created drafts โ€” CLDMV/.github#362). Creating missing +# tags and missing releases stays manual: use the dispatch switches. +# Every run is an idempotent full sweep โ€” with nothing to change it changes +# nothing and says so. Not a required check (companion workflow), so the +# `paths:` filter below is fine here, unlike in ci.yml. +# +# Thin caller: all logic lives in workflow-sync-release-notes.yml@v4, which +# also queues overlapping runs per repo and picks the runner. name: ๐Ÿ“ Sync Release Notes on: + release: + types: [published] + push: + branches: [master, main] + paths: + - "docs/changelog/**" + - "docs/changelogs/**" workflow_dispatch: inputs: dry_run: @@ -36,8 +54,13 @@ on: type: string required: false default: "" + publish_drafts: + description: "Publish draft releases whose tag exists" + type: boolean + required: false + default: true create_missing_tags: - description: "Create missing vX.Y.Z tags at their release commits (signed)" + description: "Create missing vX.Y.Z tags at their release commits (bot-signed)" type: boolean required: false default: false @@ -46,11 +69,6 @@ on: type: boolean required: false default: false - publish_drafts: - description: "Publish draft releases whose tag exists" - type: boolean - required: false - default: false normalize_all: description: "Also tidy bodies of releases with no changelog file" type: boolean @@ -62,16 +80,18 @@ permissions: jobs: sync: + # All writes go through the bot App token; GITHUB_TOKEN only reads. permissions: - contents: write + contents: read uses: CLDMV/.github/.github/workflows/workflow-sync-release-notes.yml@v4 with: - dry_run: ${{ inputs.dry_run }} - versions: ${{ inputs.versions }} - create_missing_tags: ${{ inputs.create_missing_tags }} - create_missing_releases: ${{ inputs.create_missing_releases }} - publish_drafts: ${{ inputs.publish_drafts }} - normalize_all: ${{ inputs.normalize_all }} + # Automatic runs (release / push) apply changes; manual runs use the switches. + dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + versions: ${{ inputs.versions || '' }} + publish_drafts: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_drafts }} + create_missing_tags: ${{ github.event_name == 'workflow_dispatch' && inputs.create_missing_tags }} + create_missing_releases: ${{ github.event_name == 'workflow_dispatch' && inputs.create_missing_releases }} + normalize_all: ${{ github.event_name == 'workflow_dispatch' && inputs.normalize_all }} secrets: BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} From 84227b71ee70ceb2e42ba10390ddddd3688882cf Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sun, 4 Oct 2026 01:25:01 -0700 Subject: [PATCH 2/7] fix(tags): keep Markdown headings in tag messages (--cleanup=verbatim) git's default message cleanup treats lines starting with # as comments, so the signed v1.2.4 tag in CLDMV/configs lost every changelog heading (# ... Changelog, ## Overview, ## Bug Fixes). Create annotated tags from a message file with --cleanup=verbatim everywhere: tag/create and tag/update (release tags and rolling tags), fix-orphaned-releases, and the tag-health re-sign / re-point paths via a shared annotatedTagArgs() helper. A new test asserts a ## heading survives creation and replacement. Refs #362 --- .../git/steps/fix-non-bot-tags/action.mjs | 4 +- .../git/steps/fix-orphaned-tags/action.mjs | 6 +- .../git/steps/fix-unsigned-tags/action.mjs | 4 +- .github/actions/git/utilities/git-utils.mjs | 20 ++++++ .../git/utilities/tag-message.test.mjs | 61 +++++++++++++++++++ .../actions/github/api/tag/create/_impl.mjs | 8 +-- .../actions/github/api/tag/update/_impl.mjs | 4 +- .../steps/fix-orphaned-releases/action.mjs | 4 +- package.json | 2 +- 9 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 .github/actions/git/utilities/tag-message.test.mjs diff --git a/.github/actions/git/steps/fix-non-bot-tags/action.mjs b/.github/actions/git/steps/fix-non-bot-tags/action.mjs index eec6a847..4371b231 100644 --- a/.github/actions/git/steps/fix-non-bot-tags/action.mjs +++ b/.github/actions/git/steps/fix-non-bot-tags/action.mjs @@ -7,7 +7,7 @@ import { writeFileSync, unlinkSync } from "fs"; import { execFileSync } from "node:child_process"; -import { getTagInfo } from "../../utilities/git-utils.mjs"; +import { getTagInfo, annotatedTagArgs } from "../../utilities/git-utils.mjs"; import { debugLog } from "../../../common/common/core.mjs"; import { importGpgIfNeeded, configureGitIdentity } from "../../../github/api/_api/gpg.mjs"; @@ -90,7 +90,7 @@ function fixNonBotTag(tagObj) { const msgFile = `${process.env.RUNNER_TEMP || "/tmp"}/tag-msg-${Date.now()}.txt`; writeFileSync(msgFile, tagMessage, "utf8"); try { - execFileSync("git", ["tag", "-f", "-a", ...(willSign ? ["-s"] : []), "-F", msgFile, tagObj.name, tagObj.commitSha], { + execFileSync("git", annotatedTagArgs({ tagName: tagObj.name, target: tagObj.commitSha, messageFile: msgFile, sign: !!willSign }), { stdio: ["ignore", "inherit", "inherit"] }); } finally { diff --git a/.github/actions/git/steps/fix-orphaned-tags/action.mjs b/.github/actions/git/steps/fix-orphaned-tags/action.mjs index 05f710de..c0572ca2 100644 --- a/.github/actions/git/steps/fix-orphaned-tags/action.mjs +++ b/.github/actions/git/steps/fix-orphaned-tags/action.mjs @@ -7,7 +7,7 @@ import { writeFileSync, unlinkSync } from "fs"; import { execFileSync } from "node:child_process"; -import { gitCommand } from "../../utilities/git-utils.mjs"; +import { gitCommand, annotatedTagArgs } from "../../utilities/git-utils.mjs"; import { importGpgIfNeeded, configureGitIdentity } from "../../../github/api/_api/gpg.mjs"; console.log("๐Ÿ” DEBUG: Orphaned tags action starting..."); @@ -208,9 +208,9 @@ function fixOrphanedTag(tagObj) { let tagArgs; if (GPG_ENABLED && GPG_PRIVATE_KEY) { // Always create signed annotated tags when GPG is enabled - tagArgs = ["tag", "-f", "-s", "-a", "-F", msgFile, tagName, equivalentCommit]; + tagArgs = annotatedTagArgs({ tagName, target: equivalentCommit, messageFile: msgFile, sign: true }); } else if (tagObj.isAnnotated) { - tagArgs = ["tag", "-f", "-a", "-F", msgFile, tagName, equivalentCommit]; + tagArgs = annotatedTagArgs({ tagName, target: equivalentCommit, messageFile: msgFile }); } else { tagArgs = ["tag", "-f", tagName, equivalentCommit]; } diff --git a/.github/actions/git/steps/fix-unsigned-tags/action.mjs b/.github/actions/git/steps/fix-unsigned-tags/action.mjs index 864f806e..317760cc 100644 --- a/.github/actions/git/steps/fix-unsigned-tags/action.mjs +++ b/.github/actions/git/steps/fix-unsigned-tags/action.mjs @@ -7,7 +7,7 @@ import { writeFileSync, unlinkSync } from "fs"; import { execFileSync } from "node:child_process"; -import { gitCommand } from "../../utilities/git-utils.mjs"; +import { gitCommand, annotatedTagArgs } from "../../utilities/git-utils.mjs"; import { debugLog } from "../../../common/common/core.mjs"; import { importGpgIfNeeded, configureGitIdentity } from "../../../github/api/_api/gpg.mjs"; import { api, parseRepo } from "../../../github/api/_api/core.mjs"; @@ -185,7 +185,7 @@ async function fixUnsignedTag(tagObj) { // spliced into a shell command line. const msgFile = `${process.env.RUNNER_TEMP || "/tmp"}/tag-msg-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`; writeFileSync(msgFile, tagMessage, "utf8"); - const tagArgs = ["tag", "-f", "-a", ...(GPG_ENABLED && GPG_PRIVATE_KEY ? ["-s"] : []), "-F", msgFile, tagName, commitSha]; + const tagArgs = annotatedTagArgs({ tagName, target: commitSha, messageFile: msgFile, sign: !!(GPG_ENABLED && GPG_PRIVATE_KEY) }); const made = git(tagArgs); try { unlinkSync(msgFile); diff --git a/.github/actions/git/utilities/git-utils.mjs b/.github/actions/git/utilities/git-utils.mjs index 4db879f3..46126191 100644 --- a/.github/actions/git/utilities/git-utils.mjs +++ b/.github/actions/git/utilities/git-utils.mjs @@ -175,3 +175,23 @@ export function getTagInfo(tagName, botPatterns = ["CLDMV Bot", "cldmv-bot", "gi return null; } } + +/** + * argv for creating (or replacing) an annotated tag from a message file. + * + * `--cleanup=verbatim` is required: git's default message cleanup treats every + * line starting with `#` as a comment and drops it, which stripped all the + * Markdown headings (`# โ€ฆ Changelog`, `## Overview`, โ€ฆ) from release tag + * messages built from changelog files. + * @public + * @param {object} opts + * @param {string} opts.tagName - Tag to create. + * @param {string} opts.target - Commit (or object) the tag points at. + * @param {string} opts.messageFile - Path of the file holding the tag message. + * @param {boolean} [opts.sign=false] - GPG-sign the tag (`-s`). + * @param {boolean} [opts.force=true] - Replace an existing local tag (`-f`). + * @returns {string[]} Arguments for `git` (pass to execFileSync, no shell). + */ +export function annotatedTagArgs({ tagName, target, messageFile, sign = false, force = true }) { + return ["tag", ...(force ? ["-f"] : []), "-a", ...(sign ? ["-s"] : []), "--cleanup=verbatim", "-F", messageFile, tagName, target]; +} diff --git a/.github/actions/git/utilities/tag-message.test.mjs b/.github/actions/git/utilities/tag-message.test.mjs new file mode 100644 index 00000000..f7b35e22 --- /dev/null +++ b/.github/actions/git/utilities/tag-message.test.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node +// Tag messages built from changelog files must keep their Markdown headings. +// git's default cleanup drops every line starting with `#` as a comment, which +// stripped `# โ€ฆ Changelog` / `## Overview` from release tag messages. +// Run: node .github/actions/git/utilities/tag-message.test.mjs +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { annotatedTagArgs } from "./git-utils.mjs"; + +const repo = mkdtempSync(path.join(tmpdir(), "tag-message-")); +const git = (...args) => + execFileSync( + "git", + ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "tag.gpgsign=false", "-c", "commit.gpgsign=false", ...args], + { + cwd: repo, + encoding: "utf8" + } + ); + +try { + git("init", "-q"); + git("commit", "-q", "--allow-empty", "-m", "init"); + const message = "# @cldmv/x v1.2.4 Changelog\n\n## Overview\n\nText.\n\n## ๐Ÿ› Bug Fixes\n\n- fix\n"; + const msgFile = path.join(repo, "msg.txt"); + writeFileSync(msgFile, message, "utf8"); + + git(...annotatedTagArgs({ tagName: "v1.2.4", target: "HEAD", messageFile: msgFile })); + const body = git("tag", "-l", "--format=%(contents)", "v1.2.4"); + assert.ok(body.includes("# @cldmv/x v1.2.4 Changelog"), "H1 heading kept"); + assert.ok(body.includes("## Overview"), "## heading kept"); + assert.ok(body.includes("## ๐Ÿ› Bug Fixes"), "emoji heading kept"); + + // Replacing the tag (-f, the tag-health re-sign path) keeps them too. + git(...annotatedTagArgs({ tagName: "v1.2.4", target: "HEAD", messageFile: msgFile })); + assert.ok(git("tag", "-l", "--format=%(contents)", "v1.2.4").includes("## Overview"), "## heading kept on replace"); + + // Control: without --cleanup=verbatim git drops the heading lines. + git("tag", "-a", "-F", msgFile, "v0.0.1", "HEAD"); + assert.ok(!git("tag", "-l", "--format=%(contents)", "v0.0.1").includes("## Overview"), "default cleanup strips headings (control)"); + + // Shell-string call sites keep the flag too. + const here = path.dirname(fileURLToPath(import.meta.url)); + for (const rel of [ + "../../github/api/tag/create/_impl.mjs", + "../../github/api/tag/update/_impl.mjs", + "../../github/steps/fix-orphaned-releases/action.mjs" + ]) { + const src = readFileSync(path.join(here, rel), "utf8"); + for (const line of src.split("\n").filter((l) => /\b(sh|gitCommand)\(`git tag -[as] /.test(l) && l.includes("-F"))) { + assert.ok(line.includes("--cleanup=verbatim"), `${rel}: ${line.trim()}`); + } + } + console.log("tag-message: all checks passed"); +} finally { + rmSync(repo, { recursive: true, force: true }); +} diff --git a/.github/actions/github/api/tag/create/_impl.mjs b/.github/actions/github/api/tag/create/_impl.mjs index 90b3f453..d41203b2 100644 --- a/.github/actions/github/api/tag/create/_impl.mjs +++ b/.github/actions/github/api/tag/create/_impl.mjs @@ -34,18 +34,18 @@ function runGitSmartTag({ repo, token, tag, sha, message, gpg_enabled, tagger_na debugLog(`runGitSmartTag: final tagMessage="${tagMessage}"`); if (willSign) { - debugLog(`runGitSmartTag: Creating signed tag: git tag -s -f -F tempfile ${tag} ${sha}`); + debugLog(`runGitSmartTag: Creating signed tag: git tag -s -f --cleanup=verbatim -F tempfile ${tag} ${sha}`); // Write message to temp file to handle multiline messages properly const tmpFile = `${process.env.RUNNER_TEMP || process.env.TEMP || "/tmp"}/tag-message-${Date.now()}.txt`; fs.writeFileSync(tmpFile, tagMessage, "utf8"); - sh(`git tag -s -f -F "${tmpFile}" ${tag} ${sha}`); + sh(`git tag -s -f --cleanup=verbatim -F "${tmpFile}" ${tag} ${sha}`); fs.unlinkSync(tmpFile); } else if (willAnnotate) { - debugLog(`runGitSmartTag: Creating annotated tag: git tag -a -f -F tempfile ${tag} ${sha}`); + debugLog(`runGitSmartTag: Creating annotated tag: git tag -a -f --cleanup=verbatim -F tempfile ${tag} ${sha}`); // Write message to temp file to handle multiline messages properly const tmpFile = `${process.env.RUNNER_TEMP || process.env.TEMP || "/tmp"}/tag-message-${Date.now()}.txt`; fs.writeFileSync(tmpFile, tagMessage, "utf8"); - sh(`git tag -a -f -F "${tmpFile}" ${tag} ${sha}`); + sh(`git tag -a -f --cleanup=verbatim -F "${tmpFile}" ${tag} ${sha}`); fs.unlinkSync(tmpFile); } else { debugLog(`runGitSmartTag: Creating lightweight tag: git tag -f ${tag} ${sha}`); diff --git a/.github/actions/github/api/tag/update/_impl.mjs b/.github/actions/github/api/tag/update/_impl.mjs index 6bfab181..b79e30d6 100644 --- a/.github/actions/github/api/tag/update/_impl.mjs +++ b/.github/actions/github/api/tag/update/_impl.mjs @@ -28,14 +28,14 @@ function runGitSmartTag({ repo, token, tag, sha, message, gpg_enabled, tagger_na // Write message to temp file to handle multiline messages properly const tmpFile = `${process.env.RUNNER_TEMP || process.env.TEMP || "/tmp"}/tag-message-${Date.now()}.txt`; fs.writeFileSync(tmpFile, tagMessage, "utf8"); - sh(`git tag -s -f -F "${tmpFile}" ${tag} ${sha}`); + sh(`git tag -s -f --cleanup=verbatim -F "${tmpFile}" ${tag} ${sha}`); fs.unlinkSync(tmpFile); } else if (willAnnotate) { debugLog(`runGitSmartTag: Creating annotated tag: git tag -a -f -m "${tagMessage}" ${tag} ${sha}`); // Write message to temp file to handle multiline messages properly const tmpFile = `${process.env.RUNNER_TEMP || process.env.TEMP || "/tmp"}/tag-message-${Date.now()}.txt`; fs.writeFileSync(tmpFile, tagMessage, "utf8"); - sh(`git tag -a -f -F "${tmpFile}" ${tag} ${sha}`); + sh(`git tag -a -f --cleanup=verbatim -F "${tmpFile}" ${tag} ${sha}`); fs.unlinkSync(tmpFile); } else { debugLog(`runGitSmartTag: Creating lightweight tag: git tag -f ${tag} ${sha}`); diff --git a/.github/actions/github/steps/fix-orphaned-releases/action.mjs b/.github/actions/github/steps/fix-orphaned-releases/action.mjs index a0a61869..0b16dd39 100644 --- a/.github/actions/github/steps/fix-orphaned-releases/action.mjs +++ b/.github/actions/github/steps/fix-orphaned-releases/action.mjs @@ -252,7 +252,7 @@ async function createMissingTag(tagName, targetCommit, releaseName) { // Use temp file for message to handle multiline content properly const tempFile = `/tmp/tag-message-${Date.now()}.txt`; writeFileSync(tempFile, tagMessage, "utf8"); - gitCommand(`git tag -s -f -F "${tempFile}" ${tagName} ${targetCommit}`); + gitCommand(`git tag -s -f --cleanup=verbatim -F "${tempFile}" ${tagName} ${targetCommit}`); try { require("fs").unlinkSync(tempFile); } catch { @@ -263,7 +263,7 @@ async function createMissingTag(tagName, targetCommit, releaseName) { // Use temp file for message to handle multiline content properly const tempFile = `/tmp/tag-message-${Date.now()}.txt`; writeFileSync(tempFile, tagMessage, "utf8"); - gitCommand(`git tag -a -f -F "${tempFile}" ${tagName} ${targetCommit}`); + gitCommand(`git tag -a -f --cleanup=verbatim -F "${tempFile}" ${tagName} ${targetCommit}`); try { require("fs").unlinkSync(tempFile); } catch { diff --git a/package.json b/package.json index e7b7aad7..a6f80ec2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "format": "prettier --config .configs/.prettierrc --write .", "format:check": "prettier --config .configs/.prettierrc --check .", "prepare": "node -e \"import('./.githooks/install.mjs').catch(()=>{})\"", - "test": "node .github/actions/common/utilities/bot-detection.test.mjs && node .github/actions/npm/jobs/bundle-size/action.test.mjs && node .github/actions/common/steps/paths-gate/action.test.mjs && node .github/actions/github/steps/merge-master-into-branch/test.mjs && node .github/actions/github/steps/mark-implemented-issues/action.test.mjs && node .github/actions/git/steps/reset-branch-after-release/test.mjs && node .github/actions/github/utilities/release-notes.test.mjs" + "test": "node .github/actions/common/utilities/bot-detection.test.mjs && node .github/actions/npm/jobs/bundle-size/action.test.mjs && node .github/actions/common/steps/paths-gate/action.test.mjs && node .github/actions/github/steps/merge-master-into-branch/test.mjs && node .github/actions/github/steps/mark-implemented-issues/action.test.mjs && node .github/actions/git/steps/reset-branch-after-release/test.mjs && node .github/actions/github/utilities/release-notes.test.mjs && node .github/actions/git/utilities/tag-message.test.mjs" }, "devDependencies": { "@cldmv/eslint-plugin-jsonv": "^1.0.3", From abcb453ce1fb8a977c438873857537c5101938b3 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sun, 4 Oct 2026 01:25:46 -0700 Subject: [PATCH 3/7] docs(changelogs): backfill every shipped v4 release without a changelog file Adds one file per version for the 74 v4 releases that shipped without a docs/changelogs/vX.Y.Z.md: every patch release from v4.0.1 through v4.25.1, and everything from v4.26.1 through v4.30.6. Each is written from the diff against the previous release and the PRs it contains, so the release automation and sync-release-notes can resolve an exact-version file for every v4 release. Patch releases that changed a consumer-facing default are listed under Breaking Changes with upgrade steps: v4.16.3 (LTS-only matrix default), v4.27.1 (Node matrix bounds), v4.27.3 (default-on docs check) and v4.29.1 (--ignore-scripts on the default publish command). --- docs/changelogs/v4.0.1.md | 8 ++++++++ docs/changelogs/v4.0.2.md | 7 +++++++ docs/changelogs/v4.11.1.md | 12 ++++++++++++ docs/changelogs/v4.11.2.md | 7 +++++++ docs/changelogs/v4.12.1.md | 7 +++++++ docs/changelogs/v4.13.1.md | 8 ++++++++ docs/changelogs/v4.13.2.md | 7 +++++++ docs/changelogs/v4.14.1.md | 7 +++++++ docs/changelogs/v4.14.2.md | 8 ++++++++ docs/changelogs/v4.14.3.md | 8 ++++++++ docs/changelogs/v4.14.4.md | 8 ++++++++ docs/changelogs/v4.15.1.md | 7 +++++++ docs/changelogs/v4.15.2.md | 7 +++++++ docs/changelogs/v4.16.1.md | 7 +++++++ docs/changelogs/v4.16.10.md | 7 +++++++ docs/changelogs/v4.16.11.md | 14 ++++++++++++++ docs/changelogs/v4.16.12.md | 8 ++++++++ docs/changelogs/v4.16.13.md | 7 +++++++ docs/changelogs/v4.16.2.md | 7 +++++++ docs/changelogs/v4.16.3.md | 12 ++++++++++++ docs/changelogs/v4.16.4.md | 7 +++++++ docs/changelogs/v4.16.5.md | 7 +++++++ docs/changelogs/v4.16.6.md | 9 +++++++++ docs/changelogs/v4.16.7.md | 9 +++++++++ docs/changelogs/v4.16.8.md | 7 +++++++ docs/changelogs/v4.16.9.md | 7 +++++++ docs/changelogs/v4.18.1.md | 7 +++++++ docs/changelogs/v4.18.2.md | 7 +++++++ docs/changelogs/v4.18.3.md | 11 +++++++++++ docs/changelogs/v4.18.4.md | 8 ++++++++ docs/changelogs/v4.18.5.md | 7 +++++++ docs/changelogs/v4.19.1.md | 7 +++++++ docs/changelogs/v4.19.2.md | 8 ++++++++ docs/changelogs/v4.19.3.md | 8 ++++++++ docs/changelogs/v4.21.1.md | 7 +++++++ docs/changelogs/v4.22.1.md | 8 ++++++++ docs/changelogs/v4.22.2.md | 7 +++++++ docs/changelogs/v4.25.1.md | 11 +++++++++++ docs/changelogs/v4.26.1.md | 7 +++++++ docs/changelogs/v4.26.2.md | 9 +++++++++ docs/changelogs/v4.27.0.md | 15 +++++++++++++++ docs/changelogs/v4.27.1.md | 20 ++++++++++++++++++++ docs/changelogs/v4.27.2.md | 7 +++++++ docs/changelogs/v4.27.3.md | 16 ++++++++++++++++ docs/changelogs/v4.27.4.md | 7 +++++++ docs/changelogs/v4.27.5.md | 8 ++++++++ docs/changelogs/v4.28.0.md | 12 ++++++++++++ docs/changelogs/v4.28.1.md | 9 +++++++++ docs/changelogs/v4.28.2.md | 7 +++++++ docs/changelogs/v4.28.3.md | 8 ++++++++ docs/changelogs/v4.28.4.md | 7 +++++++ docs/changelogs/v4.29.0.md | 16 ++++++++++++++++ docs/changelogs/v4.29.1.md | 16 ++++++++++++++++ docs/changelogs/v4.29.2.md | 7 +++++++ docs/changelogs/v4.29.3.md | 9 +++++++++ docs/changelogs/v4.29.4.md | 8 ++++++++ docs/changelogs/v4.3.1.md | 7 +++++++ docs/changelogs/v4.3.2.md | 7 +++++++ docs/changelogs/v4.30.0.md | 11 +++++++++++ docs/changelogs/v4.30.1.md | 11 +++++++++++ docs/changelogs/v4.30.2.md | 7 +++++++ docs/changelogs/v4.30.3.md | 11 +++++++++++ docs/changelogs/v4.30.4.md | 11 +++++++++++ docs/changelogs/v4.30.5.md | 13 +++++++++++++ docs/changelogs/v4.30.6.md | 7 +++++++ docs/changelogs/v4.8.1.md | 7 +++++++ docs/changelogs/v4.8.2.md | 7 +++++++ docs/changelogs/v4.8.3.md | 7 +++++++ docs/changelogs/v4.8.4.md | 8 ++++++++ docs/changelogs/v4.8.5.md | 7 +++++++ docs/changelogs/v4.8.6.md | 7 +++++++ docs/changelogs/v4.8.7.md | 7 +++++++ docs/changelogs/v4.8.8.md | 11 +++++++++++ docs/changelogs/v4.8.9.md | 7 +++++++ 74 files changed, 646 insertions(+) create mode 100644 docs/changelogs/v4.0.1.md create mode 100644 docs/changelogs/v4.0.2.md create mode 100644 docs/changelogs/v4.11.1.md create mode 100644 docs/changelogs/v4.11.2.md create mode 100644 docs/changelogs/v4.12.1.md create mode 100644 docs/changelogs/v4.13.1.md create mode 100644 docs/changelogs/v4.13.2.md create mode 100644 docs/changelogs/v4.14.1.md create mode 100644 docs/changelogs/v4.14.2.md create mode 100644 docs/changelogs/v4.14.3.md create mode 100644 docs/changelogs/v4.14.4.md create mode 100644 docs/changelogs/v4.15.1.md create mode 100644 docs/changelogs/v4.15.2.md create mode 100644 docs/changelogs/v4.16.1.md create mode 100644 docs/changelogs/v4.16.10.md create mode 100644 docs/changelogs/v4.16.11.md create mode 100644 docs/changelogs/v4.16.12.md create mode 100644 docs/changelogs/v4.16.13.md create mode 100644 docs/changelogs/v4.16.2.md create mode 100644 docs/changelogs/v4.16.3.md create mode 100644 docs/changelogs/v4.16.4.md create mode 100644 docs/changelogs/v4.16.5.md create mode 100644 docs/changelogs/v4.16.6.md create mode 100644 docs/changelogs/v4.16.7.md create mode 100644 docs/changelogs/v4.16.8.md create mode 100644 docs/changelogs/v4.16.9.md create mode 100644 docs/changelogs/v4.18.1.md create mode 100644 docs/changelogs/v4.18.2.md create mode 100644 docs/changelogs/v4.18.3.md create mode 100644 docs/changelogs/v4.18.4.md create mode 100644 docs/changelogs/v4.18.5.md create mode 100644 docs/changelogs/v4.19.1.md create mode 100644 docs/changelogs/v4.19.2.md create mode 100644 docs/changelogs/v4.19.3.md create mode 100644 docs/changelogs/v4.21.1.md create mode 100644 docs/changelogs/v4.22.1.md create mode 100644 docs/changelogs/v4.22.2.md create mode 100644 docs/changelogs/v4.25.1.md create mode 100644 docs/changelogs/v4.26.1.md create mode 100644 docs/changelogs/v4.26.2.md create mode 100644 docs/changelogs/v4.27.0.md create mode 100644 docs/changelogs/v4.27.1.md create mode 100644 docs/changelogs/v4.27.2.md create mode 100644 docs/changelogs/v4.27.3.md create mode 100644 docs/changelogs/v4.27.4.md create mode 100644 docs/changelogs/v4.27.5.md create mode 100644 docs/changelogs/v4.28.0.md create mode 100644 docs/changelogs/v4.28.1.md create mode 100644 docs/changelogs/v4.28.2.md create mode 100644 docs/changelogs/v4.28.3.md create mode 100644 docs/changelogs/v4.28.4.md create mode 100644 docs/changelogs/v4.29.0.md create mode 100644 docs/changelogs/v4.29.1.md create mode 100644 docs/changelogs/v4.29.2.md create mode 100644 docs/changelogs/v4.29.3.md create mode 100644 docs/changelogs/v4.29.4.md create mode 100644 docs/changelogs/v4.3.1.md create mode 100644 docs/changelogs/v4.3.2.md create mode 100644 docs/changelogs/v4.30.0.md create mode 100644 docs/changelogs/v4.30.1.md create mode 100644 docs/changelogs/v4.30.2.md create mode 100644 docs/changelogs/v4.30.3.md create mode 100644 docs/changelogs/v4.30.4.md create mode 100644 docs/changelogs/v4.30.5.md create mode 100644 docs/changelogs/v4.30.6.md create mode 100644 docs/changelogs/v4.8.1.md create mode 100644 docs/changelogs/v4.8.2.md create mode 100644 docs/changelogs/v4.8.3.md create mode 100644 docs/changelogs/v4.8.4.md create mode 100644 docs/changelogs/v4.8.5.md create mode 100644 docs/changelogs/v4.8.6.md create mode 100644 docs/changelogs/v4.8.7.md create mode 100644 docs/changelogs/v4.8.8.md create mode 100644 docs/changelogs/v4.8.9.md diff --git a/docs/changelogs/v4.0.1.md b/docs/changelogs/v4.0.1.md new file mode 100644 index 00000000..5bce00aa --- /dev/null +++ b/docs/changelogs/v4.0.1.md @@ -0,0 +1,8 @@ +# v4.0.1 โ€” 2026-05-22 + +Patch release fixing two problems found in the first hours of running the v4 staging-branch flow. + +## Fixed + +- **Integration branches are kept alive across releases.** The post-release reset path in `local-next-reset.yml` could leave `next` and `hotfixes` in a state that broke later operations; the reset, branch-retention and bootstrap workflows were revised, and `check-release-commit` gained tighter semantics with test coverage. ([#27](https://github.com/CLDMV/.github/pull/27)) +- **Release-PR label churn on refresh stopped.** Refreshing the release PR re-applied labels even when nothing had changed, producing noisy timelines; the pull-requests API helper now only edits labels that actually need changing. ([#27](https://github.com/CLDMV/.github/pull/27)) diff --git a/docs/changelogs/v4.0.2.md b/docs/changelogs/v4.0.2.md new file mode 100644 index 00000000..44b68eec --- /dev/null +++ b/docs/changelogs/v4.0.2.md @@ -0,0 +1,7 @@ +# v4.0.2 โ€” 2026-05-22 + +Patch release that activates the v4.0.1 fixes for this repository's own workflows. + +## Fixed + +- **Internal references moved from `@v3` to `@v4`.** The repository's own reusable workflows and local callers still pointed at the `v3` tag, so the v4.0.1 fixes were not in effect for them. All `uses:` references across 47 workflow and action files now point at `v4`. No logic changed. ([#28](https://github.com/CLDMV/.github/pull/28)) diff --git a/docs/changelogs/v4.11.1.md b/docs/changelogs/v4.11.1.md new file mode 100644 index 00000000..f12d60d5 --- /dev/null +++ b/docs/changelogs/v4.11.1.md @@ -0,0 +1,12 @@ +# v4.11.1 โ€” 2026-05-29 + +Patch release so that batches containing only maintenance commits still open a release PR, along with new org-onboarding defaults and stricter branch rulesets. + +## Fixed + +- **Chore-only batches trigger release-PR creation.** The release-commit check only proceeded when the range held a feature, fix, perf, revert or breaking commit, so a batch of `chore`, `docs`, `ci` or similar commits stalled the release lane. Any non-merge commit now qualifies. ([#101](https://github.com/CLDMV/.github/pull/101)) + +## Changed + +- **Org-onboarding defaults flipped.** `dry_run` now defaults to `false` (changes are applied unless preview is requested), and `code_security` and `secret_protection` default to `public-only` instead of `off`. ([#100](https://github.com/CLDMV/.github/pull/100)) +- **Rulesets restrict creation and update on `master`, `next` and `hotfixes`.** The three ruleset definitions gained `creation` and `update` rules. ([#100](https://github.com/CLDMV/.github/pull/100)) diff --git a/docs/changelogs/v4.11.2.md b/docs/changelogs/v4.11.2.md new file mode 100644 index 00000000..5be0bf44 --- /dev/null +++ b/docs/changelogs/v4.11.2.md @@ -0,0 +1,7 @@ +# v4.11.2 โ€” 2026-05-31 + +Patch release that gives `audit-commit-subject` a built-in default pattern set so callers no longer have to carry their own copy. + +## Fixed + +- **`allowed_patterns` in `audit-commit-subject` has a default.** The input is no longer required and defaults to the canonical release-flow patterns (release commits with an optional subject suffix and `(#N)`, `chore:` commits, and standard merge commits), so pattern fixes propagate through the pinned action ref instead of drifting in per-repo copies. The master-commit-audit example and workflow, and the setup guide, were updated to omit the input. An explicit empty override is still rejected. ([#104](https://github.com/CLDMV/.github/pull/104)) diff --git a/docs/changelogs/v4.12.1.md b/docs/changelogs/v4.12.1.md new file mode 100644 index 00000000..adb04bee --- /dev/null +++ b/docs/changelogs/v4.12.1.md @@ -0,0 +1,7 @@ +# v4.12.1 โ€” 2026-06-01 + +Patch release that stops release-PR bodies from listing the same change twice. + +## Fixed + +- **PR-titled merge commits are dropped from generated release-PR bodies.** The v4 flow merges feature PRs into `next` with merge commits titled like the PR (`feat: โ€ฆ (#N)`), so subject-based merge detection missed them and they appeared alongside their squashed content. Merge commits are now detected structurally by parent count, and `create-release-pr` passes the `base..head` range so the merge filter and patch-id dedup actually run. ([#109](https://github.com/CLDMV/.github/pull/109)) diff --git a/docs/changelogs/v4.13.1.md b/docs/changelogs/v4.13.1.md new file mode 100644 index 00000000..b4a88ca8 --- /dev/null +++ b/docs/changelogs/v4.13.1.md @@ -0,0 +1,8 @@ +# v4.13.1 โ€” 2026-06-08 + +Patch release fixing the OSSF Scorecard job and an over-long label description. + +## Fixed + +- **Scorecard job uses `actions/checkout` directly.** OSSF Scorecard's publish step permits only a fixed set of steps, and the custom `checkout-code` composite action caused "job has unallowed step" and an HTTP 400 on publish. ([#115](https://github.com/CLDMV/.github/pull/115)) +- **The `! feature โ†’ next` label description fits GitHub's 100-character cap.** The description in the label catalog was shortened. ([#116](https://github.com/CLDMV/.github/pull/116)) diff --git a/docs/changelogs/v4.13.2.md b/docs/changelogs/v4.13.2.md new file mode 100644 index 00000000..fe317500 --- /dev/null +++ b/docs/changelogs/v4.13.2.md @@ -0,0 +1,7 @@ +# v4.13.2 โ€” 2026-06-09 + +Patch release adding the organization profile README. + +## Added + +- **Organization profile README.** A `profile/README.md` was added to introduce the organization on its GitHub profile page. No workflow or action changes. ([#118](https://github.com/CLDMV/.github/pull/118)) diff --git a/docs/changelogs/v4.14.1.md b/docs/changelogs/v4.14.1.md new file mode 100644 index 00000000..e0acab14 --- /dev/null +++ b/docs/changelogs/v4.14.1.md @@ -0,0 +1,7 @@ +# v4.14.1 โ€” 2026-06-14 + +Patch release completing the satellite-package publishing support introduced in v4.14.0. + +## Fixed + +- **Satellite entry-point inputs are now wired through the publish workflow.** `workflow-publish.yml` did not forward the satellite-package inputs to the reusable publishing workflow, so satellite publishing added in v4.14.0 could not be driven from the entry point. The missing inputs are now passed through. ([#123](https://github.com/CLDMV/.github/pull/123), released in [#124](https://github.com/CLDMV/.github/pull/124)) diff --git a/docs/changelogs/v4.14.2.md b/docs/changelogs/v4.14.2.md new file mode 100644 index 00000000..9fcbd879 --- /dev/null +++ b/docs/changelogs/v4.14.2.md @@ -0,0 +1,8 @@ +# v4.14.2 โ€” 2026-06-14 + +Patch release making satellite publishing safe to retry after a partial failure. + +## Fixed + +- **A re-run now republishes only the satellite packages that are still missing.** Satellite discovery and publishing were gated on "a new core version exists", so once the core package was out and one satellite leg failed, a re-run skipped every satellite and the failed one never retried. The gate is removed from `reusable-publishing.yml`; each satellite publish and release step is idempotent (an already-published version is skipped), so the full matrix can safely run again. ([#125](https://github.com/CLDMV/.github/pull/125), released in [#126](https://github.com/CLDMV/.github/pull/126)) +- **Tag and release creation are idempotent and no longer downgrade annotated tags.** The tag-create action skips work when the remote tag already points at the target commit, which avoids re-signing or force-pushing an immutable release tag (and tripping tag protection) on retry. An existing annotated tag is also no longer replaced by a lightweight one when a re-run found the ref already present. Release creation was adjusted to upsert in the same way. ([#125](https://github.com/CLDMV/.github/pull/125), released in [#126](https://github.com/CLDMV/.github/pull/126)) diff --git a/docs/changelogs/v4.14.3.md b/docs/changelogs/v4.14.3.md new file mode 100644 index 00000000..e525a650 --- /dev/null +++ b/docs/changelogs/v4.14.3.md @@ -0,0 +1,8 @@ +# v4.14.3 โ€” 2026-06-14 + +Patch release making the flow label on auto-opened PRs reflect the kind of change, and hardening Dependabot auto-merge. + +## Fixed + +- **The flow label reflects the change type instead of always saying `feature`.** `feature-pr.yml` labelled every next-lane branch `! feature โ†’ next`, so docs, fix and chore PRs were mislabelled. The label is now `! โ†’ `, with the type taken from the branch prefix (`fix/`, `docs/`, `chore/`, `refactor/`, `ci/`, `perf/`, `test/`, `style/`, `release/`, `hotfix/`). The matching labels were added to `data/github-labels.json`, and the example template was updated to match. ([#128](https://github.com/CLDMV/.github/pull/128), released in [#130](https://github.com/CLDMV/.github/pull/130)) +- **Dependabot auto-merge gates on the PR's live base branch and on ruleset-aware protection.** The action now re-fetches the PR rather than trusting the frozen event payload (a re-run after a retargeted security update could check the wrong branch), skips PRs that are no longer open, refuses to act when the repository or PR payload cannot be resolved, and detects required checks from the effective branch rules so ruleset-protected branches are recognised. The parsing helpers moved to a unit-tested `_impl.mjs`. ([#129](https://github.com/CLDMV/.github/pull/129), released in [#130](https://github.com/CLDMV/.github/pull/130)) diff --git a/docs/changelogs/v4.14.4.md b/docs/changelogs/v4.14.4.md new file mode 100644 index 00000000..fb96d592 --- /dev/null +++ b/docs/changelogs/v4.14.4.md @@ -0,0 +1,8 @@ +# v4.14.4 โ€” 2026-06-16 + +Patch release making Dependabot auto-merge work on merge-only branches. + +## Fixed + +- **Auto-merge picks a merge method the target branch allows.** The `merge_method` input now defaults to `merge` instead of `squash`, matching the merge-only rulesets on `next` and `hotfixes`, and the action reads the branch's allowed merge methods and falls back to a permitted one when the configured method is rejected. ([#131](https://github.com/CLDMV/.github/pull/131), released in [#132](https://github.com/CLDMV/.github/pull/132)) +- **Already-mergeable PRs are merged directly.** When GitHub refuses to queue auto-merge because the PR is already in a clean or unstable state, the action now falls back to a direct merge. The merge endpoint still enforces required checks, and any other auto-merge failure still surfaces as an error. ([#131](https://github.com/CLDMV/.github/pull/131), released in [#132](https://github.com/CLDMV/.github/pull/132)) diff --git a/docs/changelogs/v4.15.1.md b/docs/changelogs/v4.15.1.md new file mode 100644 index 00000000..c338a4b3 --- /dev/null +++ b/docs/changelogs/v4.15.1.md @@ -0,0 +1,7 @@ +# v4.15.1 โ€” 2026-06-23 + +Patch release removing a ruleset rule that deadlocked auto-merge on `hotfixes`. + +## Fixed + +- **`required_linear_history` is dropped from the `hotfixes` ruleset.** Combined with a merge-only merge method, linear history made native auto-merge impossible on `hotfixes`, so PRs queued for auto-merge never landed. ([#135](https://github.com/CLDMV/.github/pull/135), released in [#136](https://github.com/CLDMV/.github/pull/136)) diff --git a/docs/changelogs/v4.15.2.md b/docs/changelogs/v4.15.2.md new file mode 100644 index 00000000..b03a47a1 --- /dev/null +++ b/docs/changelogs/v4.15.2.md @@ -0,0 +1,7 @@ +# v4.15.2 โ€” 2026-06-24 + +Patch release letting bot-merged PRs open the release PR. + +## Fixed + +- **The release-PR lane no longer ignores merges made by the bot.** The next and hotfixes release workflows skipped runs triggered by the bot actor, so a PR auto-merged by the bot never opened or refreshed the release PR. This was fatal on `hotfixes`, where every merge is a bot auto-merge. The actor guard is removed from `local-next-release.yml`, `local-hotfixes-release.yml` and their `release-flow-v4` example templates. ([#137](https://github.com/CLDMV/.github/pull/137), released in [#138](https://github.com/CLDMV/.github/pull/138)) diff --git a/docs/changelogs/v4.16.1.md b/docs/changelogs/v4.16.1.md new file mode 100644 index 00000000..7aa5bd20 --- /dev/null +++ b/docs/changelogs/v4.16.1.md @@ -0,0 +1,7 @@ +# v4.16.1 โ€” 2026-07-09 + +Patch release changing how Dependabot security updates are redirected to `hotfixes`. + +## Fixed + +- **Dependabot security redirects are cherry-picked onto the hotfix tip.** Previously the redirector retargeted Dependabot's PR, whose branch was forked from `next`, and merged it as-is, dragging unreleased `next` history into a hotfix. The `redirect-hotfix-pr` step now cherry-picks the dependency change onto the current `hotfixes` tip and redirects from there. Shared auto-merge helpers were extracted into `_api/auto-merge.mjs`, and the hotfix-redirector workflow was extended accordingly. ([#141](https://github.com/CLDMV/.github/pull/141), released in [#142](https://github.com/CLDMV/.github/pull/142)) diff --git a/docs/changelogs/v4.16.10.md b/docs/changelogs/v4.16.10.md new file mode 100644 index 00000000..06dfc56b --- /dev/null +++ b/docs/changelogs/v4.16.10.md @@ -0,0 +1,7 @@ +# v4.16.10 โ€” 2026-07-26 + +Patch release fixing detection of Dependabot security updates so they are redirected to the hotfix lane. + +## Fixed + +- **Dependabot security PRs are detected by base-branch mismatch instead of advisory text.** The previous check looked for a GHSA id or advisory link in the PR body, but current Dependabot security-update bodies do not embed one, so the redirect never fired. GitHub opens security updates against the default branch regardless of `target-branch`, so any Dependabot PR whose base differs from the routine target is now treated as a security update. A new `dependabot-base` input on `redirect-hotfix-pr` (default `next`) names the routine target, and the body-text check is kept as a secondary signal. The hotfix-redirector templates and documentation are updated, and tests are added. ([#161](https://github.com/CLDMV/.github/pull/161), released in [#162](https://github.com/CLDMV/.github/pull/162)) diff --git a/docs/changelogs/v4.16.11.md b/docs/changelogs/v4.16.11.md new file mode 100644 index 00000000..b0d98542 --- /dev/null +++ b/docs/changelogs/v4.16.11.md @@ -0,0 +1,14 @@ +# v4.16.11 โ€” 2026-07-28 + +Patch release hardening the Dependabot security lane so nothing auto-merges onto the release branch, fixing npm publish idempotency, and fixing the coverage build. + +## Fixed + +- **Dependabot PRs are never auto-merged onto the release branch.** `dependabot-auto-merge` now declines when the PR's base is the repository default branch or `master`/`main`, leaves the PR open with a warning and summary note, and treats only `next` and `hotfixes` as valid targets. This backstops `redirect-hotfix-pr`. The master commit audit deliberately does not allow-list Dependabot subjects, so a raw Dependabot commit on `master` still raises the audit issue. ([#163](https://github.com/CLDMV/.github/pull/163)) +- **`redirect-hotfix-pr` fails loudly when the cherry-pick path cannot run.** A caller workflow without a checkout or a configured git identity made the cherry-pick look like a conflict, post a misleading comment and exit green, leaving the security PR to land on the default branch. The action now checks both prerequisites up front, posts an explanatory comment, and fails the job. ([#164](https://github.com/CLDMV/.github/pull/164)) +- **`publish-package` recognizes npm's real "already published" error.** The old exact-substring match missed npm's wording ("You cannot publish over the previously published versions: โ€ฆ"), so re-runs of non-version-gated satellite publishes failed instead of skipping. The check is now a case-insensitive pattern in a tested helper. ([#165](https://github.com/CLDMV/.github/pull/165)) +- **Coverage is measured against a build that preserves `src/`.** The coverage-badge and coverage-pr-comment legs reused the matrix `build_command`, which can end with `ci:cleanup-src` and delete `src/`, leaving coverage nothing to measure. A new `coverage_build_command` input (default `npm run build`) is used for those legs; `build_command` remains a fallback when it is set empty. ([#166](https://github.com/CLDMV/.github/pull/166)) + +## Changed + +- **The release-flow caller workflows are now thin wrappers around new reusable workflows.** `feature-pr`, `hotfix-redirector`, `hotfixes-release`, `next-release`, `next-reset` and `pr-title-normalizer` logic moved into `workflow-*.yml` reusables, and the local workflows and `release-flow-v4` example templates now call them. This shipped with the cherry-pick prerequisite fix. ([#164](https://github.com/CLDMV/.github/pull/164)) diff --git a/docs/changelogs/v4.16.12.md b/docs/changelogs/v4.16.12.md new file mode 100644 index 00000000..2b15ce98 --- /dev/null +++ b/docs/changelogs/v4.16.12.md @@ -0,0 +1,8 @@ +# v4.16.12 โ€” 2026-07-29 + +Patch release making the CodeQL `code_scanning` ruleset rule in org bootstrap aware of repository visibility. + +## Fixed + +- **Bootstrap no longer requires CodeQL results where scanning cannot run.** The `code_scanning` rule is a merge gate, so on a private repository without GitHub Advanced Security it was unsatisfiable and blocked every PR with "code scanning needs to be enabled". The rule is now omitted when the repository is private, GHAS is off, and the run's `code_security` policy would not enable it, with a note in the run summary. ([#170](https://github.com/CLDMV/.github/pull/170), released in [#171](https://github.com/CLDMV/.github/pull/171)) +- **Default CodeQL setup is disabled only when `codeql.yml` exists.** Previously it was turned off whenever it was configured, which could leave a repository with no code scanning at all. ([#170](https://github.com/CLDMV/.github/pull/170)) diff --git a/docs/changelogs/v4.16.13.md b/docs/changelogs/v4.16.13.md new file mode 100644 index 00000000..579a69fa --- /dev/null +++ b/docs/changelogs/v4.16.13.md @@ -0,0 +1,7 @@ +# v4.16.13 โ€” 2026-07-30 + +Patch release omitting the CodeQL `code_scanning` ruleset rule for repositories with nothing CodeQL can analyze. + +## Fixed + +- **Bootstrap omits the `code_scanning` rule when the repository has no CodeQL-supported language.** Declarative repositories, such as a VS Code grammar extension with only JSON and icons, make CodeQL fail with "no source code seen", so requiring the rule blocked every PR. Org bootstrap now reads the repository's languages and drops the rule when none of the CodeQL-supported ones (C, C++, C#, Go, Java, Kotlin, JavaScript, TypeScript, Python, Ruby, Swift) are present, keeping the rule if the language lookup fails. The run note recommends deleting `codeql.yml` for such repositories. ([#172](https://github.com/CLDMV/.github/pull/172), released in [#173](https://github.com/CLDMV/.github/pull/173)) diff --git a/docs/changelogs/v4.16.2.md b/docs/changelogs/v4.16.2.md new file mode 100644 index 00000000..f956e3ef --- /dev/null +++ b/docs/changelogs/v4.16.2.md @@ -0,0 +1,7 @@ +# v4.16.2 โ€” 2026-07-12 + +Patch release making release-time issue closing find keywords in a PR's own commits. + +## Fixed + +- **`close-resolved-issues` scans the source PR's commit messages.** In the v4 merge flow a closing keyword (`Fixes #N`) lives only in the PR's individual commit bodies, whose subjects carry no `(#N)`, so the earlier passes never saw it and releases closed no issues. The step now also reads `/pulls/{n}/commits` for each source PR and sweeps those messages alongside the PR description and comments. ([#143](https://github.com/CLDMV/.github/pull/143), released in [#144](https://github.com/CLDMV/.github/pull/144)) diff --git a/docs/changelogs/v4.16.3.md b/docs/changelogs/v4.16.3.md new file mode 100644 index 00000000..559359af --- /dev/null +++ b/docs/changelogs/v4.16.3.md @@ -0,0 +1,12 @@ +# v4.16.3 โ€” 2026-07-18 + +Patch release defaulting the Node test matrix to LTS-only majors. + +## ๐Ÿ’ฅ Breaking Changes + +- **`lts_only_matrix` now defaults to `true`, despite being a patch release.** `workflow-ci.yml` and the `ci.yml` example template previously defaulted to `false`, and the template enabled LTS-only only on `master`/`main`. The matrix now covers even-numbered (LTS) Node majors only on every event, because odd majors are excluded by the native-binding test toolchain (vitest, rolldown, vite) and failed with "Cannot find native binding" rather than exposing real per-version regressions. ([#145](https://github.com/CLDMV/.github/pull/145), released in [#146](https://github.com/CLDMV/.github/pull/146)) + +## Upgrade notes + +- Callers that relied on testing odd Node majors (for example 21 or 23) must now pass `lts_only_matrix: false` explicitly. In the example `ci.yml`, set `lts_only_matrix` to `false` on the `workflow_dispatch` input to opt out. +- Callers already on LTS-only, or that never use odd majors, need no change. diff --git a/docs/changelogs/v4.16.4.md b/docs/changelogs/v4.16.4.md new file mode 100644 index 00000000..92de8397 --- /dev/null +++ b/docs/changelogs/v4.16.4.md @@ -0,0 +1,7 @@ +# v4.16.4 โ€” 2026-07-18 + +Patch release correcting the scorecard example template so the default configuration can publish to the OpenSSF transparency log. + +## Fixed + +- **The `scorecard.yml` example no longer grants `security-events: write`.** The scorecard action's publish step rejects submissions from a workflow whose token has `security-events` write access ("workflow verification failed: global perm is set to write"), so consumers copying the example with `publish_results: true` hit that failure. The permission is removed and a comment explains the trade-off: the SARIF upload to the Security tab and the public badge are mutually exclusive, and `security-events: write` should only be added back when `publish_results` is `false`. ([#147](https://github.com/CLDMV/.github/pull/147), released in [#148](https://github.com/CLDMV/.github/pull/148)) diff --git a/docs/changelogs/v4.16.5.md b/docs/changelogs/v4.16.5.md new file mode 100644 index 00000000..741a40f7 --- /dev/null +++ b/docs/changelogs/v4.16.5.md @@ -0,0 +1,7 @@ +# v4.16.5 โ€” 2026-07-18 + +Patch release that stops `reusable-scorecard.yml` from failing at startup for callers that do not grant `security-events: write`. + +## Fixed + +- **`reusable-scorecard.yml` no longer hard-requires `security-events: write`.** A reusable workflow whose own `permissions:` block requests more than the caller grants fails at startup with zero jobs created, and the scorecard publish step independently rejects any token carrying that permission, so the requirement was incompatible with the default public-badge mode for every caller. The permission is dropped from the workflow, and the SARIF-to-Security-tab upload step now runs only when `publish_results` is `false` (the caller must then grant `security-events: write` itself). The `publish_results` input description now documents that the two modes are mutually exclusive. ([#149](https://github.com/CLDMV/.github/pull/149), released in [#150](https://github.com/CLDMV/.github/pull/150)) diff --git a/docs/changelogs/v4.16.6.md b/docs/changelogs/v4.16.6.md new file mode 100644 index 00000000..9b9847cb --- /dev/null +++ b/docs/changelogs/v4.16.6.md @@ -0,0 +1,9 @@ +# v4.16.6 โ€” 2026-07-19 + +Patch release fixing the coverage badge and coverage PR comment jobs, which were silently skipped on every real push and pull request, and tightening release naming for satellite packages. + +## Fixed + +- **`coverage-badge` and `coverage-pr-comment` no longer skip silently.** Both jobs depend on a chain that runs through `ci` and `sync-gate`, and `sync-gate` is skipped on pushes to `master` (and on pull requests for the PR comment job). Without an explicit guard, GitHub's implicit `success()` treated the upstream skip as cascading, so the jobs never ran. Both now include `!cancelled()` in their conditions in `workflow-ci.yml`. ([#152](https://github.com/CLDMV/.github/pull/152), released in [#153](https://github.com/CLDMV/.github/pull/153)) +- **Release creation finds draft releases for a tag.** The create-release action looked up existing releases through the tag-scoped endpoint, which does not return drafts, so a stale draft was invisible and a duplicate release was created. It now pages through the full release list and matches by tag name, and the separate `enforce-published.mjs` step is removed in favor of a single retrying publish-and-verify pass (up to 15 attempts, 15 seconds apart) that guards against GitHub reverting a release to draft. ([#151](https://github.com/CLDMV/.github/pull/151)) +- **Satellite package releases are titled with their package name and no longer steal "Latest release".** Release names for packages other than the repo's own package are now prefixed with the package name, and satellite releases pass `make-latest: "false"` through a new `make_latest` input so they never take the badge from the core release. ([#151](https://github.com/CLDMV/.github/pull/151)) diff --git a/docs/changelogs/v4.16.7.md b/docs/changelogs/v4.16.7.md new file mode 100644 index 00000000..614d67f3 --- /dev/null +++ b/docs/changelogs/v4.16.7.md @@ -0,0 +1,9 @@ +# v4.16.7 โ€” 2026-07-19 + +Patch release that moves scorecard permissions to job scope and makes orphaned-release repair work around a GitHub tag-push bug. + +## Fixed + +- **Scorecard permissions are scoped to the job, not the workflow.** The scorecard action's publish step rejects workflows that grant write permissions at the workflow (global) level. `reusable-scorecard.yml` no longer has a workflow-level `permissions:` block; `id-token: write`, `contents: read` and `actions: read` are declared on the `analyze` job instead, matching the OSSF example workflow. The example template is updated to match. ([#154](https://github.com/CLDMV/.github/pull/154), released in [#155](https://github.com/CLDMV/.github/pull/155)) +- **Orphaned-release repair falls back to the REST Git Data API when a tag push is rejected.** GitHub rejects a git-protocol push of an old tag from a GitHub App with "refusing to allow a GitHub App to create or update workflow โ€ฆ without `workflows` permission" whenever the tagged commit's workflow files differ from the default branch tip. `fix-orphaned-releases` now detects that error and creates the annotated tag and ref through the REST API instead. ([#156](https://github.com/CLDMV/.github/pull/156)) +- **`target_commitish` resolves in detached-HEAD checkouts.** The fallback now tries `origin/` first, since `actions/checkout` leaves no local branch pointer and a bare `git rev-parse master` fails. ([#156](https://github.com/CLDMV/.github/pull/156)) diff --git a/docs/changelogs/v4.16.8.md b/docs/changelogs/v4.16.8.md new file mode 100644 index 00000000..7764e4e6 --- /dev/null +++ b/docs/changelogs/v4.16.8.md @@ -0,0 +1,7 @@ +# v4.16.8 โ€” 2026-07-19 + +Patch release making the tag-push fallback added in v4.16.7 actually trigger. + +## Fixed + +- **`fix-orphaned-releases` captures real `git push` stderr.** The shared git helpers stream stderr to the log but never attach it to the thrown error, so the workflow-permission detection added in v4.16.7 only ever saw "Command failed" and the REST fallback never fired. The tag push now runs through `execSync` with captured output, the detection matches against the real git message, and the failure log includes git's rejection text. ([#157](https://github.com/CLDMV/.github/pull/157), released in [#158](https://github.com/CLDMV/.github/pull/158)) diff --git a/docs/changelogs/v4.16.9.md b/docs/changelogs/v4.16.9.md new file mode 100644 index 00000000..1b8af3bc --- /dev/null +++ b/docs/changelogs/v4.16.9.md @@ -0,0 +1,7 @@ +# v4.16.9 โ€” 2026-07-19 + +Patch release that keeps a published release published after CI recreates its tag to sign it. + +## Fixed + +- **Release publish state is re-asserted after a tag is recreated.** When `fix-unsigned-tags` deletes and recreates a tag, GitHub can later revert an already-published release for that tag back to draft. The action now looks up the release bound to the tag (published or draft) and, when it is published, re-applies `draft: false` with verification, retrying up to six times at 20-second intervals and warning if the release still reads as draft. Releases intentionally left as drafts are not touched. ([#159](https://github.com/CLDMV/.github/pull/159), released in [#160](https://github.com/CLDMV/.github/pull/160)) diff --git a/docs/changelogs/v4.18.1.md b/docs/changelogs/v4.18.1.md new file mode 100644 index 00000000..c154e39b --- /dev/null +++ b/docs/changelogs/v4.18.1.md @@ -0,0 +1,7 @@ +# v4.18.1 โ€” 2026-08-02 + +Patch release restoring checkout access on the coverage badge job for private repositories. + +## Fixed + +- **Coverage badge job can check out private repositories again.** A job-level `permissions:` block replaces the workflow-level one wholesale, so `contents: read` has to be repeated on the badge job in `reusable-coverage-pr-comment.yml`. Without it the job token had `contents: none` and `actions/checkout` failed with `Repository not found` on private repos; public repos read anonymously, which hid the problem until the first private v4 consumer's release PR went red. ([#183](https://github.com/CLDMV/.github/pull/183)) diff --git a/docs/changelogs/v4.18.2.md b/docs/changelogs/v4.18.2.md new file mode 100644 index 00000000..b9407165 --- /dev/null +++ b/docs/changelogs/v4.18.2.md @@ -0,0 +1,7 @@ +# v4.18.2 โ€” 2026-08-02 + +Patch release that resolves licenses for first-party and third-party GitHub Actions references in the dependency-review check, which previously all showed as unknown. + +## Fixed + +- **Action and reusable-workflow dependencies no longer report an unknown license.** GitHub's dependency graph surfaces no license for the `actions` ecosystem, so every action or reusable-workflow reference landed in the unlicensed bucket even when its repository has a LICENSE. `reusable-dependency-review.yml` now looks up each action repository's license at the pinned ref (falling back to the default branch), deduplicated per repo, and applies the same allow/deny policy as the underlying action: an allowed license clears, a violating one moves to the incompatible-licenses bucket, and one that cannot be resolved stays flagged. ([#185](https://github.com/CLDMV/.github/pull/185)) diff --git a/docs/changelogs/v4.18.3.md b/docs/changelogs/v4.18.3.md new file mode 100644 index 00000000..c0b45377 --- /dev/null +++ b/docs/changelogs/v4.18.3.md @@ -0,0 +1,11 @@ +# v4.18.3 โ€” 2026-08-03 + +Patch release adding a way to keep the required code-scanning gate satisfiable on repositories with no analyzable source, and fixing the hotfix-redirector's App token permissions. + +## Added + +- **`CLDMV_SKIP_CODE_SCANNING` repository variable.** When set, `reusable-codeql.yml` skips the CodeQL analysis and uploads an empty, zero-alert SARIF instead. Repositories with no analyzable source (declarative packages, grammar-only extensions) fail CodeQL with "no source code seen during build", and dropping the workflow deadlocks the `code_scanning` ruleset rule waiting for a result; the empty SARIF is a passing result that satisfies the gate. ([#191](https://github.com/CLDMV/.github/pull/191)) + +## Fixed + +- **Hotfix-redirector App token can read and retarget pull requests.** The `create-app-token` step gained a `permission_pull_requests` input, and `workflow-hotfix-redirector.yml` now requests `pull-requests` and `issues` write alongside `contents`. The fine-grained token previously had no PR access, so the first `GET /pulls/{n}` returned 403 (`Resource not accessible by integration`) and the labeling call had no issues scope. ([#189](https://github.com/CLDMV/.github/pull/189)) diff --git a/docs/changelogs/v4.18.4.md b/docs/changelogs/v4.18.4.md new file mode 100644 index 00000000..85f39249 --- /dev/null +++ b/docs/changelogs/v4.18.4.md @@ -0,0 +1,8 @@ +# v4.18.4 โ€” 2026-08-02 + +Patch release making the release machinery resolve the repository's release base branch instead of assuming `master`, plus a next-reset fix for private repositories. + +## Fixed + +- **Release base branch is resolved, not hardcoded to `master`.** A new `resolve-release-base` utility action determines the base (the `CLDMV_RELEASE_BASE` variable, else the repo's default branch) and is used by the next/hotfixes release workflows, the create/update release-PR jobs, divergence detection, next-reset and the pending-release reminder, so repositories whose release branch is `main` or another name work. The release-PR flow label is derived from the resolved base, and base-interpolated git commands run without a shell (a CodeQL finding). Major bumps are classified by the `!` marker or a `BREAKING CHANGE` footer rather than a substring match. The action is referenced by a `next` commit SHA in this release so the release opener could resolve it before the tag rolled; v4.18.5 flips the pin back. ([#193](https://github.com/CLDMV/.github/pull/193), [#196](https://github.com/CLDMV/.github/pull/196)) +- **next-reset tag gate works on private repositories.** The tag lookup used an anonymous `git ls-remote`, which exits 128 on private repos and, under `set -eo pipefail`, killed the gate so `next`/`hotfixes` were never reset after a release. The lookup is now authenticated with the job token, and a failed lookup degrades to proceeding with a logged warning rather than aborting. ([#195](https://github.com/CLDMV/.github/pull/195)) diff --git a/docs/changelogs/v4.18.5.md b/docs/changelogs/v4.18.5.md new file mode 100644 index 00000000..d130681f --- /dev/null +++ b/docs/changelogs/v4.18.5.md @@ -0,0 +1,7 @@ +# v4.18.5 โ€” 2026-08-02 + +Patch release completing the pin-then-flip for the `resolve-release-base` action introduced in v4.18.4. + +## Changed + +- **`resolve-release-base` is referenced at the rolling `v4` tag again.** The next/hotfixes release workflows pinned the new action to a `next` commit SHA so the release opener could resolve it before it existed on the tag; with v4.18.4 shipped, both references now use `v4`. No behavior change. ([#198](https://github.com/CLDMV/.github/pull/198)) diff --git a/docs/changelogs/v4.19.1.md b/docs/changelogs/v4.19.1.md new file mode 100644 index 00000000..6406f190 --- /dev/null +++ b/docs/changelogs/v4.19.1.md @@ -0,0 +1,7 @@ +# v4.19.1 โ€” 2026-08-04 + +Patch release adding CLDMV-private auto-routing to the runner selection and version-locking the nested reusable-workflow calls. + +## Fixed + +- **Runner auto-routing falls back to `cldmv-runners` for CLDMV-owned private repositories.** When `runs_on` is empty, runner resolution now goes: the caller's `RUNS_ON_DEFAULT` variable, then `cldmv-runners` for private repositories owned by CLDMV, then `ubuntu-latest`. The fallback is applied to every job in the workflow chain, covering contexts where the event payload cannot answer the routing question. Nested reusable calls inside the `workflow-*.yml` chain were switched from local `./` paths to explicit `CLDMV/.github` references. v4.19.2 restored the deliberate `v4` pins. ([#203](https://github.com/CLDMV/.github/pull/203)) diff --git a/docs/changelogs/v4.19.2.md b/docs/changelogs/v4.19.2.md new file mode 100644 index 00000000..747ac371 --- /dev/null +++ b/docs/changelogs/v4.19.2.md @@ -0,0 +1,8 @@ +# v4.19.2 โ€” 2026-08-03 + +Patch release restoring the intended rolling `v4` pins on nested reusable calls and making release-relevant CI runs immune to cancellation. + +## Fixed + +- **Nested reusable calls pin the rolling `v4` tag again.** The version-lock in v4.19.1 overcorrected; rolling-tag pins are the intended contract between first-party reusables, so `workflow-ci.yml`, `workflow-publish.yml`, `workflow-release.yml` and `workflow-update-major-version-tags.yml` reference `CLDMV/.github/.github/workflows/.yml@v4` for their nested calls. ([#206](https://github.com/CLDMV/.github/pull/206)) +- **Release-relevant CI runs are never superseded.** Pushes to the release base branch, to `next`/`hotfixes`, and the `next`/`hotfixes` release PRs now get a unique concurrency group per run, so every run completes and posts a green check rather than an earlier one being cancelled into a red X on the release PR. Feature branches keep cancel-superseded behavior. The base branch is derived from `CLDMV_RELEASE_BASE` or the default branch instead of assuming `master`/`main`, and the `ci.yml` example template was updated to match. ([#205](https://github.com/CLDMV/.github/pull/205)) diff --git a/docs/changelogs/v4.19.3.md b/docs/changelogs/v4.19.3.md new file mode 100644 index 00000000..f5e3c875 --- /dev/null +++ b/docs/changelogs/v4.19.3.md @@ -0,0 +1,8 @@ +# v4.19.3 โ€” 2026-08-08 + +Patch release fixing private-repository CI that could not obtain a runner, and adding the source and publishing workflow for the self-hosted runner image. + +## Fixed + +- **The `โœ… Required PR Check` mirror job is routed like the reusables.** The `ci.yml` template hardcoded `runs-on: ubuntu-latest` for the mirror job, which failed to provision a runner on private repositories once the reusables moved to `cldmv-runners` in v4.19.1. The job now uses the same expression: `RUNS_ON_DEFAULT`, else `cldmv-runners` for CLDMV-owned private repos, else `ubuntu-latest`. ([#209](https://github.com/CLDMV/.github/pull/209)) +- **Self-hosted runners have `gh`.** The stock ARC runner image ships no `gh`, so every routed job that shelled out to it died with exit 127. The image is now built from `images/actions-runner/` (ARC base plus `gh`) and published as `ghcr.io/cldmv/actions-runner` by the new `build-runner-image.yml` workflow, with a README and Helm values file for the runner scale set. ([#212](https://github.com/CLDMV/.github/pull/212)) diff --git a/docs/changelogs/v4.21.1.md b/docs/changelogs/v4.21.1.md new file mode 100644 index 00000000..eacbc44e --- /dev/null +++ b/docs/changelogs/v4.21.1.md @@ -0,0 +1,7 @@ +# v4.21.1 โ€” 2026-08-09 + +Patch release baking the Node.js LTS runtime into the self-hosted runner image. + +## Fixed + +- **The `cldmv-runners` image includes Node.js.** The image previously carried only `gh` and `jq`, so run steps invoking `node` or `npm` directly failed on self-hosted runners, unlike GitHub-hosted ones. The Dockerfile now installs the current LTS from the NodeSource `lts` channel (including npm and npx), so the weekly rebuild tracks LTS promotions, and the build fails if `gh`, `jq`, `node` or `npm` is missing. Jobs that need a specific version still use `actions/setup-node`. ([#228](https://github.com/CLDMV/.github/pull/228)) diff --git a/docs/changelogs/v4.22.1.md b/docs/changelogs/v4.22.1.md new file mode 100644 index 00000000..e622454a --- /dev/null +++ b/docs/changelogs/v4.22.1.md @@ -0,0 +1,8 @@ +# v4.22.1 โ€” 2026-08-20 + +Patch release scoping coverage PR-body injection to the persistent release PRs and deriving the coverage badge filename per branch. + +## Fixed + +- **Coverage is injected only into release PR descriptions.** The coverage PR-comment job ran for every pull request, writing whole-repo coverage into feature PR descriptions. It now runs only when the PR head is `next` or `hotfixes` and comes from the same repository, which also keeps fork PRs with a branch literally named `next` away from a job holding write permission and bot secrets. ([#239](https://github.com/CLDMV/.github/pull/239)) +- **Coverage badge filename is derived per branch.** The badge job passes `coverage-next.json` or `coverage-hotfixes.json` on those branches and `coverage.json` otherwise, so widening the trigger later cannot let one branch overwrite another's badge. An explicit non-default `badge_filename` from the caller always wins. ([#239](https://github.com/CLDMV/.github/pull/239)) diff --git a/docs/changelogs/v4.22.2.md b/docs/changelogs/v4.22.2.md new file mode 100644 index 00000000..01acdf12 --- /dev/null +++ b/docs/changelogs/v4.22.2.md @@ -0,0 +1,7 @@ +# v4.22.2 โ€” 2026-08-26 + +Patch release teaching the generated changelog to recognize the org's own commit-signing bot, so it is not listed as a contributor. + +## Fixed + +- **The commit-signing bot is excluded from changelog contributor lists.** `generate-comprehensive-changelog` gained `bot-name` and `bot-email` inputs, with matching optional `BOT_NAME` and `BOT_EMAIL` secrets on the feature-PR, next-release and hotfixes-release workflows (and their example callers), passed in from the repo's `CLDMV_BOT_NAME`/`CLDMV_BOT_EMAIL` secrets. Bot detection tests cover the new identity matching. Unset secrets exclude nothing extra, so existing callers are unaffected. ([#241](https://github.com/CLDMV/.github/pull/241)) diff --git a/docs/changelogs/v4.25.1.md b/docs/changelogs/v4.25.1.md new file mode 100644 index 00000000..21c6e57c --- /dev/null +++ b/docs/changelogs/v4.25.1.md @@ -0,0 +1,11 @@ +# v4.25.1 โ€” 2026-09-02 + +Patch release fixing the hotfix-redirector's cherry-pick, which was unsigned and therefore silently blocked by the `hotfixes` required-signatures rule. + +## Fixed + +- **Redirected Dependabot security PRs carry a signed cherry-pick.** The dependabot-security redirect cherry-picked onto `hotfixes` without importing the bot GPG key and committed under the App-slug identity, producing an unsigned commit that required-signatures blocked. The redirector now imports the signing key, authors the commit under the real-user bot identity, signs it (`-S`), fails loudly when signing prerequisites are missing, and refuses to push an unsigned commit. The reusable gains four optional secrets for this: `BOT_NAME`, `BOT_EMAIL`, `BOT_GPG_PRIVATE_KEY` and `BOT_GPG_PASSPHRASE`. ([#259](https://github.com/CLDMV/.github/pull/259), fixes [#257](https://github.com/CLDMV/.github/issues/257)) + +## Upgrade notes + +- A security redirect only succeeds when the caller passes the signing secrets. Re-sync `hotfix-redirector.yml` from the `release-flow-v4` template, or add `BOT_NAME: ${{ secrets.CLDMV_BOT_NAME }}`, `BOT_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }}`, `BOT_GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }}` and `BOT_GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }}` to its `secrets:` block. Without them, a redirect that needs the cherry-pick now fails loudly instead of leaving an unsigned, blocked PR behind. Routine PRs that need no redirect are unaffected. diff --git a/docs/changelogs/v4.26.1.md b/docs/changelogs/v4.26.1.md new file mode 100644 index 00000000..12a8f3fa --- /dev/null +++ b/docs/changelogs/v4.26.1.md @@ -0,0 +1,7 @@ +# v4.26.1 โ€” 2026-09-05 + +Patch release fixing the `CLDMV_SKIP_FROZEN_LOCKFILE` opt-out, which composite actions could not read, so the variable had no effect inside them. + +## Fixed + +- **`CLDMV_SKIP_FROZEN_LOCKFILE` is now passed to composite actions as an input.** The `create-release-pr`, `create-release`, `update-release-pr` and `build-and-test` composite actions read the variable through the `vars` context, which is not available inside composite actions, so the value was always empty and installs stayed frozen regardless of the repo or org setting. Each action now takes an optional `skip-frozen-lockfile` input (empty means frozen install), and the calling reusable and entry-point workflows (build-and-test, publishing, release-management, next/hotfixes release, release) supply `${{ vars.CLDMV_SKIP_FROZEN_LOCKFILE }}`. Callers need no change. ([#276](https://github.com/CLDMV/.github/pull/276)) diff --git a/docs/changelogs/v4.26.2.md b/docs/changelogs/v4.26.2.md new file mode 100644 index 00000000..d5586448 --- /dev/null +++ b/docs/changelogs/v4.26.2.md @@ -0,0 +1,9 @@ +# v4.26.2 โ€” 2026-09-06 + +Patch release hardening the post-release `next-reset` and release-PR flow so a hotfix release can no longer reset `next` and drop its queued work, and a release version can no longer be recomputed after it already shipped. + +## Fixed + +- **`next-reset` no longer force-resets `next` after a hotfix release.** Lane detection reads the released PR's head ref with `gh pr view`, but the app token was created in fine-grained mode without any pull-requests scope, so the call was silently denied, the lane resolved to "other" for every hotfix release, and `next` was reset to `master` instead of having `master` merged in. The token now requests pull-requests read (via the new `permission_pull_requests_read` input on `create-app-token`), the `gh` error is logged instead of swallowed, and the reset path runs only when the head ref is positively a non-`hotfixes` branch. An unreadable or unknown head now falls through to the merge (preserve) path. ([#279](https://github.com/CLDMV/.github/pull/279), refs [#278](https://github.com/CLDMV/.github/issues/278)) +- **Release version is floored at the highest released tag.** The base version came from `package.json` on the default branch, which can lag a tag pushed by a concurrent release, causing an already-shipped version to be recomputed. `find-divergence` and the release-PR `divergence` step now take the maximum of that base and the highest semver tag (read via `git ls-remote --tags`), degrading to no floor if tags cannot be read. ([#280](https://github.com/CLDMV/.github/pull/280), refs [#278](https://github.com/CLDMV/.github/issues/278)) +- **A spuriously closed release PR is reopened instead of orphaned.** When no open `next` โ†’ base PR exists, the `next` release workflow now reopens the most recent closed-but-unmerged one, preserving its number and history, and creates a fresh PR only if none exists. ([#280](https://github.com/CLDMV/.github/pull/280)) diff --git a/docs/changelogs/v4.27.0.md b/docs/changelogs/v4.27.0.md new file mode 100644 index 00000000..4094d0b3 --- /dev/null +++ b/docs/changelogs/v4.27.0.md @@ -0,0 +1,15 @@ +# v4.27.0 โ€” 2026-09-06 + +Minor release extending the coverage badge to the integration branches: `next` and `hotfixes` now publish their own badge JSON alongside the default branch's, with the push step rewritten to be safe under concurrent runs. + +## Added + +- **Per-branch coverage badge JSON for `next` and `hotfixes`.** The coverage-badge job in `workflow-ci.yml` previously fired only on pushes to the default branch. It now also runs on pushes to `next` and `hotfixes`, each publishing under a per-branch filename (`coverage-next.json`, `coverage-hotfixes.json`) on the shared `badges` branch, while the default branch keeps `coverage.json`. The job is gated on branch refs rather than `ref_name`. ([#283](https://github.com/CLDMV/.github/pull/283)) + +## Fixed + +- **Concurrent badge pushes no longer drop a badge.** Because several branches now push to the single `badges` branch at once, `push-badge` re-syncs onto the latest tip and replays its badge file when a push is rejected as non-fast-forward, retrying up to five times with jitter. Only non-fast-forward rejections are retried; auth, permission and network failures surface immediately. ([#283](https://github.com/CLDMV/.github/pull/283)) + +## Changed + +- **`push-badge` is hardened against injection and token leaks.** Git commands now run through `execFileSync` with argument arrays instead of shell strings (the CodeQL-recommended fix for indirect command-line injection), `badge-filename` must be a bare filename (no separators or traversal), a push token is required up front with a clear error, and the token is redacted from any logged git output. ([#283](https://github.com/CLDMV/.github/pull/283)) diff --git a/docs/changelogs/v4.27.1.md b/docs/changelogs/v4.27.1.md new file mode 100644 index 00000000..f44e270e --- /dev/null +++ b/docs/changelogs/v4.27.1.md @@ -0,0 +1,20 @@ +# v4.27.1 โ€” 2026-09-13 + +Patch release raising the default Node.js test matrix to the vitest 5 floor (22.12.0) and Node 26, and fixing the self-hosted runner image so jobs that pin Node 26 can start. The new defaults change the matrix for every caller that does not set the inputs explicitly, so this release is listed as breaking despite being a patch. + +## ๐Ÿ’ฅ Breaking Changes + +- **Default Node.js matrix bounds changed, despite being a patch release.** `min_node_version` now defaults to `22.12.0` (was `20`) and `max_node_major` to `26` (was `22`) in `workflow-ci.yml`, `workflow-publish.yml`, `workflow-release.yml`, `reusable-build-and-test.yml` and the `generate-matrix` action, and in the matching `core-cicd` example callers. Repos that rely on the defaults stop testing Node 20 and start testing newer majors up to 26 on their next run. ([#285](https://github.com/CLDMV/.github/pull/285)) + +## Fixed + +- **Self-hosted runner image now includes `libatomic1`.** `actions/setup-node` downloads its own Node builds, and the official Node 26 Linux build links against `libatomic.so.1`, which the minimal runner base image lacked, so any job requesting Node 26 failed before printing `node --version`. The package is installed in `images/actions-runner/Dockerfile`. ([#288](https://github.com/CLDMV/.github/pull/288)) + +## Changed + +- **Dependabot example groups vitest, eslint and prettier families.** The example `dependabot.yml` gains groups that bump `vitest` with `@vitest/*`, `eslint` with `@eslint/*` and the CLDMV eslint plugins, and `prettier` with its CLDMV plugins, because these packages peer each other tightly and a partial bump breaks `npm ci` with an `ERESOLVE`. ([#287](https://github.com/CLDMV/.github/pull/287)) + +## Upgrade notes + +- To keep the previous matrix, pass `min_node_version: "20"` and `max_node_major: "22"` in the caller's `ci.yml`, `publish.yml` and `release.yml` `with:` blocks (or the workflow-dispatch defaults). +- A `min_node_version` of `22.12.0` matches the floor of the current vitest toolchain, so repos on that toolchain should not need to change anything. diff --git a/docs/changelogs/v4.27.2.md b/docs/changelogs/v4.27.2.md new file mode 100644 index 00000000..fbbd9fbd --- /dev/null +++ b/docs/changelogs/v4.27.2.md @@ -0,0 +1,7 @@ +# v4.27.2 โ€” 2026-09-13 + +Patch release so routine pull requests are no longer failed by the hotfix-redirector when the `hotfixes` branch is briefly missing. + +## Fixed + +- **Hotfix-redirector no longer hard-fails when `hotfixes` is transiently absent.** Right after a `hotfixes` โ†’ `master` release squash, `hotfixes` can be missing until it is recreated, and the redirector's checkout of it failed the whole job with a "branch not found" error on unrelated PRs such as routine Dependabot bumps. That checkout is now `continue-on-error` with a warning, and the dependent identity and signing steps run only when it succeeded. The redirect decision itself does not need the checkout, so PRs that need no redirect skip cleanly, and one that does need the cherry-pick path still gets the existing actionable prerequisite error. ([#289](https://github.com/CLDMV/.github/pull/289)) diff --git a/docs/changelogs/v4.27.3.md b/docs/changelogs/v4.27.3.md new file mode 100644 index 00000000..f7f3cee2 --- /dev/null +++ b/docs/changelogs/v4.27.3.md @@ -0,0 +1,16 @@ +# v4.27.3 โ€” 2026-09-15 + +Patch release crediting release contributors through `Co-authored-by` trailers on the squashed release commit, adding a PR-time docs-generation check, and installing `zip`/`unzip` on the self-hosted runner image. The docs check is on by default, so this release is listed as breaking despite being a patch. + +## ๐Ÿ’ฅ Breaking Changes + +- **New default-on docs-generation check on PRs, despite being a patch release.** `workflow-ci.yml` and `reusable-build-and-test.yml` gain a `docs_check_command` input (default `npm run docs:build --if-present`) that runs once per PR in a new "๐Ÿ“– Validate Docs Generation" job. Repos that define a `docs:build` script now run it on every PR, and a doc-tooling failure (for example a JSDoc type the generator cannot parse) will fail the PR, where it previously surfaced only at the post-release docs publish. Repos without the script are unaffected. ([#293](https://github.com/CLDMV/.github/pull/293)) + +## Fixed + +- **Release contributors are credited via `Co-authored-by` trailers.** A markdown `@mention` in the release body gives no contributor-graph credit, only a trailer does. `generate-comprehensive-changelog` now emits a `co-authors` output (human commit authors plus trailers found in commit bodies, bots and the signing bot excluded), `update-pr-changelog` appends it as the final paragraph of the release-PR body behind a `` marker, and `update-pr-coverage` inserts its badge above that marker so the trailers stay last, as GitHub requires. ([#292](https://github.com/CLDMV/.github/pull/292)) +- **`zip` and `unzip` are installed on the self-hosted runner image.** The package-file step of `workflow-publish.yml` shells out to `zip` and failed with "zip: not found" on private-repo releases routed to `cldmv-runners`. ([#295](https://github.com/CLDMV/.github/pull/295), refs [#291](https://github.com/CLDMV/.github/issues/291)) + +## Upgrade notes + +- To opt out of the docs check, set `docs_check_command: ""` in the caller's `ci.yml` `with:` block, or override it with a lighter command such as a source-only docs build. diff --git a/docs/changelogs/v4.27.4.md b/docs/changelogs/v4.27.4.md new file mode 100644 index 00000000..5582fd0e --- /dev/null +++ b/docs/changelogs/v4.27.4.md @@ -0,0 +1,7 @@ +# v4.27.4 โ€” 2026-09-15 + +Patch release so a contributor is no longer credited twice in the release `Co-authored-by` trailers when their commits carry different GitHub noreply email forms. + +## Fixed + +- **Co-author trailers are deduplicated by GitHub login, not raw email.** The ID-prefixed (`{id}+{login}@users.noreply.github.com`) and bare (`{login}@users.noreply.github.com`) noreply addresses identify the same account but were treated as two people, producing duplicate trailers. They now collapse to one entry keyed on the login, preferring the ID-prefixed form for a deterministic result. Non-noreply emails still dedupe on the email itself. ([#296](https://github.com/CLDMV/.github/pull/296)) diff --git a/docs/changelogs/v4.27.5.md b/docs/changelogs/v4.27.5.md new file mode 100644 index 00000000..3292826f --- /dev/null +++ b/docs/changelogs/v4.27.5.md @@ -0,0 +1,8 @@ +# v4.27.5 โ€” 2026-09-15 + +Patch release making release notes survive a title-only squash merge and ensuring a freshly created release PR carries the co-author trailers from the start. + +## Fixed + +- **Release notes are recovered from the PR body when the squash drops it.** A `next` โ†’ `master` or `hotfixes` โ†’ `master` squash can land with an empty commit body despite `squash_merge_commit_message: PR_BODY`, which stripped the curated changelog and coverage from the GitHub Release and tag. When the release commit body is empty, `generate-comprehensive-changelog` now reads the trailing `(#N)` from the subject and fetches that PR's body, using it verbatim. The lookup is best-effort and falls back to the previous behavior on any miss. ([#299](https://github.com/CLDMV/.github/pull/299), fixes [#298](https://github.com/CLDMV/.github/issues/298)) +- **Co-author trailers are threaded into the release-PR create path.** The trailer block was applied only when an existing release PR was refreshed, so a newly opened release PR lacked it until its first refresh. The `pull-requests` API action now accepts a `co-authors` input and `create-release-pr` passes it, so the trailers are present from creation. ([#302](https://github.com/CLDMV/.github/pull/302), refs [#301](https://github.com/CLDMV/.github/issues/301)) diff --git a/docs/changelogs/v4.28.0.md b/docs/changelogs/v4.28.0.md new file mode 100644 index 00000000..fd17d810 --- /dev/null +++ b/docs/changelogs/v4.28.0.md @@ -0,0 +1,12 @@ +# v4.28.0 โ€” 2026-09-16 + +Adds an opt-in, approval-triggered merge for the persistent release PR. A maintainer approves the `next` โ†’ `master` (or `hotfixes` โ†’ `master`) release PR, and once every check on the head has passed, the PR is squash-merged through the REST API with the PR body as the commit message. A human approval is still required; nothing merges unattended. + +## Added + +- **`workflow-release-merge.yml` reusable workflow and `release-merge` job action.** The merge uses `PUT .../merge` with an explicit commit message equal to the release PR body, so the release commit is exactly the curated changelog: no title-only commit from the mobile default path, and no GitHub UI `Co-authored-by:` auto-append (the `` block already in the body is the whole credit). The merge waits until every check on the head, required and non-required, has finished and passed, so the body it reads is never stale. It never approves as the bot. Inputs: `release_base_branches` (default `master,main`), `integration_branches` (default `next,hotfixes`), `allowed_associations` (default `MEMBER,OWNER`), `require_approval` (default `true`), `merge_method` (default `squash`), `allow_failing_checks` (default none), plus `runs_on`. Secrets: `BOT_APP_CLIENT_ID` and `BOT_APP_PRIVATE_KEY`. ([#304](https://github.com/CLDMV/.github/pull/304), see [#303](https://github.com/CLDMV/.github/issues/303)) +- **`release-merge.yml` caller template** under `examples/individual-repo-workflows/release-flow-v4/`, and updates to `docs/conventions/release-flow-v4.md` describing the opt-in approval-triggered merge alongside the manual "Squash and merge" click. ([#304](https://github.com/CLDMV/.github/pull/304)) + +## Fixed + +- **Event fields are passed as inputs instead of being read from the event file.** The release-merge action receives `pr_number` and `head_branch` from the workflow's `github` context rather than reading `GITHUB_EVENT_PATH`, which avoids CodeQL's `js/file-access-to-http` finding. ([#304](https://github.com/CLDMV/.github/pull/304)) diff --git a/docs/changelogs/v4.28.1.md b/docs/changelogs/v4.28.1.md new file mode 100644 index 00000000..8694dc11 --- /dev/null +++ b/docs/changelogs/v4.28.1.md @@ -0,0 +1,9 @@ +# v4.28.1 โ€” 2026-09-17 + +Patch release that stops branch-retention from deleting the permanent integration branches. Deleting `next` or `hotfixes` when its release PR merged made GitHub auto-close every other open PR based on it. + +## Fixed + +- **Branch retention no longer deletes `next` / `hotfixes`.** The default `exempt_patterns` in `reusable-branch-retention.yml` changed from `["master","main","badges","gh-pages"]` to `["master","main","badges","gh-pages","dev","next","hotfixes"]`. The release PR merges with one of the integration branches as its head, and deleting a merged PR's head branch auto-closes all open PRs still based on it. Callers that pass their own `exempt_patterns` replace the default and should include `next` and `hotfixes`. ([#308](https://github.com/CLDMV/.github/pull/308)) +- **Hard guard for permanent branches.** The branch-retention action now refuses to delete `master`, `main`, `next` or `hotfixes` regardless of configuration, and filters them out of retention-rule pruning, so a misconfigured caller cannot remove them. ([#308](https://github.com/CLDMV/.github/pull/308)) +- **Individual-repo templates inherit `max_node_major`.** The `ci.yml`, `publish.yml` and `release.yml` example workflows now default `max_node_major` to blank, so the reusable workflow's default applies instead of a hardcoded `26`. ([#306](https://github.com/CLDMV/.github/pull/306)) diff --git a/docs/changelogs/v4.28.2.md b/docs/changelogs/v4.28.2.md new file mode 100644 index 00000000..d6fdf093 --- /dev/null +++ b/docs/changelogs/v4.28.2.md @@ -0,0 +1,7 @@ +# v4.28.2 โ€” 2026-09-17 + +Patch release that removes the duplicate Node.js test leg when `lts/*` resolves to a major already in the matrix. Note: the approach taken here (dropping `lts/*`) was reversed in v4.28.3 because it broke publishing. + +## Fixed + +- **Node.js matrix is de-duplicated against the resolved `lts/*` version.** `reusable-build-and-test.yml` gains a step that installs `lts/*` with `actions/setup-node` and passes the resolved version to `generate-matrix` through a new `current-lts-version` input (empty disables dedup). When that major was already an explicit matrix entry, the trailing `lts/*` entry was dropped so the same Node version was not tested twice under two labels. The default `max-node-major` description was also corrected to 26. ([#309](https://github.com/CLDMV/.github/pull/309)) diff --git a/docs/changelogs/v4.28.3.md b/docs/changelogs/v4.28.3.md new file mode 100644 index 00000000..ed6a28c0 --- /dev/null +++ b/docs/changelogs/v4.28.3.md @@ -0,0 +1,8 @@ +# v4.28.3 โ€” 2026-09-20 + +Patch release that restores publishing after the v4.28.2 matrix dedup, and fixes how `next-reset` identifies the released PR. + +## Fixed + +- **Matrix dedup keeps `lts/*` and drops the redundant numeric entry.** v4.28.2 removed `lts/*` when its major was already explicit, but the publish flow downloads the artifact named `build-artifacts-lts`, which only the `lts/*` leg produces, so publishing failed with "Artifact not found for name: build-artifacts-lts". The explicit numeric entry is now replaced by `lts/*` in place, giving one leg with the artifact intact. ([#311](https://github.com/CLDMV/.github/pull/311)) +- **`next-reset` resolves the released PR from the commit.** The lane check previously took the last `#N` found in the release commit message, which is the changelog body, so it often picked an issue number instead of the release PR. `gh pr view` then failed and every normal release fell through to the fail-safe merge path, leaving `next` un-reset and re-cutting shipped work as a phantom release. The workflow now uses the commit-to-PRs API (`commits//pulls`) to find the merged PR's head ref. ([#311](https://github.com/CLDMV/.github/pull/311)) diff --git a/docs/changelogs/v4.28.4.md b/docs/changelogs/v4.28.4.md new file mode 100644 index 00000000..a4a8def5 --- /dev/null +++ b/docs/changelogs/v4.28.4.md @@ -0,0 +1,7 @@ +# v4.28.4 โ€” 2026-09-20 + +Patch release so that `close-resolved-issues` actually closes issues after a release. + +## Fixed + +- **`close-resolved-issues` finds the release PR through the commit-to-PR API.** The release squash commit has a custom subject (the release PR title) with no trailing `(#N)`, so parsing the subject found nothing and each release closed zero issues. The action now resolves the release PR with `GET /commits//pulls` (preferring the most recently merged PR) and falls back to the subject reference only when the API returns nothing. ([#313](https://github.com/CLDMV/.github/pull/313)) diff --git a/docs/changelogs/v4.29.0.md b/docs/changelogs/v4.29.0.md new file mode 100644 index 00000000..7205b659 --- /dev/null +++ b/docs/changelogs/v4.29.0.md @@ -0,0 +1,16 @@ +# v4.29.0 โ€” 2026-09-20 + +This release wires the approval-triggered release merge into this repository's own release PR, and corrects the trigger the caller template uses to re-evaluate it. GitHub does not send `check_suite: completed` for suites created by GitHub Actions, so an approval given before CI finished left a fully green release PR waiting indefinitely. Coverage jobs were also decoupled from the Node test matrix to shorten pipeline time. + +## Added + +- **`local-release-merge.yml`** enables the approval-triggered merge for this repository's own `next` โ†’ `master` release PR, calling the local `workflow-release-merge.yml`. ([#317](https://github.com/CLDMV/.github/pull/317)) + +## Fixed + +- **Release merge re-evaluates on `workflow_run`, not `check_suite`.** The reusable workflow now reads the head branch from `workflow_run.head_branch` and falls back to `check_suite.head_branch`, so callers that have not re-synced keep working. The `release-merge.yml` caller template switches its trigger to `workflow_run: completed` for the repository's CI workflow (matched by literal workflow `name:`); callers should re-sync their copy and check that `workflows:` matches their own CI workflow name. ([#319](https://github.com/CLDMV/.github/pull/319), see [#318](https://github.com/CLDMV/.github/issues/318)) +- **Coverage runs in parallel with the Node test matrix.** In `workflow-ci.yml` the `coverage-badge` and `coverage-pr-comment` jobs no longer `need` the `ci` matrix job, since they build and test independently. They now `need` `sync-gate` directly to read `is_master_sync` and skip on master-sync pushes; they no longer wait for matrix success, and a coverage job failure is reported on its own. ([#315](https://github.com/CLDMV/.github/pull/315)) + +## Changed + +- **Release-flow docs** describe the opt-in approval-triggered merge as part of the release flow. ([#317](https://github.com/CLDMV/.github/pull/317)) diff --git a/docs/changelogs/v4.29.1.md b/docs/changelogs/v4.29.1.md new file mode 100644 index 00000000..279b0410 --- /dev/null +++ b/docs/changelogs/v4.29.1.md @@ -0,0 +1,16 @@ +# v4.29.1 โ€” 2026-09-28 + +Patch release that makes the default publish command pass `--ignore-scripts`. The publish jobs run inside the packed `package-contents/` artifact, which has no `node_modules` and no lockfile, so a lifecycle script such as `prepack` that needs devDependencies crashed the publish. + +## ๐Ÿ’ฅ Breaking Changes + +- **The default publish command now includes `--ignore-scripts`, despite being a patch release.** Auto-generated commands for npm and GitHub Packages (with any package manager) become, for example, `npm publish --access public --ignore-scripts --provenance` on npm for public repositories and `npm publish --access restricted --ignore-scripts` for private ones. Lifecycle scripts (`prepack`, `prepublishOnly`, `postpack`, ...) no longer run during publish. The build job has already run `build_command` before packing, so most repositories are unaffected; a repository that relied on a lifecycle script running against the packed tree must move that work into `build_command`, or supply its own `publish_command` / `github_packages_publish_command`. Caller-supplied commands are used verbatim and are not modified. ([#321](https://github.com/CLDMV/.github/pull/321), fixes [#320](https://github.com/CLDMV/.github/issues/320)) + +## Fixed + +- **Publish no longer fails on `prepack` scripts that need devDependencies.** The command is built by a new shared helper, `utilities/publish-command/build.mjs`, used by repo-detection and both publish fallbacks; `--provenance` remains limited to public npm-CLI publishes to npm. `docs/conventions/package-manager.md` gained a "Publishing from the build artifact" section. ([#321](https://github.com/CLDMV/.github/pull/321)) + +## Upgrade notes + +1. If a `prepack` / `prepublishOnly` script performs real work, run it from `build_command` instead. +2. If a custom publish command is set, add `--ignore-scripts` to it unless a lifecycle script genuinely has to run against the packed tree. diff --git a/docs/changelogs/v4.29.2.md b/docs/changelogs/v4.29.2.md new file mode 100644 index 00000000..6e520b41 --- /dev/null +++ b/docs/changelogs/v4.29.2.md @@ -0,0 +1,7 @@ +# v4.29.2 โ€” 2026-09-28 + +Patch release so the approval-triggered release merge re-evaluates when any check-producing workflow finishes, not only CI. The release commit and PR title still read "pass --ignore-scripts on the default publish command" because the title was set when the release PR was first opened; the only code change in this release is the release-merge fix. The `--ignore-scripts` change shipped in v4.29.1. + +## Fixed + +- **Release merge is re-armed by every check-producing workflow.** The merge gate waits on every check-run on the release PR head, but only CI was listed under `workflow_run`, so when CodeQL finished after CI nothing re-fired and an approved, fully green release PR stayed unmerged (PR [#322](https://github.com/CLDMV/.github/pull/322)). The `release-merge.yml` template now lists the standard v4 workflow names (CI, CodeQL, Dependency Review, Bundle Size, release workflows, labeler, title normalizer, CLA, and others) and limits re-fires with `branches: [next, hotfixes]`. The local caller and reusable-workflow comments and release-flow docs were updated to match. Callers should re-sync the template; names are matched literally, and listing a workflow the repository lacks is harmless. ([#323](https://github.com/CLDMV/.github/pull/323)) diff --git a/docs/changelogs/v4.29.3.md b/docs/changelogs/v4.29.3.md new file mode 100644 index 00000000..e7d2aaa2 --- /dev/null +++ b/docs/changelogs/v4.29.3.md @@ -0,0 +1,9 @@ +# v4.29.3 โ€” 2026-09-28 + +Patch release that makes `bundle-size.yml` a standard v4 workflow, hardens `dist_paths` handling, and stops the paths gate from failing on rewritten history. + +## Fixed + +- **`bundle-size.yml` is treated as a standard v4 workflow.** The template moved from `packaging-docs/` to `core-cicd/`, and its header, the README, the migration docs, the setup guides and the `release-merge.yml` template now list it with the other core workflows. It documents that `dist_paths` should match what the package publishes. A `dist_paths` set that matches no files now logs a warning (the step still passes). ([#326](https://github.com/CLDMV/.github/pull/326)) +- **`dist_paths` entries with a leading `./` are normalized and the measure walk prunes non-matching directories.** Plain file paths match that one file, and only `*` and `**` wildcards are supported. ([#328](https://github.com/CLDMV/.github/pull/328)) +- **Paths gate falls back to full CI when the diff cannot be computed.** On a push with a missing or zero `before` SHA, a compare API failure (for example `404 No common ancestor` after a force-pushed new root commit), a compare status other than `ahead`/`identical`, or a failure listing PR files, the gate now emits `docs_only=false` with a notice instead of failing the run or, after a force-push back to an older commit, wrongly reporting a docs-only change. ([#331](https://github.com/CLDMV/.github/pull/331)) diff --git a/docs/changelogs/v4.29.4.md b/docs/changelogs/v4.29.4.md new file mode 100644 index 00000000..29e4f96f --- /dev/null +++ b/docs/changelogs/v4.29.4.md @@ -0,0 +1,8 @@ +# v4.29.4 โ€” 2026-09-28 + +Patch release adding a working `pinned` exemption to the stale sweep, and letting `next-reset` merge a hotfix into `next` when the only conflicts are release version fields. + +## Fixed + +- **`pinned` label exempts issues and PRs from the stale sweep.** A `pinned` label (colour `5319e7`) was added to `data/github-labels.json` and removed from the `semver: explicit` aliases it previously collided with. Exempt items that were already marked stale (for example, pinned during the grace period) now have the stale label removed. The default exemption lists were also corrected to the real label names: issues exempt `pinned,security,help wanted,good first issue,status: blocked`, PRs exempt `pinned,work-in-progress,dependencies,type: dependencies,status: blocked`. The stale messages mention `pinned`, and the `stale.yml` template documents it. Callers that override `exempt_issue_labels` or `exempt_pr_labels` replace the defaults and should keep `pinned`. ([#332](https://github.com/CLDMV/.github/pull/332)) +- **Hotfix releases no longer fail to merge `master` into `next` on version-only conflicts.** When `next` has a pending release, both sides bump the `version` fields and the Merges API returns 409. `merge-master-into-branch` now redoes the merge locally and, when every conflict is the root `version` in `package.json` or the root `version` / `packages[""].version` in `package-lock.json`, keeps the target branch's version and publishes the two-parent merge commit through the Git Data API. Any other conflict still fails. The action gains a `conflict-resolved` output and needs a full-history checkout. ([#335](https://github.com/CLDMV/.github/pull/335), see [#334](https://github.com/CLDMV/.github/issues/334)) diff --git a/docs/changelogs/v4.3.1.md b/docs/changelogs/v4.3.1.md new file mode 100644 index 00000000..2b3fb252 --- /dev/null +++ b/docs/changelogs/v4.3.1.md @@ -0,0 +1,7 @@ +# v4.3.1 โ€” 2026-05-25 + +Patch release so branch retention runs when pull requests close against the v4 integration branches. + +## Fixed + +- **Branch retention fires on PR-close against `next` and `hotfixes`.** `local-branch-retention.yml` did not trigger for PRs closing against the v4 integration branches, so merged feature-branch heads were not cleaned up. The trigger and the `branch-retention.yml` consumer template were updated, and the branch-naming and release-flow-v4 conventions docs describe the behavior. ([#34](https://github.com/CLDMV/.github/pull/34), released in [#35](https://github.com/CLDMV/.github/pull/35)) diff --git a/docs/changelogs/v4.3.2.md b/docs/changelogs/v4.3.2.md new file mode 100644 index 00000000..0945534b --- /dev/null +++ b/docs/changelogs/v4.3.2.md @@ -0,0 +1,7 @@ +# v4.3.2 โ€” 2026-05-26 + +Patch release moving the CLA check and record flow onto a central signature ledger. + +## Fixed + +- **CLA check and record use the central ledger.** The `cla-record` job action and the `reusable-cla.yml` workflow were rewritten to look up and write signatures in the shared ledger repository (`ledger_repo`, default `CLDMV/.cla-signatures`), and the example `cla.yml` consumer was rewired to match. A private-ledger seed template (README, versioning notes, `v1.0` CLA text and checksum, audit and verify tools) was added under `examples/repo-seeds/.cla-signatures`. The setup guide, README and scaffolding docs were updated. ([#36](https://github.com/CLDMV/.github/pull/36), released in [#37](https://github.com/CLDMV/.github/pull/37)) diff --git a/docs/changelogs/v4.30.0.md b/docs/changelogs/v4.30.0.md new file mode 100644 index 00000000..f625d525 --- /dev/null +++ b/docs/changelogs/v4.30.0.md @@ -0,0 +1,11 @@ +# v4.30.0 โ€” 2026-09-28 + +Minor release that marks an issue as `status: implemented` the moment its fix lands on `next` or `hotfixes`, so the tracker shows finished work before the release ships and closes it. + +## Added + +- **Issues resolved on an integration branch move to `status: implemented`.** In the v4 flow a fix merges into `next` or `hotfixes` long before it reaches `master`, and until `close-resolved-issues` closed the issue at release, nothing on it said the work was done. A new `mark-implemented-issues` action runs as a `mark-implemented` job in `workflow-next-release.yml` and `workflow-hotfixes-release.yml` on every non-bump push that leaves the branch ahead of the release base. It walks the pushed range, minus anything already on the base (so a master sync or a reset never re-marks shipped work), and collects `Fixes` / `Closes` / `Resolves #N` keywords and `gh-broker:resolves:` markers from each merged PR's description, comments and commits, and from direct-pushed commit messages. Each referenced open issue gets `status: implemented` in place of its other `status:` labels, never downgrading `status: verified`, plus a comment naming the resolving PR. The issue stays open for the release to close. The job is best-effort and can never turn the release lane red. The label colour for a repo that lacks the label comes from an input rather than a file read. ([#336](https://github.com/CLDMV/.github/pull/336)) + +## Changed + +- **CodeQL is skipped for `CLDMV/configs`.** The declarative shared-config package has no JS/TS outside its tests, so CodeQL failed with "no source code seen". It is added to the `skip` list in `data/code-scanning-skips.json`, which makes bootstrap upload an empty, passing SARIF and keeps the `code_scanning` ruleset gate satisfiable. ([#338](https://github.com/CLDMV/.github/pull/338)) diff --git a/docs/changelogs/v4.30.1.md b/docs/changelogs/v4.30.1.md new file mode 100644 index 00000000..e6bfa3e9 --- /dev/null +++ b/docs/changelogs/v4.30.1.md @@ -0,0 +1,11 @@ +# v4.30.1 โ€” 2026-09-28 + +Patch release that moves the org-wide onboarding fanout out of this public repository, because its runs published the names of the org's private repositories. + +## Changed + +- **The onboarding fanout now lives in a private org-admin repository.** `local-org-onboarding.yml` names every matrix job after its target repo and writes the full target list to the run summary, so an auto-discovery run from this public repository exposed the names of private repos. The workflow is removed from `.github/workflows/` and ships as a template instead, `examples/individual-repo-workflows/packaging-docs/org-onboarding.yml`, to be run from a private repository along with its batch files (the `data/onboarding-batches/` example is removed here). The logic stays in the public `org-bootstrap-repo` action. The template filters the org's `.github` repo out of the matrix by name rather than by the calling repo, and notes that a private repo's name must never go into the public `data/code-scanning-skips.json`. The README, setup guide and scaffolding guide point at the new location. ([#340](https://github.com/CLDMV/.github/pull/340)) + +## Upgrade notes + +- Org admins who dispatched `local-org-onboarding.yml` from `CLDMV/.github` should copy the template into the org's private admin repository and dispatch it there. Per-repo `v4-bootstrap.yml` is unchanged. diff --git a/docs/changelogs/v4.30.2.md b/docs/changelogs/v4.30.2.md new file mode 100644 index 00000000..42e35e3e --- /dev/null +++ b/docs/changelogs/v4.30.2.md @@ -0,0 +1,7 @@ +# v4.30.2 โ€” 2026-09-28 + +Patch release so the bundle-size check works on the first release PR of a newly onboarded repo. + +## Fixed + +- **Bundle size measures an empty baseline when the base has no build script.** On a freshly onboarded repo, `master` is still the initial commit and has no `build` script, so the base build in `reusable-bundle-size.yml` failed and took the check down with it. When the build command is `npm` / `pnpm` / `yarn run