Repository navigation
Expand file tree
/
Copy pathazure-pipelines.yml
More file actions
326 lines (309 loc) · 14.7 KB
/
Copy pathazure-pipelines.yml
File metadata and controls
326 lines (309 loc) · 14.7 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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# Azure Pipelines: compare on every pull request, then assess the result.
#
# Copy to azure-pipelines.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 secure
# files and variables each job is granted, not in which file it reads.
#
# Before this runs, provision in the DevOps UI:
# * three Secure Files holding the catalog-reader PEMs. Azure has no
# multi-line variable: a PEM cannot be masked, so the file type is the
# only safe form, and DownloadSecureFile below is the built-in task,
# * the PIACE_INFERENCE_TOKEN pipeline secret (single-line, maskable).
#
# `pool:` is a hosted Ubuntu image, not example42/piace: the published image
# is distroless, and these jobs need bash, git, curl, grep, jq and sha256sum.
# Install the verified binary into the hosted image instead.
trigger: none
pr:
branches:
include:
- '*'
variables:
- name: PIACE_VERSION
value: '0.4.0'
# cosign is downloaded rather than installed from a package mirror, so it is
# pinned by version and by digest. That digest, reviewed in this file, is the
# trust anchor for everything below it: it is what makes the signature check a
# check rather than another unverified download. Take the value from
# cosign_checksums.txt in the release you pin, not from here.
- name: COSIGN_VERSION
value: '3.1.3'
- name: COSIGN_SHA256
value: '4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71'
jobs:
- job: compare
pool:
vmImage: '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:
# the same-repo guard the other examples encode.
condition: eq(variables['System.PullRequest.IsFork'], 'False')
steps:
- checkout: self
# Pinned to a version and verified against the manifest published with it.
# A runner that fetches "the latest binary" on every run is a supply chain
# you do not control. Everything here ships with the hosted image; no
# package installation.
- bash: |
set -euo pipefail
mkdir -p "$BIN_DIR"
# cosign first, pinned by digest, because it is what establishes where
# the PIACE binaries came from. A checksum manifest published beside
# the binaries it lists proves only that a download is intact.
curl -fsSL -o cosign "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64"
echo "${COSIGN_SHA256} cosign" | sha256sum -c -
chmod 0755 cosign
base="https://github.com/example42/piace/releases/download/v${VERSION}"
curl -fsSLO "$base/piace-${VERSION}-linux-amd64"
curl -fsSLO "$base/SHA256SUMS"
curl -fsSLO "$base/SHA256SUMS.sigstore.json"
# Origin first. The identity is the exact workflow, at the exact tag,
# that published this version: a signature from any other workflow,
# repository or ref fails here.
./cosign verify-blob SHA256SUMS \
--bundle SHA256SUMS.sigstore.json \
--certificate-identity "https://github.com/example42/piace/.github/workflows/ci.yml@refs/tags/v${VERSION}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
# One line of the manifest, not the whole file: the other platforms
# were not downloaded, and a line matching nothing on disk has to fail
# rather than pass quietly.
grep " piace-${VERSION}-linux-amd64\$" SHA256SUMS | sha256sum -c -
install -m 0755 "piace-${VERSION}-linux-amd64" "$BIN_DIR/piace"
env:
BIN_DIR: $(Agent.TempDirectory)/piace-bin
VERSION: $(PIACE_VERSION)
COSIGN_VERSION: $(COSIGN_VERSION)
COSIGN_SHA256: $(COSIGN_SHA256)
# Agent.TempDirectory is per-job and outside the checkout, which is what
# makes it the right home for a private key: nothing here reaches an
# uploaded artifact or a later `git status`.
- task: DownloadSecureFile@1
name: caBundle
inputs:
secureFile: piace-ca.pem
- task: DownloadSecureFile@1
name: clientCert
inputs:
secureFile: piace-client-cert.pem
- task: DownloadSecureFile@1
name: clientKey
inputs:
secureFile: piace-client-key.pem
# No `set -x` in this step, ever. The keys themselves are never echoed; a
# trace of the commands that write them would be.
- bash: |
set -euo pipefail
umask 077
install -d -m 0700 "$RUN_DIR"
install -m 0600 "$CA_BUNDLE" "$RUN_DIR/ca.pem"
install -m 0600 "$CLIENT_CERT" "$RUN_DIR/client.pem"
install -m 0600 "$CLIENT_KEY" "$RUN_DIR/client.key"
# ci/piace/services.yaml names these three variables with
# ca_bundle_env, client_cert_env and private_key_env, so the committed
# file is read in place: nothing renders or copies it.
echo "##vso[task.setvariable variable=PIACE_CA_BUNDLE]$RUN_DIR/ca.pem"
echo "##vso[task.setvariable variable=PIACE_CLIENT_CERT]$RUN_DIR/client.pem"
echo "##vso[task.setvariable variable=PIACE_PRIVATE_KEY]$RUN_DIR/client.key"
mkdir -p report
env:
RUN_DIR: $(Agent.TempDirectory)/piace-run
CA_BUNDLE: $(caBundle.secureFilePath)
CLIENT_CERT: $(clientCert.secureFilePath)
CLIENT_KEY: $(clientKey.secureFilePath)
# Exit 10 is a policy difference, which fails this 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,
# set `continueOnError: true` on this task: the report still says what
# changed, but 20 and 30 slip through too, which is worse than GitLab's
# per-exit-code softening.
#
# The candidate environment is the one this pipeline deployed. It maps to
# the compiler by branch name, so use the source branch of this pull
# request, not the pull request number, and rewrite dashes to underscores:
# a Puppet environment name cannot contain a dash, and r10k deploys an
# environment that way. Match whatever mapping your deploy applies, not one
# invented here.
#
# Every value reaches the script as an environment variable, never through
# `$(...)` in the script text: the agent macro-expands task inputs before
# the shell runs, so an untrusted value substituted into the text would
# run. That is also why the scripts hold no shell `$( ... )` command
# substitution: the agent treats the token as a variable. SOURCE_BRANCH is
# refs/heads/feature-x; parameter expansion strips the prefix and maps the
# dash.
- bash: |
set -euo pipefail
export PATH="$BIN_DIR:$PATH"
source_branch="${SOURCE_BRANCH#refs/heads/}"
candidate_environment="${source_branch//-/_}"
piace compare \
--targets ci/piace/targets.yaml \
--services ci/piace/services.yaml \
--candidate-environment "$candidate_environment" \
--json-out report/report.json \
--html-out report/report.html
env:
BIN_DIR: $(Agent.TempDirectory)/piace-bin
RUN_DIR: $(Agent.TempDirectory)/piace-run
SOURCE_BRANCH: $(System.PullRequest.SourceBranch)
# compare writes report.json before it exits 10 or 30, so always() keeps
# the report from a gated run: that is the run most worth a human look.
# Retention is set in the pipeline settings, not here: both reports carry
# catalog values, redacted per targets.yaml but not otherwise sanitized, so
# keep it short. A hosted agent tears down after the job and a self-hosted
# one does not; the cleanup step costs nothing on the first and matters on
# the second.
- task: PublishPipelineArtifact@1
condition: always()
inputs:
targetPath: report
artifactName: piace-report
- bash: rm -rf "$RUN_DIR"
condition: always()
env:
RUN_DIR: $(Agent.TempDirectory)/piace-run
- job: explain
dependsOn: compare
# always(): the run worth assessing is usually the one that just failed the
# gate, and compare writes report.json before it exits. The fork check
# mirrors compare: a fork PR gets no secrets, and the guarded assess step
# would only no-op on a tokenless job. When there is no report (a hard
# failure at 30 usually leaves none) the guarded steps do nothing.
condition: and(always(), eq(variables['System.PullRequest.IsFork'], 'False'))
pool:
vmImage: 'ubuntu-latest'
steps:
- download: none
- checkout: self
# `piace change-context` takes a merge base, which a shallow clone does
# not have, and the PR checkout is a temporary merge commit whose target ref
# is fetched explicitly below. The credentials must survive for that
# later fetch.
fetchDepth: 0
persistCredentials: true
- bash: |
set -euo pipefail
mkdir -p "$BIN_DIR"
# cosign first, pinned by digest, because it is what establishes where
# the PIACE binaries came from. A checksum manifest published beside
# the binaries it lists proves only that a download is intact.
curl -fsSL -o cosign "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64"
echo "${COSIGN_SHA256} cosign" | sha256sum -c -
chmod 0755 cosign
base="https://github.com/example42/piace/releases/download/v${VERSION}"
curl -fsSLO "$base/piace-${VERSION}-linux-amd64"
curl -fsSLO "$base/SHA256SUMS"
curl -fsSLO "$base/SHA256SUMS.sigstore.json"
# Origin first. The identity is the exact workflow, at the exact tag,
# that published this version: a signature from any other workflow,
# repository or ref fails here.
./cosign verify-blob SHA256SUMS \
--bundle SHA256SUMS.sigstore.json \
--certificate-identity "https://github.com/example42/piace/.github/workflows/ci.yml@refs/tags/v${VERSION}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
grep " piace-${VERSION}-linux-amd64\$" SHA256SUMS | sha256sum -c -
install -m 0755 "piace-${VERSION}-linux-amd64" "$BIN_DIR/piace"
env:
BIN_DIR: $(Agent.TempDirectory)/piace-bin
VERSION: $(PIACE_VERSION)
COSIGN_VERSION: $(COSIGN_VERSION)
COSIGN_SHA256: $(COSIGN_SHA256)
# continueOnError: a missing report means compare died before publishing,
# and the guarded steps below then no-op rather than failing a job that can
# only advise.
- task: DownloadPipelineArtifact@2
continueOnError: true
inputs:
buildType: current
artifactName: piace-report
targetPath: $(Agent.SourcesDirectory)/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.
#
# Azure exposes no pull request title or description variables (unlike
# GitLab and GitHub), so they come from the pull request REST API, using
# the OAuth token mapped from System.AccessToken. If that token is not
# granted to the job, this step fails and the assessment is skipped, which
# is an advisory outcome.
#
# Nothing untrusted is passed by value. The agent macro-expands `$( ... )`
# in script text before a shell runs, so a pull request title reaching a
# command line is a command; --title-file and --base-ref-env take a
# location instead, and there is deliberately no --title flag.
- bash: |
set -euo pipefail
test -f report/report.json || exit 0
export PATH="$BIN_DIR:$PATH"
# Both refs, not only the base. A pull request build checks out a
# detached merge commit with no local branch for either side, and a
# bare branch name does not resolve there: git looks for
# refs/heads/NAME and refs/remotes/NAME, never
# refs/remotes/origin/NAME. `git merge-base` then fails with "Not a
# valid object name" and the assessment runs with no change context.
export BASE_REF="origin/${TARGET_BRANCH#refs/heads/}"
export HEAD_REF="origin/${SOURCE_BRANCH#refs/heads/}"
git fetch --no-tags origin \
"+refs/heads/${TARGET_BRANCH#refs/heads/}:refs/remotes/${BASE_REF}" \
"+refs/heads/${SOURCE_BRANCH#refs/heads/}:refs/remotes/${HEAD_REF}"
curl -fsS -o pull-request.json \
-H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
"${COLLECTION_URI}${PROJECT}/_apis/git/repositories/${REPOSITORY_ID}/pullrequests/${PR_ID}?api-version=6.0"
jq -r '.title' pull-request.json > pr-title.txt
jq -r '.description // ""' pull-request.json > pr-body.txt
piace change-context \
--base-ref-env BASE_REF \
--head-ref-env HEAD_REF \
--title-file pr-title.txt \
--description-file pr-body.txt \
> change-context.yaml
env:
BIN_DIR: $(Agent.TempDirectory)/piace-bin
TARGET_BRANCH: $(System.PullRequest.TargetBranch)
SOURCE_BRANCH: $(System.PullRequest.SourceBranch)
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
COLLECTION_URI: $(System.TeamFoundationCollectionUri)
PROJECT: $(System.TeamProject)
REPOSITORY_ID: $(Build.Repository.ID)
PR_ID: $(System.PullRequest.PullRequestId)
# 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. The change context file exists only
# when the describe step ran.
- bash: |
set -euo pipefail
export PATH="$BIN_DIR:$PATH"
test -f report/report.json || exit 0
mkdir -p assessment
piace explain \
--json-in report/report.json \
--services ci/piace/services.yaml \
--change change-context.yaml \
--ai-out assessment/assessment.json \
--html-out assessment/report.html
env:
BIN_DIR: $(Agent.TempDirectory)/piace-bin
PIACE_INFERENCE_TOKEN: $(PIACE_INFERENCE_TOKEN)
# continueOnError: when compare left no report there is no assessment to
# publish, and an absent directory must not turn an advisory job red.
- task: PublishPipelineArtifact@1
condition: always()
continueOnError: true
inputs:
targetPath: assessment
artifactName: piace-assessment