Skip to content

Commit a9470c8

Browse files
authored
Add workflow to run validation pipelines from a PR directly (#81836)
First draft of the workflow to run perf validation pipelines.
1 parent 0b75006 commit a9470c8

File tree

1 file changed

+271
-0
lines changed

1 file changed

+271
-0
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
name: PR Validation
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
pull-requests: write
9+
id-token: write # Required to fetch an OIDC token for Azure authentication
10+
11+
jobs:
12+
validate-and-trigger:
13+
name: Validate and Trigger Azure Pipeline
14+
if: |
15+
github.event.issue.pull_request &&
16+
(contains(github.event.comment.body, '/dart') || contains(github.event.comment.body, '/pr-val'))
17+
runs-on: ubuntu-latest
18+
environment: pr_val
19+
20+
steps:
21+
- name: Check if user has write access
22+
id: check-access
23+
uses: actions/github-script@v7
24+
with:
25+
script: |
26+
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
27+
owner: context.repo.owner,
28+
repo: context.repo.repo,
29+
username: context.actor
30+
});
31+
32+
const hasWriteAccess = ['admin', 'write', 'maintain'].includes(permission.permission);
33+
console.log(`User ${context.actor} has permission: ${permission.permission}`);
34+
core.setOutput('has-access', hasWriteAccess);
35+
return hasWriteAccess;
36+
37+
- name: Check Microsoft org membership
38+
id: check-org
39+
if: steps.check-access.outputs.has-access == 'true'
40+
uses: actions/github-script@v7
41+
with:
42+
script: |
43+
try {
44+
await github.rest.orgs.checkMembershipForUser({
45+
org: 'microsoft',
46+
username: context.actor
47+
});
48+
console.log(`User ${context.actor} is a member of Microsoft org`);
49+
core.setOutput('is-member', 'true');
50+
return true;
51+
} catch (error) {
52+
console.log(`User ${context.actor} is not a member of Microsoft org`);
53+
core.setOutput('is-member', 'false');
54+
return false;
55+
}
56+
57+
- name: Parse commit hash from comment
58+
id: parse-commit
59+
if: steps.check-access.outputs.has-access != 'true' || steps.check-org.outputs.is-member != 'true'
60+
uses: actions/github-script@v7
61+
with:
62+
script: |
63+
const commentBody = context.payload.comment.body;
64+
// Extract commit hash after /dart or /pr-val
65+
const match = commentBody.match(/\/(dart|pr-val)\s+([a-f0-9]{7,40})/i);
66+
67+
if (!match || !match[2]) {
68+
console.log('No commit hash found in comment');
69+
core.setOutput('has-commit', 'false');
70+
return false;
71+
}
72+
73+
const commitHash = match[2];
74+
console.log(`Extracted commit hash: ${commitHash}`);
75+
core.setOutput('has-commit', 'true');
76+
core.setOutput('commit-hash', commitHash);
77+
return true;
78+
79+
- name: Comment on missing commit hash
80+
if: |
81+
(steps.check-access.outputs.has-access != 'true' || steps.check-org.outputs.is-member != 'true') &&
82+
steps.parse-commit.outputs.has-commit != 'true'
83+
uses: actions/github-script@v7
84+
with:
85+
script: |
86+
await github.rest.issues.createComment({
87+
owner: context.repo.owner,
88+
repo: context.repo.repo,
89+
issue_number: context.issue.number,
90+
body: 'You do not have permission to trigger this workflow without specifying a commit hash. Please use the format `/dart <commit-hash>` or `/pr-val <commit-hash>.'
91+
});
92+
93+
- name: Exit if unauthorized without commit hash
94+
if: |
95+
(steps.check-access.outputs.has-access != 'true' || steps.check-org.outputs.is-member != 'true') &&
96+
steps.parse-commit.outputs.has-commit != 'true'
97+
run: exit 1
98+
99+
- name: Get PR details
100+
id: pr-details
101+
uses: actions/github-script@v7
102+
with:
103+
script: |
104+
const { data: pr } = await github.rest.pulls.get({
105+
owner: context.repo.owner,
106+
repo: context.repo.repo,
107+
pull_number: context.issue.number
108+
});
109+
110+
core.setOutput('ref', pr.head.ref);
111+
core.setOutput('repo', pr.head.repo.full_name);
112+
console.log(`PR #${context.issue.number}: ${pr.head.repo.full_name}@${pr.head.ref} (${pr.head.sha})`);
113+
114+
- name: Determine commit SHA to use
115+
id: commit-sha
116+
uses: actions/github-script@v7
117+
with:
118+
script: |
119+
const parseCommitOutput = '${{ steps.parse-commit.outputs.has-commit }}';
120+
const providedCommit = '${{ steps.parse-commit.outputs.commit-hash }}';
121+
122+
let commitSha;
123+
if (parseCommitOutput === 'true' && providedCommit) {
124+
// Use the commit hash provided in the comment
125+
commitSha = providedCommit;
126+
console.log(`Using commit hash from comment: ${commitSha}`);
127+
} else {
128+
// Use the PR head SHA for privileged users
129+
const { data: pr } = await github.rest.pulls.get({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
pull_number: context.issue.number
133+
});
134+
commitSha = pr.head.sha;
135+
console.log(`Using PR head SHA: ${commitSha}`);
136+
}
137+
138+
core.setOutput('sha', commitSha);
139+
140+
- name: Validate commit exists in PR
141+
if: steps.parse-commit.outputs.has-commit == 'true'
142+
uses: actions/github-script@v7
143+
with:
144+
script: |
145+
const commitSha = '${{ steps.commit-sha.outputs.sha }}';
146+
const { data: commits } = await github.rest.pulls.listCommits({
147+
owner: context.repo.owner,
148+
repo: context.repo.repo,
149+
pull_number: context.issue.number
150+
});
151+
152+
const commitExists = commits.some(commit => commit.sha.startsWith(commitSha));
153+
154+
if (!commitExists) {
155+
await github.rest.issues.createComment({
156+
owner: context.repo.owner,
157+
repo: context.repo.repo,
158+
issue_number: context.issue.number,
159+
body: `The specified commit hash \`${commitSha}\` was not found in this PR. Please ensure you are using a valid commit hash from this PR.`
160+
});
161+
core.setFailed(`Commit ${commitSha} not found in PR`);
162+
} else {
163+
console.log(`Validated commit ${commitSha} exists in PR`);
164+
}
165+
166+
- name: Azure Login with OpenID Connect
167+
uses: azure/login@v2
168+
with:
169+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
170+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
171+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
172+
173+
- name: Determine validation type and pipeline ID
174+
id: validation-type
175+
run: |
176+
COMMENT_BODY="${{ github.event.comment.body }}"
177+
if echo "$COMMENT_BODY" | grep -q "/dart"; then
178+
echo "type=dart" >> $GITHUB_OUTPUT
179+
echo "pipeline-id=15324" >> $GITHUB_OUTPUT
180+
elif echo "$COMMENT_BODY" | grep -q "/pr-val"; then
181+
echo "type=pr-val" >> $GITHUB_OUTPUT
182+
echo "pipeline-id=8972" >> $GITHUB_OUTPUT
183+
fi
184+
185+
- name: Trigger Pipeline
186+
id: trigger-pipeline
187+
run: |
188+
# Get Azure DevOps access token
189+
AZDO_TOKEN=$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
190+
191+
VALIDATION_TYPE="${{ steps.validation-type.outputs.type }}"
192+
PIPELINE_ID="${{ steps.validation-type.outputs.pipeline-id }}"
193+
DEVDIV_ORG="devdiv"
194+
DEVDIV_PROJECT="DevDiv"
195+
196+
echo "Triggering DevDiv $VALIDATION_TYPE pipeline (ID: $PIPELINE_ID)..."
197+
198+
# Build request body based on validation type
199+
if [ "$VALIDATION_TYPE" = "dart" ]; then
200+
# DART pipeline uses prNumber and sha parameters
201+
REQUEST_BODY=$(cat <<EOF
202+
{
203+
"templateParameters": {
204+
"prNumber": "${{ github.event.issue.number }}",
205+
"sha": "${{ steps.commit-sha.outputs.sha }}"
206+
}
207+
}
208+
EOF
209+
)
210+
else
211+
# PR-Val pipeline uses PRNumber, CommitSHA, and SourceBranch parameters
212+
REQUEST_BODY=$(cat <<EOF
213+
{
214+
"templateParameters": {
215+
"PRNumber": ${{ github.event.issue.number }},
216+
"CommitSHA": "${{ steps.commit-sha.outputs.sha }}",
217+
"SourceBranch": "${{ steps.pr-details.outputs.ref }}"
218+
}
219+
}
220+
EOF
221+
)
222+
fi
223+
224+
echo "Request body: $REQUEST_BODY"
225+
226+
# Trigger the pipeline
227+
RESPONSE=$(curl -X POST \
228+
-H "Content-Type: application/json" \
229+
-H "Authorization: Bearer $AZDO_TOKEN" \
230+
-d "$REQUEST_BODY" \
231+
"https://dev.azure.com/$DEVDIV_ORG/$DEVDIV_PROJECT/_apis/pipelines/$PIPELINE_ID/runs?api-version=7.0")
232+
233+
echo "Response: $RESPONSE"
234+
235+
# Extract pipeline run information
236+
BUILD_ID=$(echo $RESPONSE | jq -r '.id // empty')
237+
238+
if [ -z "$BUILD_ID" ]; then
239+
echo "Failed to trigger pipeline"
240+
echo "Error details: $(echo $RESPONSE | jq -r '.message // "Unknown error"')"
241+
exit 1
242+
fi
243+
244+
WEB_URL="https://dev.azure.com/$DEVDIV_ORG/$DEVDIV_PROJECT/_build/results?buildId=$BUILD_ID"
245+
echo "pipeline-url=$WEB_URL" >> $GITHUB_OUTPUT
246+
echo "build-id=$BUILD_ID" >> $GITHUB_OUTPUT
247+
echo "Successfully triggered pipeline: $WEB_URL"
248+
249+
- name: Comment pipeline link
250+
if: success()
251+
uses: actions/github-script@v7
252+
with:
253+
script: |
254+
await github.rest.issues.createComment({
255+
owner: context.repo.owner,
256+
repo: context.repo.repo,
257+
issue_number: context.issue.number,
258+
body: `Pipeline triggered by @${context.actor}\n\n[View Pipeline Run](${{ steps.trigger-pipeline.outputs.pipeline-url }})\n\n**Parameters:**\n- Validation Type: \`${{ steps.validation-type.outputs.type }}\`\n- Pipeline ID: \`${{ steps.validation-type.outputs.pipeline-id }}\`\n- PR Number: \`${{ github.event.issue.number }}\`\n- Commit SHA: \`${{ steps.commit-sha.outputs.sha }}\`\n- Source Branch: \`${{ steps.pr-details.outputs.ref }}\`\n- Build ID: \`${{ steps.trigger-pipeline.outputs.build-id }}\``
259+
});
260+
261+
- name: Comment on failure
262+
if: failure()
263+
uses: actions/github-script@v7
264+
with:
265+
script: |
266+
await github.rest.issues.createComment({
267+
owner: context.repo.owner,
268+
repo: context.repo.repo,
269+
issue_number: context.issue.number,
270+
body: 'Failed to trigger the pipeline. Please check the workflow logs for details.'
271+
});

0 commit comments

Comments
 (0)