Skip to content

fix(release): the publish lane pushes the version's git tags and creates its GitHub Releases itself — changesets/action@v1 finds no New tag: line under CLI v3 (objectui#11596) - #11597

Merged
hotlong merged 7 commits into
mainfrom
claude/objectstack-release-steps-ynhz3j
Oct 4, 2026
Merged

hotlong merged 7 commits into
mainfrom
claude/objectstack-release-steps-ynhz3j

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #11596.

What was wrong

  • changeset-release.yml publishes through changesets/action@v1, and v1's runPublish (read at v1.9.0, src/run.ts) pushes a tag and creates a GitHub Release only for the packages whose New tag: PKG@VERSION line it parses out of the publish script's stdout.
  • @changesets/cli v3 (objectui#5296) prints no such line. Its changeset publish still creates the annotated PKG@VERSION tags on the runner (createGitTags in its publish), and those tags were never pushed.
  • Result: 17.6.0 and 17.7.0 are on npm, git ls-remote --tags origin lists no @17.6.0 or @17.7.0 tag, and GET /repos/objectstack-ai/objectui/releases/tags/%40object-ui%2Fcore%4017.7.0 answers 404 (17.5.0 answers 200).
  • Older gap found while porting: @object-ui/app-shell@17.5.0 has a tag but no Release (38 of the 39 17.5.0 Releases exist). Its 17.5.0 CHANGELOG entry is 158,203 characters, over the Releases API's 125,000-character limit. objectstack hit the same limit (objectstack#4900), and the ported script was written for it.

What this PR changes

  1. scripts/release-github-releases.mjs, ported from objectstack's script of the same name at 7d0781482. It creates one Release per public package from that package's CHANGELOG entry. It looks the Release up by tag first, so a re-run updates instead of re-creating. It converges when a concurrent writer's POST answers 422 already_exists, and its duplicate-Release audit never deletes anything. Bodies are truncated at a line boundary to the API limit, with fences balanced and a link to the full entry. Adapted to this repo:
    • Packages come from pnpm-workspace.yaml through the existing workspacePackageDirs, and private packages are skipped. The self-test checks the release set against .changeset/config.json's fixed group minus its private members, comparing both ways.
    • --print-tags prints the PKG@VERSION tags owed, one per line. The push step reads this list.
    • An empty release set is an error. The upstream script treats it as a quiet no-op.
    • --dry-run prints the planned tag → Release list with body sizes and makes no API call.
    • The self-test's oversized case is the real @object-ui/types 17.7.0 entry, over 1,000,000 characters. It has 15 batteries with per-battery floors.
  2. .github/workflows/changeset-release.yml gets two new steps after Verify the release reached npm (which now has id: verify-npm). Both steps have the same condition:
    ${{ !cancelled() && github.event_name == 'push' && steps.changesets-publish.outcome == 'success' && steps.verify-npm.outcome == 'success' }}.
    On schedule / workflow_dispatch both outcomes are skipped, so neither step can run there. A version already on npm never reaches the job (the existing job guard). Both steps take RELEASE_VERSION: ${{ needs.lane.outputs.manifest_version }}.
    • Push the release tags: pushes exactly the tags --print-tags names that exist locally, in one git push origin refs/tags/... (objectstack#2191). If any owed tag is missing locally, the step fails and names it.
    • Create GitHub Releases (bodies truncated to the API limit): node scripts/release-github-releases.mjs with GITHUB_TOKEN. It runs after the tag push. A Release POSTed for a tag missing from the remote makes the API create that tag as a lightweight tag at target_commitish.
    • Permissions: the release job declares no permissions: of its own, so it inherits the workflow-level contents: write. That covers both the tag push and the Release create.
    • changesets/action stays at @v1, and createGithubReleases stays at its default. Under CLI v3 the action's own Release path never runs. If it ever does again, the script updates the Releases it finds instead of failing.
    • Comments that said v1 creates the tags and Releases now say what actually happens. That covers the dispatch-table annotation, THE LANES, the every-git sweep (now listing git rev-parse and git push), the id: note, property (2) of the @v1 pin, the feat: persist ViewConfigPanel draft to backend via DataSource.updateViewConfig #678 item and the clear-step paragraph. Line addresses were not rewritten.
  3. content/docs/guide/ci-cd-pipeline.md: new "Tags and GitHub Releases" subsection under Changeset Release. The command-parity pin needs the section to name the script, and it does.
  4. scripts/__tests__/release-github-releases.test.ts (10 tests):
    • Runs --self-test.
    • Runs --print-tags and --dry-run for the anchor's current version, with no token and an unreachable GITHUB_API_URL. The plan must equal the tag list.
    • Pins the wiring: the order Publish to npm → npm check → tag push → Releases; every condition clause, with no ||, schedule, workflow_dispatch or always(); RELEASE_VERSION from the lane output; the Release step running the script bare (not --dry-run); one tag-ref push with no --tags, --force, --delete or HEAD; effective contents: write; and the job's npm guard.
    • 7 mutation tests show the pin can fail. Each one asserts its mutation actually applied.

Deviations from the suggested route, with reasons

  • No git config user.* in the push step. The tags are created inside the action step, after v1's default setupGitUser has configured the identity. A push creates no object, so an identity set in the push step would never be used. objectstack configures one because it runs changeset publish itself.
  • Exact tag set instead of git push origin --tags. The step pushes only this version's tags, and it fails loudly when changeset publish left one out. A bare --tags push answers Everything up-to-date either way.
  • An empty release set fails. In the upstream script a lost RELEASE_VERSION would let the step report success having done nothing.

--dry-run for 17.7.0

GITHUB_REPOSITORY=objectstack-ai/objectui GITHUB_SHA=b493919c7 RELEASE_VERSION=17.7.0 node scripts/release-github-releases.mjs --dry-run, with no token in the environment:

dry run: no API call. objectstack-ai/objectui, CHANGELOG links at b493919c7. tag -> release body:
@object-ui/app-shell@17.7.0	123919 chars	(truncated from 1026237)
@object-ui/auth@17.7.0	35226 chars
@object-ui/cli@17.7.0	123970 chars	(truncated from 131830)
@object-ui/collaboration@17.7.0	22506 chars
@object-ui/components@17.7.0	123919 chars	(truncated from 605474)
@object-ui/core@17.7.0	123926 chars	(truncated from 498537)
@object-ui/create-plugin@17.7.0	8714 chars
@object-ui/data-objectstack@17.7.0	123945 chars	(truncated from 141243)
@object-ui/fields@17.7.0	123943 chars	(truncated from 375807)
@object-ui/i18n@17.7.0	123947 chars	(truncated from 310203)
@object-ui/layout@17.7.0	95867 chars
@object-ui/mobile@17.7.0	37713 chars
@object-ui/permissions@17.7.0	30565 chars
@object-ui/plugin-ai@17.7.0	49834 chars
@object-ui/plugin-calendar@17.7.0	123970 chars	(truncated from 170068)
@object-ui/plugin-charts@17.7.0	123972 chars	(truncated from 171267)
@object-ui/plugin-chatbot@17.7.0	123961 chars	(truncated from 128322)
@object-ui/plugin-dashboard@17.7.0	123934 chars	(truncated from 297362)
@object-ui/plugin-designer@17.7.0	123969 chars	(truncated from 132685)
@object-ui/plugin-detail@17.7.0	123924 chars	(truncated from 433424)
@object-ui/plugin-editor@17.7.0	35597 chars
@object-ui/plugin-form@17.7.0	123935 chars	(truncated from 284678)
@object-ui/plugin-gantt@17.7.0	123915 chars	(truncated from 183886)
@object-ui/plugin-grid@17.7.0	123645 chars	(truncated from 335493)
@object-ui/plugin-kanban@17.7.0	123966 chars	(truncated from 191209)
@object-ui/plugin-list@17.7.0	123934 chars	(truncated from 260095)
@object-ui/plugin-map@17.7.0	107170 chars
@object-ui/plugin-markdown@17.7.0	52855 chars
@object-ui/plugin-report@17.7.0	84122 chars
@object-ui/plugin-timeline@17.7.0	123961 chars	(truncated from 128776)
@object-ui/plugin-tree@17.7.0	119074 chars
@object-ui/plugin-view@17.7.0	123952 chars	(truncated from 223540)
@object-ui/providers@17.7.0	23579 chars
@object-ui/react@17.7.0	123972 chars	(truncated from 243678)
@object-ui/react-runtime@17.7.0	2764 chars
@object-ui/runner@17.7.0	62768 chars
@object-ui/sdui-parser@17.7.0	64848 chars
@object-ui/types@17.7.0	123646 chars	(truncated from 1534879)
@object-ui/console@17.7.0	123941 chars	(truncated from 211608)

39 release(s) planned (23 truncated to fit the 125000-character limit), 0 failed to plan.

RELEASE_VERSION=17.7.0 node scripts/release-github-releases.mjs --print-tags prints 39 tags, one for each line above, and exits 0. For comparison, the same dry-run plans 39 Releases with 1 truncated for 17.6.0, and 39 with 1 truncated for 17.5.0 (the truncated one is @object-ui/app-shell@17.5.0, 158,203 → 123,973 characters).

Verification (at ae09c39a3)

Command Result
node scripts/release-github-releases.mjs --self-test ✓ ... 93 assertions (15 batteries, each at its floor)
pnpm exec vitest run scripts/__tests__/release-github-releases.test.ts 10 passed
bash ../objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run scripts/__tests__ --maxWorkers=2' Test Files 179 passed, 2 skipped (181), Tests 5452 passed, 2 skipped (5454), VERDICT command-exit 0
node scripts/check-changeset-presence.mjs exit 0, no changeset owed (no released package's source or contract moved)
node scripts/check-governed-queue-guard.mjs --test with the 4 changed paths NOT GOVERNED
pnpm check:entry-guard / check:new-line-citations / check:control-bytes / check:pre-install-import-graph / check:bash32-floor / check:action-ref-convention / check:shell-escape-residue / check:test-path-roots all exit 0; new line citations: 0
pnpm type-check:scripts exit 0, and --listFilesOnly includes the new test file
eslint --no-inline-config on the 2 new files 0 problems. The test file gets 118 rules. The .mjs gets none, because the repo's eslint config gives scripts/*.mjs no rules.

Mutation check (one-off, not committed): I changed if (targets.length === 0) to if (false && targets.length === 0) with objectstack/scripts/ablation-replace.mjs. The self-test went red, with 1 of 93 failing: "with no RELEASE_VERSION the tag list refuses instead of printing nothing (exit 0)". The file was restored byte-for-byte to the HEAD blob 083b5ea38, and git diff HEAD was empty afterwards.

Not verified:

  • The two new steps have not run on a real runner. The next release is the first real run.
  • No tag was created, pushed or deleted, and no Release was created, during this work.
  • actionlint is not available in this container. The workflow was checked by YAML parse and by the pin tests only.

Notes for the backfill (not done here)

  • The issue names the commits each version was published from: 17.6.0 b65fe911de22c7e3a573b669c4cd5efbe34758f2 (run 32739909630), 17.7.0 b493919c71dc915afeb26c96338263f368e95c79 (chore: release packages #5400 merge).
  • Put the annotated tags on those commits before running the script. Then run it with RELEASE_VERSION= that version and GITHUB_SHA= that commit, so target_commitish and the CHANGELOG links point at it. A Release for a tag that is not on the remote yet makes the API create a lightweight tag at GITHUB_SHA.
  • A run with RELEASE_VERSION=17.5.0 updates the 38 existing 17.5.0 Releases and creates the missing @object-ui/app-shell@17.5.0 one.

Branch housekeeping

This branch was reused from #11587, which was squash-merged. Merging main into it brought back .changeset/11586-tailwind-source-none-console-runner.md, which the 17.7.0 release commit had already consumed. A branch commit deletes it again, so it does not appear in this PR's diff.

🤖 Generated with Claude Code

https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL


Generated by Claude Code

claude added 7 commits October 4, 2026 01:39
… the console and runner sheets take source(none), and two censuses exclude it (objectui#11586)

The 17.7.0 release head (#5400) was red in four Test shards while main was
green. All four failures come from the CHANGELOG.md files that
`pnpm changeset:version` writes:

- `apps/console/src/index.css` and `packages/runner/src/index.css` still left
  Tailwind's automatic source detection on. Detection scans prose in the
  package directory, CHANGELOG.md included. On the release head it compiled
  11 rules into the console sheet and 5 into the runner sheet out of
  changelog text. Both entries now take `source(none)`, as
  `packages/components` did in objectui#9569. An ablation on main finds that
  every rule detection alone supplied came from a .md file: `.w-100` from a
  console docs proposal, and `.flex-shrink-0` and `.isolate` from the runner
  changelog.
- The `AppComponentSchema.actions` census and the single-`bind` census
  `git grep` over CHANGELOG.md. They now exclude it with `:!*CHANGELOG.md`,
  the spelling the sibling censuses already use.

Each stylesheet test gains a pin that compiles the entry from the package root
and from an empty directory and requires the same rules. The runner test also
pins that its changelog-only `flex-shrink-0` stays out.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
…lusion — the retired 8800 spelling must appear nowhere (objectui#11586)

The census comment added in the previous commit named the retired symbol's own
pin by file name. objectui#8800's spelling census allows that spelling in
nothing but its listed exclusions, so the citation tripped it. The comment now
cites a sibling census that uses the same `:!*CHANGELOG.md` spelling.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
…e a CHANGELOG — the runner pin reads it as a control, the 7344 census only excludes it (objectui#11586)

`markdown-test-inputs` audits every test that resolves a markdown path. This
change made two tests new candidates:

- `published-stylesheet-sources.test.ts` reads `packages/runner/CHANGELOG.md`
  to prove its prose-only probe still exists. It gets the same entry as the
  components scan test (objectui#9569).
- `handler-keys-string-any-mirrors-7344.test.ts` reads no markdown. Its only
  `.md` literal is the `:!*CHANGELOG.md` pathspec, and its walk lists the
  zod mirror sources.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
…elease #5400 already consumed it

The branch's earlier PR (#11587) was squash-merged and its changeset was
consumed by the 17.7.0 release commit (b493919, #5400). Merging
origin/main into this branch kept the branch-side copy, which would
re-announce an already-published change in the next release.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
…e GitHub Release per public package, bodies truncated to the API limit, plus a tag list for the push step (objectui#11596)

Ported from objectstack's scripts/release-github-releases.mjs (at
7d0781482): idempotent create-or-update by tag, convergence on a racing
writer's 422 already_exists, a duplicate-Release audit that never
deletes, and every body truncated to the Releases API's
125,000-character limit with a link to the full CHANGELOG entry.

Adapted to this workspace:
- package enumeration reads pnpm-workspace.yaml through the existing
  workspacePackageDirs (check-side-effects-array.mjs); private packages
  (the vscode extension, the site, the examples) are never targets;
- `--print-tags` prints the `<pkg>@<version>` tags the release owes, for
  the workflow's tag-push step;
- an empty release set is an error, not a quiet no-op;
- `--dry-run` prints the planned tag -> Release list with body sizes and
  calls no API.

The self-test's oversized repro is real: @object-ui/types' 17.7.0 entry.
scripts/__tests__/release-github-releases.test.ts runs the self-test and
the dry-run (no token, unreachable API URL) and pins the workflow wiring
the next commit adds.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
…its GitHub Releases itself — changesets/action@v1 finds no `New tag:` line under CLI v3 (objectui#11596)

changesets/action@v1 pushes a tag and creates a Release only for the
packages whose `New tag: <pkg>@<version>` line it parses from the
publish script's stdout. @changesets/cli v3 (objectui#5296) prints none,
so 17.6.0 and 17.7.0 reached npm with no git tag and no GitHub Release
while every step stayed green.

Two steps after "Verify the release reached npm", on the publish lane
only (`push`, gated on the publish step's and the npm check's outcome,
under `!cancelled()`):

- "Push the release tags": pushes exactly the `<pkg>@<version>` tags
  `changeset publish` created on the runner (the list
  `release-github-releases.mjs --print-tags` prints), in one push, and
  fails naming any that is missing;
- "Create GitHub Releases": runs scripts/release-github-releases.mjs
  with RELEASE_VERSION = needs.lane.outputs.manifest_version.

changesets/action stays at @v1. The workflow's comments that said v1
creates the tags and Releases now say what happens, and the CI guide's
Changeset Release section documents both steps.

Claude-Session: https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation configuration ci/cd tests labels Oct 4, 2026
@hotlong
hotlong marked this pull request as ready for review October 4, 2026 05:34
@hotlong
hotlong added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 973fc20 Oct 4, 2026
42 checks passed
@hotlong
hotlong deleted the claude/objectstack-release-steps-ynhz3j branch October 4, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd configuration documentation Improvements or additions to documentation tests

Projects

None yet

2 participants