-
Notifications
You must be signed in to change notification settings - Fork 446
ci(repo): add dispatched release path #8308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0e04c89
46e097c
06477c6
2c6c965
1f50cf1
7eb76a2
c42c0ab
d550fec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| name: Release | ||
| run-name: >- | ||
| ${{ | ||
| github.event_name == 'issue_comment' | ||
| github.event_name == 'workflow_dispatch' | ||
| && format('[Legacy] Release from {0}', inputs.source_ref) | ||
| || github.event_name == 'issue_comment' | ||
| && format('[Snapshot] Release by {0}', github.actor) | ||
| || contains(github.event.head_commit.message, 'Version packages') | ||
| && format('[Production] Release from {0}', github.ref_name) | ||
|
|
@@ -14,10 +16,24 @@ | |
| - main | ||
| issue_comment: | ||
| types: [created] | ||
| workflow_dispatch: | ||
| inputs: | ||
| source_ref: | ||
| description: 'Full git SHA to build from' | ||
| type: string | ||
| required: true | ||
| packages: | ||
| description: 'JSON array, e.g. [{"name":"@clerk/nextjs","version":"5.7.6"}]. dist-tag is derived from convention latest-<short>-v<major>.' | ||
| type: string | ||
| required: true | ||
| dry_run: | ||
| description: 'Log what would publish without actually publishing' | ||
| type: boolean | ||
| default: true | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event_name == 'issue_comment' && format('issue-{0}-{1}', github.event.issue.number, github.actor) || github.ref }} | ||
| cancel-in-progress: true | ||
| group: ${{ github.workflow }}-${{ github.event_name == 'issue_comment' && format('issue-{0}-{1}', github.event.issue.number, github.actor) || github.event_name == 'workflow_dispatch' && format('legacy-{0}', inputs.source_ref) || github.ref }} | ||
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} | ||
|
|
||
| jobs: | ||
| release: | ||
|
|
@@ -508,6 +524,192 @@ | |
| Tip: Use the snippet copy button below to quickly install the required packages. | ||
| ${{ steps.package-info.outputs.snippets }} | ||
|
|
||
| legacy-release: | ||
| name: Legacy Release | ||
| if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'clerk/javascript' && github.run_attempt == 1 }} | ||
| runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} | ||
| timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 30 }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
|
|
||
| steps: | ||
| - name: Verify triggering actor is repo admin | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | ||
| with: | ||
| script: | | ||
| const username = process.env.TRIGGERING_ACTOR; | ||
| const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| username, | ||
| }); | ||
| if (data.permission !== 'admin') { | ||
| core.setFailed(`Only repo admins can dispatch this workflow. Actor: ${username} (permission: ${data.permission})`); | ||
| } | ||
|
|
||
| - name: Validate source_ref is a SHA | ||
| env: | ||
| SOURCE_REF: ${{ inputs.source_ref }} | ||
| run: | | ||
| if ! printf '%s' "$SOURCE_REF" | grep -Eq '^[0-9a-f]{40}$'; then | ||
| echo "::error::source_ref must be a full 40-char git SHA" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Validate packages | ||
| env: | ||
| PACKAGES: ${{ inputs.packages }} | ||
| run: | | ||
| echo "$PACKAGES" | jq -e 'type == "array" and length > 0 and all(.[]; type == "object" and (.name | type == "string") and (.version | type == "string"))' > /dev/null || { | ||
| echo "::error::packages must be a non-empty JSON array of {name, version} objects" | ||
| exit 1 | ||
| } | ||
| invalid_names=$(echo "$PACKAGES" | jq -r '.[] | select(.name | test("^@clerk/[a-z0-9][a-z0-9-]*$") | not) | .name') | ||
| if [ -n "$invalid_names" ]; then | ||
| echo "::error::Invalid package name(s). Expected @clerk/<kebab-case>. Got: $invalid_names" | ||
| exit 1 | ||
| fi | ||
| invalid_versions=$(echo "$PACKAGES" | jq -r '.[] | select(.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+") | not) | .version') | ||
| if [ -n "$invalid_versions" ]; then | ||
| echo "::error::Invalid version(s). Expected semver <major>.<minor>.<patch>. Got: $invalid_versions" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - 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 | ||
| env: | ||
| NPM_CONFIG_ENGINE_STRICT: "false" | ||
| run: | | ||
| if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then | ||
| pnpm install --frozen-lockfile | ||
| else | ||
| # Pin npm to the version declared in the source_ref's packageManager | ||
| # so `npm ci` matches the lockfile's origin exactly. | ||
| pm=$(jq -r '.packageManager // ""' package.json) | ||
| if [[ "$pm" =~ ^npm@([0-9]+\.[0-9]+\.[0-9]+) ]]; then | ||
| npm install -g "npm@${BASH_REMATCH[1]}" | ||
| fi | ||
| if ! npm ci --audit=false --fund=false; then | ||
| echo "::warning::npm ci failed - lockfile inconsistent with package.json at this ref." | ||
| echo "::warning::Falling back to npm install. For reproducibility, refresh the lockfile in your fix branch." | ||
| npm install --audit=false --fund=false --no-save | ||
| fi | ||
| fi | ||
|
|
||
| - name: Build | ||
Check failureCode scanning / CodeQL Untrusted Checkout TOCTOU Critical
Insufficient protection against execution of untrusted code on a privileged workflow (
issue_comment Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment on lines
+606
to
+626
|
||
| run: | | ||
| if [ "${{ steps.pm.outputs.manager }}" = "pnpm" ]; then | ||
| pnpm build | ||
| else | ||
| npm run build | ||
| fi | ||
|
|
||
| - name: Upgrade npm for trusted publishing | ||
Check failureCode scanning / CodeQL Untrusted Checkout TOCTOU Critical
Insufficient protection against execution of untrusted code on a privileged workflow (
issue_comment Error loading related location Loading |
||
|
Comment on lines
+626
to
+634
|
||
| 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] | @tsv' | while IFS=$'\t' read -r name version; do | ||
| short="${name#@clerk/}" | ||
| major="${version%%.*}" | ||
| tag="latest-${short}-v${major}" | ||
| dir="packages/$short" | ||
|
|
||
| if [ "$tag" = "latest" ]; then | ||
| echo "::error::refuse to publish under 'latest' dist-tag (derived from $name@$version)" | ||
| exit 1 | ||
| fi | ||
|
|
||
| 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::" | ||
|
Comment on lines
+666
to
+683
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error suppression in pnpm pack masks root cause failures. The Suggested improvement 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
+ if out=$(cd "$dir" && pnpm pack --json 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 "{}")
+ # Fallback: --json may not be supported in older pnpm
+ tarball=$(cd "$dir" && pnpm pack 2>&1 | tail -n1 | xargs -I{} basename "{}") || {
+ echo "::error::pnpm pack failed in $dir"
+ exit 1
+ }
fi🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
nikosdouvlis marked this conversation as resolved.
|
||
|
|
||
| - name: Summary | ||
Check failureCode scanning / CodeQL Untrusted Checkout TOCTOU Critical
Insufficient protection against execution of untrusted code on a privileged workflow (
issue_comment Error loading related location Loading |
||
|
Comment on lines
+637
to
+694
|
||
| if: always() | ||
| env: | ||
| SOURCE_REF: ${{ inputs.source_ref }} | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| PACKAGES: ${{ inputs.packages }} | ||
| run: | | ||
| { | ||
| echo "### Legacy Release" | ||
| echo "" | ||
| echo "- source_ref: \`$SOURCE_REF\`" | ||
| echo "- dry_run: \`$DRY_RUN\`" | ||
| echo "" | ||
| echo "#### Packages" | ||
| echo '```json' | ||
| echo "$PACKAGES" | jq . | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| # We're running the CI workflow (where node v20 modules are cached) in | ||
| # merge_group and not on main, we need to explicitly cache node_modules here so | ||
| # that follow-on branches can use the cached version of node_modules rather | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| #!/usr/bin/env bash | ||
| # Dispatch the Release workflow's legacy-release job for a single package. | ||
| # | ||
| # Usage: | ||
| # scripts/legacy-release.sh <package> <version> [--publish] | ||
| # | ||
| # Defaults to dry-run. Pass --publish to actually publish. | ||
| # Dist-tag is derived from convention: latest-<short>-v<major>. | ||
| # | ||
| # Examples: | ||
| # scripts/legacy-release.sh @clerk/nextjs 5.7.6 | ||
| # scripts/legacy-release.sh @clerk/nextjs 5.7.6 --publish | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| PKG="${1:-}" | ||
| VERSION="${2:-}" | ||
| MODE="${3:-}" | ||
|
|
||
| if [[ -z "$PKG" || -z "$VERSION" ]]; then | ||
| echo "Usage: $0 <package> <version> [--publish]" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ $# -gt 3 ]]; then | ||
| echo "Unexpected arguments: ${*:4}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ ! "$PKG" =~ ^@clerk/[a-z0-9][a-z0-9-]*$ ]]; then | ||
| echo "Package must be in the form @clerk/<name>, got: $PKG" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "Version must be semver <major>.<minor>.<patch>, got: $VERSION" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| DRY_RUN=true | ||
| if [[ "$MODE" == "--publish" ]]; then | ||
| DRY_RUN=false | ||
| elif [[ -n "$MODE" ]]; then | ||
| echo "Unknown mode: $MODE (expected --publish or nothing)" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| SHORT="${PKG#@clerk/}" | ||
| MAJOR="${VERSION%%.*}" | ||
| DIST_TAG="latest-${SHORT}-v${MAJOR}" | ||
|
|
||
| if ! command -v gh >/dev/null 2>&1; then | ||
| echo "gh CLI not found. Install: https://cli.github.com/" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! command -v jq >/dev/null 2>&1; then | ||
| echo "jq not found. Install: brew install jq" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| BRANCH=$(git rev-parse --abbrev-ref HEAD) | ||
| if [[ "$BRANCH" == "HEAD" ]]; then | ||
| echo "Detached HEAD. Checkout a branch first." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then | ||
| echo "Branch '$BRANCH' not pushed to origin. Push first." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| LOCAL_SHA=$(git rev-parse HEAD) | ||
| REMOTE_SHA=$(git rev-parse "origin/$BRANCH") | ||
| if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then | ||
| echo "Local '$BRANCH' is out of sync with origin. Push first." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| PKG_JSON="./packages/$SHORT/package.json" | ||
| if [[ ! -f "$PKG_JSON" ]]; then | ||
| echo "No package.json at $PKG_JSON." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| PKG_VERSION=$(jq -r .version "$PKG_JSON") | ||
| if [[ "$PKG_VERSION" != "$VERSION" ]]; then | ||
| echo "$PKG_JSON has version $PKG_VERSION, expected $VERSION." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| PACKAGES=$(jq -c -n --arg n "$PKG" --arg v "$VERSION" \ | ||
| '[{name:$n, version:$v}]') | ||
|
|
||
| cat <<EOF | ||
| Dispatching Release workflow: | ||
| package: $PKG | ||
| version: $VERSION | ||
| dist-tag (derived): $DIST_TAG | ||
| branch: $BRANCH | ||
| source_ref: $LOCAL_SHA | ||
| dry_run: $DRY_RUN | ||
|
|
||
| EOF | ||
|
|
||
| read -r -p "Continue? [y/N] " yn | ||
| case "$yn" in | ||
| y|Y) ;; | ||
| *) echo "Aborted."; exit 0 ;; | ||
| esac | ||
|
|
||
| gh workflow run release.yml \ | ||
| --ref main \ | ||
| -f source_ref="$LOCAL_SHA" \ | ||
| -f packages="$PACKAGES" \ | ||
| -f dry_run="$DRY_RUN" | ||
|
|
||
| echo "" | ||
| echo "Dispatched. Approve at:" | ||
| echo " https://github.com/clerk/javascript/actions/workflows/release.yml" |
Uh oh!
There was an error while loading. Please reload this page.