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.
Files changed (97) hide show
  1. sayfirst_cli-0.2.0/.github/workflows/ci.yml +233 -0
  2. sayfirst_cli-0.2.0/.github/workflows/release.yml +213 -0
  3. sayfirst_cli-0.2.0/.gitignore +8 -0
  4. sayfirst_cli-0.2.0/CHANGELOG.md +44 -0
  5. sayfirst_cli-0.2.0/LICENSE +202 -0
  6. sayfirst_cli-0.2.0/NOTICE +13 -0
  7. sayfirst_cli-0.2.0/PKG-INFO +297 -0
  8. sayfirst_cli-0.2.0/QUICKSTART.md +345 -0
  9. sayfirst_cli-0.2.0/README.md +276 -0
  10. sayfirst_cli-0.2.0/SECURITY.md +176 -0
  11. sayfirst_cli-0.2.0/TRADEMARKS.md +49 -0
  12. sayfirst_cli-0.2.0/docs/EVIDENCE-SURFACE.md +102 -0
  13. sayfirst_cli-0.2.0/docs/PACKS.md +187 -0
  14. sayfirst_cli-0.2.0/docs/PARTITION.md +217 -0
  15. sayfirst_cli-0.2.0/docs/PROVENANCE.md +80 -0
  16. sayfirst_cli-0.2.0/docs/publication-checklist.md +142 -0
  17. sayfirst_cli-0.2.0/pyproject.toml +76 -0
  18. sayfirst_cli-0.2.0/scripts/check_dependency_closure.py +425 -0
  19. sayfirst_cli-0.2.0/scripts/check_developer_certificate_of_origin.py +111 -0
  20. sayfirst_cli-0.2.0/scripts/gate.sh +215 -0
  21. sayfirst_cli-0.2.0/scripts/release_version.py +95 -0
  22. sayfirst_cli-0.2.0/src/sayfirst_cli/__init__.py +2 -0
  23. sayfirst_cli-0.2.0/src/sayfirst_cli/approvals.py +127 -0
  24. sayfirst_cli-0.2.0/src/sayfirst_cli/ask.py +167 -0
  25. sayfirst_cli-0.2.0/src/sayfirst_cli/evidence.py +668 -0
  26. sayfirst_cli-0.2.0/src/sayfirst_cli/exit_codes.py +88 -0
  27. sayfirst_cli-0.2.0/src/sayfirst_cli/explain.py +35 -0
  28. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/__init__.py +14 -0
  29. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/commands.py +191 -0
  30. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/engine.py +396 -0
  31. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/harness.py +1162 -0
  32. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/launch.py +417 -0
  33. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/manifest.py +409 -0
  34. sayfirst_cli-0.2.0/src/sayfirst_cli/instrument/verify.py +553 -0
  35. sayfirst_cli-0.2.0/src/sayfirst_cli/main.py +78 -0
  36. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/__init__.py +10 -0
  37. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/NOTE.md +4 -0
  38. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/interpose.py +85 -0
  39. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/database/pack.toml +13 -0
  40. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/NOTE.md +4 -0
  41. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/interpose.py +72 -0
  42. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/http-client/pack.toml +13 -0
  43. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/NOTE.md +4 -0
  44. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/interpose.py +90 -0
  45. sayfirst_cli-0.2.0/src/sayfirst_cli/packs/subprocess/pack.toml +13 -0
  46. sayfirst_cli-0.2.0/src/sayfirst_cli/packs_cmd.py +115 -0
  47. sayfirst_cli-0.2.0/src/sayfirst_cli/pages.py +87 -0
  48. sayfirst_cli-0.2.0/src/sayfirst_cli/reads.py +254 -0
  49. sayfirst_cli-0.2.0/src/sayfirst_cli/render.py +138 -0
  50. sayfirst_cli-0.2.0/src/sayfirst_cli/trace.py +88 -0
  51. sayfirst_cli-0.2.0/tests/canned_daemon.py +177 -0
  52. sayfirst_cli-0.2.0/tests/conftest.py +95 -0
  53. sayfirst_cli-0.2.0/tests/contract_absence.py +183 -0
  54. sayfirst_cli-0.2.0/tests/documents.py +259 -0
  55. sayfirst_cli-0.2.0/tests/governed_programs.py +329 -0
  56. sayfirst_cli-0.2.0/tests/pack_doubles.py +178 -0
  57. sayfirst_cli-0.2.0/tests/replies.py +58 -0
  58. sayfirst_cli-0.2.0/tests/test_approvals.py +242 -0
  59. sayfirst_cli-0.2.0/tests/test_ask_connection_loss.py +84 -0
  60. sayfirst_cli-0.2.0/tests/test_ask_end_to_end.py +233 -0
  61. sayfirst_cli-0.2.0/tests/test_boundary_runtime.py +16 -0
  62. sayfirst_cli-0.2.0/tests/test_changelog_names_the_version.py +78 -0
  63. sayfirst_cli-0.2.0/tests/test_contract_absence.py +227 -0
  64. sayfirst_cli-0.2.0/tests/test_database_pack.py +260 -0
  65. sayfirst_cli-0.2.0/tests/test_decided_name.py +193 -0
  66. sayfirst_cli-0.2.0/tests/test_dependency_closure.py +170 -0
  67. sayfirst_cli-0.2.0/tests/test_developer_certificate_of_origin.py +147 -0
  68. sayfirst_cli-0.2.0/tests/test_engine.py +511 -0
  69. sayfirst_cli-0.2.0/tests/test_engine_is_agnostic.py +732 -0
  70. sayfirst_cli-0.2.0/tests/test_evidence_export_and_exports.py +678 -0
  71. sayfirst_cli-0.2.0/tests/test_evidence_history_and_audit.py +390 -0
  72. sayfirst_cli-0.2.0/tests/test_evidence_reads.py +323 -0
  73. sayfirst_cli-0.2.0/tests/test_exit_codes.py +67 -0
  74. sayfirst_cli-0.2.0/tests/test_gate_modes.py +392 -0
  75. sayfirst_cli-0.2.0/tests/test_governed_run_leaves_no_trace.py +156 -0
  76. sayfirst_cli-0.2.0/tests/test_http_client_pack.py +294 -0
  77. sayfirst_cli-0.2.0/tests/test_instrument_run.py +868 -0
  78. sayfirst_cli-0.2.0/tests/test_instrument_verify.py +2090 -0
  79. sayfirst_cli-0.2.0/tests/test_manifest.py +442 -0
  80. sayfirst_cli-0.2.0/tests/test_offline_audit_vectors.py +368 -0
  81. sayfirst_cli-0.2.0/tests/test_pack_doubles.py +54 -0
  82. sayfirst_cli-0.2.0/tests/test_packs_cmd.py +282 -0
  83. sayfirst_cli-0.2.0/tests/test_packs_shipped.py +366 -0
  84. sayfirst_cli-0.2.0/tests/test_partition_boundary.py +225 -0
  85. sayfirst_cli-0.2.0/tests/test_pointers_survive_publication.py +157 -0
  86. sayfirst_cli-0.2.0/tests/test_public_vocabulary.py +384 -0
  87. sayfirst_cli-0.2.0/tests/test_publication_checklist_names_every_act.py +329 -0
  88. sayfirst_cli-0.2.0/tests/test_quickstart_names_what_exists.py +113 -0
  89. sayfirst_cli-0.2.0/tests/test_read_rendering.py +74 -0
  90. sayfirst_cli-0.2.0/tests/test_reads.py +315 -0
  91. sayfirst_cli-0.2.0/tests/test_release_workflow_publishes_only_on_a_tag.py +660 -0
  92. sayfirst_cli-0.2.0/tests/test_running_the_module.py +165 -0
  93. sayfirst_cli-0.2.0/tests/test_shipped_wheel_notices.py +76 -0
  94. sayfirst_cli-0.2.0/tests/test_source_distribution_notices.py +74 -0
  95. sayfirst_cli-0.2.0/tests/test_spdx_identifiers.py +66 -0
  96. sayfirst_cli-0.2.0/tests/test_subprocess_pack.py +290 -0
  97. 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,8 @@
1
+ .venv/
2
+ .wheelhouse/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ .venv-reduced/
@@ -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.