LearnNewsExamplesServices
Frontmatter
id13273
titleRelease pipeline leaves a top-level release-note orphan → duplicate /news/releases sitemap + index entry
stateClosed
labels
bugaibuild
assigneesneo-opus-grace
createdAtJun 15, 2026, 1:29 AM
updatedAtJun 21, 2026, 2:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/13273
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 15, 2026, 2:08 AM

Release pipeline leaves a top-level release-note orphan → duplicate /news/releases sitemap + index entry

neo-opus-grace
neo-opus-grace commented on Jun 15, 2026, 1:29 AM

Context / Problem

resources/content/release-notes/ is bucketed into chunk-N/ (ordinal-100, ADR 0004), but v13.0.0.md exists in BOTH chunk-2/ (canonical) and top-level (orphan). The two diverge:

  • chunk-2/v13.0.0.md (559 lines) — carries the YAML frontmatter (tagName, name, publishedAt, isPrerelease, isDraft) the content sync stamps from the GitHub release metadata.
  • release-notes/v13.0.0.md (551 lines) — the raw note, no frontmatter; git history is all release-pipeline edits (add atomic changelog hash, add social names).

Root Cause — two writers, no cleanup

  1. buildScripts/release/publish.mjs (lines 83-89) hard-requires resources/content/release-notes/v{version}.md (top-level) to exist; the human authors it pre-release; publish reads it as the GitHub-release body and appends the atomic-changelog hash. It never moves or removes it.
  2. The content sync (SyncService.runFullSync() / the data-sync pipeline) later pulls the published GitHub release and buckets it into chunk-N/v{version}.md with frontmatter (its git anchor is chore: ticket sync [skip ci]).

Neither path removes the top-level → it lingers as an orphan.

Impact (verified)

  • /news/releases/13.0.0 appears twice in apps/portal/sitemap.xml (grep count = 2).
  • apps/portal/resources/data/releases.json double-counts 13.0.0 (count = 2) → 168 indexed vs 167 real releases.
  • The orphan diverges from canonical (no frontmatter), so consumers reading the top-level get a subtly different record.
  • Only v13.0.0 is affected — it's the single release cut since chunk-bucketing landed; every prior release lives only in a chunk.

The Fix

The top-level note is a transient authoring input; once the canonical chunk-N copy exists, the top-level must be removed.

  1. Immediate cleanup: delete the stale resources/content/release-notes/v13.0.0.md (chunk-2 copy is canonical) + regenerate the release index + sitemap to dedupe.
  2. Durable guard (as-built, operator's call): publish.mjs-owns-cleanup. After gh release create (Step 5), publish.mjs deletes the top-level staging note; the Step-6 broad git add . stages the removal. The content sync re-materializes the canonical chunk-N copy from the published release and no longer owns orphan deletion. (Sync-owns-cleanup was the original draft alternative; superseded — publish.mjs already owns the staging note's lifecycle, so the cleanup lives with the creation.)

Contract Ledger Matrix

(Updated to the as-built owner — publish.mjs, not sync. See ## Evolution.)

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
Release-note location contract ADR 0004 (ordinal-100 chunks) + this ticket Exactly one canonical chunk-N/v{version}.md per release; no top-level release-notes/v*.md survives a release+sync cycle If a top-level orphan exists, the release-pipeline cleanup removes it n/a (internal pipeline) sitemap + releases.json have exactly one entry per version
publish.mjs release-note lifecycle (buildScripts/release/publish.mjs) Existing release flow + this ticket Still requires the top-level release-notes/v{version}.md as the authoring input; after gh release create (Step 5), fs.removeSync(releaseNotePath) deletes it, and the Step-6 broad git add . stages the removal Unchanged release-authoring UX; fs.existsSync guards the removal if the note is already absent inline comment marking it a transient staging input PublishReleaseNoteOrphan.spec asserts the cleanup follows gh release create
Content-sync release-notes bucketing (SyncService.runFullSync / ReleaseNotesSyncer) Existing sync Re-materializes the canonical chunk-N/v{version}.md (with frontmatter) from the published GitHub release; does NOT own orphan deletion — publish.mjs deletes the staging note before the post-release sync runs If the chunked copy already exists, idempotent upsert; no top-level interaction n/a unchanged sync behavior (no new sync code in this PR)

Evolution

Owner decision (as-built, PR #13276 — operator's call): the durable guard is publish.mjs-owns-cleanup, not the originally-drafted sync-owns-cleanup. publish.mjs already hard-requires and reads the top-level staging note, so it owns that note's full lifecycle: it deletes the staging note immediately after gh release create. The content sync (ReleaseNotesSyncer) continues to materialize the canonical chunked copy but carries no orphan-deletion responsibility, and this PR adds no sync code. The Contract Ledger above reflects this as-built owner; the original draft's sync-bucketing-owns-cleanup row was superseded at implementation.

Acceptance Criteria

  • No top-level release-notes/v*.md orphan survives a release + sync cycle.
  • /news/releases/{version} appears exactly once in the sitemap; releases.json has exactly one entry per version.
  • The v13.0.0 orphan is removed and the index/sitemap regenerated (167 unique releases).
  • A guard (in publish.mjs) prevents future orphans, with a regression test.

Out of Scope

  • Reworking the ordinal-100 chunk math itself (ADR 0004 is stable).
  • Migrating already-bucketed release notes (only the v13.0.0 top-level orphan needs cleanup).

Related

  • buildScripts/release/publish.mjs (creates/requires the top-level note; as-built owner of the cleanup)
  • ai/services/github-workflow/SyncService.mjs (runFullSync content sync; buckets into chunk-N; no longer owns deletion)
  • buildScripts/docs/index/release.mjs (release index builder — candidate for a defensive dedupe)
  • ADR 0004 (Universal Ordinal-100 Content Architecture)
  • #13260 / #13266 (content↔derived coherence; same content-pipeline area)

Evidence: L2 (unit/regression on the cleanup guard + a sitemap/index uniqueness assertion) → L2 required (deterministic file-pipeline behavior).

Authored by Claude Opus 4.8 (Claude Code), @neo-opus-grace (Grace).