fix(ci): stop publishing on paths that cannot reach a consumer (fixes #3023) - #3098
Open
mfal wants to merge 7 commits into
Open
fix(ci): stop publishing on paths that cannot reach a consumer (fixes #3023)#3098mfal wants to merge 7 commits into
mfal wants to merge 7 commits into
Conversation
This was referenced Sep 2, 2026
Relevance was path-based but only knew repo-ROOT prefixes, so everything under `packages/**` counted as publish-relevant. #3010 cut 1.0.14 from `packages/components/.storybook/preview.tsx` alone; #3006 cut 1.0.12 from a scripts-only manifest, a `CONTRIBUTE.md`, `dev/cross-version/**` and `src/tests/visual/**`. Neither can reach a consumer. Two changes to the classifier: - Package-local denylist entries, segment-exact under `packages/<name>/`: `.storybook/**`, `e2e/**`, `src/tests/**`, `dev/cross-version/**`, `dev/vitest/**`, `CONTRIBUTE.md`, plus `*.stories.tsx` and `*.test.*` anywhere. `classifyPath` replaces the bare `startsWith` and returns the rule that decided, which the skip notice now names. - The key-diff refinement applies to every `packages/*/package.json`, not just the root one: a `scripts`-only diff cannot reach a consumer. `classifyRootManifestChange` becomes `classifyManifestChange` and the `decide` job fetches before/after for each changed manifest into `MANIFEST_SNAPSHOT_DIR`. `packages/*/dev/**` is deliberately NOT denylisted, although #3023 proposed it. In `components` and `codemods` that directory IS the build: `dev/vite/*` holds the plugins `vite.build.config.ts` imports, `dev/createDocPropertiesJson.ts` writes the shipped `dist/assets/doc-properties.json`, `dev/remote-components-generator/**` generates `view.ts` and `src/auto-generated/**`, and `codemods`' build script is `tsx dev/generateCli.ts && …`. Only the two test-harness subtrees are safe. Relevance stays path-based — no gate on commit type. `chore(deps):` and `refactor:` change what consumers resolve. For the empty changelog on real releases, `lerna.json` now configures `changelogPreset` as an object and unhides every type. A release exists only because something can reach a consumer, so it deserves a line; `chore(deps): bump …` and a `docs:` commit on a shipped `AGENTS.md` (#2954, 1.0.11) are exactly the cases a path gate must not suppress. `changelog-preset.test.mjs` asserts every type renders. No bump semantics change: without `bumpStrict` the preset's `whatBump` returns patch for any non-empty range regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The path classifier decided releases alone, and a file list nobody reads is a poor place for a decision a maintainer has to understand. The title is now the decision and the paths verify it (#3023). The type gives the default — feat/fix/perf/revert release, docs/ci/chore/test/ build/style/refactor do not — and a `[release]` or `[no-release]` tag at the end of the title overrides it. Only titles that contradict their type need a tag. The type alone cannot decide: it states the author's intent, not the effect on the artifact. Over the 80 merges since the gate landed the two disagreed 21 times, and a type-only gate would have swallowed 8 real releases, the 1.1.0 promotion among them. Three classes cannot answer from the subject and fall back to the paths: `chore(sync):` forward-merges (the subject cannot know what came over, ADR 0004 §6), Dependabot subjects (a group is not homogeneous in effect and `commit-message.prefix` is per ecosystem, not per group), and an unparsable subject or unknown type. `chore(release):` never releases, `chore(promotion):` always does. Verification happens twice, because the PR check can be outrun — GitHub lets the merger edit the squash subject after every check has passed: - `commit-guard.yml` fails the PR on a disagreement, naming the files. - `publish.yml`'s `decide` job fails the RELEASE RUN on one, instead of guessing. Nothing publishes and nothing is skipped silently; the remedy is a corrected follow-up commit or a manual dispatch. `release-relevance-guard.mjs` is replaced by `release-intent-guard.mjs`, which serves both callers through `MODE=check|decide` rather than duplicating the manifest-snapshot handling. Tests and stories also leave `dist/types`. `unplugin-dts` emitted declarations for all of `src`, which put 196 of flow-remote-react-components@1.1.10's 799 tarball entries under `dist/types/tests/**` and 252 story entries into flow-react-components. All eight release builds now share `publishedDtsOptions` from packages/core. No consumer loses a type: no package's `exports` has a wildcard subpath, so those declarations were unreachable. A story's helper stays — `dev/createDocPropertiesJson.ts` parses every `.tsx` under `src/` and ignores only `*.stories.tsx`, so `Button/stories/lib.tsx` contributes five entries to the published `doc-properties.json`. `@mittwald/flow-core` becomes a declared devDependency of ext-bridge, remote-core and remote-elements, which newly import it — that is also the nx graph edge their build cache needs to invalidate on a core change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mfal
force-pushed
the
claude/title-driven-release-gate
branch
from
September 2, 2026 12:06
cb6e273 to
b529f13
Compare
The title-driven gate landed with the path work still running on every push, so it added machinery instead of removing it. It now runs only where it is needed: - `publish.yml` asks the title first (`MODE=title`, one node call on a string). The compare API and the manifest snapshots run only for the subjects that cannot answer — a `chore(sync):` forward-merge, a Dependabot bump, an unparsable one. Every ordinary release skips both. - The push no longer re-verifies the title against the paths. `commit-guard.yml` already did that on the PR, and repeating it existed only to catch a squash subject edited in the merge dialog after the checks passed. That hole is now deliberate and documented: the remedy is a manual `workflow_dispatch`, and closing it would mean re-fetching and re-classifying every push for a rare manual edit. - The manifest-snapshot loop stood duplicated in both workflows and is now `fetch-manifest-snapshots.sh`, which serves the PR check from git history and the release job from the contents API. - The guard's modes are one job each: `title` decides from the subject, `paths` from the file list, `check` compares the two. Only `check` reconciles. `docs/release-workflow.md` is cut back to the mechanics — what decides, what overrides it, what the classifier excludes — with the reasoning and the historical case numbers left in the code comments and the issues where they belong. Net 52 lines shorter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Coverage Report for ./packages/components/
File CoverageNo changed files found. |
Contributor
🚀 Preview DeploymentPreview environments are ready:
Images:
|
Porting the `publishedDtsOptions` work from the previous branch brought `version: 1.1.10` back with it — that branch sat on the 1.1.10 release, this one on 1.1.11. The version-consistency guard caught it, which is exactly the merge shape it exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The title-driven gate is withdrawn. `[release]` / `[no-release]` tags put the decision in a hand-written token, and the paths answer the question directly — so the paths keep deciding, as they have since #2931. Removed: `release-title-lib.mjs` and its tests, `release-intent-guard.mjs`, the `release-intent` job in `commit-guard.yml`, the title-first `decide` steps in `publish.yml`, and `fetch-manifest-snapshots.sh` (extracted only to serve the second caller that no longer exists). `release-relevance-guard.mjs` is back; both workflows are byte-identical to the path-based version again. Kept: the package-local denylist and the per-manifest key diff, the changelog preset that renders every type, `publishedDtsOptions`, and the three manifest versions. `docs/release-workflow.md` describes the path-based model, cut back to the mechanics — what the classifier excludes, what it judges by content, and the two `dist/types` and changelog facts. 47 lines in, 50 out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The declaration-emit change is orthogonal to the release gate and reviews on its own terms — it changes what the packages publish, not when they publish. It lives in its own pull request now, together with the release-workflow note that describes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two paths were still counted as consumer-relevant although they cannot reach one, and both cut releases: - A package's ROOT Markdown was relevant wholesale. Nothing builds a root `.md`, so it reaches a consumer exactly if the package publishes it — and that only `files` can say. `flow-react-components` lists AGENTS.md, MIGRATION.md and USAGE.md; `remote-react-components` lists only USAGE.md, so its AGENTS.md reaches nobody. #3009 released through exactly that file. The `files` are read from the checkout rather than fetched: only the current state matters, and the job already has the tree. An unreadable manifest counts as shipped. Markdown DEEPER in a package keeps its path-level relevance — `codemods/src/migrations/*/entry.md` is a generator input, not documentation. - `Dockerfile` and `.dockerignore` join the package-local denylist. They build the Storybook preview image from `dev/` and `.github/`, and #3008 pushed nothing else under `packages/`. Replayed over `main` since #2933: the merges that reach no consumer under a non-releasing type drop from 9 to 7 of 81. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release relevance is decided by PATH, the changelog by TYPE, and the two could never agree (#3023). Both halves are fixed here, and the decision stays on the paths.
Needless releases
The denylist knew only repo-root prefixes, matched with
startsWith, so everything underpackages/**counted as publish-relevant wholesale:packages/components/.storybook/preview.tsxalonescripts-only package manifest, a package-localCONTRIBUTE.md,dev/cross-version/**,src/tests/visual/**Package-local denylist entries, segment-exact under
packages/<name>/—e2e-helpers/is note2e/:.storybook/**,e2e/**,src/tests/**,dev/cross-version/**,dev/vitest/**CONTRIBUTE.md(no package lists it infiles)*.stories.tsxand*.test.*anywhere —.test.is not always the last extension (*.browser.test.remote.tsx)packages/*/dev/**is deliberately not denylisted wholesale: incomponentsandcodemodsthat directory is the build —dev/vite/*holds the pluginsvite.build.config.tsimports,dev/createDocPropertiesJson.tswrites the shippeddist/assets/doc-properties.json,dev/remote-components-generator/**generatesview.tsandsrc/auto-generated/**, andcodemods' build script istsx dev/generateCli.ts && ….The criterion is no consumer effect, not "not in the tarball": a tarball carries the package's
scripts, yet nothing a consumer installs reads them.isPublishRelevantis now a wrapper aroundclassifyPath, which also returns the rule that decided — that is what the skip notice prints.The key-diff refinement applies to every
packages/*/package.json, not just the root one: ascripts-only diff cannot reach a consumer.Releases with an empty changelog
changelogPresetrenders every commit type now, so achore,build,testorcichange that touches a package is no longer invisible — asserted bychangelog-preset.test.mjsagainst the preset's ownwriter.transform, which needs no git history and no rendering.What stays unrenderable, and is documented rather than fixed: a release whose cause lies outside every package —
pnpm-lock.yaml, the root manifest,lerna.json. Lerna attributes commits per package, no package changed, so every changelog honestly reads "Version bump only". 1.1.11 is the live example, a lock-only Rollup bump.The anomaly #3083 required explaining first is resolved: 1.1.6 shows a
fix(codemods):entry in the components changelog because #3041 regeneratedpackages/components/MIGRATION.md, so components genuinely changed; #3040 (1.1.1) touched no package butcodemodsand correctly reads "Version bump only". The per-package attribution is consistent — only the rendered scope is the commit's, never the changelog's package.Verified
decidejob before it classifies.mainsince ci: skip the release for docs- and CI-only merges #2933 through the changed classifier: build(components): use the next-themes client entry in Storybook #3010 and ci: build the preview apps in CI and let the image only package them #3008 flip to no-publish, and no commit that produced a legitimate release flips with them.nextstays symmetric — docs-onlychore(sync):merges produce no-next.Nbump.docs/release-workflow.mdis cut back to the mechanics: what the classifier excludes, what it judges by content, and the changelog facts.Split out of this PR
dist/types. Orthogonal: it changes what the packages publish, not when.A title-driven variant of this gate (
[release]/[no-release]tags in the PR title) was built and withdrawn: the tag puts the decision in a hand-written token while the paths answer the question directly.Fixes #3023. Fixes #3083.
🤖 Generated with Claude Code