Skip to content

fix(ci): stop publishing on package-local non-shipping paths - #3080

Closed
mfal wants to merge 1 commit into
mainfrom
fix/release-relevance-package-local-paths-3023
Closed

fix(ci): stop publishing on package-local non-shipping paths#3080
mfal wants to merge 1 commit into
mainfrom
fix/release-relevance-package-local-paths-3023

Conversation

@mfal

@mfal mfal commented Sep 2, 2026

Copy link
Copy Markdown
Member

Release relevance is decided by PATH, the changelog by TYPE, and the two never agreed. This fixes both halves.

Problem 1 — needless releases

The denylist knew only repo-ROOT prefixes, matched with startsWith, so everything under packages/** was publish-relevant wholesale. #3010 cut 1.0.14 from packages/components/.storybook/preview.tsx alone. #3006 cut 1.0.12 from a scripts-only package manifest, a package-local CONTRIBUTE.md, dev/cross-version/** and src/tests/visual/**. None of it reaches a consumer.

Package-local denylist entries, segment-exact under packages/<name>/e2e-helpers/ is not e2e/:

  • .storybook/**, e2e/**, src/tests/**, dev/cross-version/**, dev/vitest/**
  • the package's CONTRIBUTE.md (no package lists it in files)
  • *.stories.tsx and *.test.* anywhere — .test. is not always the last extension (*.browser.test.remote.tsx)

isPublishRelevant is now a wrapper around classifyPath, which also returns the rule that decided. That is what the skip notice prints.

The key-diff refinement now applies to every packages/*/package.json, not just the root one — a scripts-only diff cannot reach a consumer. classifyRootManifestChangeclassifyManifestChange (one extra path argument for the reason), and the decide job fetches before/after for each changed root/packages/* manifest into MANIFEST_SNAPSHOT_DIR. apps/* manifests are skipped — they are already irrelevant by path.

Published tarballs do contain scripts. The argument is "no consumer effect", not "not in the tarball" — nothing a consumer installs, imports or runs reads a package's test:unit. Same for the story and test .d.ts that unplugin-dts emits: they ship, and no exports path reaches them. Written into the code where the rules live.

packages/*/dev/** is deliberately NOT denylisted

The issue proposed it; the repo says no. In components and codemods that directory is the build:

  • dev/vite/* — the PostCSS/rollup plugins vite.build.config.ts imports; a change there moves the emitted CSS
  • dev/createDocPropertiesJson.ts — writes the shipped dist/assets/doc-properties.json
  • dev/remote-components-generator/** — generates view.ts and src/auto-generated/**
  • packages/codemods' whole build script is tsx dev/generateCli.ts && tsc … && tsx dev/buildTransforms.ts

Two of those produce dist-only artifacts, so such a change arrives with no committed generated file beside it — a blanket dev/** entry would swallow that release silently, the one failure mode the denylist direction exists to prevent. Only the two provably test-only subtrees (dev/cross-version/, dev/vitest/) are listed, which is enough for the 1.0.12 evidence. Four counter-examples are pinned by tests.

Relevance stays path-based: no gate on commit type. chore(deps): and refactor: routinely change what consumers resolve.

Problem 2 — real releases with an empty changelog

Decision: unhide every type. lerna.json's changelogPreset becomes an object and respells the full types list with every hidden: true dropped.

Why not "accept Version bump only and document it": relevance is already decided by path, so after this PR a release exists only because something can reach a consumer. If a commit was worth a release it is worth a line. Suppressing those releases would be wrong — chore(deps): bump framer-motion … changes what consumers resolve, and a docs: commit on a shipped AGENTS.md really does change the tarball (#2954 → 1.0.11). Unhiding per type (chore/build/refactor only) would leave test:, style: and docs: able to release into an empty changelog; unhiding all leaves no residual case.

Cost is bounded: a section renders only when it has commits, and the types that previously released while hidden are now largely skipped by problem 1.

No bump semantics change: without bumpStrict the preset's whatBump returns patch for any non-empty commit range regardless of hidden. The preset stays pinned to ^9 per docs/release-workflow.md — 10.x trips Lerna-Lite 5's legacy-writer path. Lerna-Lite passes the object straight to the preset factory (GetChangelogConfig.getChangelogConfig reads changelogPreset.name and forwards the object as presetConfig), so the auto-prefix to conventional-changelog-conventionalcommits still happens.

Verification

  • node --test .github/scripts/*.test.mjs — 63 pass; this is the command test.yml runs, and publish.yml's decide job self-tests the classifier the same way. 17 new/changed relevance cases cover every new path, the mixed case, the scripts-only manifest at root and package level, and the segment-boundary negatives.
  • One test reads every non-private packages/*/package.json and asserts files lists nothing but dist and shipped .md. That is the invariant the package-local denylist rests on; if a package starts shipping src or CONTRIBUTE.md, it fails.
  • changelog-preset.test.mjs drives the real preset's writer.transform per type. Reverted to the old string preset it fails 4/4, so it is not vacuous.
  • The guard was run end to end against fake snapshots for the 1.0.14 push, the 1.0.12 push, a mixed push, and a package manifest with no snapshot (fail-safe → publish).
  • The decide job's run block: YAML parsed, bash -n clean, and the fetch loop exercised offline — slugs match the guard's path.replaceAll("/", "__"), apps/docs/package.json is not fetched, and a push with no manifest iterates zero times under set -euo pipefail.
  • pnpm lint clean (0 errors; the 71 pre-existing react-hooks/exhaustive-deps warnings are untouched).
  • workflow_dispatch is untouched: the github.event_name != "push" branch still returns publish=true before any classification.
  • Both lines run through the same decide job, so next behaves identically — on next the classified set is what the chore(sync): forward-merge carried over. Symmetry verified by reading, not by a run.

Not verifiable locally: the actual gh api contents fetch and the rendered ::notice:: in a real run, and that a merge produces no tag/GitHub Release. Both need a push to a release line.

SKIP_INSTALL_SIMPLE_GIT_HOOKS already sits in publish.yml's env and stays. No nx wiring needed — .github/scripts/** is outside the graph, and test.yml picks the new test up through its *.test.mjs glob.

fixes #3023

🤖 Generated with Claude Code

@mfal mfal self-assigned this Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./packages/components/

Status Category Percentage Covered / Total
🔵 Lines 78.69% 746 / 948
🔵 Statements 78.57% 763 / 971
🔵 Functions 80.09% 165 / 206
🔵 Branches 70.33% 377 / 536
File CoverageNo changed files found.
Generated in workflow #6566 for commit 28fdb39 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-3080.docs.review.flow-components.de
storybook pr-3080.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-3080
  • storybook: ghcr.io/mittwald/flow/storybook:pr-3080

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>
@mfal

mfal commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Superseded by #3098 and closing — this branch's commit is contained in it verbatim, so nothing here is lost.

#3098 builds the next step on top: the PR title decides whether a merge releases and this classifier becomes its verifier, on the PR (a new blocking release-intent job) and again on the push (decide fails the release run on a mismatch instead of guessing). Rationale for the inversion: the type states intent, not effect — over the 80 merges since the gate landed, type and paths disagreed 21 times, and a type-only gate would have swallowed 8 real releases including the 1.1.0 promotion. Hence a [release] / [no-release] tag that only the contradicting titles carry.

It also adds publishedDtsOptions (story and test declarations out of dist/types, 196 of remote-react-components' 799 tarball entries) and closes #3083 — the 1.1.1/1.1.6 attribution question is answered there.

🤖 Closed by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Patch releases with an empty changelog: relevance is path-based, the changelog is type-based

1 participant