Skip to content

ci(repo): add dispatched release path#8308

Open
nikosdouvlis wants to merge 4 commits intomainfrom
nikos/release-workflow
Open

ci(repo): add dispatched release path#8308
nikosdouvlis wants to merge 4 commits intomainfrom
nikos/release-workflow

Conversation

@nikosdouvlis
Copy link
Copy Markdown
Member

Adds a workflow_dispatch trigger to the release workflow for releasing from arbitrary refs.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

⚠️ No Changeset found

Latest commit: 2c6c965

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

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Apr 14, 2026 11:13pm

Request Review

Comment on lines +603 to +611
- name: Build
run: |
if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then
pnpm build
else
npm run build
fi

- name: Upgrade npm for trusted publishing
Comment on lines +614 to +664
- name: Publish or dry-run
env:
NPM_CONFIG_PROVENANCE: true
PACKAGES: ${{ inputs.packages }}
DRY_RUN: ${{ inputs.dry_run }}
PACK: ${{ steps.pm.outputs.manager }}
run: |
echo "$PACKAGES" | jq -r '.[] | [.name, .version, .dist_tag] | @tsv' | while IFS=$'\t' read -r name version tag; do
short="${name#@clerk/}"
dir="packages/$short"

if [ ! -d "$dir" ]; then
echo "::error::Package directory not found: $dir"
exit 1
fi

pkg_version=$(jq -r .version "$dir/package.json")
if [ "$pkg_version" != "$version" ]; then
echo "::error::$dir/package.json has version $pkg_version, expected $version"
exit 1
fi

echo "::group::Pack $name@$version"
if [ "$PACK" = "pnpm" ]; then
out=$(cd "$dir" && pnpm pack --json 2>/dev/null || true)
if [ -n "$out" ] && echo "$out" | jq -e . >/dev/null 2>&1; then
tarball=$(echo "$out" | jq -r '.filename')
else
# pnpm pack without --json prints the tarball path on stdout
tarball=$(cd "$dir" && pnpm pack 2>&1 | tail -n1 | xargs -I{} basename "{}")
fi
else
tarball=$(cd "$dir" && npm pack --json | jq -r '.[0].filename')
fi
if [ -z "$tarball" ] || [ ! -f "$dir/$tarball" ]; then
echo "::error::Failed to resolve tarball filename in $dir"
exit 1
fi
echo "packed: $dir/$tarball"
echo "::endgroup::"

if [ "$DRY_RUN" = "true" ]; then
echo "::notice::DRY RUN: would publish $name@$version --tag $tag"
else
echo "::group::Publish $name@$version --tag $tag"
(cd "$dir" && npm publish "$tarball" --tag "$tag" --provenance)
echo "::endgroup::"
fi
done

- name: Summary
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 14, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8308

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8308

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8308

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8308

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8308

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8308

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8308

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8308

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8308

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8308

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8308

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8308

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8308

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8308

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8308

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8308

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8308

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8308

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8308

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8308

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8308

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8308

commit: 2c6c965

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Adds a workflow_dispatch trigger to .github/workflows/release.yml with required source_ref and packages inputs and optional dry_run (default true). Updates run-name, changes concurrency to use legacy-{inputs.source_ref} for dispatch runs and sets cancel-in-progress: false for workflow_dispatch. Introduces a legacy-release job (runs only for workflow_dispatch on clerk/javascript and github.run_attempt == 1) that enforces admin actor permission, validates source_ref as a 40-hex SHA, validates packages JSON (each dist_tag != "latest" and name matches @clerk/<kebab-case>), checks out the SHA, sets up Node from .nvmrc, enables corepack, detects pnpm vs npm, installs, builds, packs, and either dry-runs or publishes each package with npm publish --tag <dist_tag> --provenance; writes a step summary. Adds scripts/legacy-release.sh to validate CLI args/git state and package version, construct the packages payload, prompt confirmation, and dispatch the workflow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a workflow_dispatch trigger to enable releases from arbitrary refs via GitHub Actions.
Description check ✅ Passed The description is directly related to the changeset, explaining that a workflow_dispatch trigger was added to the release workflow for releasing from arbitrary refs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 571-662: The workflow allows checking out arbitrary commits
(inputs.source_ref) and iterating unvalidated package names (PACKAGES) which
enables TOCTOU/execution of untrusted code and path traversal; fix by (1) after
the Checkout step verify that inputs.source_ref is an ancestor of a protected
branch (e.g., origin/main) using git fetch + git merge-base --is-ancestor and
fail the job if not, referencing the checkout/ref logic around
inputs.source_ref, and (2) validate each package name before the existing
PACKAGES parsing loop (the echo "$PACKAGES" | jq ... | while ... read -r name
...) by enforcing the regex ^@clerk/[a-z0-9][a-z0-9-]*$ and rejecting any names
that do not match or contain path traversal (../) before computing
short="${name#@clerk/}" and dir="packages/$short".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: cef47547-72a6-4312-b155-3192e592612e

📥 Commits

Reviewing files that changed from the base of the PR and between b1f9bbe and 0e04c89.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • scripts/legacy-release.sh

Comment on lines +571 to +662
- name: Checkout source_ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.source_ref }}
fetch-depth: 1
show-progress: false

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Enable corepack
run: corepack enable

- name: Detect package manager
id: pm
run: |
if [ -f pnpm-lock.yaml ]; then
echo "manager=pnpm" >> "$GITHUB_OUTPUT"
else
echo "manager=npm" >> "$GITHUB_OUTPUT"
fi

- name: Install dependencies
run: |
if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
else
npm ci
fi

- name: Build
run: |
if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then
pnpm build
else
npm run build
fi

- name: Upgrade npm for trusted publishing
run: npx npm@11 install -g npm@11

- name: Publish or dry-run
env:
NPM_CONFIG_PROVENANCE: true
PACKAGES: ${{ inputs.packages }}
DRY_RUN: ${{ inputs.dry_run }}
PACK: ${{ steps.pm.outputs.manager }}
run: |
echo "$PACKAGES" | jq -r '.[] | [.name, .version, .dist_tag] | @tsv' | while IFS=$'\t' read -r name version tag; do
short="${name#@clerk/}"
dir="packages/$short"

if [ ! -d "$dir" ]; then
echo "::error::Package directory not found: $dir"
exit 1
fi

pkg_version=$(jq -r .version "$dir/package.json")
if [ "$pkg_version" != "$version" ]; then
echo "::error::$dir/package.json has version $pkg_version, expected $version"
exit 1
fi

echo "::group::Pack $name@$version"
if [ "$PACK" = "pnpm" ]; then
out=$(cd "$dir" && pnpm pack --json 2>/dev/null || true)
if [ -n "$out" ] && echo "$out" | jq -e . >/dev/null 2>&1; then
tarball=$(echo "$out" | jq -r '.filename')
else
# pnpm pack without --json prints the tarball path on stdout
tarball=$(cd "$dir" && pnpm pack 2>&1 | tail -n1 | xargs -I{} basename "{}")
fi
else
tarball=$(cd "$dir" && npm pack --json | jq -r '.[0].filename')
fi
if [ -z "$tarball" ] || [ ! -f "$dir/$tarball" ]; then
echo "::error::Failed to resolve tarball filename in $dir"
exit 1
fi
echo "packed: $dir/$tarball"
echo "::endgroup::"

if [ "$DRY_RUN" = "true" ]; then
echo "::notice::DRY RUN: would publish $name@$version --tag $tag"
else
echo "::group::Publish $name@$version --tag $tag"
(cd "$dir" && npm publish "$tarball" --tag "$tag" --provenance)
echo "::endgroup::"
fi
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.

⚠️ Potential issue | 🔴 Critical

Security: Arbitrary commit checkout allows execution of untrusted code (CodeQL TOCTOU)

The workflow checks out any commit SHA in the repository, including commits from unmerged PRs. Since PRs from external contributors create commits that exist in the repo, an attacker could:

  1. Open a PR with malicious postinstall scripts in package.json
  2. Social-engineer an admin to dispatch this workflow with that commit's SHA
  3. Execute arbitrary code with id-token: write permission, enabling unauthorized npm publishes

Additionally, the packages input lacks validation for the package name format. A malicious actor could supply @clerk/../../../<path> to traverse directories.

Consider adding:

  1. Verification that source_ref is an ancestor of a protected branch (e.g., main)
  2. Package name validation in the workflow matching the script's regex: ^@clerk/[a-z0-9][a-z0-9-]*$
Suggested fix for package name validation
      - name: Reject "latest" dist_tag
        env:
          PACKAGES: ${{ inputs.packages }}
        run: |
          echo "$PACKAGES" | jq -e 'all(.[]; .dist_tag != "latest")' > /dev/null || {
            echo "::error::'latest' dist_tag is not allowed on this path"; exit 1;
          }

+     - name: Validate package names
+       env:
+         PACKAGES: ${{ inputs.packages }}
+       run: |
+         echo "$PACKAGES" | jq -r '.[].name' | while read -r name; do
+           if ! printf '%s' "$name" | grep -Eq '^@clerk/[a-z0-9][a-z0-9-]*$'; then
+             echo "::error::Invalid package name: $name"
+             exit 1
+           fi
+         done
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 595-603: Untrusted Checkout TOCTOU
Insufficient protection against execution of untrusted code on a privileged workflow (issue_comment).


[failure] 603-611: Untrusted Checkout TOCTOU
Insufficient protection against execution of untrusted code on a privileged workflow (issue_comment).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 571 - 662, The workflow allows
checking out arbitrary commits (inputs.source_ref) and iterating unvalidated
package names (PACKAGES) which enables TOCTOU/execution of untrusted code and
path traversal; fix by (1) after the Checkout step verify that inputs.source_ref
is an ancestor of a protected branch (e.g., origin/main) using git fetch + git
merge-base --is-ancestor and fail the job if not, referencing the checkout/ref
logic around inputs.source_ref, and (2) validate each package name before the
existing PACKAGES parsing loop (the echo "$PACKAGES" | jq ... | while ... read
-r name ...) by enforcing the regex ^@clerk/[a-z0-9][a-z0-9-]*$ and rejecting
any names that do not match or contain path traversal (../) before computing
short="${name#@clerk/}" and dir="packages/$short".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
.github/workflows/release.yml (2)

626-634: Assumption: package directory name matches npm package short name.

Line 628 constructs dir="packages/$short" where short is derived from stripping @clerk/ from the package name. This assumes the directory name under packages/ matches the npm short name exactly.

While this convention holds for most Clerk packages and aligns with scripts/legacy-release.sh, it could fail for packages where the directory name differs (e.g., a package @clerk/foo-bar in packages/foobar/).

Consider adding a fallback that searches for the package by its name field in package.json if the directory doesn't exist, or document this naming requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 626 - 634, The workflow assumes
dir="packages/$short" where short="${name#@clerk/}" matches the filesystem,
which can break if package dirs are named differently; update the loop that
reads PACKAGES (the echo ... | while IFS=... read -r name version tag block) to,
when [ ! -d "$dir" ], attempt a fallback: search under packages/ for a
package.json whose "name" equals "$name" and set dir to that matching directory
(or fail with a clear error if none/ambiguous), ensuring you reference the
variables name, short, dir and the PACKAGES parsing logic when implementing the
lookup or alternatively add a clear comment/documentation enforcing the naming
requirement.

642-656: pnpm pack fallback logic could be more robust.

The fallback at line 648 merges stderr with stdout and takes the last line. If pnpm pack fails with an error message that happens to look like a filename, the subsequent existence check at line 653 would catch it, but the error flow is unclear.

Suggested improvement
             if [ -n "$out" ] && echo "$out" | jq -e . >/dev/null 2>&1; then
               tarball=$(echo "$out" | jq -r '.filename')
             else
-              # pnpm pack without --json prints the tarball path on stdout
-              tarball=$(cd "$dir" && pnpm pack 2>&1 | tail -n1 | xargs -I{} basename "{}")
+              # pnpm pack without --json prints the tarball path on stdout (stderr separate)
+              tarball=$(cd "$dir" && pnpm pack 2>/dev/null | tail -n1 | xargs -I{} basename "{}")
             fi

Redirecting stderr to /dev/null (or a separate log) keeps error messages from polluting the tarball variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 642 - 656, The pnpm fallback
merges stderr into stdout which can pollute the tarball variable; change the
fallback invocation that sets tarball (currently using cd "$dir" && pnpm pack
2>&1 | tail -n1 | xargs -I{} basename "{}") to capture only stdout and silence
stderr (e.g., cd "$dir" && pnpm pack 2>/dev/null | tail -n1 | xargs -I{}
basename "{}") and also check the pnpm exit status before using the result so
tarball is only assigned on successful pack; refer to the PACK variable handling
and the tarball assignment code when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 626-634: The workflow assumes dir="packages/$short" where
short="${name#@clerk/}" matches the filesystem, which can break if package dirs
are named differently; update the loop that reads PACKAGES (the echo ... | while
IFS=... read -r name version tag block) to, when [ ! -d "$dir" ], attempt a
fallback: search under packages/ for a package.json whose "name" equals "$name"
and set dir to that matching directory (or fail with a clear error if
none/ambiguous), ensuring you reference the variables name, short, dir and the
PACKAGES parsing logic when implementing the lookup or alternatively add a clear
comment/documentation enforcing the naming requirement.
- Around line 642-656: The pnpm fallback merges stderr into stdout which can
pollute the tarball variable; change the fallback invocation that sets tarball
(currently using cd "$dir" && pnpm pack 2>&1 | tail -n1 | xargs -I{} basename
"{}") to capture only stdout and silence stderr (e.g., cd "$dir" && pnpm pack
2>/dev/null | tail -n1 | xargs -I{} basename "{}") and also check the pnpm exit
status before using the result so tarball is only assigned on successful pack;
refer to the PACK variable handling and the tarball assignment code when making
this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 023b158a-e078-46e0-9b16-0ecb55a242de

📥 Commits

Reviewing files that changed from the base of the PR and between 0e04c89 and 46e097c.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment on lines +600 to +610
- name: Install dependencies
env:
NPM_CONFIG_ENGINE_STRICT: "false"
run: |
if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
else
npm ci --audit=false --fund=false
fi

- name: Build
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
.github/workflows/release.yml (1)

576-619: ⚠️ Potential issue | 🔴 Critical

Reject SHAs that are not reachable from a protected branch before install/build.

This still lets an arbitrary 40-char repo SHA reach checkout, dependency installation, and build in a job that can mint provenance and publish. The CodeQL TOCTOU issue is still open for unmerged PR commits.

Suggested guard
       - name: Checkout source_ref
         uses: actions/checkout@v4
         with:
           ref: ${{ inputs.source_ref }}
-          fetch-depth: 1
+          fetch-depth: 0
           show-progress: false
+
+      - name: Verify source_ref is reachable from origin/main
+        env:
+          SOURCE_REF: ${{ inputs.source_ref }}
+        run: |
+          git fetch --no-tags origin main
+          git merge-base --is-ancestor "$SOURCE_REF" origin/main || {
+            echo "::error::source_ref must be reachable from origin/main"
+            exit 1
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 576 - 619, The workflow currently
checks out an arbitrary commit via the "Checkout source_ref" step using
inputs.source_ref and proceeds to install/build; add a pre-build validation step
(immediately after "Checkout source_ref" and before "Install
dependencies"/"Build") that verifies the checked-out commit is reachable from
one or more protected branches (e.g., main, release) and fails the job if it is
not; implement the check using git reachability commands (for example, fetch the
protected branch refs and use git merge-base --is-ancestor or git branch
--contains) referencing inputs.source_ref and the checkout produced by
actions/checkout@v4 so unmerged or unreachable SHAs cannot continue to the
pnpm/npm install and build steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 563-575: The Validate packages step currently only rejects
dist_tag == "latest" but doesn't verify the full package object shape; update
the jq validations in the "Validate packages" step (using the PACKAGES env
variable) to assert for each object that it has non-empty "name", "version", and
"dist_tag" fields, that .dist_tag is not "latest", and that .name matches the
`@clerk/`<kebab-case> pattern (and optionally .version matches a semver regex).
Replace the existing jq checks with a single all(.[]; <predicate>) expression
that checks has("name") and has("version") and has("dist_tag") and .name |
test("^@clerk/[a-z0-9][a-z0-9-]*$") and (.version != "" and (.version |
test("^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$"))) and (.dist_tag != "" and .dist_tag !=
"latest"), and fail the step with an explicit error if any package fails.

---

Duplicate comments:
In @.github/workflows/release.yml:
- Around line 576-619: The workflow currently checks out an arbitrary commit via
the "Checkout source_ref" step using inputs.source_ref and proceeds to
install/build; add a pre-build validation step (immediately after "Checkout
source_ref" and before "Install dependencies"/"Build") that verifies the
checked-out commit is reachable from one or more protected branches (e.g., main,
release) and fails the job if it is not; implement the check using git
reachability commands (for example, fetch the protected branch refs and use git
merge-base --is-ancestor or git branch --contains) referencing inputs.source_ref
and the checkout produced by actions/checkout@v4 so unmerged or unreachable SHAs
cannot continue to the pnpm/npm install and build steps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: aa5acf05-fa51-4040-8541-68020f40c122

📥 Commits

Reviewing files that changed from the base of the PR and between 46e097c and 06477c6.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment on lines +563 to +575
- name: Validate packages
env:
PACKAGES: ${{ inputs.packages }}
run: |
echo "$PACKAGES" | jq -e 'all(.[]; .dist_tag != "latest")' > /dev/null || {
echo "::error::'latest' dist_tag is not allowed on this path"; exit 1;
}
invalid=$(echo "$PACKAGES" | jq -r '.[] | select(.name | test("^@clerk/[a-z0-9][a-z0-9-]*$") | not) | .name')
if [ -n "$invalid" ]; then
echo "::error::Invalid package name(s). Expected @clerk/<kebab-case>. Got: $invalid"
exit 1
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.

⚠️ Potential issue | 🟠 Major

Validate the full packages payload shape, not just "latest".

Line 567 only rejects one forbidden tag. Missing or empty version / dist_tag values still pass this gate and reach the publish loop, which makes malformed dispatch input fail late or publish under the wrong tag.

Suggested hardening
       - name: Validate packages
         env:
           PACKAGES: ${{ inputs.packages }}
         run: |
-          echo "$PACKAGES" | jq -e 'all(.[]; .dist_tag != "latest")' > /dev/null || {
-            echo "::error::'latest' dist_tag is not allowed on this path"; exit 1;
-          }
-          invalid=$(echo "$PACKAGES" | jq -r '.[] | select(.name | test("^@clerk/[a-z0-9][a-z0-9-]*$") | not) | .name')
-          if [ -n "$invalid" ]; then
-            echo "::error::Invalid package name(s). Expected `@clerk/`<kebab-case>. Got: $invalid"
+          echo "$PACKAGES" | jq -e '
+            type == "array" and length > 0 and
+            all(.[]; 
+              type == "object" and
+              (.name | type == "string" and test("^@clerk/[a-z0-9][a-z0-9-]*$")) and
+              (.version | type == "string" and length > 0) and
+              (.dist_tag | type == "string" and length > 0 and . != "latest")
+            )
+          ' > /dev/null || {
+            echo "::error::packages must be a non-empty JSON array of {name, version, dist_tag} objects"
             exit 1
-          fi
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 563 - 575, The Validate packages
step currently only rejects dist_tag == "latest" but doesn't verify the full
package object shape; update the jq validations in the "Validate packages" step
(using the PACKAGES env variable) to assert for each object that it has
non-empty "name", "version", and "dist_tag" fields, that .dist_tag is not
"latest", and that .name matches the `@clerk/`<kebab-case> pattern (and optionally
.version matches a semver regex). Replace the existing jq checks with a single
all(.[]; <predicate>) expression that checks has("name") and has("version") and
has("dist_tag") and .name | test("^@clerk/[a-z0-9][a-z0-9-]*$") and (.version !=
"" and (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$"))) and (.dist_tag !=
"" and .dist_tag != "latest"), and fail the step with an explicit error if any
package fails.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
.github/workflows/release.yml (2)

554-561: ⚠️ Potential issue | 🔴 Critical

Constrain source_ref to trusted history before running this path.

Line 558 only proves the input looks like a SHA. This job still checks out that ref and runs setup/install/build with id-token: write, so the dispatch gate controls who can start the run but not what code executes. That is the same TOCTOU path the static analysis is still flagging.

🔒 Suggested hardening
       - name: Checkout source_ref
         uses: actions/checkout@v4
         with:
           ref: ${{ inputs.source_ref }}
           fetch-depth: 1
+          persist-credentials: false
           show-progress: false

+      - name: Ensure source_ref is reachable from a protected release line
+        env:
+          SOURCE_REF: ${{ inputs.source_ref }}
+        run: |
+          git fetch --no-tags origin main
+          git merge-base --is-ancestor "$SOURCE_REF" FETCH_HEAD || {
+            echo "::error::source_ref must be reachable from origin/main"
+            exit 1
+          }

Also applies to: 576-619

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 554 - 561, The current "Validate
source_ref is a SHA" step only checks format (SOURCE_REF) but doesn't ensure the
SHA exists in trusted repository history before the actions/checkout and
subsequent steps run; update the workflow to, after validating format, fetch
trusted refs (e.g., origin/main or origin/refs/heads/*) and verify SOURCE_REF is
contained in those trusted refs (for example by fetching and using git branch
--contains or git merge-base --is-ancestor) before running the actions/checkout
and any steps that use id-token: write; reference the "Validate source_ref is a
SHA" step and the actions/checkout step when adding the fetch+verify logic so
the job aborts if SOURCE_REF is not provably in trusted history.

563-575: ⚠️ Potential issue | 🟠 Major

Validate the full packages payload here, not just name and "latest".

Line 567 still treats [] as valid, and missing/empty version or dist_tag values fall through into the publish loop as late failures or silent no-ops. The helper script builds a well-formed payload, but manual workflow_dispatch calls bypass that safety net.

🧪 Suggested validation
       - name: Validate packages
         env:
           PACKAGES: ${{ inputs.packages }}
         run: |
-          echo "$PACKAGES" | jq -e 'all(.[]; .dist_tag != "latest")' > /dev/null || {
-            echo "::error::'latest' dist_tag is not allowed on this path"; exit 1;
-          }
-          invalid=$(echo "$PACKAGES" | jq -r '.[] | select(.name | test("^@clerk/[a-z0-9][a-z0-9-]*$") | not) | .name')
-          if [ -n "$invalid" ]; then
-            echo "::error::Invalid package name(s). Expected `@clerk/`<kebab-case>. Got: $invalid"
-            exit 1
-          fi
+          echo "$PACKAGES" | jq -e '
+            type == "array" and length > 0 and
+            all(.[];
+              type == "object" and
+              has("name") and has("version") and has("dist_tag") and
+              (.name | type == "string" and test("^@clerk/[a-z0-9][a-z0-9-]*$")) and
+              (.version | type == "string" and . != "") and
+              (.dist_tag | type == "string" and . != "" and . != "latest")
+            )
+          ' > /dev/null || {
+            echo "::error::packages must be a non-empty JSON array of {name, version, dist_tag} objects"
+            exit 1
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 563 - 575, The "Validate
packages" step currently only checks package names and forbids dist_tag "latest"
but allows an empty array and missing/empty fields; update the PACKAGES
validation to fully validate the payload: ensure PACKAGES is a non-empty JSON
array and for each item verify .name matches the `@clerk/`<kebab-case> regex,
.version exists and is a non-empty string, and .dist_tag exists and is a
non-empty string (and not "latest"); implement this by replacing or augmenting
the existing jq checks in the Validate packages step (the block using PACKAGES
and jq, and the invalid variable computation) with a single jq expression that
fails if any of these conditions are not met and surface clear error messages to
stop the workflow early.
🧹 Nitpick comments (1)
.github/workflows/release.yml (1)

583-587: Verify that npm@11 matches the oldest legacy refs this path needs to support.

This job takes Node from the checked-out .nvmrc and then unconditionally upgrades to npm@11. npm CLI 11 expects a currently supported Node.js line, and as of March 24, 2026 the active Node release lines were 20.x, 22.x, 24.x, and 25.x, so any ref pinned to 18 or older sits outside that support matrix. If older refs are in scope, consider switching back to a modern Node just for npm publish, or fail early with a clearer message. (github.com)

Also applies to: 618-619

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 583 - 587, The CI unconditionally
upgrades to npm@11 after using actions/setup-node with node-version-file:
.nvmrc, which may break older refs pinned to Node <20; change the workflow so
the npm upgrade is gated: detect the resolved Node.js major version (from the
setup-node step using node-version-file/.nvmrc) and only run the npm install -g
npm@11 step when Node >=20 (or alternatively select an npm version compatible
with the resolved Node), and if an older Node is detected either skip the npm@11
step or fail early with a clear message; update the steps referencing
actions/setup-node@v4 and the npm@11 install step (lines that install npm@11)
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/release.yml:
- Around line 554-561: The current "Validate source_ref is a SHA" step only
checks format (SOURCE_REF) but doesn't ensure the SHA exists in trusted
repository history before the actions/checkout and subsequent steps run; update
the workflow to, after validating format, fetch trusted refs (e.g., origin/main
or origin/refs/heads/*) and verify SOURCE_REF is contained in those trusted refs
(for example by fetching and using git branch --contains or git merge-base
--is-ancestor) before running the actions/checkout and any steps that use
id-token: write; reference the "Validate source_ref is a SHA" step and the
actions/checkout step when adding the fetch+verify logic so the job aborts if
SOURCE_REF is not provably in trusted history.
- Around line 563-575: The "Validate packages" step currently only checks
package names and forbids dist_tag "latest" but allows an empty array and
missing/empty fields; update the PACKAGES validation to fully validate the
payload: ensure PACKAGES is a non-empty JSON array and for each item verify
.name matches the `@clerk/`<kebab-case> regex, .version exists and is a non-empty
string, and .dist_tag exists and is a non-empty string (and not "latest");
implement this by replacing or augmenting the existing jq checks in the Validate
packages step (the block using PACKAGES and jq, and the invalid variable
computation) with a single jq expression that fails if any of these conditions
are not met and surface clear error messages to stop the workflow early.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 583-587: The CI unconditionally upgrades to npm@11 after using
actions/setup-node with node-version-file: .nvmrc, which may break older refs
pinned to Node <20; change the workflow so the npm upgrade is gated: detect the
resolved Node.js major version (from the setup-node step using
node-version-file/.nvmrc) and only run the npm install -g npm@11 step when Node
>=20 (or alternatively select an npm version compatible with the resolved Node),
and if an older Node is detected either skip the npm@11 step or fail early with
a clear message; update the steps referencing actions/setup-node@v4 and the
npm@11 install step (lines that install npm@11) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7616e008-676e-43ea-b35a-a5f81f2f6d24

📥 Commits

Reviewing files that changed from the base of the PR and between 06477c6 and 2c6c965.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants