Skip to content

Commit ad1b0fe

Browse files
Merge pull request #108 from Fieldnote-Echo/codex/v0.3.0-release-cleanup
Audit release environment settings
2 parents f05461e + f7a77f0 commit ad1b0fe

6 files changed

Lines changed: 137 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
#
3838
# Provenance / attestation, soup to nuts (all genuine, nothing faked):
3939
# * SLSA generator -> `*.intoto.jsonl` on the Release (OpenSSF Scorecard
40-
# Signed-Releases provenance probe -> 10/10; SLSA Build L3).
40+
# Signed-Releases provenance probe; older unsigned releases may keep that
41+
# score below 10 temporarily; SLSA Build L3).
4142
# * actions/attest-build-provenance -> GitHub attestation store + a
4243
# `*.sigstore.json` bundle on the Release (`gh attestation verify`; also the
43-
# Scorecard signing probe -> 8, a backup if the .intoto.jsonl ever regresses).
44+
# Scorecard signing probe sees this asset as a backup if the .intoto.jsonl
45+
# ever regresses).
4446
# * gh-action-pypi-publish -> PEP 740 attestations on PyPI (Integrity API).
4547
# * post-publish PyPI JSON hash check -> every served wheel/sdist digest
4648
# matches the staged dist files.
@@ -218,8 +220,9 @@ jobs:
218220
with:
219221
toolchain: stable
220222
- name: Package the crate
221-
# Emits target/package/ordvec-<version>.crate — the same content
222-
# `cargo publish` uploads, so the provenance covers the published artifact.
223+
# Emits the SLSA-attested .crate artifact. `publish-crate` later
224+
# compares both a local repackage and the crates.io-served artifact to
225+
# this file.
223226
run: cargo package -p ordvec --locked
224227
- name: Generate CycloneDX SBOM for the crate
225228
run: |

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ exclude = [
4242
"ordvec-manifest/",
4343
"ordvec-python/",
4444
"tests/__pycache__/",
45+
"tests/release_environment_settings.sh",
4546
"tests/release_publish_invariants.py",
4647
"tests/release_publish_invariants.sh",
4748
"tests/release_signed_release_invariants.sh",

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,14 @@ Collaboration we're actively seeking:
263263
- **Independent reproduction** — re-running the benchmark on other hardware
264264
and reporting the numbers.
265265

266-
If that's your area, see [GOVERNANCE.md](GOVERNANCE.md) and open an issue or a
267-
discussion.
266+
If that's your area, see
267+
[GOVERNANCE.md](https://github.com/Fieldnote-Echo/ordvec/blob/main/GOVERNANCE.md)
268+
and open an issue or a discussion.
268269

269270
## Contributing
270271

271272
Contributions to the code, the docs, and the paper are all welcome — see
272-
[CONTRIBUTING.md](CONTRIBUTING.md).
273+
[CONTRIBUTING.md](https://github.com/Fieldnote-Echo/ordvec/blob/main/CONTRIBUTING.md).
273274

274275
## Minimum supported Rust version
275276

RELEASING.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,18 @@ filename. Until either is updated, the corresponding gated publish fails
126126
strategy. An interior commit that exists in history only from a PR branch
127127
has no push-to-main run (its CI ran as a `pull_request` on the branch)
128128
and so is not releasable.
129-
4. Get the maintainer's explicit go to publish.
130-
5. Push the version tag from `main` (signed):
129+
4. Run the manual release-settings audit before creating the tag:
130+
131+
```sh
132+
bash tests/release_environment_settings.sh
133+
```
134+
135+
This verifies the GitHub Environments still require the expected reviewer
136+
and accept only the stable release tag pattern. Separately verify the
137+
registry Trusted Publisher records by hand: crates.io must point to
138+
`release.yml` / `crates-io`, and PyPI must point to `release.yml` / `pypi`.
139+
5. Get the maintainer's explicit go to publish.
140+
6. Push the version tag from `main` (signed):
131141

132142
```sh
133143
git tag -s vX.Y.Z -m "vX.Y.Z"
@@ -139,7 +149,7 @@ filename. Until either is updated, the corresponding gated publish fails
139149
generates the SLSA `*.intoto.jsonl`; and stages every artifact, the
140150
attestation bundle, and the provenance on the GitHub Release — **as a
141151
DRAFT**. It then pauses at the two registry environment gates.
142-
6. **Approve the two publish environments** when they pause in the Actions UI
152+
7. **Approve the two publish environments** when they pause in the Actions UI
143153
(one for `crates-io`, one for `pypi`). The required-reviewer approval is
144154
what authorises the registry push.
145155
- `publish-crate` first sha256-compares its repackaged `.crate` to the
@@ -152,7 +162,7 @@ filename. Until either is updated, the corresponding gated publish fails
152162
- `publish-pypi` also queries PyPI after upload and compares every served
153163
wheel/sdist SHA-256 digest against the staged `dist/` files before the
154164
GitHub Release can un-draft.
155-
7. Verify each published artifact and its provenance:
165+
8. Verify each published artifact and its provenance:
156166
- crates.io / docs.rs;
157167
- PyPI (confirm the post-publish hash-verification log, optionally
158168
`pip download ordvec==X.Y.Z` and inspect, plus check the PEP 740 attestation
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Manual pre-tag audit for GitHub Environment release gates.
4+
#
5+
# This is intentionally not a normal CI check: it requires an authenticated
6+
# gh token that can read repository environment settings.
7+
set -euo pipefail
8+
9+
REPO="${REPO:-Fieldnote-Echo/ordvec}"
10+
EXPECTED_REVIEWER="${EXPECTED_REVIEWER:-Fieldnote-Echo}"
11+
EXPECTED_POLICY="${EXPECTED_POLICY:-v[0-9]*.[0-9]*.[0-9]*}"
12+
ENVIRONMENTS=(crates-io pypi)
13+
14+
fail() {
15+
echo "::error::release environment settings audit failed: $*"
16+
exit 1
17+
}
18+
19+
api_jq() {
20+
local path="$1"
21+
local filter="$2"
22+
local err output stderr
23+
24+
if ! err="$(mktemp)"; then
25+
fail "could not create temporary file for gh api stderr"
26+
fi
27+
28+
if ! output="$(gh api "$path" --jq "$filter" 2>"$err")"; then
29+
stderr="$(cat "$err")"
30+
rm -f "$err"
31+
fail "cannot read ${path}; authenticate with a token that can read ${REPO} repository environment settings. gh api: ${stderr}"
32+
fi
33+
rm -f "$err"
34+
35+
printf '%s\n' "$output"
36+
}
37+
38+
command -v gh >/dev/null 2>&1 \
39+
|| fail "gh CLI not found; install GitHub CLI (gh) and authenticate before running this audit"
40+
41+
if ! gh auth status -h github.com; then
42+
fail "gh auth status failed; run gh auth login with an account/token that can read ${REPO} repository environment settings"
43+
fi
44+
45+
check_environment() {
46+
local env="$1"
47+
local env_path="repos/${REPO}/environments/${env}"
48+
local policies_path="${env_path}/deployment-branch-policies?per_page=100"
49+
local env_data policies_data
50+
local env_name required_rule_count reviewer_count reviewer_summary
51+
local custom_branch_policies protected_branches
52+
local policy_total policy_summary policy_type policy_name
53+
54+
echo "Auditing ${REPO} environment ${env}..."
55+
56+
env_data="$(api_jq "$env_path" '[
57+
(.name // ""),
58+
([.protection_rules[]? | select(.type == "required_reviewers")] | length | tostring),
59+
([.protection_rules[]? | select(.type == "required_reviewers") | .reviewers[]?] | length | tostring),
60+
([.protection_rules[]? | select(.type == "required_reviewers") | .reviewers[]? | "\(.type):\(.reviewer.login // .reviewer.slug // .reviewer.name // "unknown")"] | join(", ")),
61+
(.deployment_branch_policy.custom_branch_policies | tostring),
62+
(.deployment_branch_policy.protected_branches | tostring)
63+
] | @tsv')"
64+
IFS=$'\t' read -r env_name required_rule_count reviewer_count reviewer_summary custom_branch_policies protected_branches <<< "$env_data"
65+
66+
[ "$env_name" = "$env" ] \
67+
|| fail "${env}: environment not found"
68+
69+
[ "$required_rule_count" = "1" ] \
70+
|| fail "${env}: expected exactly one required_reviewers protection rule; found ${required_rule_count}"
71+
72+
[ "$reviewer_count" = "1" ] \
73+
|| fail "${env}: expected exactly one required reviewer User:${EXPECTED_REVIEWER}; found ${reviewer_count} (${reviewer_summary:-none})"
74+
[ "$reviewer_summary" = "User:${EXPECTED_REVIEWER}" ] \
75+
|| fail "${env}: expected required reviewer User:${EXPECTED_REVIEWER}; found ${reviewer_summary:-none}"
76+
77+
[ "$custom_branch_policies" = "true" ] \
78+
|| fail "${env}: expected deployment_branch_policy.custom_branch_policies == true; found ${custom_branch_policies}"
79+
80+
[ "$protected_branches" = "false" ] \
81+
|| fail "${env}: expected deployment_branch_policy.protected_branches == false; found ${protected_branches}"
82+
83+
policies_data="$(api_jq "$policies_path" '[
84+
(.total_count | tostring),
85+
([.branch_policies[]? | "\(.type):\(.name)"] | join(", ")),
86+
(.branch_policies[0].type // ""),
87+
(.branch_policies[0].name // "")
88+
] | @tsv')"
89+
IFS=$'\t' read -r policy_total policy_summary policy_type policy_name <<< "$policies_data"
90+
91+
[ "$policy_total" = "1" ] \
92+
|| fail "${env}: expected exactly one deployment branch/tag policy tag:${EXPECTED_POLICY}; found ${policy_total} (${policy_summary:-none})"
93+
94+
[ "$policy_type" = "tag" ] \
95+
|| fail "${env}: expected deployment policy type tag; found ${policy_type:-none}"
96+
97+
[ "$policy_name" = "$EXPECTED_POLICY" ] \
98+
|| fail "${env}: expected deployment policy name ${EXPECTED_POLICY}; found ${policy_name:-none}"
99+
100+
echo "OK: ${env} requires User:${EXPECTED_REVIEWER} and only tag:${EXPECTED_POLICY}."
101+
}
102+
103+
for env in "${ENVIRONMENTS[@]}"; do
104+
check_environment "$env"
105+
done
106+
107+
echo "OK: release environment settings match the pre-tag policy."

tests/release_signed_release_invariants.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#
33
# Signed-release / provenance invariants — pinned in CI.
44
#
5-
# release.yml's signed-release graph is what gets us OpenSSF Scorecard
6-
# Signed-Releases = 10 and keeps the build-attest-publish chain honest:
5+
# release.yml's signed-release graph attaches the .intoto.jsonl and Sigstore
6+
# assets that OpenSSF Scorecard detects for Signed-Releases, while older
7+
# unsigned releases may keep the score below 10 temporarily. The same graph
8+
# keeps the build-attest-publish chain honest:
79
#
810
# build-{crate,wheels,sdist} (artifacts)
911
# |

0 commit comments

Comments
 (0)