Skip to content

fix(registry): deploy components after npm publish - #8009

Open
Kinfe123 wants to merge 15 commits into
mainfrom
fix/7938-registry-release-order
Open

Kinfe123 wants to merge 15 commits into
mainfrom
fix/7938-registry-release-order

Conversation

@Kinfe123

@Kinfe123 Kinfe123 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

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/core and fails to type-check on approval.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 versions commit, 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 protected npm Publish environment, requiring maintainer approval on main before 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 named chore: 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 mentions chore: 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 live assistant-ui-registry Vercel project has no Git link, and its five latest production deployments all report source: cli with no gitSource; 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.

@Kinfe123
Kinfe123 requested a review from a team September 22, 2026 22:41
@rupic-app

rupic-app Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Comment Reviewed in 6m 36s · Round 7 · comment · 5ace072

Merge as is — approval routing fixes duplicate release gating without changing deploy guards

The PR remains suitable to merge as written: the environment-only delta routes the automated repository_dispatch hand-off to Production while keeping manual, non-bot, and human-rerun paths on protected npm Publish; it does not change the release-SHA, ancestry, source-drift, or npm-availability guards in .github/workflows/registry.yaml:31-99.

The live merge state is still operationally blocked by unresolved review threads at .github/workflows/registry.yaml:5, .github/workflows/registry.yaml:65-66, .github/workflows/registry.yaml:85, and .github/workflows/npm-publish.yaml:730-731, with six checks still pending. Those conditions do not add a code-quality finding or change the recommendation.

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 .github/workflows/registry.yaml:5; Unresolved review thread at .github/workflows/npm-publish.yaml:730-731; Unresolved review thread at .github/workflows/registry.yaml:65-66; Unresolved review thread at .github/workflows/registry.yaml:85

Evidence · 8 verified · 2 not verified · 3 carried

Verified

  • The registry/npm release-ordering defect is real and remains unsolved without this hand-off.
    Carried from the prior round because the issue and the untouched npm publish hand-off remain unchanged; the interdiff only changes the receiving job's environment.
  • Automatic registry deployment runs only after npm publish for the exact release SHA.
    The relevant hand-off and checkout lines are unchanged; the delta alters only which environment gates the receiving job.
  • Manual deployment requires a full main SHA and permits registry-only changes without a new package release.
    The manual contract remains intact; the new environment expression does not alter input resolution or compatibility guards.
  • The release guard accepts a version-PR merge commit handed off by npm publish.
    The merge-commit fix is outside the interdiff but was re-read at its current anchor; environment selection does not affect it.
  • The npm gate checks core, react, and every public package manifest changed by the release.
    The package gate is unchanged and remains independent of the environment expression.
  • Manual and human-triggered deployments use protected npm Publish while automated hand-offs use Production.
    The routing expression is verified statically; whether GitHub supplies the expected actor values and whether both environments carry the required secrets are external runtime configuration claims.
  • A dispatch failure leaves npm publishing green and provides registry-only recovery guidance.
    Carried from the prior round because the anchored npm publish job and its importer are untouched by this interdiff; the receiving environment change cannot alter that job graph.
  • This workflow-only change needs no package changeset or public package surface update.
    Carried from the prior round because the unchanged governance rule and package surface remain outside the one-file interdiff.

Not verified

  • This change fixes the fresh-consumer type-check failure in registry: components ship before the npm release they compile against #7938.
    A fresh consumer install and TypeScript build against the resulting deployed registry artifact would settle the end-to-end claim.
  • The workflow files and embedded JavaScript and Bash pass the syntax checks listed in the PR.
    A YAML parse and GitHub Actions expression validation would settle the syntax and expression-evaluation claim.

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.

Round Commit Verdict Took When
1 2598787 comment 7m 45s 2026-09-22 22:53 UTC
2 2d45b7b comment 12m 10s 2026-09-22 23:17 UTC
3 12c0bf5 comment 7m 16s 2026-09-22 23:29 UTC
4 93eeb69 comment 9m 16s 2026-09-22 23:50 UTC
5 1348c69 comment 7m 38s 2026-09-23 00:06 UTC
6 ebbe8b4 comment 9m 14s 2026-09-23 00:39 UTC
7 5ace072 comment 6m 36s 2026-09-23 00:55 UTC

Comment @rupic-app stop reviewing to stop reviewing this PR.

Review by Rupic.

@changeset-bot

changeset-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5ace072

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@rupic-app rupic-app Bot added stage/needs-review No reviews yet; waiting for a maintainer R0 Act now: stale, blocked, or high impact type/bugfix Bug fix size/S 10–49 lines changed labels Sep 22, 2026
@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

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 release

Every finding from my earlier rounds is resolved, and the resolutions are the right ones rather than the ones the bots proposed:

Prior finding Status
repository_dispatch as the only trigger left no deploy path Fixed — workflow_dispatch with a required full SHA (registry.yaml:6-11)
Unvalidated client_payload.releaseSha Fixed — normalize + ^[0-9a-f]{40}$ + ancestry check (:47-51, :65-67)
origin/main doesn't exist under a SHA checkout, so ancestry failed on every run Fixed — explicit refspec fetch with a fail annotation (:66)
Dispatch failure marking a successful publish red Fixed properly — moved to a separate registry-dispatch job with its own recovery text (npm-publish.yaml:711-736)
HEAD^2 merge branch was dead code on a linear squash-merged main Fixed — collapsed to the single search path (:70-86)
Private packages / tests / Markdown blocking a registry-only deploy Fixed — skip list plus a private manifest check (:76-83)
--grep matches body lines, so a PR description could be selected as the release commit Fixed — awk re-asserts the match on %s (:71)
packages/*/package.json also matched nested subpath stubs → npm view undefined@undefined Fixed — :(glob) (:87)
Double approval per release Fixed — conditional environment (:29); see the inline note
Silent bare assertions Fixed — every check routes through fail()

I re-read the whole verify step for the failure modes the earlier rounds kept surfacing and didn't find another. Specifically: published can't carry over between iterations (a failed npm view yields an empty substitution, not the previous value); the [ … ] && continue forms are exempt from -e because they precede the final &&; an empty $changed_manifests becomes a blank line that the -n guard drops at :89; and the manifest read at :90-92 is consistent with the diff range because any public package.json change between the release commit and HEAD is already rejected by the source guard at :73-85.

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 (:9) and the failure message (:84). That's a deliberate answer to the question I raised last round rather than a leftover, and I think it's the right one for this repo.

Worth noting, not blocking

  • Production environment / actor gating — see the inline comment. The short version: the environment protects the manual path only; the automated path is protected by the verify step, and it depends on github.actor resolving to github-actions[bot] for a GITHUB_TOKEN-initiated dispatch. Please confirm on the first release after merge.
  • Concurrency group (:16-18) — ${{ github.workflow }}-${{ github.ref }} used to collapse to one group because the only trigger was push: main. A workflow_dispatch run from a branch and an automatic repository_dispatch now land in different groups and can race on the Vercel production alias. Dropping -${{ github.ref }} would restore the single lane. Fix this →
  • git log without --first-parent (:71) — relies on commit-date ordering to pick the newest version commit. Correct on today's linear squash-merged main; only a hazard if the repo ever takes a merge commit. Fine as is.

CI is green on this head (Unmanaged Pins, Source Snapshot Budget, Changeset Semver Check, autofix.ci, PR #8009 all passing). Workflow-only change, no changeset needed — the semver check agrees.
· branch

Reviewed by claude-opus-5

Comment on lines 3 to +5
on:
push:
branches:
- main
paths:
- apps/registry/**
- packages/ui/**
- .github/workflows/registry.yaml
repository_dispatch:
types: [registry-deploy]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/ui is "private": true, so it is never an npm publish target.
  • apps/registry isn't published either.
  • The publish job is gated on if: 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.

Suggested change
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)

Comment thread .github/workflows/registry.yaml Outdated
with:
fetch-depth: 1
fetch-depth: 0
ref: ${{ github.event.client_payload.releaseSha }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +705 to +714
- 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 },
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rupic-app rupic-app Bot added stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed and removed stage/needs-review No reviews yet; waiting for a maintainer labels Sep 22, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .github/workflows/registry.yaml Outdated
Comment thread .github/workflows/npm-publish.yaml
Comment on lines 3 to +5
on:
push:
branches:
- main
paths:
- apps/registry/**
- packages/ui/**
- .github/workflows/registry.yaml
repository_dispatch:
types: [registry-deploy]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 versions subject assertion rejects a workflow_dispatch release (see below)
  • Vercel pull/build/deploy flakes
  • createDispatchEvent itself 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: string

with 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.)

Comment thread .github/workflows/registry.yaml Outdated
Comment on lines +29 to +45
- 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +705 to +713
- 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 },
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0

The 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.

@rupic-app rupic-app Bot added size/M 50–199 lines changed and removed size/S 10–49 lines changed labels Sep 22, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .github/workflows/registry.yaml
Comment thread .github/workflows/registry.yaml Outdated
Comment on lines +53 to +58
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Comment thread .github/workflows/registry.yaml Outdated
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
[[ "$(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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .github/workflows/registry.yaml Outdated
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Walkthrough

The 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 origin/main and checks that the relevant package versions are published on npm before deployment.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 2d45b

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)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning For #7938, the repository-dispatch path waits for npm publish and verifies the release SHA, its version-commit message, and the core/react package versions on npm. The manual path checks main ancestry… Apply the release version-commit gate to workflow_dispatch as well, or add an equivalent compatibility check against published packages before deployment.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The whole-PR diff changes only the npm-publish and registry deployment workflows. Dispatching after publishing, validating the deploy SHA, checking npm package availability, and providing recovery gui…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Title check ✅ Passed The title clearly and concisely describes the main change: registry components deploy after npm publishing.
Description check ✅ Passed The description is directly related to the workflow changes and explains the problem, root cause, implementation, and verification.
Full details: Linked Issues check

Explanation

For #7938, the repository-dispatch path waits for npm publish and verifies the release SHA, its version-commit message, and the core/react package versions on npm. The manual path checks main ancestry and npm package versions, but it does not require a version commit or check registry source compatibility. A manual deployment of a regular main commit can therefore publish registry components that use an API absent from the already-published packages, which is the reported failure mode.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

Comment on lines +67 to +78
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

Comment thread .github/workflows/registry.yaml Outdated
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/registry.yaml Outdated
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

@rupic-app rupic-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 inline comment below, on .github/workflows/registry.yaml.

Full review: #8009 (comment)

Comment thread .github/workflows/registry.yaml Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .github/workflows/registry.yaml Outdated
@rupic-app rupic-app Bot added stage/awaiting-author Changes requested or CI failing; ball is with the author R2 Ball is in the author's court; not actionable today stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed R0 Act now: stale, blocked, or high impact and removed stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed R0 Act now: stale, blocked, or high impact stage/awaiting-author Changes requested or CI failing; ball is with the author R2 Ball is in the author's court; not actionable today labels Sep 23, 2026
Comment thread .github/workflows/registry.yaml Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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):

Suggested change
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"

@rupic-app rupic-app Bot added stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed R0 Act now: stale, blocked, or high impact and removed stage/awaiting-author Changes requested or CI failing; ball is with the author R2 Ball is in the author's court; not actionable today labels Sep 23, 2026
Comment thread .github/workflows/registry.yaml Outdated
deploy-production:
name: Deploy Production
runs-on: ubuntu-latest
environment: npm Publish

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

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

Labels

R0 Act now: stale, blocked, or high impact size/M 50–199 lines changed stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

registry: components ship before the npm release they compile against

1 participant