[Snapshot] Release by changeset-bot[bot] #5983
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| run-name: >- | |
| ${{ | |
| 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) | |
| || format('[Canary] Release from {0}', github.ref_name) | |
| }} | |
| on: | |
| push: | |
| branches: | |
| - main | |
| issue_comment: | |
| types: [created] | |
| 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 | |
| jobs: | |
| release: | |
| name: Release | |
| if: ${{ github.event_name == 'push' && github.repository == 'clerk/javascript' }} | |
| runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} | |
| timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 10 }} | |
| permissions: | |
| contents: write | |
| id-token: write | |
| packages: write | |
| pull-requests: write | |
| issues: read | |
| statuses: write | |
| checks: write | |
| steps: | |
| - name: Echo github context | |
| run: echo "$GITHUB_CONTEXT" | |
| env: | |
| GITHUB_CONTEXT: ${{ toJson(github) }} | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| show-progress: false | |
| - name: Setup | |
| id: config | |
| uses: ./.github/actions/init | |
| with: | |
| playwright-enabled: true # Must be present to enable caching on branched workflows | |
| turbo-enabled: false # Release uses --force, so turbo cache is not needed | |
| # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} | |
| # turbo-team: ${{ vars.TURBO_TEAM }} | |
| # turbo-token: ${{ secrets.TURBO_TOKEN }} | |
| turbo-team: "" | |
| turbo-token: "" | |
| - name: Upgrade npm for trusted publishing | |
| run: npx npm@11 install -g npm@11 | |
| - name: Build release | |
| run: pnpm turbo build $TURBO_ARGS --force | |
| - name: Create Release PR | |
| id: changesets | |
| uses: changesets/action@v1 | |
| with: | |
| commit: "ci(repo): Version packages" | |
| title: "ci(repo): Version packages" | |
| publish: pnpm release | |
| # Workaround for https://github.com/changesets/changesets/issues/421 | |
| version: pnpm version-packages | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.CLERK_COOKIE_PAT }} | |
| HUSKY: "0" | |
| NPM_CONFIG_PROVENANCE: true | |
| - name: Trigger workflows on related repos | |
| if: steps.changesets.outputs.published == 'true' | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| retries: 3 | |
| retry-exempt-status-codes: 400,401 | |
| github-token: ${{ secrets.CLERK_COOKIE_PAT }} | |
| script: | | |
| const preMode = require("fs").existsSync("./.changeset/pre.json"); | |
| if (!preMode) { | |
| const clerkjsVersion = require('./packages/clerk-js/package.json').version; | |
| const clerkUiVersion = require('./packages/ui/package.json').version; | |
| const nextjsVersion = require('./packages/nextjs/package.json').version; | |
| // NOTE: Keep in sync with the `targets` array in the "Recover downstream notifications" step below. | |
| const targets = [ | |
| { repo: 'sdk-infra-workers', workflow_id: 'update-pkg-versions.yml', inputs: { clerkjsVersion, clerkUiVersion } }, | |
| { repo: 'dashboard', workflow_id: 'prepare-nextjs-sdk-update.yml', inputs: { version: nextjsVersion } }, | |
| { repo: 'clerk-docs', workflow_id: 'typedoc.yml' }, | |
| ]; | |
| const results = await Promise.allSettled( | |
| targets.map(t => github.rest.actions.createWorkflowDispatch({ owner: 'clerk', ref: 'main', ...t })) | |
| ); | |
| const failures = results | |
| .map((r, i) => r.status === 'rejected' ? { target: targets[i], reason: r.reason } : null) | |
| .filter(Boolean); | |
| if (failures.length) { | |
| failures.forEach(f => core.error(`Dispatch to ${f.target.repo}/${f.target.workflow_id} failed: ${f.reason?.message ?? f.reason}`)); | |
| core.setFailed(`${failures.length} downstream dispatch(es) failed`); | |
| } | |
| } else{ | |
| core.warning("Changeset in pre-mode should not prepare a ClerkJS production release") | |
| } | |
| # Recovery: if the changesets action published to npm but then failed | |
| # (e.g. git push --follow-tags error), the `published` output is never | |
| # set and downstream repos are not notified. This step detects that | |
| # scenario by checking npm for the local package version and dispatches | |
| # if the packages are already live. | |
| - name: Recover downstream notifications | |
| if: always() && steps.changesets.conclusion == 'failure' | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| retries: 3 | |
| retry-exempt-status-codes: 400,401 | |
| github-token: ${{ secrets.CLERK_COOKIE_PAT }} | |
| script: | | |
| const { execSync } = require('child_process'); | |
| const clerkjsVersion = require('./packages/clerk-js/package.json').version; | |
| const clerkUiVersion = require('./packages/ui/package.json').version; | |
| // Only recover stable releases | |
| const preReleases = [ | |
| clerkjsVersion.includes('-') && `@clerk/clerk-js@${clerkjsVersion}`, | |
| clerkUiVersion.includes('-') && `@clerk/ui@${clerkUiVersion}`, | |
| ].filter(Boolean); | |
| if (preReleases.length > 0) { | |
| console.log(`Skipping recovery: ${preReleases.join(', ')} is a pre-release`); | |
| return; | |
| } | |
| const preMode = require("fs").existsSync("./.changeset/pre.json"); | |
| if (preMode) { | |
| core.warning("Changeset in pre-mode, skipping recovery dispatch"); | |
| return; | |
| } | |
| // Check if either version was actually published to npm | |
| function isPublished(name, version) { | |
| try { | |
| return execSync(`npm view ${name}@${version} version`, { encoding: 'utf8' }).trim() === version; | |
| } catch (e) { | |
| console.log(`npm view ${name}@${version} failed: ${e.message}`); | |
| return false; | |
| } | |
| } | |
| const clerkjsPublished = isPublished('@clerk/clerk-js', clerkjsVersion); | |
| const clerkUiPublished = isPublished('@clerk/ui', clerkUiVersion); | |
| if (!clerkjsPublished && !clerkUiPublished) { | |
| console.log('Neither @clerk/clerk-js nor @clerk/ui were published to npm, no recovery needed'); | |
| return; | |
| } | |
| const published = [ | |
| clerkjsPublished && `@clerk/clerk-js@${clerkjsVersion}`, | |
| clerkUiPublished && `@clerk/ui@${clerkUiVersion}`, | |
| ].filter(Boolean).join(', '); | |
| core.warning(`Recovery: ${published} published to npm but downstream repos were not notified. Dispatching now.`); | |
| const nextjsVersion = require('./packages/nextjs/package.json').version; | |
| // NOTE: Keep in sync with the `targets` array in the "Trigger workflows on related repos" step above. | |
| const targets = [ | |
| { repo: 'sdk-infra-workers', workflow_id: 'update-pkg-versions.yml', inputs: { clerkjsVersion, clerkUiVersion } }, | |
| { repo: 'dashboard', workflow_id: 'prepare-nextjs-sdk-update.yml', inputs: { version: nextjsVersion } }, | |
| { repo: 'clerk-docs', workflow_id: 'typedoc.yml' }, | |
| ]; | |
| const results = await Promise.allSettled( | |
| targets.map(t => github.rest.actions.createWorkflowDispatch({ owner: 'clerk', ref: 'main', ...t })) | |
| ); | |
| const failures = results | |
| .map((r, i) => r.status === 'rejected' ? { target: targets[i], reason: r.reason } : null) | |
| .filter(Boolean); | |
| if (failures.length) { | |
| failures.forEach(f => core.error(`Recovery dispatch to ${f.target.repo}/${f.target.workflow_id} failed: ${f.reason?.message ?? f.reason}`)); | |
| core.setFailed(`${failures.length} recovery dispatch(es) failed`); | |
| } else { | |
| core.notice('Recovery dispatch completed successfully'); | |
| } | |
| - name: Generate notification payload | |
| id: notification | |
| if: steps.changesets.outputs.published == 'true' | |
| run: payload=$(node scripts/notify.mjs '${{ steps.changesets.outputs.publishedPackages }}' '${{ github.actor }}') && echo ::set-output name=payload::${payload//$'\n'/'%0A'} | |
| - name: Send commit log to Slack | |
| id: slack | |
| if: steps.changesets.outputs.published == 'true' | |
| uses: slackapi/slack-github-action@v1.24.0 | |
| with: | |
| payload: ${{ steps.notification.outputs.payload }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CHANGELOG_WEBHOOK_URL }} | |
| SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK | |
| - name: Notify Slack on failure | |
| if: ${{ always() && steps.changesets.outcome == 'failure' }} | |
| uses: slackapi/slack-github-action@v1.24.0 | |
| with: | |
| payload: | | |
| { | |
| "blocks": [ | |
| { | |
| "type": "section", | |
| "text": { | |
| "type": "mrkdwn", | |
| "text": "*:red_circle: Stable release failed*\n*Repo:* `${{ github.repository }}`\n*Workflow:* `${{ github.workflow }}`\n*Commit:* `${{ github.sha }}`\n*Triggered by:* `${{ github.actor }}`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>" | |
| } | |
| } | |
| ] | |
| } | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SDK_SLACKER_WEBHOOK_URL }} | |
| SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK | |
| canary-release: | |
| name: Canary release | |
| if: ${{ github.event_name == 'push' && github.repository == 'clerk/javascript' }} | |
| runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} | |
| timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 10 }} | |
| env: | |
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| TURBO_TEAM: ${{ vars.TURBO_TEAM }} | |
| TURBO_CACHE: remote:rw | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| filter: 'blob:none' | |
| - name: Setup | |
| id: config | |
| uses: ./.github/actions/init | |
| with: | |
| turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} | |
| turbo-team: ${{ vars.TURBO_TEAM }} | |
| turbo-token: ${{ secrets.TURBO_TOKEN }} | |
| playwright-enabled: true # Must be present to enable caching on branched workflows | |
| - name: Upgrade npm for trusted publishing | |
| run: npx npm@11 install -g npm@11 | |
| - name: Version packages for canary | |
| id: version-packages | |
| run: pnpm version-packages:canary | tail -1 >> "$GITHUB_OUTPUT" | |
| - name: Build release | |
| if: steps.version-packages.outputs.success == '1' | |
| run: pnpm turbo build $TURBO_ARGS | |
| - name: Canary release | |
| id: publish | |
| if: steps.version-packages.outputs.success == '1' | |
| run: pnpm release:canary | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| - name: Trigger workflows on related repos | |
| if: steps.publish.outcome == 'success' | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| retries: 3 | |
| retry-exempt-status-codes: 400,401 | |
| github-token: ${{ secrets.CLERK_COOKIE_PAT }} | |
| script: | | |
| const clerkjsVersion = require('./packages/clerk-js/package.json').version; | |
| const clerkUiVersion = require('./packages/ui/package.json').version; | |
| const nextjsVersion = require('./packages/nextjs/package.json').version; | |
| const targets = [ | |
| { repo: 'sdk-infra-workers', workflow_id: 'update-pkg-versions.yml', inputs: { clerkjsVersion, clerkUiVersion, sourceCommit: context.sha } }, | |
| ]; | |
| if (nextjsVersion.includes('canary')) { | |
| console.log('clerk/nextjs changed, will notify clerk/accounts'); | |
| targets.push( | |
| { repo: 'accounts', workflow_id: 'release-staging.yml', inputs: { version: nextjsVersion } }, | |
| ); | |
| } | |
| const results = await Promise.allSettled( | |
| targets.map(t => github.rest.actions.createWorkflowDispatch({ owner: 'clerk', ref: 'main', ...t })) | |
| ); | |
| const failures = results | |
| .map((r, i) => r.status === 'rejected' ? { target: targets[i], reason: r.reason } : null) | |
| .filter(Boolean); | |
| if (failures.length) { | |
| failures.forEach(f => core.error(`Dispatch to ${f.target.repo}/${f.target.workflow_id} failed: ${f.reason?.message ?? f.reason}`)); | |
| core.setFailed(`${failures.length} downstream dispatch(es) failed`); | |
| } | |
| - name: Notify Slack on failure | |
| if: ${{ always() && steps.publish.outcome == 'failure' }} | |
| uses: slackapi/slack-github-action@v1.24.0 | |
| with: | |
| payload: | | |
| { | |
| "blocks": [ | |
| { | |
| "type": "section", | |
| "text": { | |
| "type": "mrkdwn", | |
| "text": "*:red_circle: Canary release failed*\n*Repo:* `${{ github.repository }}`\n*Workflow:* `${{ github.workflow }}`\n*Commit:* `${{ github.sha }}`\n*Triggered by:* `${{ github.actor }}`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>" | |
| } | |
| } | |
| ] | |
| } | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SDK_SLACKER_WEBHOOK_URL }} | |
| SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK | |
| snapshot-release: | |
| name: Snapshot release | |
| if: ${{ github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '!snapshot') && github.repository == 'clerk/javascript' && github.event.issue.pull_request }} | |
| runs-on: ${{ vars.RUNNER_LARGE || 'ubuntu-latest-l' }} | |
| timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 10 }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pull-requests: write | |
| steps: | |
| - name: Limit action to Clerk members | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| retries: 3 | |
| retry-exempt-status-codes: 400,401 | |
| github-token: ${{ secrets.CLERK_COOKIE_PAT }} | |
| script: | | |
| try { | |
| const { data } = await github.rest.orgs.getMembershipForUser({ | |
| org: 'clerk', | |
| username: context.actor | |
| }); | |
| if (data.state !== 'active') { | |
| core.setFailed(`@${context.actor} is not an active member of the Clerk organization`); | |
| } | |
| } catch (e) { | |
| core.setFailed(`@${context.actor} is not a member of the Clerk organization`); | |
| } | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: refs/pull/${{ github.event.issue.number }}/head | |
| persist-credentials: false | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| filter: 'blob:none' | |
| - name: Ensure the PR hasn't changed since initiating the !snapshot command. | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| retries: 3 | |
| retry-exempt-status-codes: 400,401 | |
| script: | | |
| const commentCreated = new Date(context.payload.comment.created_at); | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: 'clerk', | |
| repo: 'javascript', | |
| pull_number: context.issue.number, | |
| }); | |
| const prLastUpdated = new Date(pr.updated_at); | |
| if (prLastUpdated > commentCreated) { | |
| core.setFailed("The PR has been updated since !snapshot was initiated. Please review the changes and re-run the !snapshot command."); | |
| } | |
| - name: Setup | |
| id: config | |
| uses: ./.github/actions/init | |
| with: | |
| turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} | |
| turbo-team: ${{ vars.TURBO_TEAM }} | |
| turbo-token: ${{ secrets.TURBO_TOKEN }} | |
| - name: Upgrade npm for trusted publishing | |
| run: npx npm@11 install -g npm@11 | |
| - name: Extract snapshot name | |
| id: extract-snapshot-name | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const match = context.payload.comment.body.match(/!snapshot (.*)/) | |
| const name = match && match[1] || ''; | |
| const isKebabCase = name.match(/^[a-z]+(-[a-z]+)*$/) | |
| if(name && !isKebabCase) { | |
| core.setFailed(`Invalid snapshot name: ${name}`); | |
| } | |
| core.setOutput('name', name); | |
| - name: Version packages for snapshot | |
| id: version-packages | |
| run: pnpm version-packages:snapshot ${{ steps.extract-snapshot-name.outputs.name }} | tail -1 >> "$GITHUB_OUTPUT" | |
| - name: Build release | |
| if: steps.version-packages.outputs.success == '1' | |
| run: pnpm turbo build $TURBO_ARGS | |
| - name: Snapshot release | |
| if: steps.version-packages.outputs.success == '1' | |
| run: pnpm release:snapshot | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| - name: Package info | |
| if: steps.version-packages.outputs.success == '1' | |
| id: package-info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const files = await (await glob.create("./packages/*/package.json")).glob(); | |
| const descriptors = files | |
| .map((file) => { | |
| const { name, version } = JSON.parse(fs.readFileSync(file, "utf8")); | |
| return { name, version }; | |
| }) | |
| .filter(({ version }) => version.includes("-${{ steps.extract-snapshot-name.outputs.name }}")); | |
| let table = `| Package | Version |\n| --- | --- |\n`; | |
| descriptors.forEach(({ name, version }) => { | |
| table += `| ${name} | ${version} |\n`; | |
| }); | |
| const snippets = descriptors | |
| .map( | |
| ({ name, version }) => | |
| `\`${name}\`\n\`\`\`sh\nnpm i ${name}@${version} --save-exact\n\`\`\`` | |
| ) | |
| .join("\n"); | |
| core.setOutput("table", table); | |
| core.setOutput("snippets", snippets); | |
| - name: Update Comment | |
| if: steps.version-packages.outputs.success == '1' | |
| uses: peter-evans/create-or-update-comment@v3.0.0 | |
| with: | |
| comment-id: ${{ github.event.comment.id }} | |
| reactions: heart | |
| - name: Minimize previous snapshot comments | |
| if: steps.version-packages.outputs.success == '1' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }); | |
| const snapshotComments = comments.filter( | |
| (comment) => | |
| comment.body?.includes('the snapshot version command generated the following package versions') | |
| ); | |
| for (const comment of snapshotComments) { | |
| await github.graphql(` | |
| mutation MinimizeComment($id: ID!) { | |
| minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) { | |
| minimizedComment { | |
| isMinimized | |
| } | |
| } | |
| } | |
| `, { id: comment.node_id }); | |
| } | |
| - name: Create snapshot release comment | |
| if: steps.version-packages.outputs.success == '1' | |
| uses: peter-evans/create-or-update-comment@v3.0.0 | |
| with: | |
| issue-number: ${{ github.event.issue.number }} | |
| body: | | |
| Hey @${{ github.event.comment.user.login }} - the snapshot version command generated the following package versions: | |
| ${{ steps.package-info.outputs.table }} | |
| Tip: Use the snippet copy button below to quickly install the required packages. | |
| ${{ steps.package-info.outputs.snippets }} | |
| # 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 | |
| # than recreating them every time. | |
| cache-for-alternate-node-versions: | |
| name: Cache for Alternate Node Versions | |
| if: ${{ github.event_name == 'push' }} | |
| runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} | |
| timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 10 }} | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| strategy: | |
| matrix: | |
| version: [22] # NOTE: 18 is cached in the main release workflow | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| filter: "blob:none" | |
| show-progress: false | |
| - name: Cache node_modules (Node v${{ matrix.version }}) | |
| uses: ./.github/actions/init | |
| with: | |
| node-version: ${{ matrix.version }} | |
| turbo-enabled: false | |
| turbo-team: "" | |
| turbo-token: "" |