-
Notifications
You must be signed in to change notification settings - Fork 15
203 lines (188 loc) · 8.56 KB
/
Copy pathrefresh_sbom.yml
File metadata and controls
203 lines (188 loc) · 8.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
name: Refresh SBOM for Dependency PRs
# Auto-refreshes the cached CycloneDX SBOMs (`client/sbom/`, `server/sbom/`)
# on dependency-update PRs opened by Renovate or Dependabot. Without this,
# every bot PR fails the `Verify ... SBOM is up-to-date` checks in
# `run_tests.yml` because the bots only update `package.json` /
# `pnpm-lock.yaml` (or `build.gradle` / `gradle.properties` /
# `settings.gradle`) — they don't run `pnpm run sbom:force` or
# `./gradlew cyclonedxBom`.
#
# Event choice: pull_request_target
# ---------------------------------
# We use `pull_request_target` (not `pull_request`) so this workflow can
# access repo secrets when triggered by Dependabot — `pull_request` events
# from Dependabot don't expose Actions secrets by default. The workflow
# definition is read from the BASE branch (safe from PR tampering), but the
# checked-out code is the PR head, so we add a same-repo guard
# (`head.repo.full_name == github.repository`) to ensure we only run on
# branches Renovate/Dependabot pushed into THIS repo (they never use forks).
#
# Token requirement
# -----------------
# We deliberately do NOT push with the default `GITHUB_TOKEN`: pushes signed
# by `GITHUB_TOKEN` are excluded from triggering further workflow runs, so
# the failed test workflow on the PR would stay red even after the SBOM
# commit lands. Instead we either:
# 1. Mint a least-privilege installation token from a GitHub App
# (preferred), via the `SBOM_BOT_APP_ID` repo/org variable and the
# `SBOM_BOT_PRIVATE_KEY` secret. Scoped to `contents: write` on the
# single PR head repo.
# 2. Fall back to a fine-grained PAT in `SBOM_BOT_PAT`.
#
# Credential hygiene
# ------------------
# Checkout runs with the default (read-only) `GITHUB_TOKEN` and
# `persist-credentials: false`, so the write-capable App/PAT token is
# never written into the runner's git config. The push step authenticates
# explicitly via a one-shot URL so the token can't be picked up by
# subsequent steps or build-script lifecycle hooks.
on:
pull_request_target:
branches: [develop, main]
types: [opened, synchronize, reopened]
paths:
- 'client/package.json'
- 'client/pnpm-lock.yaml'
- 'server/build.gradle'
- 'server/gradle.properties'
- 'server/settings.gradle'
permissions:
contents: read
concurrency:
group: refresh-sbom-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
refresh:
# Only run on bot-opened PRs that target THIS repo (no forks). The
# same-repo check is defense-in-depth: Renovate and Dependabot always
# push into the base repo on their own branches.
if: >
(github.event.pull_request.user.login == 'renovate[bot]' ||
github.event.pull_request.user.login == 'dependabot[bot]') &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
# GitHub disallows the `secrets` context inside step-level `if:`
# expressions (only env/github/inputs/job/matrix/needs/runner/steps/
# strategy/vars are permitted there) — referencing it directly causes
# the entire workflow to fail to load. Bounce the secret presence
# check through an env var so the gate is expressible.
- name: Detect App token availability
id: app-token-config
env:
APP_PRIVATE_KEY: ${{ secrets.SBOM_BOT_PRIVATE_KEY }}
APP_ID: ${{ vars.SBOM_BOT_APP_ID }}
run: |
if [ -n "${APP_ID:-}" ] && [ -n "${APP_PRIVATE_KEY:-}" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
fi
- name: Mint GitHub App token (preferred)
id: app-token
if: steps.app-token-config.outputs.available == 'true'
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.SBOM_BOT_APP_ID }}
private-key: ${{ secrets.SBOM_BOT_PRIVATE_KEY }}
# Scope to least privilege: only `contents: write` on the PR head repo.
owner: ${{ github.event.pull_request.head.repo.owner.login }}
repositories: ${{ github.event.pull_request.head.repo.name }}
permission-contents: write
- name: Pick push token (App token > PAT fallback)
id: token
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
PAT_TOKEN: ${{ secrets.SBOM_BOT_PAT }}
run: |
if [ -n "${APP_TOKEN:-}" ]; then
echo "source=app" >> "$GITHUB_OUTPUT"
echo "::add-mask::$APP_TOKEN"
echo "value=$APP_TOKEN" >> "$GITHUB_OUTPUT"
elif [ -n "${PAT_TOKEN:-}" ]; then
echo "source=pat" >> "$GITHUB_OUTPUT"
echo "::add-mask::$PAT_TOKEN"
echo "value=$PAT_TOKEN" >> "$GITHUB_OUTPUT"
else
echo "::error::Neither SBOM_BOT_APP_ID/SBOM_BOT_PRIVATE_KEY nor SBOM_BOT_PAT is configured. See .github/workflows/refresh_sbom.yml header for setup details."
exit 1
fi
- name: Checkout PR branch (read-only, no token persisted)
uses: actions/checkout@v6
with:
# pull_request_target defaults to the base ref; we want the PR head.
ref: ${{ github.event.pull_request.head.sha }}
# No `token:` here — default GITHUB_TOKEN is sufficient for reading
# this same repo, and `persist-credentials: false` ensures nothing
# writable is left in the git config for later steps (e.g. Gradle
# build scripts or pnpm lifecycle hooks) to pick up.
persist-credentials: false
# Full history so `git diff "$BASE_SHA"...HEAD` always resolves the
# PR base, even on long-lived branches where a shallow clone might
# not include it.
fetch-depth: 0
- name: Detect which SBOM(s) need refreshing
id: scopes
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
changed=$(git diff --name-only "$BASE_SHA"...HEAD)
client=false
server=false
if echo "$changed" | grep -qE '^client/(package\.json|pnpm-lock\.yaml)$'; then
client=true
fi
if echo "$changed" | grep -qE '^server/(build\.gradle|gradle\.properties|settings\.gradle)$'; then
server=true
fi
echo "client=$client" >> "$GITHUB_OUTPUT"
echo "server=$server" >> "$GITHUB_OUTPUT"
echo "Client SBOM refresh needed: $client"
echo "Server SBOM refresh needed: $server"
# ---------- Server SBOM ----------
- name: Set up JDK 25
if: steps.scopes.outputs.server == 'true'
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'zulu'
cache: 'gradle'
- name: Regenerate server SBOM
if: steps.scopes.outputs.server == 'true'
working-directory: ./server
run: |
chmod +x ./gradlew
./gradlew cyclonedxBom
# ---------- Client SBOM ----------
# `pnpm sbom` reads the lockfile directly, so we deliberately do NOT run
# `pnpm install` — skipping it both saves time and avoids running
# dependency lifecycle scripts. Corepack provides pnpm itself.
- name: Enable Corepack
if: steps.scopes.outputs.client == 'true'
run: corepack enable
- name: Set up Node.js
if: steps.scopes.outputs.client == 'true'
uses: actions/setup-node@v6
with:
node-version: '24.16.0'
- name: Regenerate client SBOM
if: steps.scopes.outputs.client == 'true'
working-directory: ./client
run: pnpm run sbom:force
- name: Commit and push if SBOM changed
env:
PUSH_TOKEN: ${{ steps.token.outputs.value }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
if git diff --quiet -- client/sbom server/sbom; then
echo "SBOM already current — nothing to commit. This typically means the workflow re-ran on its own commit; safe to skip."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add client/sbom server/sbom
git commit -m "chore: refresh SBOM after dependency update"
# Authenticate the push via a one-shot URL rather than persisting
# the write token into git config. The token is masked in logs.
git push "https://x-access-token:${PUSH_TOKEN}@github.com/${PR_REPO}.git" "HEAD:${PR_BRANCH}"