Skip to content

fix(release): restore release-note generation and stop masking failures - #2083

Merged
steilerDev merged 1 commit into
betafrom
fix/release-semantic-release-writer
Sep 8, 2026
Merged

fix(release): restore release-note generation and stop masking failures#2083
steilerDev merged 1 commit into
betafrom
fix/release-semantic-release-writer

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Summary

Fixes #2082semantic-release has been failing on every push since the v2.15.0-beta.6 tag while the Release workflow reported success and published nothing, including the v2.15.0 promotion (merge commit df45a58b).

The bug is older and worse than the crash suggested

Investigating turned up a third defect the issue did not describe: release notes have been silently empty for months.

Release Body size
v2.15.0-beta.6 116 bytes — just the compare-link header
v2.15.0-beta.5 116 bytes
v2.14.0 2845 bytes — only because RELEASE_SUMMARY.md is prepended; its auto-generated half is also just the header

Mechanism. Two incompatible rendering engines:

  • conventional-changelog-writer@8 is handlebars-based and expects partials as template strings.
  • Writer 9 dropped handlebars for @conventional-changelog/template, whose partials are JS functions.
  • conventional-changelog-conventionalcommits 10.x emits function partials, but @semantic-release/release-notes-generator@14 still pins writer ^8.

Handing functions to handlebars doesn't throw — it renders an empty changelog. From ccc 10.3.0 upstream added a deliberate poison pill, whose own doc comment names the disease: "make legacy handlebars-based writers fail loudly instead of silently rendering an empty changelog … Update the tooling or use an older major version of the preset."

So 038a9431 (ccc 10.2.110.4.0) did not create this bug — it made a long-silent one audible.

Remedy: pin the preset to 9.3.1, not 10.2.1

Pinning back to 10.2.1 — the version that "successfully" cut v2.15.0-beta.6 — would have restored the silence, not the notes. 9.3.1 is the last release on the writer-8 line and was verified to emit handlebars template strings.

Tested against the repo's exact locked graph (semantic-release@25.0.9, release-notes-generator@14.1.0, writer@8.4.0):

Preset generateNotes exit notes produced
10.4.0 (current) crash 1 none
10.2.1 completes 0 header only — bug restored silently
writer ^9 override completes 0 full
9.3.1 (this PR) completes 0 full

A writer ^9 override was rejected on evidence despite working: upstream's own migration (release-notes-generator@15.0.0-beta.2) bumps conventional-commits-parser 6→7 and conventional-commits-filter 5→6 alongside the writer. Overriding only the writer ships a parser-6/filter-5/writer-9 triple upstream never tests — and root overrides apply repo-wide. 9.3.1 keeps every package inside its declared range with no overrides.

Verified output after the fix:

✔ Completed step "generateNotes" of plugin "@semantic-release/release-notes-generator"
ℹ Release note for version 2.15.0-beta.7:
## 2.15.0-beta.7 (compare/v2.15.0-beta.6...v2.15.0-beta.7) (2026-09-08)
### Documentation
    * update documentation for the v2.15.0 release (c27a850)
### Chores
    * **deps-dev:** bump the dev-dependencies group (038a943)
### Continuous Integration
    * give Jest shards CI-only timeout headroom ... closes #2076 #2078
SEMANTIC_RELEASE_EXIT=0

Defect 2 — the workflow no longer hides this

The step ran npx semantic-release 2>&1 || true and then decided what had happened by grepping stdout for Created tag v, so a crash and a clean "no relevant commits" run were indistinguishable. The job went green with new-release-published=false and every downstream job (Merge Back to Beta, Docker, Docker Scout, DockerHub README, Docs Screenshots, Docs Deploy) skipped.

Now the exit status is captured and fails the job loudly with an ::error annotation, and the job summary gained a distinct "Release Failed" branch so it can no longer claim "commits since last tag did not trigger a version bump" after a crash. The outputs contract (new-release-published, new-release-version, is-prerelease) is unchanged — all 8 downstream jobs' if: conditions still parse. All four states were simulated with a stub binary.

Lockfile

Regenerated with a full npm install (not --package-lock-only). npm install fails with ENOTDIR on the session worktree's mount (98% full), so it was regenerated in a clean tree on a different filesystem and copied in. The delta is 35 lines, confined to the ccc downgrade plus the now-orphaned @conventional-changelog/template.

For the security reviewer

  • A dependency downgrade of a devDependency used only by the release pipeline. npm audit on the regenerated tree reports no advisory against conventional-changelog-* or compare-func; the 20 pre-existing findings are Docusaurus-related and unchanged from beta.
  • ccc 9.3.1's only dependency, compare-func@2.0.0, was already in the tree via conventional-changelog-angular.
  • The new ::error annotation echoes only an exit status; $OUTPUT was already printed. semantic-release redacts tokens.

Known follow-ups (not addressed here)

  1. The empty-notes history is unrecovered — every release back to at least v2.14.0 has a hollow auto-changelog. Needs a decision on whether to backfill.
  2. Residual stdout-parsing fragility: if semantic-release exits 0 but the grep -q "Created tag v" misses (upstream wording change), the job still reports "no release" silently. Same class as defect 2; left alone to keep this PR scoped.

🤖 Generated with Claude Code

semantic-release has been failing on every push since the v2.15.0-beta.6
tag, while the Release workflow reported success and published nothing --
including the v2.15.0 promotion (merge commit df45a58).

Root cause is a two-engine split in the changelog toolchain.
conventional-changelog-writer@8 is handlebars-based and expects partials
as template strings; writer 9 replaced handlebars with
@conventional-changelog/template, whose partials are JS functions.
conventional-changelog-conventionalcommits 10.x emits function partials,
but @semantic-release/release-notes-generator@14 still pins writer ^8.

Handing functions to handlebars does not throw -- it renders an empty
changelog. That is why v2.15.0-beta.6's release body is 116 bytes of
compare-link header and nothing else, and why every release back to at
least v2.14.0 has a hollow auto-generated section. ccc 10.3.0 added a
deliberate poison pill to turn that silent failure into a loud one, so
the dev-dependencies bump in 038a943 (10.2.1 -> 10.4.0) did not create
this bug; it made an already-broken pipeline audible.

Pinning back to 10.2.1 would therefore have restored the silence, not the
notes. Pin to 9.3.1 instead -- the last release on the writer-8 line,
verified to emit handlebars template strings. A writer ^9 override was
rejected: upstream's own migration (release-notes-generator 15.0.0-beta.2)
moves conventional-commits-parser 6->7 and -filter 5->6 alongside the
writer, so overriding the writer alone would ship a combination upstream
never tests, repo-wide.

Verified against the repo's exact locked graph: generateNotes now
completes and produces full grouped notes (Documentation / Chores /
Continuous Integration) instead of crashing.

Also stop the workflow from hiding this class of failure. The step ran
`npx semantic-release 2>&1 || true` and then decided what happened by
grepping stdout for "Created tag v", so a crash and a clean "no relevant
commits" run were indistinguishable: the job went green with
new-release-published=false and every downstream job skipped. Capture the
exit status, fail loudly on non-zero, and give the job summary a distinct
"Release Failed" branch so it can no longer claim "commits since last tag
did not trigger a version bump" after a crash.

Lockfile regenerated with a full npm install; the delta is 35 lines,
confined to the ccc downgrade plus the now-orphaned
@conventional-changelog/template. npm audit reports no advisory against
conventional-changelog-* or compare-func.

Fixes #2082

Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@steilerDev

Copy link
Copy Markdown
Owner Author

[security-engineer]

VERDICT: APPROVE

Reviewed package.json/package-lock.json (dependency change), .github/dependabot.yml, and .github/workflows/release.yml against the four review questions. No Critical/High/Medium findings; approving with informational notes only.

1. conventional-changelog-conventionalcommits 10.4.0 → 9.3.1 — safe

Independently verified rather than trusting the PR body:

  • Extracted every package name+version pair from the regenerated package-lock.json (1798 unique packages) and POSTed the full set to the npm registry's bulk advisory endpoint (/-/npm/v1/security/advisories/bulk, the same data source npm audit uses). Result: 2 distinct advisories total, both against qs/image-size (Docusaurus-workspace transitive deps, dev: true, unchanged by this PR). Zero advisories against conventional-changelog-conventionalcommits (any version in history), compare-func, conventional-changelog-writer, semantic-release, or @semantic-release/release-notes-generator. This confirms the PR's npm audit claim independently.
  • compare-func@2.0.0 resolves to a single root node_modules/compare-func entry shared by both conventional-changelog-angular and conventional-changelog-conventionalcommits@9.3.1 (grep on the lockfile — no nested duplicate copy). The "already in the tree" claim holds.
  • conventional-changelog-conventionalcommits@9.3.1: not deprecated, published 2026-03-29 (5 months old, not an abandoned/stale release), same maintainer set and GitHub org (conventional-changelog/conventional-changelog) as 10.4.0 — no typosquat/maintainer-takeover signal. Lockfile integrity hash matches the registry's published dist.integrity for that exact version.
  • This is a downgrade of a devDependency scoped to the release pipeline only (not shipped in the production Docker image / server or client bundles) — blast radius is CI, not the deployed app.

2. Dependabot ignore scope — appropriately narrow, not a lasting exposure

- dependency-name: 'conventional-changelog-conventionalcommits'
  update-types: ['version-update:semver-major']

Scoped to major-version bumps only for one specific devDependency — minor/patch releases within the 9.x line (including any future security patches Dependabot would otherwise open) still flow through normally. This mirrors the pre-existing typescript major-version-hold entry directly above it in the same file, so it's consistent with established repo convention. The accompanying comment documents the precise removal condition (adoption of @semantic-release/release-notes-generator@15, which is what carries the writer@9 upgrade) rather than leaving it open-ended, and it's cross-referenced to issue #2082 plus a new product-architect memory note recording the same trigger. No expiry-date automation exists in this file for any ignore rule (not just this one) — that's a pre-existing repo-wide gap, not something introduced here, so I'm not treating it as a new finding against this PR specifically.

3. Workflow change — does not widen log/secret exposure

Read the full step before and after in context. Confirmed:

  • echo "$OUTPUT" (the full captured stdout+stderr of npx semantic-release) is unchanged — same unconditional line, same position, both before and after this PR. The only new output is ::error title=semantic-release failed::semantic-release exited with status ${STATUS}..., which interpolates only the numeric exit code — no secret material, no part of $OUTPUT re-echoed.
  • GITHUB_TOKEN is consumed via env: by the semantic-release process itself (unchanged); GitHub Actions' runner-level secret masking on secrets.GITHUB_TOKEN applies to $OUTPUT's echo regardless of exit path — this PR doesn't touch that mechanism.
  • On failure, the step now does exit "$STATUS" before reaching the new-release-published=... output-writing block, so needs.release.outputs.new-release-published is left unset (not 'true') on the crash path. Verified every downstream job (docker-build, and by extension docker, docker-scout, etc.) gates on needs.release.outputs.new-release-published == 'true', so a failed release correctly fails closed — no job runs against a half-completed release state.
  • The Job summary step (if: always()) added a "Release Failed" branch that only prints static text plus $VERSION/$PUBLISHED/$PRERELEASE (empty on the failure path since outputs were never set) — no new sensitive data reaches the step summary.

Net effect: strictly a control-flow fix (fail loud vs. silently continuing) with no change to what reaches public logs.

4. Other diff contents

  • package-lock.json also drops a stray lockfile-only react-i18next: "^17.0.13""17.0.13" (root packages[""].dependencies entry) to match client/package.json's existing exact pin. This is a side effect of a full npm install regeneration (per the repo's documented "always regenerate via full npm install" policy) fixing pre-existing lockfile drift, not a new dependency change — confirmed client/package.json was already pinned to the exact version before this PR.
  • .claude/agent-memory/product-architect/** — documentation/memory only, no production or CI-executable content.
  • No new dependencies were added; the orphaned @conventional-changelog/template (writer@9-only transitive dep) was correctly dropped, not left dangling.
  • No injection surface: .releaserc.json's presetConfig consumed by the downgraded preset is a static object, not user-controlled input.

No fix-in-session items. Approving.

@steilerDev
steilerDev merged commit c7c1044 into beta Sep 8, 2026
33 checks passed
@steilerDev
steilerDev deleted the fix/release-semantic-release-writer branch September 8, 2026 10:28
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.15.0-beta.7 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.15.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant