Skip to content

Commit e9089dd

Browse files
committed
feat(workflow): refactor E2E test workflow to use security checks for PR approvals
1 parent 78a2969 commit e9089dd

1 file changed

Lines changed: 62 additions & 148 deletions

File tree

.github/workflows/test-e2e-pr.yaml

Lines changed: 62 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,59 @@
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
33
name: test-e2e-pr
44

55
on:
6-
issue_comment:
7-
types: [created]
6+
pull_request_target:
7+
branches:
8+
- main
9+
types: [opened, synchronize, reopened, labeled]
810

911
permissions:
1012
id-token: write
1113
contents: read
12-
pull-requests: write
13-
issues: write
1414

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

Comments
 (0)