2727 - latest
2828 - experimental
2929 - snapshot
30+ snapit_comment_id :
31+ description : ' PR comment requesting a snapshot (set automatically by /snapit)'
32+ type : string
33+ snapit_sha :
34+ description : ' PR commit to publish (set automatically by /snapit)'
35+ type : string
3036
3137concurrency :
3238 group : changeset-${{ github.head_ref || github.run_id }}
@@ -37,53 +43,209 @@ env:
3743 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
3844
3945jobs :
40- # Snapit job - runs when /snapit comment is made on a PR
46+ # npm rejects issue_comment OIDC tokens, so comments only request a separate run.
4147 snapit :
42- name : Snapit
48+ name : Request snapshot
4349 if : ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.body == '/snapit' }}
4450 runs-on : ubuntu-latest
51+ timeout-minutes : 5
52+ permissions :
53+ actions : write
54+ contents : read
55+ pull-requests : write
56+ steps :
57+ - name : Request a snapshot release
58+ uses : actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
59+ with :
60+ script : |
61+ const {comment, issue, repository} = context.payload;
62+ try {
63+ const {data: collaborator} = await github.rest.repos.getCollaboratorPermissionLevel({
64+ ...context.repo,
65+ username: comment.user.login,
66+ });
67+ if (!['write', 'admin'].includes(collaborator.permission)) {
68+ throw new Error('Only users with write permission to the repository can run /snapit.');
69+ }
70+
71+ const {data: pullRequest} = await github.rest.pulls.get({
72+ ...context.repo,
73+ pull_number: issue.number,
74+ });
75+ if (pullRequest.state !== 'open') {
76+ throw new Error('Snapshots can only be requested for open pull requests.');
77+ }
78+ if (pullRequest.head.repo?.full_name !== repository.full_name) {
79+ throw new Error('/snapit is not supported on pull requests from forked repositories.');
80+ }
81+
82+ await github.rest.actions.createWorkflowDispatch({
83+ ...context.repo,
84+ workflow_id: 'release.yml',
85+ ref: repository.default_branch,
86+ inputs: {
87+ tag: 'snapshot',
88+ snapit_comment_id: String(comment.id),
89+ snapit_sha: pullRequest.head.sha,
90+ },
91+ });
92+ await github.rest.reactions.createForIssueComment({
93+ ...context.repo,
94+ comment_id: comment.id,
95+ content: 'eyes',
96+ }).catch((error) => core.warning(`Snapshot requested, but the reaction failed: ${error.message}`));
97+ core.info(`Requested a snapshot for PR #${issue.number} at ${pullRequest.head.sha}.`);
98+ } catch (error) {
99+ core.setFailed(error.message);
100+ await github.rest.issues.createComment({
101+ ...context.repo,
102+ issue_number: issue.number,
103+ body: `Unable to request a snapshot: ${error.message}`,
104+ });
105+ }
106+
107+ snapshot-release :
108+ name : Publish PR snapshot
109+ if : ${{ github.event_name == 'workflow_dispatch' && (inputs.snapit_comment_id != '' || inputs.snapit_sha != '') }}
110+ runs-on : ubuntu-latest
45111 permissions :
46112 contents : read
47113 pull-requests : write
48114 id-token : write
49115 steps :
50- # WARNING: DO NOT RUN ANY CUSTOM LOCAL SCRIPT BEFORE RUNNING THE SNAPIT ACTION
51- # This action can be executed by 3rd party users and it should not be able to run arbitrary code from a PR.
52- - name : Checkout current branch
116+ - name : Validate snapshot request
117+ id : request
118+ uses : actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
119+ with :
120+ script : |
121+ const {tag, snapit_comment_id: commentId, snapit_sha: sha} = context.payload.inputs;
122+ if (
123+ tag !== 'snapshot' ||
124+ !/^[1-9]\d*$/.test(commentId) ||
125+ !Number.isSafeInteger(Number(commentId)) ||
126+ !/^[a-f0-9]{40}$/.test(sha)
127+ ) {
128+ throw new Error('A snapshot request requires a comment ID and a full PR commit SHA.');
129+ }
130+
131+ // Recheck the original request before checking out or executing any PR code.
132+ const {data: comment} = await github.rest.issues.getComment({
133+ ...context.repo,
134+ comment_id: Number(commentId),
135+ });
136+ if (comment.body !== '/snapit') {
137+ throw new Error('The requesting comment must contain /snapit.');
138+ }
139+ const {data: collaborator} = await github.rest.repos.getCollaboratorPermissionLevel({
140+ ...context.repo,
141+ username: comment.user.login,
142+ });
143+ if (!['write', 'admin'].includes(collaborator.permission)) {
144+ throw new Error('Only users with write permission to the repository can run /snapit.');
145+ }
146+
147+ const issueUrlPrefix = `https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/issues/`;
148+ const pullRequestNumber = Number(comment.issue_url.slice(issueUrlPrefix.length));
149+ if (
150+ !comment.issue_url.startsWith(issueUrlPrefix) ||
151+ !Number.isSafeInteger(pullRequestNumber) ||
152+ pullRequestNumber < 1
153+ ) {
154+ throw new Error('The requesting comment must belong to a PR in this repository.');
155+ }
156+ const {data: pullRequest} = await github.rest.pulls.get({
157+ ...context.repo,
158+ pull_number: pullRequestNumber,
159+ });
160+ // Keep enough context to report failures for an authorized request.
161+ core.setOutput('pull_request', pullRequestNumber);
162+ core.setOutput('comment_id', comment.id);
163+ core.setOutput('requester', comment.user.login);
164+ core.setOutput('sha', sha);
165+ if (
166+ pullRequest.state !== 'open' ||
167+ pullRequest.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`
168+ ) {
169+ throw new Error('Snapshots require an open PR from this repository.');
170+ }
171+ if (pullRequest.head.sha !== sha) {
172+ throw new Error('The PR changed after the request. Post /snapit again to publish the current commit.');
173+ }
174+
175+ - name : Checkout PR commit
53176 uses : actions/checkout@v6
54177 with :
178+ ref : ${{ steps.request.outputs.sha }}
55179 fetch-depth : 0
180+ persist-credentials : false
56181 - name : Setup deps
57182 uses : ./.github/actions/setup-cli-deps
58183 with :
59184 node-version : 24.12.0
60- - name : Force snapshot changeset
61- run : " mv .changeset/force-snapshot-build.md.ignore .changeset/force-snapshot-build.md"
62- - name : Create snapshot version
63- uses : Shopify/snapit@2a7ca29133cfeb2c8703654d54bc6dbc565caa69 # registry-and-package-manager
64- with :
65- comment_is_global : ' true'
66- comment_packages : ' @shopify/cli'
67- comment_suffix : "
68- > [!CAUTION]
69-
70- > After installing, validate the version by running `shopify version` in your terminal.
71-
72- > If the versions don't match, you might have multiple global instances installed.
73-
74- > Use `which shopify` to find out which one you are running and uninstall it."
75- comment_command_flags : ' --@shopify:registry=https://registry.npmjs.org'
76- build_script : " node bin/update-cli-kit-version.js && pnpm nx run-many --target=bundle --all --skip-nx-cache --output-style=stream && pnpm refresh-manifests"
185+ - name : Publish snapshot
186+ id : publish
187+ run : |
188+ pnpm release snapshot
189+ VERSION=$(node -p "require('./packages/cli/package.json').version")
190+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
77191 env :
78192 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
79193 NPM_TOKEN : ' '
80194 NPM_CONFIG_PROVENANCE : true
81195 SHOPIFY_CLI_BUILD_REPO : ${{ github.repository }}
196+ - name : Report snapshot result
197+ if : ${{ always() && steps.request.outputs.pull_request != '' }}
198+ uses : actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
199+ env :
200+ SNAPSHOT_OUTCOME : ${{ steps.publish.outcome }}
201+ SNAPSHOT_VERSION : ${{ steps.publish.outputs.version }}
202+ SNAPSHOT_PR : ${{ steps.request.outputs.pull_request }}
203+ SNAPSHOT_COMMENT : ${{ steps.request.outputs.comment_id }}
204+ SNAPSHOT_REQUESTER : ${{ steps.request.outputs.requester }}
205+ SNAPSHOT_SHA : ${{ steps.request.outputs.sha }}
206+ with :
207+ script : |
208+ const {
209+ SNAPSHOT_OUTCOME: outcome,
210+ SNAPSHOT_VERSION: version,
211+ SNAPSHOT_REQUESTER: requester,
212+ SNAPSHOT_SHA: sha,
213+ } = process.env;
214+ const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
215+ const published = outcome === 'success';
216+ const body = published
217+ ? [
218+ `🫰✨ **Thanks @${requester}! Your snapshot has been published to npm.**`,
219+ `Built from \`${sha}\`. [Workflow run](${runUrl}).`,
220+ 'Test the snapshot by installing your package globally:',
221+ [
222+ '```bash',
223+ `pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@${version}`,
224+ '```',
225+ ].join('\n'),
226+ [
227+ '> [!CAUTION]',
228+ '> After installing, validate the version by running `shopify version` in your terminal.',
229+ "> If the versions don't match, you might have multiple global instances installed.",
230+ '> Use `which shopify` to find out which one you are running and uninstall it.',
231+ ].join('\n'),
232+ ].join('\n\n')
233+ : `The snapshot requested by @${requester} for \`${sha}\` could not be published. [View the workflow run](${runUrl}).`;
234+ await github.rest.issues.createComment({
235+ ...context.repo,
236+ issue_number: Number(process.env.SNAPSHOT_PR),
237+ body,
238+ });
239+ await github.rest.reactions.createForIssueComment({
240+ ...context.repo,
241+ comment_id: Number(process.env.SNAPSHOT_COMMENT),
242+ content: published ? 'rocket' : 'confused',
243+ });
82244
83245 # Changeset release job - runs on push to main or stable branches
84246 changeset-release :
85247 name : Changeset Release
86- if : ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag == '') }}
248+ if : ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag == '' && inputs.snapit_comment_id == '' && inputs.snapit_sha == '' ) }}
87249 runs-on : ubuntu-latest
88250 permissions :
89251 contents : write
@@ -187,7 +349,7 @@ jobs:
187349 # Manual/Cron release job - runs on schedule or manual trigger with tag
188350 manual-cron-release :
189351 name : Manual & Cron Release
190- if : ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '') }}
352+ if : ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' && inputs.snapit_comment_id == '' && inputs.snapit_sha == '' ) }}
191353 runs-on : ubuntu-latest
192354 permissions :
193355 contents : read
0 commit comments