-
Notifications
You must be signed in to change notification settings - Fork 0
556 lines (513 loc) · 28.3 KB
/
Copy pathci.yml
File metadata and controls
556 lines (513 loc) · 28.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
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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
name: CI
on:
push:
branches: [main]
pull_request:
# Read-only by default, so the single `contents: write` below is visibly an exception rather
# than something the whole workflow happens to have.
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
# SPEC-v0.6 §4. The Postgres tests skip when CTRLRUN_TEST_POSTGRES is unset, and a
# skipping test proves nothing -- so CI supplies a real server rather than letting the
# backend's whole suite pass by not running. `ctrlrun_test` is superuser here because
# T154c has to CREATE DATABASE with a deliberately lossy encoding.
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ctrlrun
POSTGRES_PASSWORD: ctrlrun
POSTGRES_DB: ctrlrun_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
CTRLRUN_TEST_POSTGRES: postgresql://ctrlrun:ctrlrun@127.0.0.1:5432/ctrlrun_test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install
# Every dependency by hash from `requirements/ci.txt`, then the checkout itself with
# `--no-deps` and against the backend that lock carries (`requirements/in/backend.in`):
# what this job resolves is what `scripts/lock.sh` wrote down and somebody reviewed, not
# what PyPI happened to serve this morning. The gateway's tests need its
# extra; T30 still proves `import ctrlrun` does not touch it, in a subprocess, whether
# or not it happens to be installed here.
run: |
pip install --require-hashes -r requirements/ci.txt
pip install --no-deps --no-build-isolation -e .
# A claim about the environment is worth nothing until something checks it: if the
# service failed to start, the Postgres tests would silently skip and the backend
# would ship ungraded.
- name: The Postgres tests will actually run
run: |
python - <<'PY'
import os, sys
url = os.environ["CTRLRUN_TEST_POSTGRES"]
import psycopg
with psycopg.connect(url, connect_timeout=15) as connection:
print("postgres:", connection.execute("select version()").fetchone()[0])
PY
# One script, called here and by a developer before pushing, so the two cannot
# diverge. `test_ci_runs_the_check_script` fails if this step stops calling it.
# SPEC-v0.6 §3.5 item 2 asks a migration to be proved against the previous release's
# own code. Those fixtures pip-install real releases, and without this they SKIP on a
# network failure and the run is green -- the same false green `ci.yml` already refuses
# one job over, where a skipped adapter suite "is a failure and not a configuration".
- name: check
env:
CTRLRUN_REQUIRE_RELEASE_FIXTURES: "1"
# Measured on one version only; the number is the same on every version, and the
# measuring is not free.
CTRLRUN_COVERAGE: ${{ matrix.python-version == '3.12' && '1' || '' }}
run: ./scripts/check.sh
# CONTRIBUTING.md states the floors; this is what holds them. A drop below either one is
# a red check on the pull request that caused it, not a number somebody notices later.
- name: Coverage floors
if: matrix.python-version == '3.12'
run: python scripts/coverage_floor.py coverage.json --statements 90 --branches 80
# SPEC-v0.4 §5, T118. The composite action, run against this repository's own
# configurations, with `install: .` so it dogfoods the checkout rather than the last
# release. Two runs, because the second is the one that matters: the N/A path has to be
# dogfooded and not merely described.
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 10/10. Effect templates, three grants, a delegable parent — the only configuration in
# the repository that can exercise the whole catalogue.
- name: Verify examples/authority/payments.yaml
id: authority
uses: ./
with:
policy: examples/authority/payments.yaml
install: .
badge-path: verify-badge.json
artifact-name: ctrlrun-verify-authority
# 5/5, 5 not applicable. A `ctrlrun.policy/v1` document with no effect templates and no
# grants, and the assertion is on **that shape**: a change that made verify silently
# count N/As as passes is caught here rather than in a badge.
- name: Verify examples/policies/payments.yaml
id: templates
uses: ./
with:
policy: examples/policies/payments.yaml
install: .
badge-path: verify-badge-templates.json
artifact-name: ctrlrun-verify-templates
- name: The reported shapes are the ones the specification names
env:
AUTHORITY: ${{ steps.authority.outputs.badge-message }}
AUTHORITY_NA: ${{ steps.authority.outputs.not-applicable }}
TEMPLATES: ${{ steps.templates.outputs.badge-message }}
TEMPLATES_NA: ${{ steps.templates.outputs.not-applicable }}
run: |
set -eu
echo "authority: $AUTHORITY ($AUTHORITY_NA not applicable)"
echo "templates: $TEMPLATES ($TEMPLATES_NA not applicable)"
test "$AUTHORITY" = "verified 29/29"
# G13 is N/A on SQLite, the action's default store: SQLite has no clock of its own
# to diverge from; G15 is N/A because neither document declares `max_attempts`.
# G16 is graded on both: verify brings its own precondition provider (SPEC-v0.7 §8.9).
# G17 is N/A on both since v0.8 item 3: neither document names an
# `approver_role`, which is a statement about what the operator wrote
# (SPEC-v0.8 §3.5, §11.7). G18 is graded since item 2, which supplies its
# own approver identity: whether
# an operator configured one is a fact about their application and never an N/A
# reason here (SPEC-v0.8 §11.7). Both counts moved by one when it landed.
#
# SPEC-v0.9 §6 moved the templates count again: G24 is N/A for a document whose
# grants name no task, which the templates example's do not. The authority example
# binds one, so its count is unchanged and its passing total moved 19 to 20 instead.
test "$AUTHORITY_NA" = "3"
#
# SPEC-v0.11 §8 moved the templates count again: G31 needs only an action to
# build a chain from, so it is graded wherever any guarantee is, and both
# examples gained one passing row when item 4 landed.
test "$TEMPLATES" = "verified 16/16"
test "$TEMPLATES_NA" = "16"
test -s verify-badge.json
test -s verify-report.json
test -s verify-report.xml
# SPEC-v0.4 §5.2 — the action *writes* the badge and never publishes it, because publishing
# needs `contents: write` and asking every consumer for that is a bad trade for a tool whose
# subject is least privilege. Publishing is this project's own decision, and this job is it.
#
# Three constraints, and each is load-bearing:
#
# - `contents: write` is **job-level**. The workflow is read-only; nothing else here can
# write to the repository.
# - It runs on a **push to `main`** and on nothing else. A pull request never reaches it —
# a PR from a fork must not be able to write the badge, and relying on the fork token
# being read-only would be relying on a default rather than refusing.
# - It publishes the badge the `verify` job already produced, downloaded as an artifact
# rather than regenerated. §5.1's rule holds across the job boundary: the badge, the job
# summary and the uploaded report all come from one verify run.
badge:
# `docs` as well as the two it always needed: the test-count artifact is produced there
# now, and a `badge` job that did not wait for it would download nothing and publish a
# stale count -- or fail on a race, which is the same bug on a good day.
needs: [verify, check, docs]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
# Shared, verbatim, with the `clones` job in `traffic.yml`: the two publishers of the
# `badges` branch. Without it both can fetch the same head, and the second push is rejected
# as a non-fast-forward -- which shows up as a badge that quietly stopped moving.
concurrency:
group: badges-branch
cancel-in-progress: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ctrlrun-verify-authority
path: badge
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ctrlrun-tests-badge
path: badge
- name: Publish to the badges branch
run: |
set -eu
test -s badge/verify-badge.json
message=$(python -c 'import json;print(json.load(open("badge/verify-badge.json"))["message"])')
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# `actions/checkout` configures a single-branch refspec, so a plain
# `git fetch origin badges` writes FETCH_HEAD and *not* `refs/remotes/origin/badges`
# - and `git switch badges` would then fail, fall through to a fresh orphan, and be
# rejected as a non-fast-forward. That failure appears on the **second** publish and
# not the first, so the destination ref is named explicitly.
if git fetch origin badges:refs/remotes/origin/badges 2>/dev/null; then
git switch -c badges refs/remotes/origin/badges
else
git switch --orphan badges
fi
# Nothing to clear: `git switch` replaces the working tree with the target's, and
# `--orphan` starts from an empty one. The downloaded artifact is untracked, so it
# survives either path - which is the only file that must.
test -s badge/tests-badge.json
tests=$(python -c 'import json;print(json.load(open("badge/tests-badge.json"))["message"])')
cp badge/verify-badge.json verify-badge.json
cp badge/tests-badge.json tests-badge.json
git add -f verify-badge.json tests-badge.json
git commit -m "verify: $message; tests: $tests" || { echo "badges unchanged"; exit 0; }
git push origin badges
# SPEC-v0.5 §6, T135/T135b/T137. Both adapter test modules are module-level
# `pytest.importorskip`, and neither framework nor either adapter distribution is installed
# by any other job -- so before this job existed, every test that proves a reference adapter
# passes the conformance kit skipped on every machine but its author's. That is the same
# false green as T136 skipping for a missing `build`, one file over, and it was found by an
# independent review rather than by CI, which is the point.
#
# A job of its own rather than dependencies added to `check`: these are two agent frameworks
# and their transitive trees, they are not dependencies of `ctrlrun`, and pulling them into
# the matrix would make every kernel run pay for them. §6.3's "read from what its CI actually
# ran against" is what this job makes true of the declared framework ranges.
adapters:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install the kernel, the frameworks and both adapters
# `--no-deps` for the adapters: each declares `ctrlrun>=0.5,<0.6` and the checkout is
# `0.5.0.dev0`, which pip orders *below* `0.5` -- so resolving them would reach PyPI for
# a release that does not exist yet and install it over the checkout. The kernel under
# test must be this checkout, so its dependencies are installed on the line above and
# the adapters are installed against it.
run: |
pip install --require-hashes -r requirements/adapters.txt
pip install --no-deps --no-build-isolation -e .
pip install --no-deps --no-build-isolation -e adapters/langgraph -e adapters/openai-agents
# The frameworks are installed, so a skip here is a failure and not a configuration.
# `-p no:randomly` is not used and no marker is filtered: this is the whole of both files.
- name: The adapter suites actually ran
run: |
set -eu
python -m pytest -q -rs \
tests/test_adapters_langgraph.py \
tests/test_adapters_openai_agents.py \
--junitxml=adapters.xml
python - <<'PY'
import xml.etree.ElementTree as ET
suite = ET.parse("adapters.xml").getroot()
totals = suite.find("testsuite") if suite.tag == "testsuites" else suite
tests = int(totals.get("tests", 0))
skipped = int(totals.get("skipped", 0))
print(f"{tests} adapter tests, {skipped} skipped")
# The guard this job exists for: `importorskip` at module scope turns a missing
# framework into a green run of nothing. A skip here means the install step above
# stopped working, which must be a red build and not a quiet one.
assert tests > 0, "no adapter tests were collected"
assert skipped == 0, f"{skipped} adapter tests skipped; the frameworks did not install"
PY
# The documentation audit, and the test-count badge that only a green suite may produce.
#
# The pages moved to `CTRLRun/ctrlrun-docs`, and the checks moved with them -- but what they
# check is *this* commit, so the job runs them there against a checkout of here. One
# implementation of every rule, two triggers: this job on a change to the library, and that
# repository's own CI on a change to a page. Which checkout of there is decided below: the
# branch named after this one when there is one, and `main` otherwise.
#
# Every step is a hard failure. Three of them carried `continue-on-error: true` from session
# 0 to session 6, while a baseline nobody had cleared yet would have made the job red on
# arrival and taught everyone to ignore it. The baseline is clear, so the flags are gone: a
# red step that is allowed to be red is documentation, not a guard.
docs:
# **The badge below is the reason for this dependency, and the dependency is the claim.**
# `--write-count` publishes the size of the suite, and a count produced before the suite
# ran would be the size of a run that might be red. It used to be a later *step* in the
# same job as `check.sh`; the audit lives in another repository now, so the ordering is a
# job dependency instead -- which is the stronger form, because a step's ordering is a
# convention and `needs:` is enforced.
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: ctrlrun
# The pages are rendered from this commit's docstrings and `--help`, so a commit that
# changes either has changed a page, and the two land as a pair: a branch here that
# alters a signature is accompanied by a branch of the same name there, regenerated
# against it, and each is checked against the other. `main` is checked against `main`,
# so the pages there match the library's `main` at all times, and the docs branch merges
# when the kernel branch does.
#
# Before this every branch was checked against that repository's `main`, and a page could
# not be regenerated until the commit it describes had merged, so every pull request that
# touched a docstring was red by construction -- eleven runs in a row from the first v0.10
# item -- and a check that is always red is a check nobody reads. The fallback is `main`
# and not a skip, for the reason the flags above are gone: a branch that forgot its pages
# is a red job, not a green one that verified nothing.
#
# The branch name reaches the shell through `env:`, never inline in the script, so a
# branch called `$(...)` is a string here and not a command. The comparison is exact and
# fixed-string: `ls-remote` matches a pattern against the tail of a ref, and `.` in a
# version would be a regex wildcard.
- name: Pick the ctrlrun-docs ref
id: docs
env:
BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
set -eu
if git ls-remote --heads https://github.com/CTRLRun/ctrlrun-docs.git "refs/heads/$BRANCH" \
| cut -f2 | grep -qxF "refs/heads/$BRANCH"; then
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
echo "ctrlrun-docs at $BRANCH, the branch of the same name"
else
echo "ref=main" >> "$GITHUB_OUTPUT"
echo "ctrlrun-docs at main: it has no branch named $BRANCH"
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: CTRLRun/ctrlrun-docs
ref: ${{ steps.docs.outputs.ref }}
path: ctrlrun-docs
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install
# Every extra: a runnable guide block that exports to OpenTelemetry or opens the
# gateway's client needs the extra installed, and a block that skipped would be a
# sample nobody ran. `postgres` too, for a different reason: the readiness block records
# what `pytest --collect-only` finds, and 76 tests are collected only when psycopg is
# importable. `docs.txt` therefore locks the same extras `ci.txt` does, and
# `test_the_docs_lock_covers_the_suite_the_check_job_runs` holds the two together.
run: |
pip install --require-hashes -r ctrlrun/requirements/docs.txt
pip install --no-deps --no-build-isolation -e ./ctrlrun
# `_core.py` raises when it cannot find the library, so this prints a path or the job
# stops here. A documentation check that ran against no source would be green and
# would have verified nothing.
- name: The audit can see this checkout
working-directory: ctrlrun-docs
env:
CTRLRUN_SOURCE: ${{ github.workspace }}/ctrlrun
run: python -c "import sys; sys.path.insert(0, 'tools/docs_audit'); from _core import CORE_ROOT; print(CORE_ROOT)"
# Nine generators, each with `--check`, so no rendered table, reference page, recipe
# directory, readiness block, badge row or study table can be hand-edited; then the
# runnable snippets, the forbidden-words lint and the link check.
- name: The audit
working-directory: ctrlrun-docs
env:
CTRLRUN_SOURCE: ${{ github.workspace }}/ctrlrun
run: |
set -eu
python tools/docs_audit/render_capabilities.py --check
python tools/docs_audit/render_cli.py --check
python tools/docs_audit/render_schemas.py --check
python tools/docs_audit/render_api.py --check
python tools/docs_audit/render_cookbook.py --check
python tools/docs_audit/render_probe.py --check
python tools/docs_audit/render_soak.py --check
python tools/docs_audit/render_readiness.py --check
python tools/docs_audit/render_badges.py --check
python tools/docs_audit/snippets.py
python tools/docs_audit/lint.py
python tools/docs_audit/links.py
# Uploaded from here and published by the `badge` job, which runs on a push to `main`
# and on nothing else.
- name: The test-count badge
working-directory: ctrlrun-docs
env:
CTRLRUN_SOURCE: ${{ github.workspace }}/ctrlrun
run: python tools/docs_audit/render_badges.py --write-count "$GITHUB_WORKSPACE/tests-badge.json"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ctrlrun-tests-badge
path: tests-badge.json
# A change here can make a page there wrong without anything there changing, so a push to
# `main` tells that repository to re-run its checks against this commit.
#
# **It needs `DOCS_DISPATCH_TOKEN`**, a token with `contents: write` on CTRLRun/ctrlrun-docs;
# `GITHUB_TOKEN` cannot reach another repository. Without the secret this step warns and does
# nothing, and that is a gap rather than a design -- the backstop is the weekly `schedule:`
# run in `ctrlrun-docs/.github/workflows/ci.yml`, which turns a dispatch that stopped
# arriving into a red run within a week instead of into silence.
notify-docs:
needs: [check, docs]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Tell ctrlrun-docs the library changed
env:
TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }}
run: |
set -eu
if [ -z "${TOKEN:-}" ]; then
echo "::warning::DOCS_DISPATCH_TOKEN is not set; ctrlrun-docs re-checks on its weekly schedule instead"
exit 0
fi
curl -sS -f -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/CTRLRun/ctrlrun-docs/dispatches \
-d "{\"event_type\":\"library-changed\",\"client_payload\":{\"sha\":\"$GITHUB_SHA\"}}"
# The definition of done says the demo runs with no network and the README quick start
# works verbatim. Both were only ever checked by hand, which is how a README stops being
# true. This job runs them against an installed wheel, the way a new user meets them.
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
# `SOURCE_DATE_EPOCH` is the commit's own timestamp, so a wheel built from this commit by
# anyone, anywhere, with the backend the lock names, is the same bytes. The sdist needs
# `scripts/normalize_sdist.py` for what setuptools records from the scratch directory
# (copy times, uid, filesystem order). The step after builds a second time and compares:
# "reproducible" is a claim until something checks it.
- name: Build
run: |
pip install --require-hashes -r requirements/build.txt
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
python -m build --no-isolation
python scripts/normalize_sdist.py dist/*.tar.gz
- name: The build is reproducible
run: |
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
python -m build --no-isolation --outdir /tmp/again
python scripts/normalize_sdist.py /tmp/again/*.tar.gz
(cd dist && sha256sum ./*) > /tmp/first.sha256
(cd /tmp/again && sha256sum ./*) | diff /tmp/first.sha256 -
echo "both builds produced:"; cat /tmp/first.sha256
- name: Metadata renders on PyPI
run: twine check --strict dist/*
- name: The sdist can run its own tests
# An sdist that ships tests it cannot run reads as a broken release to anyone
# packaging it downstream. This is the guard; MANIFEST.in is the fix.
run: |
mkdir -p /tmp/sdist && tar xzf dist/*.tar.gz -C /tmp/sdist --strip-components=1
pip install --require-hashes -r requirements/ci.txt
cd /tmp/sdist
pip install --no-deps --no-build-isolation -e .
pytest -q
# An SBOM of the wheel, generated the same way the release generates it, so a change that
# would ship a wrong one goes red on the pull request rather than at the tag. The
# assertions live in `tests/test_sbom.py`; this is the half that proves the script runs in
# a clean checkout with only its own pinned requirements installed.
- name: The SBOM of the wheel
run: |
pip install --require-hashes -r requirements/sbom.txt
./scripts/sbom.sh dist/*.whl /tmp/ctrlrun.cdx.json
python -c "
import json, sys
bom = json.load(open('/tmp/ctrlrun.cdx.json'))
names = sorted(c['name'] for c in bom.get('components', []))
assert names == ['PyYAML', 'click'], names
print('sbom:', bom['metadata']['component']['name'], bom['specVersion'], names)
"
- name: The quick start works from the wheel
run: |
python -m venv /tmp/qs
/tmp/qs/bin/pip install dist/*.whl
mkdir -p /tmp/qs-run && cd /tmp/qs-run
timeout 60 /tmp/qs/bin/ctrlrun demo
/tmp/qs/bin/ctrlrun init
test -f ctrlrun.yaml
dco:
# Every commit on a pull request carries a `Signed-off-by:` trailer naming its author's
# email: the Developer Certificate of Origin, per CONTRIBUTING.md. A shell loop rather than
# the DCO app, so the rule lives in the repository, pinned like everything else, with
# nothing to install and nothing that can be quietly uninstalled. Only git's own trailer
# block counts, so a sentence in the body that happens to mention the trailer does not.
# Nobody is exempt, bots included: an exemption keyed on a name or an email is a string
# anyone can set. Dependabot does sign its commits off, as `support@github.com` while
# authoring from its noreply address, so its trailer never matches its author. The one
# thing about such a commit that is not a string anyone can set is GitHub's signature on
# it, so a sign-off under another address is accepted on that signature and on nothing
# else: `verification.verified` is GitHub reporting that it signed the commit itself,
# which only a commit made through GitHub (an App such as Dependabot, or the web editor)
# carries.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Every commit is signed off by its author
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
GH_TOKEN: ${{ github.token }}
run: |
missing=0
for sha in $(git rev-list --no-merges "$BASE..$HEAD"); do
name="$(git log -1 --format=%an "$sha")"
email="$(git log -1 --format=%ae "$sha")"
# `only` restricts the output to the trailer block git itself recognises, and
# `valueonly` leaves the `Name <email>` part; the value has to end in the author's
# email, angle brackets included.
trailers="$(git log -1 --format='%(trailers:key=Signed-off-by,valueonly,only)' "$sha")"
if printf '%s\n' "$trailers" \
| awk -v want="<$email>" 'substr($0, length($0) - length(want) + 1) == want { found = 1 } END { exit !found }'; then
continue
fi
# A sign-off under another address, on a commit GitHub signed itself. See the job
# comment: the signature is the only fact here that is not a string anyone can set.
if [ -n "$trailers" ] && [ "$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha" --jq '.commit.verification.verified')" = "true" ]; then
echo "$sha: signed off as '$trailers' by $name <$email>; accepted on GitHub's signature"
continue
fi
echo "::error::$sha ($(git log -1 --format=%s "$sha")) has no 'Signed-off-by: $name <$email>' trailer. Add one with 'git commit --amend -s', or to every commit on the branch with 'git rebase --signoff $BASE'."
missing=1
done
[ "$missing" -eq 0 ]