Conversation
Merge as is — approval routing fixes duplicate release gating without changing deploy guardsThe PR remains suitable to merge as written: the environment-only delta routes the automated The live merge state is still operationally blocked by unresolved review threads at Merge state: The PR is open, non-draft, and mergeable, but GitHub reports a blocked merge state. Nine of 15 checks are complete and six remain pending; completed results include five successes, three skips, and one neutral result. No gating human CHANGES_REQUESTED review is shown in the supplied live state.; blockers: Unresolved review thread at Evidence · 8 verified · 2 not verified · 3 carriedVerified
Not verified
This delta pass read the complete interdiff and current registry workflow, re-read the npm publish hand-off, sibling workflow precedents, governance files, prior reviews, all listed review threads, issue comments, and refreshed live PR state. No tests, parsers, workflow runs, npm queries, or deployments were executed. One interdiff file was examined; the five settled ledger findings were rechecked and no new finding was opened.
Comment Review by Rupic. |
|
|
Claude finished @Kinfe123's task in 5m 37s —— View job No blockers left — good to ship, with one thing to watch on the first live releaseEvery finding from my earlier rounds is resolved, and the resolutions are the right ones rather than the ones the bots proposed:
I re-read the whole verify step for the failure modes the earlier rounds kept surfacing and didn't find another. Specifically: The contract this lands on — a registry-only fix ships only if no public package source changed since the last release, otherwise cut a release first — is now stated in the input description ( Worth noting, not blocking
CI is green on this head ( Reviewed by |
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - apps/registry/** | ||
| - packages/ui/** | ||
| - .github/workflows/registry.yaml | ||
| repository_dispatch: | ||
| types: [registry-deploy] |
There was a problem hiding this comment.
repository_dispatch is now the only trigger, and this is the only workflow in the repo that deploys the production registry (no git-connected Vercel path, no preview workflow). That leaves registry content with no route to production of its own:
packages/uiis"private": true, so it is never an npm publish target.apps/registryisn't published either.- The
publishjob is gated onif: needs.summary.outputs.target_count != '0'(npm-publish.yaml:667), so a release cycle with no npm targets dispatches nothing.
So a registry-only fix — including a broken registry.ts entry or a bad registryDependencies list, i.e. exactly the class of bug that makes shadcn add fail — cannot be deployed at all until some unrelated package happens to release. There's also no re-run escape hatch for the very first deploy.
Please keep a workflow_dispatch with an optional releaseSha input (defaulting to default-branch HEAD) alongside the dispatch trigger. That preserves the ordering guarantee this PR is after while leaving a maintainer a way to ship the registry on demand.
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - apps/registry/** | |
| - packages/ui/** | |
| - .github/workflows/registry.yaml | |
| repository_dispatch: | |
| types: [registry-deploy] | |
| on: | |
| repository_dispatch: | |
| types: [registry-deploy] | |
| workflow_dispatch: | |
| inputs: | |
| releaseSha: | |
| description: "Release commit SHA to deploy (defaults to latest default-branch commit)" | |
| required: false | |
| type: string |
(paired with resolving the ref from either source in the checkout step)
| with: | ||
| fetch-depth: 1 | ||
| fetch-depth: 0 | ||
| ref: ${{ github.event.client_payload.releaseSha }} |
There was a problem hiding this comment.
Nothing validates client_payload.releaseSha. If it arrives empty or absent, actions/checkout treats ref: "" as unset and falls back to the default ref for the event — default-branch HEAD. That is silently the exact unpinned behavior this PR exists to remove, and because the deploy still goes green nobody finds out.
The repo already has the pattern for this: npm-publish.yaml:66-67 and pypi-publish.yaml:68-69 both core.setFailed("repository_dispatch payload is missing releaseSha"), and both additionally assert the SHA is an ancestor of the default branch. Worth a short guard step here for the same reason — a repository_dispatch payload is attacker-controlled by anyone with write access, and this job holds VERCEL_TOKEN and deploys to production.
| - name: Deploy registry from the published release | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # ratchet:actions/github-script@v9.0.0 | ||
| with: | ||
| script: | | ||
| await github.rest.repos.createDispatchEvent({ | ||
| ...context.repo, | ||
| event_type: "registry-deploy", | ||
| client_payload: { releaseSha: process.env.RELEASE_SHA }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
This step can turn a successful publish red. It sits between changesets-publish and the if: failure() "Recovery guidance" step, so a transient createDispatchEvent API failure — after every package is already on npm — fails the job and prints ::error::Publish failed for release SHA … plus instructions to re-run the publish. That's a false report on the one job where a wrong recovery instruction is most expensive.
continue-on-error: true (or moving the dispatch to a separate needs: publish job) keeps a failed hand-off from misrepresenting the publish outcome.
There was a problem hiding this comment.
All reported issues were addressed
You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - apps/registry/** | ||
| - packages/ui/** | ||
| - .github/workflows/registry.yaml | ||
| repository_dispatch: | ||
| types: [registry-deploy] |
There was a problem hiding this comment.
Blocking: this leaves no way to deploy the registry once a publish run has finished.
repository_dispatch is now the only trigger, and the only dispatcher is the publish job, which is gated on if: needs.summary.outputs.target_count != '0'. After a successful publish every version is on npm, so summary resolves zero targets and a re-run of the publish workflow skips the publish job entirely — the dispatch step never fires again.
Concretely, if the registry run fails for any reason after the release succeeded:
- npm propagation exceeds the 60 s retry budget in
Verify published release - the
chore: update versionssubject assertion rejects aworkflow_dispatchrelease (see below) - Vercel pull/build/deploy flakes
createDispatchEventitself errors in the publish job
…production stays on the previous registry with no rerun path short of hand-crafting a POST /repos/{o}/{r}/dispatches call with a personal token, or reverting this workflow. Same for a registry-only regression (a bad registry.json entry, a broken component) — it can't be shipped until the next package release.
A workflow_dispatch escape hatch closes all of these and costs ~8 lines, since the verify step already validates whatever SHA it is handed:
on:
repository_dispatch:
types: [registry-deploy]
workflow_dispatch:
inputs:
releaseSha:
description: "Release commit SHA to deploy (must be an ancestor of main)"
required: true
type: stringwith ref: and the verify step's RELEASE_SHA reading ${{ github.event.client_payload.releaseSha || inputs.releaseSha }}. (npm-publish.yaml already carries exactly this dual-trigger shape.)
| - name: Verify published release | ||
| env: | ||
| RELEASE_SHA: ${{ github.event.client_payload.releaseSha }} | ||
| run: | | ||
| [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] | ||
| test "$(git rev-parse HEAD)" = "$RELEASE_SHA" | ||
| git merge-base --is-ancestor HEAD origin/main | ||
| [[ "$(git log -1 --format=%s HEAD)" == "chore: update versions"* ]] | ||
| for package in core react; do | ||
| version=$(node -p "require('./packages/$package/package.json').version") | ||
| for attempt in 1 2 3 4 5 6; do | ||
| published=$(npm view "@assistant-ui/$package@$version" version 2>/dev/null) || true | ||
| if [ "$published" = "$version" ]; then break; fi | ||
| sleep 10 | ||
| done | ||
| test "$published" = "$version" | ||
| done |
There was a problem hiding this comment.
Two things about this gate, given it now runs unattended in a workflow run nobody is watching:
1. Every assertion fails silently. Four bare [[ ]]/test expressions under bash -eo pipefail exit non-zero with no output, and npm view's stderr is sent to /dev/null, so a registry that quietly stops deploying gives the operator a red step and zero signal about which invariant broke. Worth an ::error:: on each, especially the propagation timeout and the subject check:
[[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::releaseSha is not a 40-char SHA: $RELEASE_SHA"; exit 1; }2. The commit-subject assertion rejects a legitimate release. npm-publish.yaml accepts workflow_dispatch with an optional releaseSha documented as "defaults to latest default-branch commit", and its own Recovery guidance step points maintainers at that path. A manual publish from main HEAD (any commit landed after the version commit) publishes successfully and then fails here. Failing closed is the right direction — deploying from a HEAD ahead of the version commit is the bug this PR fixes — but combined with the missing rerun path above it strands the registry. Please make the failure message say explicitly that the registry was not deployed and how to redeploy.
Also: scripts/vercel-ignore-changeset-release.sh matches the same "chore: update versions"* prefix and carries # Keep in sync with commit: in .github/workflows/changeset.yaml. This copy is a third place that silently depends on that string; the same pointer would help.
Minor: the npm propagation loop hardcodes core and react, while registry items also pull @assistant-ui/react-markdown and assistant-stream. changesets/action exposes publishedPackages — passing it through client_payload would check what was actually released instead of a two-package proxy. And origin/main is hardcoded here while npm-publish.yaml goes to some length to resolve default_branch dynamically.
| - name: Deploy registry from the published release | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # ratchet:actions/github-script@v9.0.0 | ||
| with: | ||
| script: | | ||
| await github.rest.repos.createDispatchEvent({ | ||
| ...context.repo, | ||
| event_type: "registry-deploy", | ||
| client_payload: { releaseSha: process.env.RELEASE_SHA }, | ||
| }); |
There was a problem hiding this comment.
This step runs after packages are already on npm, so a transient GitHub API error here marks a successful release as a failed run — and then Recovery guidance below prints "Rerun this workflow run, or run workflow_dispatch with releaseSha=…", which is now wrong advice: summary will resolve zero targets and skip the publish job.
Suggest isolating it so a dispatch hiccup can't misreport the release, paired with the workflow_dispatch escape hatch on the registry side:
- name: Deploy registry from the published release
continue-on-error: true
if: steps.changesets-publish.outputs.published == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # ratchet:actions/github-script@v9.0.0The published guard also makes the trigger precise: right now the dispatch fires on the job-level target_count even if changesets publish itself published nothing.
There was a problem hiding this comment.
All reported issues were addressed
You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| deploySha = deploySha.trim(); | ||
| if (!/^[0-9a-f]{40}$/i.test(deploySha)) { | ||
| core.setFailed("releaseSha must be a full 40-character commit SHA"); | ||
| return; | ||
| } | ||
| core.setOutput("value", deploySha); |
There was a problem hiding this comment.
The regex accepts uppercase hex (/i) but the SHA is emitted verbatim, and line 71 compares it against git rev-parse HEAD, which always prints lowercase. A maintainer pasting an uppercase SHA into the workflow_dispatch input gets past validation, gets a correct checkout, and then fails with Checkout does not match deploy SHA … — an error that points at the wrong thing.
| deploySha = deploySha.trim(); | |
| if (!/^[0-9a-f]{40}$/i.test(deploySha)) { | |
| core.setFailed("releaseSha must be a full 40-character commit SHA"); | |
| return; | |
| } | |
| core.setOutput("value", deploySha); | |
| deploySha = deploySha.trim().toLowerCase(); | |
| if (!/^[0-9a-f]{40}$/.test(deploySha)) { | |
| core.setFailed(`releaseSha must be a full 40-character commit SHA: ${deploySha}`); | |
| return; | |
| } | |
| core.setOutput("value", deploySha); |
| test "$(git rev-parse HEAD)" = "$DEPLOY_SHA" || fail "Checkout does not match deploy SHA $DEPLOY_SHA" | ||
| git merge-base --is-ancestor HEAD origin/main || fail "Deploy SHA $DEPLOY_SHA is not on main" | ||
| if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ]; then | ||
| [[ "$(git log -1 --format=%s HEAD)" == "chore: update versions"* ]] || fail "Published release dispatch must target a version commit" |
There was a problem hiding this comment.
Scoping this to repository_dispatch doesn't retire the false-failure path from the earlier review, because the case that trips it is a repository_dispatch: npm-publish.yaml accepts workflow_dispatch with an optional releaseSha that defaults to default-branch HEAD, and its own Recovery guidance step tells maintainers to use exactly that. A manual publish run after main has moved past the version commit publishes fine, then the new registry-dispatch job hands that HEAD SHA here and this assertion rejects it.
The invariant itself is right — deploying the registry only from version commits is the whole point of the PR — so I'd keep the check and fix the message, since the operator lands on a red run with no idea the registry is now stale:
| [[ "$(git log -1 --format=%s HEAD)" == "chore: update versions"* ]] || fail "Published release dispatch must target a version commit" | |
| [[ "$(git log -1 --format=%s HEAD)" == "chore: update versions"* ]] || fail "$DEPLOY_SHA is not a version commit; registry NOT deployed. Re-run Deploy Shadcn Registry via workflow_dispatch with the version commit SHA." |
| run: | | ||
| fail() { echo "::error::$1"; exit 1; } | ||
| test "$(git rev-parse HEAD)" = "$DEPLOY_SHA" || fail "Checkout does not match deploy SHA $DEPLOY_SHA" | ||
| git merge-base --is-ancestor HEAD origin/main || fail "Deploy SHA $DEPLOY_SHA is not on main" |
There was a problem hiding this comment.
Blocking: origin/main does not exist here, so this fails on every run.
actions/checkout treats a 40-char ref as a commit, not a branch, and its refspec becomes the bare SHA — git fetch origin <sha> writes FETCH_HEAD and no remote-tracking ref. fetch-depth: 0 only lifts the depth limit; it does not widen the refspec. So origin/main is an unknown revision, git merge-base exits 128, and fail fires with "Deploy SHA … is not on main" — on a commit that is on main.
The net effect is worse than the bug being fixed: after this merges, the registry stops deploying entirely, from both triggers, and the error message points the maintainer at the wrong cause.
This repo already has the fix — npm-publish.yaml:239 runs git fetch origin "$DEFAULT_BRANCH" immediately before git rev-parse "origin/$DEFAULT_BRANCH", under an identical fetch-depth: 0 + ref: <sha> checkout. That explicit fetch is there for exactly this reason.
| git merge-base --is-ancestor HEAD origin/main || fail "Deploy SHA $DEPLOY_SHA is not on main" | |
| git fetch --no-tags --depth=1 origin main || fail "Unable to fetch main for the ancestry check" | |
| git merge-base --is-ancestor HEAD origin/main || fail "Deploy SHA $DEPLOY_SHA is not on main" |
Separately, main is hardcoded in three places in this step while npm-publish.yaml resolves default_branch dynamically and threads it through needs.approve.outputs.default_branch. Passing defaultBranch in the client_payload (and resolving it in the Resolve deploy commit SHA step for the manual path) would keep the two workflows from disagreeing.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. WalkthroughThe npm publish workflow now dispatches a registry deployment event with the approved release SHA. The registry workflow supports event-based and manual dispatch, resolves the deploy SHA, and checks it out. It verifies the commit against Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Severity of issue fixed: Medium Merge Risk: 🟡 Moderate · up to A manual deployment could serve components that fail against packages available to consumers. Verify compatibility with published packages before merging. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation For
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| if [[ "$(git log -1 --format=%s HEAD)" != "chore: update versions"* ]]; then | ||
| if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ]; then | ||
| git rev-parse -q --verify HEAD^2 >/dev/null || fail "$DEPLOY_SHA is not a version commit or version PR merge commit" | ||
| release_commit=$(git log -1 --format=%H --grep='^chore: update versions' HEAD^1..HEAD^2) | ||
| test -n "$release_commit" || fail "No version commit was found in the release merge" | ||
| git diff --quiet "$release_commit" HEAD^2 -- packages ':(exclude)packages/ui' || fail "Published package sources changed after the version commit" | ||
| else | ||
| release_commit=$(git log --first-parent -1 --format=%H --grep='^chore: update versions' HEAD) | ||
| test -n "$release_commit" || fail "No published version commit precedes this registry deploy" | ||
| git diff --quiet "$release_commit" HEAD -- packages ':(exclude)packages/ui' || fail "Published package sources changed since the last release; publish packages before deploying the registry" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
The repository_dispatch branch is dead code, and the split between the two branches breaks the recovery path this PR is meant to preserve.
HEAD^2 never resolves. main is squash-merged and linear — every commit in the recent range has a single parent and a (#NNNN) subject suffix:
a0990ea|8403fdb|fix: coerce malformed LangChain tool-call args... (#7993)
8403fdb|ea92dd1|fix: clear A2A and AG-UI messages... (#7989)
So git rev-parse -q --verify HEAD^2 always fails and lines 70–72 are unreachable. They were added for a merge-commit scenario the bots hypothesized, which doesn't exist here.
The consequence isn't just dead code. Because this branch fails instead of falling through to the first-parent search, a manual npm Publish run — the recovery path npm-publish.yaml:709 itself tells operators to use — dispatches a SHA that is not a version commit (approve defaults releaseSha to default-branch HEAD, npm-publish.yaml:71–79). That lands here as a repository_dispatch, hits git rev-parse HEAD^2 → fail, and the registry stays stale. The dispatching job in npm-publish is already green by then, so nothing surfaces it.
The manual branch's logic is strictly more general and equally safe — find the last version commit reachable, then require published sources unchanged since. Collapsing to it removes the dead code and fixes the recovery path in one move:
| if [[ "$(git log -1 --format=%s HEAD)" != "chore: update versions"* ]]; then | |
| if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ]; then | |
| git rev-parse -q --verify HEAD^2 >/dev/null || fail "$DEPLOY_SHA is not a version commit or version PR merge commit" | |
| release_commit=$(git log -1 --format=%H --grep='^chore: update versions' HEAD^1..HEAD^2) | |
| test -n "$release_commit" || fail "No version commit was found in the release merge" | |
| git diff --quiet "$release_commit" HEAD^2 -- packages ':(exclude)packages/ui' || fail "Published package sources changed after the version commit" | |
| else | |
| release_commit=$(git log --first-parent -1 --format=%H --grep='^chore: update versions' HEAD) | |
| test -n "$release_commit" || fail "No published version commit precedes this registry deploy" | |
| git diff --quiet "$release_commit" HEAD -- packages ':(exclude)packages/ui' || fail "Published package sources changed since the last release; publish packages before deploying the registry" | |
| fi | |
| fi | |
| release_commit=$DEPLOY_SHA | |
| if [[ "$(git log -1 --format=%s HEAD)" != "chore: update versions"* ]]; then | |
| release_commit=$(git log --first-parent -1 --format=%H --grep='^chore: update versions' HEAD) | |
| test -n "$release_commit" || fail "No published version commit precedes $DEPLOY_SHA; registry was not deployed" | |
| git diff --quiet "$release_commit" HEAD -- packages ':(exclude)packages/ui' || fail "Published package sources changed since the last release; publish packages before deploying the registry" | |
| fi |
Minor, while you're in here: "chore: update versions" is owned by commit-message: in .github/workflows/changeset.yaml:43, and scripts/vercel-ignore-changeset-release.sh:16 carries a # Keep in sync with ... pointer for it. This is the third copy and the only one without one.
| else | ||
| release_commit=$(git log --first-parent -1 --format=%H --grep='^chore: update versions' HEAD) | ||
| test -n "$release_commit" || fail "No published version commit precedes this registry deploy" | ||
| git diff --quiet "$release_commit" HEAD -- packages ':(exclude)packages/ui' || fail "Published package sources changed since the last release; publish packages before deploying the registry" |
There was a problem hiding this comment.
This gate counts private packages and test files as "published package sources," which closes the escape hatch 42c5b9e just opened.
Ran it against a real main range:
$ git diff --name-only 6e128be a0990ea -- packages ':(exclude)packages/ui'
packages/core/src/runtime/api/message-runtime.test.ts
packages/react-langchain/src/convertMessages.test.ts
...
packages/vue/src/index.ts
packages/vue/src/primitives/MessagePrimitiveParts.ts
packages/vue, packages/svelte, packages/x-performance, and packages/x-changelog are all "private": true — nothing about them can be unavailable to a consumer. Same for *.test.ts. Over any realistic window between releases this repo merges packages/** changes constantly, so the manual path fails on commits that carry zero compatibility risk, and a registry-only hotfix still can't ship without cutting a release.
The loop below already has the right notion of "published" (require('./$manifest').private ? 'yes' : 'no', line 82). Reusing it here — build the exclude pathspecs from the private manifests rather than hardcoding packages/ui — makes the hatch actually usable and keeps the two checks from disagreeing.
Separately: even after narrowing, any published-source change blocks a registry-only deploy, because the gate can't tell "core changed" from "the registry uses the new core API." That may well be the contract you want — but then say so in the releaseSha input description and the failure message, rather than offering a hatch that is closed most of the time.
| fail "Public package source $file changed since the last release; publish packages before deploying the registry" | ||
| done <<< "$changed_files" | ||
| fi | ||
| changed_manifests=$(git diff --name-only "$release_commit^" "$release_commit" -- 'packages/*/package.json') || fail "Unable to inspect release package versions" |
There was a problem hiding this comment.
Non-blocking, but this pathspec is wider than it reads. Git pathspec globs match /, so packages/*/package.json also selects the subpath stub manifests — 10 tracked today:
$ git ls-files -- 'packages/*/package.json' | grep '/.*/.*/'
packages/assistant-stream/utils/package.json
packages/react-google-adk/server/package.json
packages/tap/react-shim/package.json
...
Those stubs are {"main": "../dist/server/index.js"} — no name, no version, no private. So the .private check below reads undefined → "no" → the stub is treated as a published package, and the loop spends 6×10 s on npm view undefined@undefined before failing the deploy with undefined@undefined is not available on npm.
Only reachable if a chore: update versions commit ever touches one (changesets alone won't, but a hand-fixed version PR could), so it's latent. :(glob) costs nothing:
| changed_manifests=$(git diff --name-only "$release_commit^" "$release_commit" -- 'packages/*/package.json') || fail "Unable to inspect release package versions" | |
| changed_manifests=$(git diff --name-only "$release_commit^" "$release_commit" -- ':(glob)packages/*/package.json') || fail "Unable to inspect release package versions" |
The first loop is unaffected — it derives the package dir from the first path segment, so a nested path still resolves to the right top-level manifest.
There was a problem hiding this comment.
1 inline comment below, on .github/workflows/registry.yaml.
Full review: #8009 (comment)
| release_commit=$DEPLOY_SHA | ||
| # Keep in sync with commit-message in .github/workflows/changeset.yaml. | ||
| if [[ "$(git log -1 --format=%s HEAD)" != "chore: update versions"* ]]; then | ||
| release_commit=$(git log --first-parent -1 --format=%H --grep='^chore: update versions' HEAD) |
There was a problem hiding this comment.
Please make the release lookup inspect a merge commit's parents or the full release range. git log --first-parent -1 --grep='^chore: update versions' HEAD does not inspect a version commit carried by the second parent of a Merge pull request... release SHA, so the fallback can select an older release and fail at .github/workflows/registry.yaml:82 after npm publication has succeeded.
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| release_commit=$DEPLOY_SHA | ||
| # Keep in sync with commit-message in .github/workflows/changeset.yaml. | ||
| if [[ "$(git log -1 --format=%s HEAD)" != "chore: update versions"* ]]; then | ||
| release_commit=$(git log -1 --format=%H --grep='^chore: update versions' HEAD) |
There was a problem hiding this comment.
--grep matches the whole commit message line-wise, not the subject, so this can resolve a non-release commit.
git compiles log grep patterns with REG_NEWLINE (grep_init() sets opt->regflags = REG_NEWLINE), which is why git log --grep='^Signed-off-by' matches trailers in the body. So ^chore: update versions matches any body line starting with that string — and squash commits on main carry the PR description as the body. A release-tooling PR whose description has a line beginning chore: update versions (plausible; this PR's own body is close) would be picked as release_commit.
The consequence is silent and is the exact failure this PR exists to prevent: release_commit lands on a recent non-release commit, the drift check at line 71 compares almost nothing, it passes, and the registry deploys component code that depends on unpublished package sources. Line 85's manifest diff then inspects that commit's parent range and finds no manifests to verify, so the npm availability check doesn't catch it either.
Line 68 already checks the subject correctly; asserting the same on the resolved commit closes it and fails safe (a bad match becomes a clear failure rather than a bad deploy):
| release_commit=$(git log -1 --format=%H --grep='^chore: update versions' HEAD) | |
| release_commit=$(git log -1 --format=%H --grep='^chore: update versions' HEAD) | |
| test -n "$release_commit" || fail "No published version commit precedes $DEPLOY_SHA" | |
| [[ "$(git log -1 --format=%s "$release_commit")" == "chore: update versions"* ]] || fail "Resolved release commit $release_commit is not a version commit" |
| deploy-production: | ||
| name: Deploy Production | ||
| runs-on: ubuntu-latest | ||
| environment: npm Publish |
There was a problem hiding this comment.
This is the one thing I want settled before merge, and it's a maintainer call, not a code fix.
The environment is the right idea: every other guard in this job (ancestry, version-commit, npm availability) lives in registry.yaml itself, and a workflow_dispatch run executes the workflow file from the selected branch. So anyone with write access could push a branch with those guards deleted and dispatch it. The environment's deployment-branch policy is the only control that actually holds, because it's configured in repo settings rather than in the attacker-editable file. Keep it.
The problem is which environment. npm Publish is the gate on publish (npm-publish.yaml:668), so it carries required reviewers. Reusing it means each release needs a second approval, in a different workflow run, that the approver has to go find. Miss it and the registry stays on the previous release — a quieter version of the bug this PR fixes (stale registry instead of premature registry), and GitHub cancels a waiting run after 30 days. It also puts anything scoped to that environment into this job's secrets context, and this job runs pnpm install + vercel build over repo code.
What I'd ship: a dedicated registry environment with deployment branches limited to main and no required reviewers — same protection against the branch-dispatch hole, no approval in the automated path. I can create it; the PR body says you can't. Say the word and I'll add it, then this line becomes environment: registry.
If you'd rather keep the double approval as a deliberate policy, that's fine too — but then it belongs in the PR body as a chosen trade-off rather than a workaround for missing permissions.
| name: Deploy Production | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: ${{ github.event_name == 'repository_dispatch' && github.actor == 'github-actions[bot]' && github.triggering_actor == 'github-actions[bot]' && 'Production' || 'npm Publish' }} |
There was a problem hiding this comment.
This resolves my earlier objection to the double approval, and splitting on event_name/actor/triggering_actor is the right shape — a human re-run correctly falls back to the gate. Two things to be clear-eyed about before merge, neither of which I think blocks:
Production does not exist yet. The only environments referenced anywhere in .github/workflows/ are npm Publish (npm-publish.yaml:668) and pypi publish (pypi-publish.yaml:220). A job referencing an unknown environment creates it on first run with no protection rules, which is the behavior you want here — but it means the automated path is protected by the checks in the Verify step below, not by the environment. Worth saying that in the PR body rather than "requires maintainer approval on main", which reads as if the environment is the control on both paths. (It also leaves an unowned Production entry in Settings → Environments that a future admin could add reviewers to, silently stalling every release deploy.)
The secrets are repo-level, so neither environment scopes them. deploy-examples.yaml deploys production Vercel with the same VERCEL_TOKEN and no environment: at all, so the token is a repository secret. Any collaborator with write access can already read it from a workflow on a pushed branch. That's fine — it just means the npm Publish gate on the manual path is an accident interlock, not a security boundary. Worth scoping VERCEL_TOKEN to an environment later if you want it to be one; out of scope here.
Last thing, and the only one I'd want confirmed on the first live release: the whole automated path hinges on github.actor being github-actions[bot] for a repository_dispatch created with GITHUB_TOKEN. The PR body says you checked this against real npm-publish runs (dispatched the same way from changeset.yaml:129), which I can't re-verify from here. If it's wrong, the deploy silently waits for approval — a quieter version of the bug this PR fixes. Please watch the first release after merge.
Problem
The production shadcn registry can deploy from a main commit before the packages it uses are published. A fresh install then resolves the older
@assistant-ui/coreand fails to type-check onapproval.dismissible. Fixes #7938.Root cause
The registry push workflow and npm publish workflow run independently. The registry build resolves workspace packages, so it can succeed while the corresponding npm versions remain unavailable to consumers.
Change
Dispatch registry deployment after a successful npm publish and check out that exact release SHA. Manual runs require a full main SHA. For any commit after the latest reachable
chore: update versionscommit, deployment is allowed only when public package sources have not changed since that release; private packages, tests, and Markdown do not block registry-only fixes. Before building, verify that core/react and every public package version changed by the release are available on npm. The same guard applies to automatic dispatches, including retries from a manual npm publish. A dispatch failure leaves npm publishing green and reports how to retry the registry deployment. The registry deploys automatically after the approved npm publish when GitHub Actions initiates the dispatch. Manual deployments, dispatches from other actors, and human reruns use the protectednpm Publishenvironment, requiring maintainer approval onmainbefore the Vercel deployment job starts.Verification
Both workflow files parse as YAML; the registry workflow's embedded JavaScript and Bash pass syntax checks. A fresh checkout of a real main SHA lacked
origin/main; fetching the explicit remote-tracking ref restored the ancestry check. A prior version PR squash commit on main is namedchore: update versions (#7471), matching the release guard. A synthetic merge-commit case confirms the guard finds the version commit through merge history while ignoring a newer non-release commit whose body mentionschore: update versions. The manifest pathspec selects 46 top-level packages and excludes 10 nested stubs without publishable names. The package-source guard accepts an actual UI-only main commit and rejects a core-changing main commit. Recent repository-dispatch npm publish runs confirm GitHub Actions appears as both the run actor and triggering actor; manual runs and human reruns select the approval gate. The liveassistant-ui-registryVercel project has no Git link, and its five latest production deployments all reportsource: cliwith nogitSource; the repository push workflow is the deploy path this PR replaces. No Vercel deployment was run locally.Public surface
Workflow only; no package exports, docs, templates, or changeset.