Repository navigation
Merge pull request #8766 from Shopify/unhide-app-security-commands #7846
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 | |
| on: | |
| # Trigger for snapit functionality | |
| issue_comment: | |
| types: | |
| - created | |
| # Trigger for changeset release functionality | |
| push: | |
| branches: | |
| - main | |
| - stable/* | |
| # Trigger for manual/cron release functionality | |
| schedule: | |
| - cron: '0 6 * * *' # 6:00 AM UTC every day | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag' | |
| default: 'nightly' | |
| type: choice | |
| options: | |
| - nightly | |
| - latest | |
| - experimental | |
| - snapshot | |
| snapit_comment_id: | |
| description: 'PR comment requesting a snapshot (set automatically by /snapit)' | |
| type: string | |
| snapit_sha: | |
| description: 'PR commit to publish (set automatically by /snapit)' | |
| type: string | |
| concurrency: | |
| group: changeset-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| env: | |
| PNPM_VERSION: '10.11.1' | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| jobs: | |
| # npm rejects issue_comment OIDC tokens, so comments only request a separate run. | |
| snapit: | |
| name: Request snapshot | |
| if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.body == '/snapit' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: write | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Request a snapshot release | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const {comment, issue, repository} = context.payload; | |
| try { | |
| const {data: collaborator} = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| ...context.repo, | |
| username: comment.user.login, | |
| }); | |
| if (!['write', 'admin'].includes(collaborator.permission)) { | |
| throw new Error('Only users with write permission to the repository can run /snapit.'); | |
| } | |
| const {data: pullRequest} = await github.rest.pulls.get({ | |
| ...context.repo, | |
| pull_number: issue.number, | |
| }); | |
| if (pullRequest.state !== 'open') { | |
| throw new Error('Snapshots can only be requested for open pull requests.'); | |
| } | |
| if (pullRequest.head.repo?.full_name !== repository.full_name) { | |
| throw new Error('/snapit is not supported on pull requests from forked repositories.'); | |
| } | |
| await github.rest.actions.createWorkflowDispatch({ | |
| ...context.repo, | |
| workflow_id: 'release.yml', | |
| ref: repository.default_branch, | |
| inputs: { | |
| tag: 'snapshot', | |
| snapit_comment_id: String(comment.id), | |
| snapit_sha: pullRequest.head.sha, | |
| }, | |
| }); | |
| await github.rest.reactions.createForIssueComment({ | |
| ...context.repo, | |
| comment_id: comment.id, | |
| content: 'eyes', | |
| }).catch((error) => core.warning(`Snapshot requested, but the reaction failed: ${error.message}`)); | |
| core.info(`Requested a snapshot for PR #${issue.number} at ${pullRequest.head.sha}.`); | |
| } catch (error) { | |
| core.setFailed(error.message); | |
| await github.rest.issues.createComment({ | |
| ...context.repo, | |
| issue_number: issue.number, | |
| body: `Unable to request a snapshot: ${error.message}`, | |
| }); | |
| } | |
| snapshot-release: | |
| name: Publish PR snapshot | |
| if: ${{ github.event_name == 'workflow_dispatch' && (inputs.snapit_comment_id != '' || inputs.snapit_sha != '') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - name: Validate snapshot request | |
| id: request | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const {tag, snapit_comment_id: commentId, snapit_sha: sha} = context.payload.inputs; | |
| if ( | |
| tag !== 'snapshot' || | |
| !/^[1-9]\d*$/.test(commentId) || | |
| !Number.isSafeInteger(Number(commentId)) || | |
| !/^[a-f0-9]{40}$/.test(sha) | |
| ) { | |
| throw new Error('A snapshot request requires a comment ID and a full PR commit SHA.'); | |
| } | |
| // Recheck the original request before checking out or executing any PR code. | |
| const {data: comment} = await github.rest.issues.getComment({ | |
| ...context.repo, | |
| comment_id: Number(commentId), | |
| }); | |
| if (comment.body !== '/snapit') { | |
| throw new Error('The requesting comment must contain /snapit.'); | |
| } | |
| const {data: collaborator} = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| ...context.repo, | |
| username: comment.user.login, | |
| }); | |
| if (!['write', 'admin'].includes(collaborator.permission)) { | |
| throw new Error('Only users with write permission to the repository can run /snapit.'); | |
| } | |
| const issueUrlPrefix = `https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/issues/`; | |
| const pullRequestNumber = Number(comment.issue_url.slice(issueUrlPrefix.length)); | |
| if ( | |
| !comment.issue_url.startsWith(issueUrlPrefix) || | |
| !Number.isSafeInteger(pullRequestNumber) || | |
| pullRequestNumber < 1 | |
| ) { | |
| throw new Error('The requesting comment must belong to a PR in this repository.'); | |
| } | |
| const {data: pullRequest} = await github.rest.pulls.get({ | |
| ...context.repo, | |
| pull_number: pullRequestNumber, | |
| }); | |
| // Keep enough context to report failures for an authorized request. | |
| core.setOutput('pull_request', pullRequestNumber); | |
| core.setOutput('comment_id', comment.id); | |
| core.setOutput('requester', comment.user.login); | |
| core.setOutput('sha', sha); | |
| if ( | |
| pullRequest.state !== 'open' || | |
| pullRequest.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}` | |
| ) { | |
| throw new Error('Snapshots require an open PR from this repository.'); | |
| } | |
| if (pullRequest.head.sha !== sha) { | |
| throw new Error('The PR changed after the request. Post /snapit again to publish the current commit.'); | |
| } | |
| - name: Checkout PR commit | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.request.outputs.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup deps | |
| uses: ./.github/actions/setup-cli-deps | |
| with: | |
| node-version: 24.12.0 | |
| - name: Publish snapshot | |
| id: publish | |
| run: | | |
| pnpm release snapshot | |
| VERSION=$(node -p "require('./packages/cli/package.json').version") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NPM_TOKEN: '' | |
| NPM_CONFIG_PROVENANCE: true | |
| SHOPIFY_CLI_BUILD_REPO: ${{ github.repository }} | |
| - name: Report snapshot result | |
| if: ${{ always() && steps.request.outputs.pull_request != '' }} | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| SNAPSHOT_OUTCOME: ${{ steps.publish.outcome }} | |
| SNAPSHOT_VERSION: ${{ steps.publish.outputs.version }} | |
| SNAPSHOT_PR: ${{ steps.request.outputs.pull_request }} | |
| SNAPSHOT_COMMENT: ${{ steps.request.outputs.comment_id }} | |
| SNAPSHOT_REQUESTER: ${{ steps.request.outputs.requester }} | |
| SNAPSHOT_SHA: ${{ steps.request.outputs.sha }} | |
| with: | |
| script: | | |
| const { | |
| SNAPSHOT_OUTCOME: outcome, | |
| SNAPSHOT_VERSION: version, | |
| SNAPSHOT_REQUESTER: requester, | |
| SNAPSHOT_SHA: sha, | |
| } = process.env; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const published = outcome === 'success'; | |
| const body = published | |
| ? [ | |
| `🫰✨ **Thanks @${requester}! Your snapshot has been published to npm.**`, | |
| `Built from \`${sha}\`. [Workflow run](${runUrl}).`, | |
| 'Test the snapshot by installing your package globally:', | |
| [ | |
| '```bash', | |
| `pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@${version}`, | |
| '```', | |
| ].join('\n'), | |
| [ | |
| '> [!CAUTION]', | |
| '> After installing, validate the version by running `shopify version` in your terminal.', | |
| "> If the versions don't match, you might have multiple global instances installed.", | |
| '> Use `which shopify` to find out which one you are running and uninstall it.', | |
| ].join('\n'), | |
| ].join('\n\n') | |
| : `The snapshot requested by @${requester} for \`${sha}\` could not be published. [View the workflow run](${runUrl}).`; | |
| await github.rest.issues.createComment({ | |
| ...context.repo, | |
| issue_number: Number(process.env.SNAPSHOT_PR), | |
| body, | |
| }); | |
| await github.rest.reactions.createForIssueComment({ | |
| ...context.repo, | |
| comment_id: Number(process.env.SNAPSHOT_COMMENT), | |
| content: published ? 'rocket' : 'confused', | |
| }); | |
| # Changeset release job - runs on push to main or stable branches | |
| changeset-release: | |
| name: Changeset Release | |
| if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag == '' && inputs.snapit_comment_id == '' && inputs.snapit_sha == '') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup deps | |
| uses: ./.github/actions/setup-cli-deps | |
| with: | |
| node-version: 24.12.0 | |
| - name: Create Release Pull Request | |
| id: changesets | |
| uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1 | |
| with: | |
| version: pnpm changeset-manifests | |
| title: Version Packages - ${{ github.ref_name }} | |
| createGithubReleases: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Publish packages | |
| if: steps.changesets.outputs.hasChangesets == 'false' | |
| run: pnpm release latest | |
| env: | |
| NPM_TOKEN: '' | |
| NPM_CONFIG_PROVENANCE: true | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SHOPIFY_CLI_BUILD_REPO: ${{ github.repository }} | |
| - name: Get version | |
| id: version | |
| if: steps.changesets.outputs.hasChangesets == 'false' | |
| run: | | |
| VERSION=$(node -p "require('./packages/cli/package.json').version") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Version: $VERSION" | |
| - name: Create tag | |
| if: steps.changesets.outputs.hasChangesets == 'false' | |
| env: | |
| TAG: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then | |
| echo "Tag $TAG already exists, skipping" | |
| exit 0 | |
| fi | |
| git tag "$TAG" | |
| git push origin "$TAG" | |
| echo "Created tag $TAG" | |
| - name: Create stable branch | |
| if: steps.changesets.outputs.hasChangesets == 'false' && github.ref_name == 'main' && endsWith(steps.version.outputs.version, '.0') | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| MINOR=${VERSION%.0} | |
| BRANCH="stable/$MINOR" | |
| if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then | |
| echo "Branch $BRANCH already exists, skipping" | |
| exit 0 | |
| fi | |
| git push origin "HEAD:refs/heads/$BRANCH" | |
| echo "Created branch $BRANCH" | |
| - name: Create GitHub release | |
| if: steps.changesets.outputs.hasChangesets == 'false' | |
| env: | |
| TAG: ${{ steps.version.outputs.version }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # gh release create does not support the REST API's make_latest=legacy option. | |
| RELEASE_ID="$(gh release view "$TAG" --json databaseId --jq .databaseId 2>/dev/null || true)" | |
| if [ -n "$RELEASE_ID" ]; then | |
| gh api \ | |
| --method PATCH \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \ | |
| -f make_latest=legacy >/dev/null | |
| echo "Release $TAG already exists; ensured make_latest=legacy" | |
| exit 0 | |
| fi | |
| gh api \ | |
| --method POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "repos/$GITHUB_REPOSITORY/releases" \ | |
| -f tag_name="$TAG" \ | |
| -f name="$TAG" \ | |
| -F generate_release_notes=true \ | |
| -f make_latest=legacy >/dev/null | |
| echo "Created release $TAG" | |
| # Manual/Cron release job - runs on schedule or manual trigger with tag | |
| manual-cron-release: | |
| name: Manual & Cron Release | |
| if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' && inputs.snapit_comment_id == '' && inputs.snapit_sha == '') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup deps | |
| uses: ./.github/actions/setup-cli-deps | |
| with: | |
| node-version: 24.12.0 | |
| - name: Release | |
| run: pnpm release ${{ github.event.inputs.tag || 'nightly' }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NPM_TOKEN: '' | |
| NPM_CONFIG_PROVENANCE: true | |
| SHOPIFY_CLI_BUILD_REPO: ${{ github.repository }} |