1- # E2E test workflow triggered by authorized PR comments
2- # Users with write access can comment "/test" or "/run-e2e" to trigger tests
1+ # composite workflow to test the azd deployment of the app
2+ # uses a github federated identity
33name : test-e2e-pr
44
55on :
6- issue_comment :
7- types : [created]
6+ pull_request_target :
7+ branches :
8+ - main
9+ types : [opened, synchronize, reopened, labeled]
810
911permissions :
1012 id-token : write
1113 contents : read
12- pull-requests : write
13- issues : write
1414
1515jobs :
16- # Check if the comment should trigger E2E tests
17- check-authorization :
18- if : |
19- github.event.issue.pull_request &&
20- (contains(github.event.comment.body, '/test') || contains(github.event.comment.body, '/run-e2e'))
16+ # Security check - only run for trusted contributors or with explicit approval
17+ security-check :
2118 runs-on : ubuntu-latest
2219 outputs :
23- authorized : ${{ steps.auth-check.outputs.authorized }}
24- pr-number : ${{ steps.pr-info.outputs.pr-number }}
25- pr-sha : ${{ steps.pr-info.outputs.pr-sha }}
20+ approved : ${{ steps.check.outputs.approved }}
2621 steps :
27- - name : Check user authorization
28- id : auth-check
29- uses : actions/github-script@v7
30- with :
31- script : |
32- try {
33- // Check if user has write access to the repository
34- const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
35- owner: context.repo.owner,
36- repo: context.repo.repo,
37- username: context.actor
38- });
39-
40- const hasWriteAccess = ['admin', 'write'].includes(permission.permission);
41-
42- if (hasWriteAccess) {
43- console.log(`User ${context.actor} has ${permission.permission} access - authorized`);
44- core.setOutput('authorized', 'true');
45-
46- // React with 👀 to show the command was received
47- await github.rest.reactions.createForIssueComment({
48- owner: context.repo.owner,
49- repo: context.repo.repo,
50- comment_id: context.payload.comment.id,
51- content: 'eyes'
52- });
53- } else {
54- console.log(`User ${context.actor} has ${permission.permission} access - not authorized`);
55- core.setOutput('authorized', 'false');
56-
57- // React with ❌ to show unauthorized
58- await github.rest.reactions.createForIssueComment({
59- owner: context.repo.owner,
60- repo: context.repo.repo,
61- comment_id: context.payload.comment.id,
62- content: 'confused'
63- });
64-
65- // Comment explaining the requirement
66- await github.rest.issues.createComment({
67- owner: context.repo.owner,
68- repo: context.repo.repo,
69- issue_number: context.issue.number,
70- body: '❌ Only users with write access can trigger E2E tests. Please ask a maintainer to run the tests.'
71- });
72- }
73- } catch (error) {
74- console.log(`Error checking permissions: ${error}`);
75- core.setOutput('authorized', 'false');
76- }
77-
78- - name : Get PR information
79- id : pr-info
80- if : steps.auth-check.outputs.authorized == 'true'
81- uses : actions/github-script@v7
82- with :
83- script : |
84- const { data: pr } = await github.rest.pulls.get({
85- owner: context.repo.owner,
86- repo: context.repo.repo,
87- pull_number: context.issue.number
88- });
89-
90- core.setOutput('pr-number', pr.number);
91- core.setOutput('pr-sha', pr.head.sha);
22+ - name : Check if PR is from trusted source
23+ id : check
24+ run : |
25+ # Allow if PR author is a maintainer/collaborator
26+ if [[ "${{ github.event.pull_request.author_association }}" == "MEMBER" ||
27+ "${{ github.event.pull_request.author_association }}" == "COLLABORATOR" ||
28+ "${{ github.event.pull_request.author_association }}" == "OWNER" ]]; then
29+ echo "approved=true" >> "$GITHUB_OUTPUT"
30+ echo "✅ PR from trusted contributor"
31+ # Allow if PR has 'safe-to-test' label (requires manual review)
32+ elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'safe-to-test') }}" == "true" ]]; then
33+ echo "approved=true" >> "$GITHUB_OUTPUT"
34+ echo "✅ PR manually approved with 'safe-to-test' label"
35+ else
36+ echo "approved=false" >> "$GITHUB_OUTPUT"
37+ echo "❌ PR requires manual approval - add 'safe-to-test' label after review"
38+ exit 1
39+ fi
9240
93- # Run the E2E tests
9441 deploy :
95- needs : check-authorization
96- if : needs.check-authorization.outputs.authorized == 'true'
42+ needs : security-check
43+ if : needs.security-check.outputs.approved == 'true'
44+ concurrency :
45+ group : pr-testing-${{ github.event.pull_request.number }}
46+ cancel-in-progress : true
9747 outputs :
9848 storeAdminIp : ${{ steps.kubectl_get_service.outputs.STORE_ADMIN_IP }}
9949 storeFrontIp : ${{ steps.kubectl_get_service.outputs.STORE_FRONT_IP }}
10050 runs-on : ubuntu-latest
10151 steps :
102- - name : Add starting comment
103- uses : actions/github-script@v7
104- with :
105- script : |
106- await github.rest.issues.createComment({
107- owner: context.repo.owner,
108- repo: context.repo.repo,
109- issue_number: ${{ needs.check-authorization.outputs.pr-number }},
110- body: '🚀 Starting E2E tests for PR #${{ needs.check-authorization.outputs.pr-number }}...\n\n📊 [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
111- });
112-
113- - name : Checkout PR code
52+ - name : Checkout base repository (secure)
11453 uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
11554 with :
116- ref : ${{ needs.check-authorization.outputs.pr-sha }}
55+ # Checkout the base branch, not the PR branch for security
56+ ref : ${{ github.event.repository.default_branch }}
11757
11858 - name : Install Terraform
11959 uses : hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
@@ -147,22 +87,22 @@ jobs:
14787 id : provision_deploy
14888 continue-on-error : true
14989 run : |
150- # Create unique environment name for PR
151- export PR_ENV_NAME="${{ vars.AZURE_ENV_NAME }}-pr-${{ needs.check-authorization.outputs.pr-number }}"
152- azd env new $PR_ENV_NAME
90+ # Set resource limits for security
91+ azd env new ${{ vars.AZURE_ENV_NAME }}
15392 azd env set AKS_NODE_POOL_VM_SIZE Standard_D2_v4
154- azd env set BUILD_CONTAINERS true
93+ azd env set BUILD_CONTAINERS false
15594 azd env set DEPLOY_AZURE_CONTAINER_REGISTRY true
15695 azd env set DEPLOY_AZURE_OPENAI true
157- azd env set AZURE_OPENAI_LOCATION ${{ vars.AZURE_LOCATION }}
15896 azd env set DEPLOY_AZURE_OPENAI_DALL_E_MODEL false
15997 azd env set DEPLOY_AZURE_SERVICE_BUS true
16098 azd env set DEPLOY_AZURE_COSMOSDB true
16199 azd env set AZURE_COSMOSDB_ACCOUNT_KIND MongoDB
162100 azd env set DEPLOY_OBSERVABILITY_TOOLS true
101+ azd env set SOURCE_REGISTRY "ghcr.io/${{ github.repository_owner }}"
102+
163103 azd up --no-prompt
164104 env :
165- AZURE_ENV_NAME : ${{ vars.AZURE_ENV_NAME }}-pr-${{ needs.check-authorization.outputs.pr-number }}
105+ AZURE_ENV_NAME : ${{ vars.AZURE_ENV_NAME }}
166106 AZURE_LOCATION : ${{ vars.AZURE_LOCATION }}
167107 AZURE_SUBSCRIPTION_ID : ${{ secrets.AZURE_SUBSCRIPTION_ID }}
168108
@@ -172,7 +112,7 @@ jobs:
172112 with :
173113 path : |
174114 .azure/
175- key : ${{ runner.os }}-azd-pr- ${{ needs.check-authorization.outputs.pr-number }}-${{ github.run_id }}
115+ key : ${{ runner.os }}-azd-${{ github.run_id }}-${{ github.sha }}
176116
177117 - name : Check provision and deploy result
178118 if : steps.provision_deploy.outcome == 'failure'
@@ -200,44 +140,23 @@ jobs:
200140 echo "STORE_FRONT_IP=${storeFrontIp}"
201141 echo "STORE_FRONT_IP=${storeFrontIp}" >> "$GITHUB_OUTPUT"
202142
203- - name : Comment on deployment status
204- if : always()
205- uses : actions/github-script@v7
206- with :
207- script : |
208- const success = '${{ steps.provision_deploy.outcome }}' === 'success';
209- const emoji = success ? '✅' : '❌';
210- const status = success ? 'succeeded' : 'failed';
211-
212- let body = `${emoji} Deployment ${status} for PR #${{ needs.check-authorization.outputs.pr-number }}`;
213-
214- if (success) {
215- body += `\n\n🔗 **Test URLs:**\n- Store Admin: http://${{ steps.kubectl_get_service.outputs.STORE_ADMIN_IP }}\n- Store Front: http://${{ steps.kubectl_get_service.outputs.STORE_FRONT_IP }}`;
216- }
217-
218- await github.rest.issues.createComment({
219- owner: context.repo.owner,
220- repo: context.repo.repo,
221- issue_number: ${{ needs.check-authorization.outputs.pr-number }},
222- body: body
223- });
224-
225143 playwright-tests :
226- needs : [check-authorization, deploy]
227- if : needs.check-authorization.outputs.authorized == 'true' && needs.deploy.result == 'success'
144+ needs : deploy
228145 uses : ./.github/workflows/test-playwright.yaml
229146 secrets : inherit
230147 with :
231148 storeAdminUrl : " http://${{ needs.deploy.outputs.storeAdminIp }}"
232149 storeFrontUrl : " http://${{ needs.deploy.outputs.storeFrontIp }}"
233150
234151 teardown :
235- if : always() && needs.check-authorization.outputs.authorized == 'true'
236- needs : [check-authorization , playwright-tests]
152+ if : always()
153+ needs : [deploy , playwright-tests]
237154 runs-on : ubuntu-latest
238155 steps :
239- - name : Checkout
156+ - name : Checkout base repository
240157 uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
158+ with :
159+ ref : ${{ github.event.repository.default_branch }}
241160
242161 - name : Install Terraform
243162 uses : hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
@@ -250,7 +169,7 @@ jobs:
250169 with :
251170 path : |
252171 .azure/
253- key : ${{ runner.os }}-azd-pr- ${{ needs.check-authorization.outputs.pr-number }}-${{ github.run_id }}
172+ key : ${{ runner.os }}-azd-${{ github.run_id }}-${{ github.sha }}
254173
255174 - name : Azure CLI login
256175 uses : azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
@@ -267,27 +186,22 @@ jobs:
267186 --tenant-id ${{ secrets.AZURE_TENANT_ID }}
268187
269188 - name : Destroy environment
270- run : azd down --force --purge
189+ run : |
190+ # Force cleanup with multiple attempts
191+ azd down --force --purge || true
192+ sleep 30
193+ azd down --force --purge || true
271194 env :
272- AZURE_ENV_NAME : ${{ vars.AZURE_ENV_NAME }}-pr-${{ needs.check-authorization.outputs.pr-number }}
195+ AZURE_ENV_NAME : ${{ vars.AZURE_ENV_NAME }}
273196 AZURE_LOCATION : ${{ vars.AZURE_LOCATION }}
274197 AZURE_SUBSCRIPTION_ID : ${{ secrets.AZURE_SUBSCRIPTION_ID }}
275198
199+ - name : Emergency cleanup (if azd fails)
200+ if : failure()
201+ run : |
202+ # Last resort: delete resource group directly
203+ az group delete --name "rg-${{ vars.AZURE_ENV_NAME }}" --yes --no-wait || true
204+ echo "Emergency cleanup attempted"
205+
276206 - name : Remove azd folder
277207 run : rm -rf .azure
278-
279- - name : Final comment on test results
280- if : always()
281- uses : actions/github-script@v7
282- with :
283- script : |
284- const testResult = '${{ needs.playwright-tests.result }}';
285- const emoji = testResult === 'success' ? '✅' : '❌';
286- const status = testResult === 'success' ? 'passed' : 'failed';
287-
288- await github.rest.issues.createComment({
289- owner: context.repo.owner,
290- repo: context.repo.repo,
291- issue_number: ${{ needs.check-authorization.outputs.pr-number }},
292- body: `${emoji} E2E tests ${status} for PR #${{ needs.check-authorization.outputs.pr-number }}. Environment has been cleaned up.\n\n📊 [View full workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`
293- });
0 commit comments