Skip to content

Commit 3334bc5

Browse files
Merge pull request #8721 from Shopify/gonzalo/snapit-workflow-dispatch
Restore /snapit using workflow dispatch
2 parents 135e12d + 859e8ca commit 3334bc5

1 file changed

Lines changed: 186 additions & 24 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 186 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ on:
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

3137
concurrency:
3238
group: changeset-${{ github.head_ref || github.run_id }}
@@ -37,53 +43,209 @@ env:
3743
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3844

3945
jobs:
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

Comments
 (0)