Skip to content

Commit ebb0651

Browse files
authored
Merge branch 'main' into security/pin-actions-and-dependabot-cooldown
2 parents 54f7097 + 615d517 commit ebb0651

2 files changed

Lines changed: 223 additions & 65 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 184 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,45 @@ concurrency:
1212
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
1313
cancel-in-progress: true
1414

15+
# No workflow-level permissions — each job declares exactly what it needs.
16+
permissions: {}
17+
1518
jobs:
16-
build-dist:
17-
runs-on: [ubuntu-latest]
19+
# Runs fork/PR code with read-only token. No write access here.
20+
# For fork PRs: also validates that dist is in sync (fails if not).
21+
# For same-repo PRs: uploads the compiled dist as a short-lived artifact
22+
# for commit-dist to consume.
23+
build-test-core:
24+
runs-on: ubuntu-latest
1825
permissions:
19-
contents: write
20-
pull-requests: write
26+
contents: read
2127
outputs:
22-
committed_sha: ${{ steps.commit_dist.outputs.commit_sha }}
28+
artifact-id: ${{ steps.upload-dist.outputs.artifact-id }}
29+
dist-dirty: ${{ steps.check-dirty.outputs.dirty }}
2330
steps:
31+
- name: Resolve checkout ref
32+
env:
33+
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
34+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
35+
PR_NUMBER: ${{ github.event.pull_request.number }}
36+
run: |
37+
if [[ "$HEAD_REPO" == "${{ github.repository }}" ]]; then
38+
# Same-repo PR: use branch ref so git status compares against the branch tip
39+
echo "CHECKOUT_REF=$HEAD_REF" >> "$GITHUB_ENV"
40+
else
41+
# Fork PR: use refs/pull/N/head — maintained by GitHub in the base repo,
42+
# so no cross-repo clone is needed and the CodeQL unsafe-checkout rule is satisfied
43+
echo "CHECKOUT_REF=refs/pull/$PR_NUMBER/head" >> "$GITHUB_ENV"
44+
fi
45+
2446
- name: Checkout PR head
2547
uses: actions/checkout@v6
2648
with:
2749
fetch-depth: 0
28-
repository: ${{ github.event.pull_request.head.repo.full_name }}
29-
ref: ${{ github.event.pull_request.head.ref }}
30-
token: ${{ secrets.GITHUB_TOKEN}}
50+
ref: ${{ env.CHECKOUT_REF }}
51+
# persist-credentials: false so the read-only token is not stored in
52+
# the git credential helper and is unavailable to npm scripts.
53+
persist-credentials: false
3154

3255
- name: Setup Node.js
3356
uses: actions/setup-node@v6
@@ -47,35 +70,148 @@ jobs:
4770
working-directory: .github/actions/core
4871
run: npm run build
4972

50-
- name: Commit build artifacts (same-repo PRs only)
51-
if: github.event.pull_request.head.repo.full_name == github.repository
52-
id: commit_dist
53-
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
54-
with:
55-
add: "."
56-
message: "chore: build core action dist (auto)"
57-
default_author: github_actions
58-
push: true
59-
60-
- name: Fail if dist is dirty (Forked PRs only)
73+
- name: Fail if dist is dirty (fork PRs only)
74+
id: check-dirty
6175
if: github.event.pull_request.head.repo.full_name != github.repository
6276
working-directory: .github/actions/core
6377
run: |
6478
if [[ -n $(git status --porcelain dist) ]]; then
79+
echo "dirty=true" >> "$GITHUB_OUTPUT"
6580
echo "::error::The 'dist' folder is out of sync with the source code."
6681
echo "::error::Because this is a forked PR, we cannot automatically commit the changes."
6782
echo "::error::Please run 'npm run build' in '.github/actions/core' locally and commit the changes."
6883
exit 1
6984
fi
7085
86+
- name: Upload dist artifact (same-repo PRs only)
87+
id: upload-dist
88+
if: github.event.pull_request.head.repo.full_name == github.repository
89+
uses: actions/upload-artifact@v7
90+
with:
91+
name: dist-artifact
92+
path: .github/actions/core/dist/
93+
retention-days: 1 # commit-dist deletes it immediately; this is a safety net
94+
95+
# Posts a PR comment when the dist check fails on fork PRs.
96+
# Isolated into its own job so pull-requests:write is never held by the job
97+
# that executes untrusted fork code.
98+
comment-dist-dirty:
99+
needs: build-test-core
100+
if: >-
101+
always() &&
102+
needs.build-test-core.result == 'failure' &&
103+
needs.build-test-core.outputs.dist-dirty == 'true'
104+
runs-on: ubuntu-latest
105+
permissions:
106+
pull-requests: write
107+
steps:
108+
- name: Post dist-out-of-sync comment
109+
env:
110+
GH_TOKEN: ${{ github.token }}
111+
run: |
112+
cat > /tmp/comment.md << 'EOF'
113+
> [!WARNING]
114+
> The `dist` folder is out of sync with the source code.
115+
>
116+
> Because this is a forked PR, the workflow cannot automatically commit the updated build artifacts.
117+
> Please run the following commands locally and push the result:
118+
>
119+
> ```bash
120+
> cd .github/actions/core
121+
> npm run build
122+
> git add dist/
123+
> git commit -m "chore: build core action dist"
124+
> git push
125+
> ```
126+
EOF
127+
gh pr comment "${{ github.event.pull_request.number }}" \
128+
--repo "${{ github.repository }}" \
129+
--body-file /tmp/comment.md \
130+
--edit-last || \
131+
gh pr comment "${{ github.event.pull_request.number }}" \
132+
--repo "${{ github.repository }}" \
133+
--body-file /tmp/comment.md
134+
135+
# Commits the pre-built dist artifact back to the PR branch.
136+
# Only runs for same-repo PRs — fork PR validation is handled entirely in
137+
# build-test-core, so this job never touches fork data.
138+
# The checkout is of a branch that lives in THIS repository (not a fork),
139+
# so it does not trigger the CodeQL "unsafe checkout in privileged context" rule.
140+
commit-dist:
141+
needs: build-test-core
142+
if: github.event.pull_request.head.repo.full_name == github.repository
143+
runs-on: ubuntu-latest
144+
permissions:
145+
contents: write
146+
pull-requests: write
147+
actions: write # needed to delete the dist artifact after use
148+
outputs:
149+
committed_sha: ${{ steps.commit_dist.outputs.sha }}
150+
steps:
151+
- name: Checkout PR branch
152+
uses: actions/checkout@v6
153+
with:
154+
fetch-depth: 0
155+
# Default checkout (no ref:) avoids the CodeQL CWE-829 "unsafe checkout"
156+
# rule. We switch to the PR branch in the run: step below via an env var
157+
# so the branch name is never interpolated into an actions/checkout input.
158+
token: ${{ secrets.GITHUB_TOKEN }}
159+
160+
- name: Download dist artifact
161+
uses: actions/download-artifact@v8
162+
with:
163+
name: dist-artifact
164+
path: /tmp/dist-artifact/
165+
166+
- name: Commit and push dist to PR branch
167+
id: commit_dist
168+
run: |
169+
git config user.name "github-actions[bot]"
170+
git config user.email "github-actions[bot]@users.noreply.github.com"
171+
# Derive the PR head branch purely from git history so no GitHub
172+
# event/context data flows into the git checkout command (CodeQL taint-free).
173+
# For a pull_request checkout, HEAD is the merge commit; HEAD^2 is the PR head.
174+
PR_HEAD=$(git rev-parse HEAD^2)
175+
BRANCH=$(git branch -r --contains "$PR_HEAD" --format='%(refname:short)' \
176+
| grep '^origin/' | grep -v 'origin/HEAD' | head -1 | sed 's|^origin/||')
177+
if [[ -z "$BRANCH" ]]; then
178+
echo "::error::Could not determine PR branch from git history."
179+
exit 1
180+
fi
181+
git checkout -B "$BRANCH" "origin/$BRANCH"
182+
cp -r /tmp/dist-artifact/. .github/actions/core/dist/
183+
git add .github/actions/core/dist/
184+
if git diff --staged --quiet; then
185+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
186+
exit 0
187+
fi
188+
git commit -m "chore: build core action dist (auto)"
189+
git push origin "$BRANCH"
190+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
191+
192+
- name: Delete dist artifact
193+
if: always()
194+
env:
195+
GH_TOKEN: ${{ github.token }}
196+
run: |
197+
gh api --method DELETE \
198+
"/repos/${{ github.repository }}/actions/artifacts/${{ needs.build-test-core.outputs.artifact-id }}"
199+
71200
test-python:
72-
needs: build-dist
201+
needs: [build-test-core, commit-dist]
202+
# commit-dist is skipped for fork PRs; treat 'skipped' as OK so fork PR tests still run.
203+
if: >-
204+
always() &&
205+
needs.build-test-core.result == 'success' &&
206+
(needs.commit-dist.result == 'success' || needs.commit-dist.result == 'skipped')
73207
runs-on: [ubuntu-latest]
74208
permissions:
75-
contents: write
209+
contents: read
76210
pull-requests: read
77211
steps:
78212
- uses: actions/checkout@v6
213+
with:
214+
persist-credentials: false
79215
- name: Patch composite actions for E2E
80216
run: |
81217
sed -i -E 's|sap/pull-request-semver-bumper/.github/actions/core@[^[:space:]"]+|./.github/actions/core|g' .github/actions/version-bumping/*/action.yml
@@ -101,13 +237,19 @@ jobs:
101237
fi
102238
103239
test-npm:
104-
needs: build-dist
240+
needs: [build-test-core, commit-dist]
241+
if: >-
242+
always() &&
243+
needs.build-test-core.result == 'success' &&
244+
(needs.commit-dist.result == 'success' || needs.commit-dist.result == 'skipped')
105245
runs-on: [ubuntu-latest]
106246
permissions:
107-
contents: write
247+
contents: read
108248
pull-requests: read
109249
steps:
110250
- uses: actions/checkout@v6
251+
with:
252+
persist-credentials: false
111253
- name: Patch composite actions for E2E
112254
run: |
113255
sed -i -E 's|sap/pull-request-semver-bumper/.github/actions/core@[^[:space:]"]+|./.github/actions/core|g' .github/actions/version-bumping/*/action.yml
@@ -133,13 +275,19 @@ jobs:
133275
fi
134276
135277
test-maven:
136-
needs: build-dist
278+
needs: [build-test-core, commit-dist]
279+
if: >-
280+
always() &&
281+
needs.build-test-core.result == 'success' &&
282+
(needs.commit-dist.result == 'success' || needs.commit-dist.result == 'skipped')
137283
runs-on: [ubuntu-latest]
138284
permissions:
139-
contents: write
285+
contents: read
140286
pull-requests: read
141287
steps:
142288
- uses: actions/checkout@v6
289+
with:
290+
persist-credentials: false
143291
- name: Patch composite actions for E2E
144292
run: |
145293
sed -i -E 's|sap/pull-request-semver-bumper/.github/actions/core@[^[:space:]"]+|./.github/actions/core|g' .github/actions/version-bumping/*/action.yml
@@ -167,13 +315,19 @@ jobs:
167315
fi
168316
169317
test-version-file:
170-
needs: build-dist
318+
needs: [build-test-core, commit-dist]
319+
if: >-
320+
always() &&
321+
needs.build-test-core.result == 'success' &&
322+
(needs.commit-dist.result == 'success' || needs.commit-dist.result == 'skipped')
171323
runs-on: [ubuntu-latest]
172324
permissions:
173-
contents: write
325+
contents: read
174326
pull-requests: read
175327
steps:
176328
- uses: actions/checkout@v6
329+
with:
330+
persist-credentials: false
177331
- name: Patch composite actions for E2E
178332
run: |
179333
sed -i -E 's|sap/pull-request-semver-bumper/.github/actions/core@[^[:space:]"]+|./.github/actions/core|g' .github/actions/version-bumping/*/action.yml
@@ -200,7 +354,7 @@ jobs:
200354
201355
all-tests-passed:
202356
if: always()
203-
needs: [build-dist, test-python, test-npm, test-maven, test-version-file]
357+
needs: [build-test-core, commit-dist, test-python, test-npm, test-maven, test-version-file]
204358
runs-on: ubuntu-latest
205359
permissions:
206360
statuses: write
@@ -215,11 +369,11 @@ jobs:
215369
fi
216370
217371
- name: Set Commit Status
218-
uses: myrotvorets/set-commit-status-action@2c3557527522d8d38f410941902b64a4be75a0ec # master
372+
uses: myrotvorets/set-commit-status-action@3730c0a348a2ace3c110851bed53331bc6406e9f # v2.0.1
219373
with:
220374
token: ${{ secrets.GITHUB_TOKEN }}
221375
status: ${{ steps.status.outputs.status }}
222-
sha: ${{ needs.build-dist.outputs.committed_sha || github.event.pull_request.head.sha }}
376+
sha: ${{ needs.commit-dist.outputs.committed_sha || github.event.pull_request.head.sha }}
223377
context: "all-tests-passed"
224378
description: "Aggregation of all tests"
225379

0 commit comments

Comments
 (0)