sayfirst-cli 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sayfirst_cli-0.2.0/.github/workflows/ci.yml +233 -0
- sayfirst_cli-0.2.0/.github/workflows/release.yml +213 -0
- sayfirst_cli-0.2.0/.gitignore +8 -0
- sayfirst_cli-0.2.0/CHANGELOG.md +44 -0
- sayfirst_cli-0.2.0/LICENSE +202 -0
- sayfirst_cli-0.2.0/NOTICE +13 -0
- sayfirst_cli-0.2.0/PKG-INFO +297 -0
- sayfirst_cli-0.2.0/QUICKSTART.md +345 -0
- sayfirst_cli-0.2.0/README.md +276 -0
- sayfirst_cli-0.2.0/SECURITY.md +176 -0
- sayfirst_cli-0.2.0/TRADEMARKS.md +49 -0
- sayfirst_cli-0.2.0/docs/EVIDENCE-SURFACE.md +102 -0
- sayfirst_cli-0.2.0/docs/PACKS.md +187 -0
- sayfirst_cli-0.2.0/docs/PARTITION.md +217 -0
- sayfirst_cli-0.2.0/docs/PROVENANCE.md +80 -0
- sayfirst_cli-0.2.0/docs/publication-checklist.md +142 -0
- sayfirst_cli-0.2.0/pyproject.toml +76 -0
- sayfirst_cli-0.2.0/scripts/check_dependency_closure.py +425 -0
- sayfirst_cli-0.2.0/scripts/check_developer_certificate_of_origin.py +111 -0
- sayfirst_cli-0.2.0/scripts/gate.sh +215 -0
- sayfirst_cli-0.2.0/scripts/release_version.py +95 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/__init__.py +2 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/approvals.py +127 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/ask.py +167 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/evidence.py +668 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/exit_codes.py +88 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/explain.py +35 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/__init__.py +14 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/commands.py +191 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/engine.py +396 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/harness.py +1162 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/launch.py +417 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/manifest.py +409 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/verify.py +553 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/main.py +78 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/__init__.py +10 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/NOTE.md +4 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/interpose.py +85 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/pack.toml +13 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/NOTE.md +4 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/interpose.py +72 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/pack.toml +13 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/NOTE.md +4 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/interpose.py +90 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/pack.toml +13 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/packs_cmd.py +115 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/pages.py +87 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/reads.py +254 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/render.py +138 -0
- sayfirst_cli-0.2.0/src/sayfirst_cli/trace.py +88 -0
- sayfirst_cli-0.2.0/tests/canned_daemon.py +177 -0
- sayfirst_cli-0.2.0/tests/conftest.py +95 -0
- sayfirst_cli-0.2.0/tests/contract_absence.py +183 -0
- sayfirst_cli-0.2.0/tests/documents.py +259 -0
- sayfirst_cli-0.2.0/tests/governed_programs.py +329 -0
- sayfirst_cli-0.2.0/tests/pack_doubles.py +178 -0
- sayfirst_cli-0.2.0/tests/replies.py +58 -0
- sayfirst_cli-0.2.0/tests/test_approvals.py +242 -0
- sayfirst_cli-0.2.0/tests/test_ask_connection_loss.py +84 -0
- sayfirst_cli-0.2.0/tests/test_ask_end_to_end.py +233 -0
- sayfirst_cli-0.2.0/tests/test_boundary_runtime.py +16 -0
- sayfirst_cli-0.2.0/tests/test_changelog_names_the_version.py +78 -0
- sayfirst_cli-0.2.0/tests/test_contract_absence.py +227 -0
- sayfirst_cli-0.2.0/tests/test_database_pack.py +260 -0
- sayfirst_cli-0.2.0/tests/test_decided_name.py +193 -0
- sayfirst_cli-0.2.0/tests/test_dependency_closure.py +170 -0
- sayfirst_cli-0.2.0/tests/test_developer_certificate_of_origin.py +147 -0
- sayfirst_cli-0.2.0/tests/test_engine.py +511 -0
- sayfirst_cli-0.2.0/tests/test_engine_is_agnostic.py +732 -0
- sayfirst_cli-0.2.0/tests/test_evidence_export_and_exports.py +678 -0
- sayfirst_cli-0.2.0/tests/test_evidence_history_and_audit.py +390 -0
- sayfirst_cli-0.2.0/tests/test_evidence_reads.py +323 -0
- sayfirst_cli-0.2.0/tests/test_exit_codes.py +67 -0
- sayfirst_cli-0.2.0/tests/test_gate_modes.py +392 -0
- sayfirst_cli-0.2.0/tests/test_governed_run_leaves_no_trace.py +156 -0
- sayfirst_cli-0.2.0/tests/test_http_client_pack.py +294 -0
- sayfirst_cli-0.2.0/tests/test_instrument_run.py +868 -0
- sayfirst_cli-0.2.0/tests/test_instrument_verify.py +2090 -0
- sayfirst_cli-0.2.0/tests/test_manifest.py +442 -0
- sayfirst_cli-0.2.0/tests/test_offline_audit_vectors.py +368 -0
- sayfirst_cli-0.2.0/tests/test_pack_doubles.py +54 -0
- sayfirst_cli-0.2.0/tests/test_packs_cmd.py +282 -0
- sayfirst_cli-0.2.0/tests/test_packs_shipped.py +366 -0
- sayfirst_cli-0.2.0/tests/test_partition_boundary.py +225 -0
- sayfirst_cli-0.2.0/tests/test_pointers_survive_publication.py +157 -0
- sayfirst_cli-0.2.0/tests/test_public_vocabulary.py +384 -0
- sayfirst_cli-0.2.0/tests/test_publication_checklist_names_every_act.py +329 -0
- sayfirst_cli-0.2.0/tests/test_quickstart_names_what_exists.py +113 -0
- sayfirst_cli-0.2.0/tests/test_read_rendering.py +74 -0
- sayfirst_cli-0.2.0/tests/test_reads.py +315 -0
- sayfirst_cli-0.2.0/tests/test_release_workflow_publishes_only_on_a_tag.py +660 -0
- sayfirst_cli-0.2.0/tests/test_running_the_module.py +165 -0
- sayfirst_cli-0.2.0/tests/test_shipped_wheel_notices.py +76 -0
- sayfirst_cli-0.2.0/tests/test_source_distribution_notices.py +74 -0
- sayfirst_cli-0.2.0/tests/test_spdx_identifiers.py +66 -0
- sayfirst_cli-0.2.0/tests/test_subprocess_pack.py +290 -0
- sayfirst_cli-0.2.0/tests/test_trace_and_explain.py +408 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# The gate, run by a machine. Every check it runs is `scripts/gate.sh`, with one
|
|
4
|
+
# stated exception below — the sign-off check, which runs on a pull request and
|
|
5
|
+
# reads the commits of the change against the ref it started from, a range no
|
|
6
|
+
# run on a machine has to be given. Apart from it, what a contributor runs
|
|
7
|
+
# locally is what decides a merge; a step that exists only here is a step nobody
|
|
8
|
+
# can reproduce, and `tests/test_gate_modes.py` fails if this file grows a
|
|
9
|
+
# second one. The rest of what is written below decides which of the gate's two
|
|
10
|
+
# green outcomes this run reached and renders it, which is a rendering job
|
|
11
|
+
# rather than a check.
|
|
12
|
+
#
|
|
13
|
+
# This workflow publishes nothing. `release.yml` is the only file here that can,
|
|
14
|
+
# and only on a tag; a publish step added here is a constitutional change, not a
|
|
15
|
+
# convenience.
|
|
16
|
+
#
|
|
17
|
+
# WHERE THIS WORKFLOW READS THE CONTRACT, AND WHY IT NEEDS NOTHING TO DO IT.
|
|
18
|
+
# `scripts/gate.sh` builds `sayfirst-contract` from a source checkout rather
|
|
19
|
+
# than installing it, so that what the gate reports on is the source the tag
|
|
20
|
+
# this client pins actually names; the dependency-closure guard of articles 13
|
|
21
|
+
# and 14 measures a real install, and does it separately. This file only says
|
|
22
|
+
# where that checkout comes from. It is the control plane's PUBLIC repository,
|
|
23
|
+
# at the tag this client's own pin names, and it carries NO credential: nothing
|
|
24
|
+
# in this file can reach
|
|
25
|
+
# anything a stranger reading it cannot reach too. That is article 16's « a fork
|
|
26
|
+
# can build, test and contribute with nothing but this repository » held as a
|
|
27
|
+
# file rather than promised in a sentence, and it is what publication changed
|
|
28
|
+
# here — while the contract's repository was not open, this step needed a read
|
|
29
|
+
# credential, and a fork that held none got the reduced leg below and nothing
|
|
30
|
+
# else. The tag is `v` followed by the pinned contract version, read out of
|
|
31
|
+
# `pyproject.toml` by the step below. `release.yml` reads the same pin with the
|
|
32
|
+
# same script, and `tests/test_gate_modes.py` requires the two to be the same
|
|
33
|
+
# text: neither file spells the version, because a version spelled twice is a
|
|
34
|
+
# version that will eventually be spelled two ways.
|
|
35
|
+
#
|
|
36
|
+
# WHY A REDUCED LEG STILL RUNS, AND NOW RUNS ON EVERY RUN. A machine that cannot
|
|
37
|
+
# reach that repository still exists — a fork with no network, a runner behind a
|
|
38
|
+
# proxy, a contributor offline — and what it gets is a contract that is simply
|
|
39
|
+
# absent. An absent contract is a fact about the world, not a crash. Article 2
|
|
40
|
+
# says an absence is "never rendered as a negative fact, a zero or a healthy
|
|
41
|
+
# state", so `scripts/gate.sh` has three outcomes and this workflow renders
|
|
42
|
+
# three and not two:
|
|
43
|
+
#
|
|
44
|
+
# gate (full) the contract was built from the control
|
|
45
|
+
# plane's public repository, at the tag this
|
|
46
|
+
# client pins, and every check ran.
|
|
47
|
+
# `scripts/gate.sh` exits 0.
|
|
48
|
+
# gate (reduced — contract no checkout of the control plane was made,
|
|
49
|
+
# absent, some checks not run) so the contract is absent. Format, lint and
|
|
50
|
+
# every test that does not import the
|
|
51
|
+
# contract ran; the rest are named and
|
|
52
|
+
# counted in the job's summary and in an
|
|
53
|
+
# annotation. `scripts/gate.sh` exits 75. The
|
|
54
|
+
# tick is green and the job says, in its own
|
|
55
|
+
# name, what it did not prove.
|
|
56
|
+
# failure anything else — including a full job whose
|
|
57
|
+
# gate came back reduced, and a reduced job
|
|
58
|
+
# whose gate came back full.
|
|
59
|
+
#
|
|
60
|
+
# The two legs run side by side now, rather than one of them instead of the
|
|
61
|
+
# other. Which leg ran used to be decided by whether this repository held a
|
|
62
|
+
# credential, and that question no longer exists; what is left is a leg that
|
|
63
|
+
# proves the tree against the contract and a leg that proves the reduced path
|
|
64
|
+
# still renders honestly on a machine that has nothing, which is the machine
|
|
65
|
+
# article 16 is about. A reduced run proves nothing about the contract and
|
|
66
|
+
# nothing about the installed dependency closure of articles 13 and 14, and
|
|
67
|
+
# today nothing about this client against a daemon either — the job lists what
|
|
68
|
+
# it did not run, so that sentence is checkable against the run rather than
|
|
69
|
+
# trusted from here.
|
|
70
|
+
name: gate
|
|
71
|
+
|
|
72
|
+
on:
|
|
73
|
+
push:
|
|
74
|
+
pull_request:
|
|
75
|
+
|
|
76
|
+
permissions:
|
|
77
|
+
contents: read
|
|
78
|
+
|
|
79
|
+
jobs:
|
|
80
|
+
full:
|
|
81
|
+
name: gate (full)
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v5
|
|
85
|
+
with:
|
|
86
|
+
path: sayfirst-cli
|
|
87
|
+
|
|
88
|
+
- uses: astral-sh/setup-uv@v5
|
|
89
|
+
with:
|
|
90
|
+
version: "0.12.5"
|
|
91
|
+
|
|
92
|
+
# Read out of the project file, never spelled here: `scripts/gate.sh`
|
|
93
|
+
# reads the same pin out of the same file, and `release.yml` derives the
|
|
94
|
+
# same tag with this same script — held as one text by
|
|
95
|
+
# `tests/test_gate_modes.py`, because two readings of one rule is how two
|
|
96
|
+
# files stop agreeing about it. Shell rather than an interpreter, so that
|
|
97
|
+
# the only program of this repository this job runs is the gate.
|
|
98
|
+
#
|
|
99
|
+
# THE ASSUMPTION THIS DERIVATION RESTS ON, stated rather than left to be
|
|
100
|
+
# discovered. One tag of the control plane is checked out here, and this
|
|
101
|
+
# client pins TWO of the distributions that tag builds. The tag is
|
|
102
|
+
# derived from the `sayfirst-contract` pin because the control plane moves
|
|
103
|
+
# every distribution it publishes to one version together — its own rule,
|
|
104
|
+
# held by its own guard, and by nothing in this repository. So the step
|
|
105
|
+
# reads both pins and refuses when they disagree: two pins that name two
|
|
106
|
+
# versions name no tag at all, and the honest answer is to say so here
|
|
107
|
+
# rather than to check one of them out and let the other pin fail later.
|
|
108
|
+
- name: The contract version this client pins
|
|
109
|
+
id: contract
|
|
110
|
+
working-directory: sayfirst-cli
|
|
111
|
+
run: |
|
|
112
|
+
pinned="$(sed -n 's/.*"sayfirst-contract==\([^"]*\)".*/\1/p' pyproject.toml | head -n 1)"
|
|
113
|
+
boundary="$(sed -n 's/.*"sayfirst-boundary==\([^"]*\)".*/\1/p' pyproject.toml | head -n 1)"
|
|
114
|
+
if [ -z "$pinned" ]; then
|
|
115
|
+
echo "::error::pyproject.toml pins no version of sayfirst-contract, so there is no ref to read the contract at"
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
if [ -z "$boundary" ]; then
|
|
119
|
+
echo "::error::pyproject.toml pins no version of sayfirst-boundary, and one tag of the control plane has to carry both"
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
if [ "$pinned" != "$boundary" ]; then
|
|
123
|
+
echo "::error::the pins disagree — sayfirst-contract ${pinned}, sayfirst-boundary ${boundary} — and one tag of the control plane carries one version of both, so there is no ref to read them at"
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
echo "tag=v${pinned}" >> "$GITHUB_OUTPUT"
|
|
127
|
+
echo "The contract is read at v${pinned}." >> "$GITHUB_STEP_SUMMARY"
|
|
128
|
+
|
|
129
|
+
- uses: actions/checkout@v5
|
|
130
|
+
with:
|
|
131
|
+
repository: fredaime/sayfirst-control-plane
|
|
132
|
+
ref: ${{ steps.contract.outputs.tag }}
|
|
133
|
+
path: sayfirst-control-plane
|
|
134
|
+
fetch-depth: 0
|
|
135
|
+
|
|
136
|
+
- name: gate
|
|
137
|
+
working-directory: sayfirst-cli
|
|
138
|
+
env:
|
|
139
|
+
SAYFIRST_CONTRACT_SOURCE: ${{ github.workspace }}/sayfirst-control-plane
|
|
140
|
+
SAYFIRST_CONTRACT_REF: ${{ steps.contract.outputs.tag }}
|
|
141
|
+
run: |
|
|
142
|
+
status=0
|
|
143
|
+
./scripts/gate.sh || status=$?
|
|
144
|
+
case "$status" in
|
|
145
|
+
0)
|
|
146
|
+
echo "### gate: full green" >> "$GITHUB_STEP_SUMMARY"
|
|
147
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
148
|
+
echo "Format, lint, every test and the dependency-closure guard of articles 13" >> "$GITHUB_STEP_SUMMARY"
|
|
149
|
+
echo "and 14 ran against a contract built from the control plane's public" >> "$GITHUB_STEP_SUMMARY"
|
|
150
|
+
echo "repository, at the tag this client pins." >> "$GITHUB_STEP_SUMMARY"
|
|
151
|
+
;;
|
|
152
|
+
75)
|
|
153
|
+
echo "::error::the control plane was checked out and the gate still came back reduced: the contract source was not there"
|
|
154
|
+
exit 1
|
|
155
|
+
;;
|
|
156
|
+
*)
|
|
157
|
+
exit "$status"
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
160
|
+
|
|
161
|
+
reduced:
|
|
162
|
+
name: gate (reduced — contract absent, some checks not run)
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
steps:
|
|
165
|
+
- uses: actions/checkout@v5
|
|
166
|
+
with:
|
|
167
|
+
path: sayfirst-cli
|
|
168
|
+
|
|
169
|
+
- uses: astral-sh/setup-uv@v5
|
|
170
|
+
with:
|
|
171
|
+
version: "0.12.5"
|
|
172
|
+
|
|
173
|
+
# No second checkout, and that is the whole point of this leg: it is the
|
|
174
|
+
# machine that has nothing. `SAYFIRST_CONTRACT_SOURCE` is left unset so
|
|
175
|
+
# that the gate's own default finds nothing and says so, which is what a
|
|
176
|
+
# fork with no network meets.
|
|
177
|
+
- name: gate (reduced)
|
|
178
|
+
working-directory: sayfirst-cli
|
|
179
|
+
run: |
|
|
180
|
+
set +e
|
|
181
|
+
./scripts/gate.sh 2>&1 | tee gate.log
|
|
182
|
+
status=${PIPESTATUS[0]}
|
|
183
|
+
set -e
|
|
184
|
+
case "$status" in
|
|
185
|
+
75)
|
|
186
|
+
headline=$(grep -m1 '^gate: contract absent:' gate.log | sed 's/^gate: //')
|
|
187
|
+
{
|
|
188
|
+
echo "### gate (reduced): green, and it proves less than a full gate"
|
|
189
|
+
echo ""
|
|
190
|
+
echo "No checkout of the control plane was made on this runner, so"
|
|
191
|
+
echo "\`sayfirst-contract\` was not built. **${headline}**"
|
|
192
|
+
echo ""
|
|
193
|
+
grep '^gate: not run: ' gate.log | sed 's/^gate: not run: /- not run: /'
|
|
194
|
+
echo ""
|
|
195
|
+
echo "This run proves that the source is formatted, that it lints, and that the"
|
|
196
|
+
echo "guards reading only this repository's own files hold. It proves nothing"
|
|
197
|
+
echo "about the contract, nothing about this client against a daemon, and nothing"
|
|
198
|
+
echo "about the installed dependency closure of articles 13 and 14."
|
|
199
|
+
echo ""
|
|
200
|
+
echo "What would make it full: point \`SAYFIRST_CONTRACT_SOURCE\` at a checkout"
|
|
201
|
+
echo "of the control plane's public repository, which needs no credential and is"
|
|
202
|
+
echo "exactly what the full leg of this workflow does."
|
|
203
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
204
|
+
echo "::warning::${headline} — this is a reduced gate, not a full one"
|
|
205
|
+
;;
|
|
206
|
+
0)
|
|
207
|
+
echo "::error::the gate reported a full run with no contract source; the reduced job cannot vouch for that"
|
|
208
|
+
exit 1
|
|
209
|
+
;;
|
|
210
|
+
*)
|
|
211
|
+
exit "$status"
|
|
212
|
+
;;
|
|
213
|
+
esac
|
|
214
|
+
|
|
215
|
+
# Article 15 asks every contributor to sign off each commit, and article 16
|
|
216
|
+
# calls this check required. It is the one check here that is not
|
|
217
|
+
# `scripts/gate.sh`: it reads the commits of the change against the ref the
|
|
218
|
+
# change started from, and a run on a machine has no such range to be given.
|
|
219
|
+
# The whole history is checked out for the same reason a shallow one is
|
|
220
|
+
# refused — a range that cannot be read answers 2, and 2 is not a pass.
|
|
221
|
+
developer-certificate-of-origin:
|
|
222
|
+
name: sign-off on every commit
|
|
223
|
+
if: github.event_name == 'pull_request'
|
|
224
|
+
runs-on: ubuntu-latest
|
|
225
|
+
steps:
|
|
226
|
+
- uses: actions/checkout@v5
|
|
227
|
+
with:
|
|
228
|
+
fetch-depth: 0
|
|
229
|
+
- name: Every commit certifies the Developer Certificate of Origin
|
|
230
|
+
run: >-
|
|
231
|
+
python scripts/check_developer_certificate_of_origin.py
|
|
232
|
+
--base=${{ github.event.pull_request.base.sha }}
|
|
233
|
+
--head=${{ github.event.pull_request.head.sha }}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
name: release
|
|
3
|
+
|
|
4
|
+
# The only file in this repository that can publish anything.
|
|
5
|
+
#
|
|
6
|
+
# Article 0 forbids publishing under the decided name — no package on an index,
|
|
7
|
+
# no public repository, no announcement — until the marks are filed. So the act
|
|
8
|
+
# that starts a publication is the operator's tag and nothing else: there is no
|
|
9
|
+
# `branches:` under `push` and no `pull_request:` here, and
|
|
10
|
+
# `tests/test_release_workflow_publishes_only_on_a_tag.py` fails if either
|
|
11
|
+
# arrives. A tag naming a version this distribution does not carry is refused
|
|
12
|
+
# by name in the build and publishes nothing.
|
|
13
|
+
#
|
|
14
|
+
# The dry run exists so the mechanics are proven BEFORE the marks rather than
|
|
15
|
+
# discovered after them: started by hand with publishing switched off, it runs
|
|
16
|
+
# the gate, builds the distribution and checks the artefact, and stops.
|
|
17
|
+
#
|
|
18
|
+
# WHERE THIS WORKFLOW READS THE CONTRACT, AND WHY IT IS NOT WHERE `ci.yml`
|
|
19
|
+
# READS IT. The gate builds `sayfirst-contract` from a checkout of the control
|
|
20
|
+
# plane rather than installing it from an index. `ci.yml` takes that checkout
|
|
21
|
+
# from the repository this project has not opened yet, and carries a read
|
|
22
|
+
# credential to reach it. A release is different, because a release happens
|
|
23
|
+
# after publication and in a fixed order: the contract is published before the
|
|
24
|
+
# client that pins it. So this workflow reads the PUBLIC control plane,
|
|
25
|
+
# `fredaime/sayfirst-control-plane`, at the tag the client's own pin names, and
|
|
26
|
+
# carries NO credential — nothing here can reach a repository a stranger
|
|
27
|
+
# cannot. The tag is `v` followed by the pinned contract version, read out of
|
|
28
|
+
# `pyproject.toml` by the step below: the version is not spelled a second time
|
|
29
|
+
# anywhere in this file, because a version spelled twice is a version that will
|
|
30
|
+
# eventually be spelled two ways.
|
|
31
|
+
#
|
|
32
|
+
# One consequence, written here rather than met in a red run. BEFORE the
|
|
33
|
+
# contract is published, NEITHER path of this workflow can finish on a runner:
|
|
34
|
+
# the public repository does not exist, so the second checkout fails and the
|
|
35
|
+
# gate never starts. That is the honest failure — a release checked against
|
|
36
|
+
# nothing is not a release — and it is why the dry run is proven on the
|
|
37
|
+
# operator's own machine instead, where the version reader, the build and the
|
|
38
|
+
# artefact check run against a local checkout of the control plane. The first
|
|
39
|
+
# green run of this file in CI is therefore a run after the contract is
|
|
40
|
+
# published, which is also the day the second checkout becomes an ordinary
|
|
41
|
+
# install and article 16's « a fork can build, test and contribute with nothing
|
|
42
|
+
# but this repository » is met.
|
|
43
|
+
#
|
|
44
|
+
# A reduced gate is never a release. `scripts/gate.sh` has three outcomes and
|
|
45
|
+
# not two: 75 means the contract was absent and some checks did not run. A
|
|
46
|
+
# release cut from that would be a release nothing had checked against the
|
|
47
|
+
# contract, so the gate step below reads the status and refuses it by name.
|
|
48
|
+
#
|
|
49
|
+
# The `pypi` environment must require a reviewer. That is a setting of the
|
|
50
|
+
# repository — switched on the environment's own settings page, and readable by
|
|
51
|
+
# no file here: this workflow names the environment, and the operator's
|
|
52
|
+
# publication checklist carries this setting. No token is stored anywhere
|
|
53
|
+
# below — the index verifies this workflow's own identity.
|
|
54
|
+
#
|
|
55
|
+
# Every interpreter here is uv's, never the runner's system `python`: this
|
|
56
|
+
# project's own `uv run` cannot resolve the contract packages from an index, so
|
|
57
|
+
# the two version-reader lines pass `--no-project` and name the interpreter,
|
|
58
|
+
# and the reader needs nothing beyond the standard library's TOML module.
|
|
59
|
+
|
|
60
|
+
on:
|
|
61
|
+
push:
|
|
62
|
+
tags: ["v*"]
|
|
63
|
+
workflow_dispatch:
|
|
64
|
+
inputs:
|
|
65
|
+
publish:
|
|
66
|
+
description: "Leave false for a dry run: build and check, publish nothing."
|
|
67
|
+
type: boolean
|
|
68
|
+
default: false
|
|
69
|
+
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
build:
|
|
75
|
+
name: build and check the distribution
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v5
|
|
79
|
+
with:
|
|
80
|
+
fetch-depth: 0
|
|
81
|
+
path: sayfirst-cli
|
|
82
|
+
|
|
83
|
+
- uses: astral-sh/setup-uv@v5
|
|
84
|
+
with:
|
|
85
|
+
version: "0.12.5"
|
|
86
|
+
|
|
87
|
+
# The switch is a statement of intent, and this step is what keeps it from
|
|
88
|
+
# being read as a permission. What actually refuses to publish from a
|
|
89
|
+
# dispatch is the `publish` job's own condition, further down; an operator
|
|
90
|
+
# who switches this on is told where publication comes from rather than
|
|
91
|
+
# left with a run that quietly ignored them.
|
|
92
|
+
- name: Publication comes from a tag, not from this button
|
|
93
|
+
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
|
94
|
+
run: |
|
|
95
|
+
echo "::error::a dispatch publishes nothing: re-run it with publish false to dry-run, and publish by tagging"
|
|
96
|
+
exit 1
|
|
97
|
+
|
|
98
|
+
- name: The tag names the version this distribution carries
|
|
99
|
+
if: github.event_name == 'push'
|
|
100
|
+
working-directory: sayfirst-cli
|
|
101
|
+
run: uv run --no-project --python 3.12 python scripts/release_version.py --expect "${GITHUB_REF_NAME#v}"
|
|
102
|
+
|
|
103
|
+
- name: The distribution carries a version
|
|
104
|
+
if: github.event_name == 'workflow_dispatch'
|
|
105
|
+
working-directory: sayfirst-cli
|
|
106
|
+
run: uv run --no-project --python 3.12 python scripts/release_version.py
|
|
107
|
+
|
|
108
|
+
# Read out of the project file, never spelled here: `scripts/gate.sh`
|
|
109
|
+
# reads the same pin out of the same file for the same reason. Shell
|
|
110
|
+
# rather than an interpreter, so that the only lines of this workflow
|
|
111
|
+
# that run a program of this repository are the two above.
|
|
112
|
+
#
|
|
113
|
+
# THE ASSUMPTION THIS DERIVATION RESTS ON, stated rather than left to be
|
|
114
|
+
# discovered. One tag of the control plane is checked out here, and this
|
|
115
|
+
# client pins TWO of the distributions that tag builds. The tag is
|
|
116
|
+
# derived from the `sayfirst-contract` pin because the control plane moves
|
|
117
|
+
# every distribution it publishes to one version together — its own rule,
|
|
118
|
+
# held by its own guard, and by nothing in this repository. So the step
|
|
119
|
+
# reads both pins and refuses when they disagree: two pins that name two
|
|
120
|
+
# versions name no tag at all, and the honest answer is to say so here
|
|
121
|
+
# rather than to check one of them out and let the other pin fail later.
|
|
122
|
+
- name: The contract version this client pins
|
|
123
|
+
id: contract
|
|
124
|
+
working-directory: sayfirst-cli
|
|
125
|
+
run: |
|
|
126
|
+
pinned="$(sed -n 's/.*"sayfirst-contract==\([^"]*\)".*/\1/p' pyproject.toml | head -n 1)"
|
|
127
|
+
boundary="$(sed -n 's/.*"sayfirst-boundary==\([^"]*\)".*/\1/p' pyproject.toml | head -n 1)"
|
|
128
|
+
if [ -z "$pinned" ]; then
|
|
129
|
+
echo "::error::pyproject.toml pins no version of sayfirst-contract, so there is no ref to read the contract at"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
if [ -z "$boundary" ]; then
|
|
133
|
+
echo "::error::pyproject.toml pins no version of sayfirst-boundary, and one tag of the control plane has to carry both"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
if [ "$pinned" != "$boundary" ]; then
|
|
137
|
+
echo "::error::the pins disagree — sayfirst-contract ${pinned}, sayfirst-boundary ${boundary} — and one tag of the control plane carries one version of both, so there is no ref to read them at"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo "tag=v${pinned}" >> "$GITHUB_OUTPUT"
|
|
141
|
+
echo "The contract is read at v${pinned}." >> "$GITHUB_STEP_SUMMARY"
|
|
142
|
+
|
|
143
|
+
- uses: actions/checkout@v5
|
|
144
|
+
with:
|
|
145
|
+
fetch-depth: 0
|
|
146
|
+
repository: fredaime/sayfirst-control-plane
|
|
147
|
+
ref: ${{ steps.contract.outputs.tag }}
|
|
148
|
+
path: sayfirst-control-plane
|
|
149
|
+
|
|
150
|
+
- name: gate
|
|
151
|
+
id: gate
|
|
152
|
+
working-directory: sayfirst-cli
|
|
153
|
+
env:
|
|
154
|
+
SAYFIRST_CONTRACT_SOURCE: ${{ github.workspace }}/sayfirst-control-plane
|
|
155
|
+
SAYFIRST_CONTRACT_REF: ${{ steps.contract.outputs.tag }}
|
|
156
|
+
run: |
|
|
157
|
+
status=0
|
|
158
|
+
./scripts/gate.sh || status=$?
|
|
159
|
+
case "$status" in
|
|
160
|
+
0)
|
|
161
|
+
echo "### gate: full green" >> "$GITHUB_STEP_SUMMARY"
|
|
162
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
163
|
+
echo "Format, lint, every test and the dependency-closure guard of articles 13" >> "$GITHUB_STEP_SUMMARY"
|
|
164
|
+
echo "and 14 ran against the contract this release pins." >> "$GITHUB_STEP_SUMMARY"
|
|
165
|
+
;;
|
|
166
|
+
75)
|
|
167
|
+
echo "::error::the gate came back reduced: a release is never cut from a reduced gate"
|
|
168
|
+
exit 1
|
|
169
|
+
;;
|
|
170
|
+
*)
|
|
171
|
+
exit "$status"
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
|
|
175
|
+
- name: Build the distribution
|
|
176
|
+
working-directory: sayfirst-cli
|
|
177
|
+
run: uv build --out-dir dist
|
|
178
|
+
# The checker is pinned, as the control plane's is and to the same version:
|
|
179
|
+
# an unpinned checker is a check whose rules can change between two
|
|
180
|
+
# releases without either release changing.
|
|
181
|
+
- name: The built artefacts are well formed
|
|
182
|
+
working-directory: sayfirst-cli
|
|
183
|
+
run: uvx twine@7.0.0 check dist/*
|
|
184
|
+
- name: What this run built
|
|
185
|
+
working-directory: sayfirst-cli
|
|
186
|
+
run: ls -l dist/ >> "$GITHUB_STEP_SUMMARY"
|
|
187
|
+
|
|
188
|
+
- uses: actions/upload-artifact@v4
|
|
189
|
+
with:
|
|
190
|
+
name: distributions
|
|
191
|
+
path: sayfirst-cli/dist/
|
|
192
|
+
|
|
193
|
+
publish:
|
|
194
|
+
name: publish to the index
|
|
195
|
+
needs: build
|
|
196
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
197
|
+
runs-on: ubuntu-latest
|
|
198
|
+
environment:
|
|
199
|
+
name: pypi
|
|
200
|
+
permissions:
|
|
201
|
+
id-token: write
|
|
202
|
+
steps:
|
|
203
|
+
- uses: actions/download-artifact@v4
|
|
204
|
+
with:
|
|
205
|
+
name: distributions
|
|
206
|
+
path: dist/
|
|
207
|
+
# Pinned to a release, as the uv action and the artefact checker above
|
|
208
|
+
# are: the step that hands a distribution to the index is the last thing
|
|
209
|
+
# standing between a build and the world, and a moving ref is a step
|
|
210
|
+
# whose rules can change between two releases without either release
|
|
211
|
+
# changing — the change arriving as a red release, or as a published
|
|
212
|
+
# artefact, rather than as a diff somebody reviewed.
|
|
213
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.2
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
# Changelog
|
|
3
|
+
|
|
4
|
+
Every release of this client carries a section here, named for the version the project file
|
|
5
|
+
declares; a release with no section fails the gate.
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
- Every commit of a change is read for its sign-off on every pull request, and one that carries
|
|
10
|
+
none is refused by name. A range that cannot be read is refused too, rather than reported as
|
|
11
|
+
passing.
|
|
12
|
+
- The publication checklist carries every act the constitution names, in the order they are
|
|
13
|
+
performed, and a guard fails when one of them stops being a line of it. The security policy says
|
|
14
|
+
which of article 0's two paths this repository is published by: a fresh repository, never a
|
|
15
|
+
change of visibility.
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
- The evidence surface reads and verifies: `history` pages a scope, `audit` puts the served
|
|
20
|
+
verdict beside a local check and names a finding wherever the two disagree, `export` saves a
|
|
21
|
+
bundle a third party verifies offline with the contract distribution alone, and `exports`
|
|
22
|
+
checks a directory of saved bundles without opening a socket.
|
|
23
|
+
- `instrument run` puts the boundary in front of somebody else's program, reversibly and writing
|
|
24
|
+
nothing; `instrument verify` runs it again under the interpreter's own audit hook and proves,
|
|
25
|
+
from that and the scope's evidence chain alone, that every effect of a named kind was preceded
|
|
26
|
+
by a decision; `instrument apply` is reserved and refuses, saying so.
|
|
27
|
+
- `trace` reads back the record of one decision and follows it into the evidence that holds it:
|
|
28
|
+
the record itself, and where it sits in the chain, bounded to the pages the read walks. Like
|
|
29
|
+
every read from a daemon it names its scope explicitly, and it exits `0` on a read whatever the
|
|
30
|
+
decision it read said.
|
|
31
|
+
- `explain` renders the reason the control plane gave for a decision — the rule it applied and
|
|
32
|
+
the policy version it ran under — in the plane's own words, and composes none of its own. A
|
|
33
|
+
client explains and invokes control semantics; it never derives them, and a command that
|
|
34
|
+
reasoned here would be a second control plane with no evidence behind it.
|
|
35
|
+
- `packs list` prints the convenience packs this distribution ships, one line each with the path
|
|
36
|
+
`--pack` accepts, and `packs check` reads one the way the engine will.
|
|
37
|
+
- `approvals` shows a suspended ask and ends the wait, so the next ask runs the body once.
|
|
38
|
+
- A verdict the verifier could not reach is rendered as it was given rather than as a negative
|
|
39
|
+
fact about the chain.
|
|
40
|
+
- The release itself: this distribution moves to `0.2.0`, and installing it pins
|
|
41
|
+
`sayfirst-contract==0.2.0` and `sayfirst-boundary==0.2.0` — the contract this client speaks and
|
|
42
|
+
the runtime a governed program holds its grant in — at that version and no other. An
|
|
43
|
+
installation of this version therefore carries exactly those two, and upgrading it moves all
|
|
44
|
+
three together.
|