Skip to content

DRAFT (phase 3, after T063): T099, publish openDox to PyPI by trusted publishing (10.3's install line) - #78

Draft
brettheap wants to merge 18 commits into
mainfrom
build/034-p3w-t099-pypi-release
Draft

brettheap wants to merge 18 commits into
mainfrom
build/034-p3w-t099-pypi-release

Conversation

@brettheap

@brettheap brettheap commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034's T101, "T099's release step" (with openDox-code#79): a release workflow that publishes openDox to PyPI by trusted publishing (OIDC). It makes release 1's ruled install line, pip install "opendox[local]", work as written (#1144 10.3, as T007's batch H addendum reads, R1Q15 (b) and R1Q16 (iii), RULED 5850003126). Today https://pypi.org/pypi/opendox/json and https://test.pypi.org/pypi/opendox/json both answer 404.

  • Ruled: 5962754358, item 1 (Brett Heap, 2026-10-02): "Publish to PyPI at the cut (Recommended)". The release commit and version are RULED in 5963162921, "0.1.0 (Recommended)". The bump to 0.1.0 is the last phase-3 openDox-code landing, T087 pins that commit, and the workflow publishes only the commit the openDox root's contracts/code-pin.yaml names.
  • The plan's split is the holder's ruling, after Copilot's review of openxFactory#1220. T101 is this PR plus the bump (DRAFT (phase 3, after T063): T099 release step: version 0.1.0 #79), and lands before T087. T099 is the publish alone, after T101, T087 and T089. The plan entry is openxFactory#1220 (bookkeeping, no Arc: trailer).
  • DRAFT, after T063. The holder posts READY, and the landers merge. Nothing has been published to PyPI or TestPyPI. Nothing in this PR can publish until Brett configures the two indexes and the two environments (below) and dispatches it.
  • openDox-code#69 (T072) and T075, 10.2 and 10.2a — an installed entry point serves all 42 bundle files; intent-feed.js is not owed (plan 034) #73 (T075) have landed, and this branch merges them (main 8e377823 was merged at 7cc65f93; the head is now 7ee2b425, with main ca9e1bd5 merged, as the refresh section below reads). The build job passes on this head's own tree (proof 1). Before they landed, the verify step refused, naming exactly what those two PRs add (proof 2).

What it adds

.github/workflows/release.yml. Its only trigger is workflow_dispatch, with one input, version. It has no push, tag or pull-request trigger. Four jobs run in a chain:

  1. build (contents: read, actions: read; it mints no identity token, and its GITHUB_TOKEN reads only). In order:
    • The release is the pinned commit, on main, in two steps:

      • (a) the ref and main: a dispatch on a tag must name v<version>, and the run's commit must be on this repository's main. The compare API must read identical or ahead.
      • (b) the pin: the run's commit must equal commit: in opensoft/openDox main's contracts/code-pin.yaml, whose source_repository must be this repository. Both publish jobs run this same step again, right before their upload (below).
      • main's head is never required, because T095 may land after T087 has pinned the bump.
      • A dispatch runs at the head of the ref it names. So once main has moved past the pinned commit, the dispatch names the tag v<version>, which the holder creates at that commit at the cut, on Brett's word. A dispatch on any other tag is refused, and the refusal of a moved main says to use the tag.
      • The root's pin is read anonymously over git: a depth-1 fetch of opensoft/openDox main, with every credential helper cleared and no prompt, in a repository of its own under $RUNNER_TEMP. So the read of another repository depends on no token. The compare reads this repository, through the API.
    • Both environments ask a reviewer. GitHub creates an environment that a job names if it does not exist yet, and creates it with no protection rule. So a dispatch made before Brett set up pypi would publish with nobody approving it. The step refuses unless testpypi and pypi each have a required_reviewers rule with at least one reviewer.

    • The release tools come from a hash lock, .github/release-tools-cpython312-linux.txt, installed with --only-binary :all: --require-hashes into a venv of their own.

    • The version gate. The input must be a PEP 440 version in normalized form, with no epoch (a wheel's file name escapes its !) and no local label. It must be a final release. A pre-release or a development release is refused, because the dry run installs the unversioned opendox[local], which pip resolves to a final release once one exists. It must not be 0.0.0, the scaffold's placeholder, and it must equal pyproject.toml's [project] version.

    • The build. python -m build --no-isolation builds the sdist, then the wheel from that sdist, with SOURCE_DATE_EPOCH set to the commit's time.

    • twine check --strict.

    • The artifact checks, before any upload:

      1. dist/ holds exactly the sdist and the wheel named for the version;
      2. the wheel's metadata names opendox at that version;
      3. the wheel declares exactly the requirements pyproject.toml does, for the base install and every extra, and its local extra (T072) carries opendox[runtime] and pixeltable-pgserver. Requirements are compared as normalized requirements: the name, the extras, the version specifier or direct URL, and the marker. Only the extra == "X" clause that setuptools adds to record ownership is removed, read from the parsed marker. An extra anywhere else in a marker is refused;
      4. the console script opendox = opendox.cli:main is declared;
      5. every file git tracks under src/opendox/web/ is in the wheel, dotfiles included (T075's web/**/.*);
      6. every file git tracks under src/ is in the wheel at its import path, less src/.gitkeep, and the wheel carries nothing else but its .dist-info and .data;
      7. every tracked migrations/*.sql is in the wheel's share/opendox/migrations/ (T072).

      The tracked tree is the reference, so a bundle file added later is checked with no edit here.

    • The built wheel runs from a fresh venv. opendox[local] is installed from the built file, with dependencies through constraints-cpython312-linux.txt and wheels only. Then, from outside the checkout, the step:

      • asserts the import path and the version;
      • walks the whole requirement closure of opendox[local] from the installed metadata, extras and markers included, and requires every requirement to be installed and satisfied;
      • runs the bundled server's initdb --version and postgres --version, through opendox.runtime.bundle.server_binaries();
      • runs opendox --help, and requires opendox generate-and-open --help to name --local.
    • It records the two file names and their sha256 as job outputs, and uploads dist/.

  2. testpypi, the dry run, in the testpypi environment, with id-token: write and actions: read. It downloads dist/ and checks the two files against the build job's digests, and that there is no third file. It re-checks its approval at use time: the environment must still name a required reviewer, and this run's review history must hold an approved review of a deployment to it. A preflight follows: TestPyPI must hold no file of this version except a verified one at its verified digest (a 404 means no file yet). It checks the root's pin again, the build job's step, since the run waited on an approval. Then it runs pypa/gh-action-pypi-publish with repository-url: https://test.pypi.org/legacy/, bounded at 10 minutes. Every step declares its own timeout, and the job's (25 minutes) covers their sum (24).
  3. testpypi-install (contents: read):
    • TestPyPI's JSON API must serve exactly the two verified files and digests. It is read until it does, up to 20 reads 15 s apart, so a partial or stale answer during propagation is retried, not taken as final. A read that times out is retried too.
    • Then T099's falsifier runs in a fresh venv: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "opendox[local]". The requirement is unversioned, as written. The lock, wheels-only (so no package's code runs at install) and --report are added.
    • The installed opendox must be the verified TestPyPI wheel, proven before anything from it runs. pip merges candidates from both indexes, and opendox is not reserved on PyPI before its first release. So this job's own python3, never the venv's interpreter, reads pip's report. The one opendox installed must come from test-files.pythonhosted.org, at the version just uploaded, with the verified wheel's sha256.
      • An older opendox means a lagging index, and that try is repeated.
      • Any other opendox refuses at once, and nothing from it is run: another host, another digest, or a newer version.
      • Each try is a fresh venv, up to 10 tries, 30 s apart, and each is cut off after 300 s. Then opendox --help.
    • Every step declares its own timeout, and the job's (90 minutes) covers their sum: checkout 5, setup-python 5, the JSON step 16 (20 reads, each up to 30 s, 15 s apart), the install 60 (10 tries, each up to 300 s, 30 s apart). A test reads each bound from the workflow.
    • The trade of using two indexes is stated in the step's comment. The install step receives no token, no OIDC grant and no secret. The job's GITHUB_TOKEN (contents: read) is used only by its checkout, which does not persist it. The files PyPI receives are still checked by digest.
  4. pypi, in the pypi environment, with id-token: write and actions: read, and a 45-minute timeout that covers its steps' declared bounds (5+2+2+2+3+10+16). The same digest check, the same approval re-check, the same preflight against PyPI, the root's pin checked again (the run waited on two approvals and on TestPyPI), then pypa/gh-action-pypi-publish, bounded at 10 minutes, so the check after it always has its 16. Then PyPI's JSON API must serve exactly the two verified files, by digest. That is the same script as TestPyPI's check, reading INDEX, JSON_BASE, READS and PAUSE from its step's env.

Both uploads set skip-existing: true. A re-run after a partial upload then uploads what is missing, instead of stopping on the file the index already holds. A skipped file could differ from the verified one, so the preflight refuses before any upload if the index already holds a file at any other digest, and an index never replaces a file. The JSON check after each upload then requires exactly the build job's two digests.

No PyPI token and no stored secret appear anywhere. id-token: write is granted to testpypi and pypi only, and the workflow's default is permissions: {}. The one other credential is GitHub's own automatic GITHUB_TOKEN, passed to gh for reads of this repository's own API only. In the build job, it reads the compare with main and the environments (contents: read, actions: read). In each publish job, it reads that job's environment and this run's review history (actions: read). The openDox root's pin is read over git with no credential at all. Inputs reach every script through env:, never through an expression pasted into a script.

Every action is pinned by full commit SHA, each SHA resolved from its release tag through the git refs API:

action tag commit
actions/checkout v7.0.1 3d3c42e5aac5ba805825da76410c181273ba90b1
actions/setup-python v7.0.0 5fda3b95a4ea91299a34e894583c3862153e4b97
actions/upload-artifact v7.0.1 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
actions/download-artifact v8.0.1 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
pypa/gh-action-pypi-publish v1.14.2 dc37677b2e1c63e2034f94d8a5b11f265b73ba33

validate.yml pins by tag. This file departs from that on the brief's word: a tag can be moved after review, and a commit cannot. It edits no other workflow. openDox-code#75 (T095) edits validate.yml only, so the two do not overlap.

.github/release-tools-cpython312-linux.txt. It hash-locks build==1.6.1, twine==7.0.0 and setuptools==84.0.0, with their dependencies (30 packages), compiled by uv pip compile --generate-hashes for cpython 3.12 on x86_64 linux. Its header gives the command.

tests/test_release_workflow.py was added at Copilot's request. It holds 108 hermetic cases that run the steps' own scripts, extracted from release.yml, as tests/test_triple_pin.py runs validate.yml's pin:

  • the shape: dispatch only, one input, no secret, id-token on the two publish jobs alone, each in its own environment, every action pinned by full SHA, and the job chain;
  • the release-commit steps (10 cases, run as the build job runs them, the ref step then the pin step), with a stand-in gh first on PATH for the compare. Each case builds a stand-in openDox root and redirects the root's URL to it through git's url.<base>.insteadOf, set in the environment. It also sets GIT_ALLOW_PROTOCOL=file, so a redirect that failed would refuse rather than reach GitHub. A structural case holds the anonymous read: no contents/ API call, the fetch line with -c credential.helper=, no gh call and no env in the pin step, and exactly one gh call in the ref step, which is the compare;
  • the pin again before each upload: in each publish job, the step right before the upload is the build job's pin step. A moved pin refuses there, and an unchanged one passes, in each job (4 cases);
  • the environment step (3 cases), with the stand-in gh;
  • the version gate (10 cases);
  • the artifact checks (17 cases), over a hand-built wheel and sdist in a git tree of their own. Their pyproject carries a platform marker on a base requirement, a parenthesized or marker on an extra's requirement, and a direct URL. Refused: a marker changed or dropped (an extra's, and a base requirement's), a direct URL in place of a version, a direct URL changed, and an extra under an or. Accepted: the extra's clause written first;
  • the preflight, in both publish jobs: its place before the pin's re-check, the two jobs sharing one script, and 6 index cases each (no file yet, both files verified, one verified file from a partial upload, the sdist at another digest, a file nobody verified, and a 503);
  • both publish jobs' digest check (6 cases);
  • skip-existing on both uploads, the two index checks as one script, and PyPI's check as the step after its upload;
  • the index check against a local JSON server, in both jobs: exact, lagging then exact, another sdist, a third file, and a refused read (10 cases);
  • each job's timeout and each step's, all read from the workflow: every step declares one, each job covers the sum of its steps', each upload is at most 10 minutes, and each retrying step covers its script's retries;
  • the approval at use time, in both publish jobs: the step's place, env and the jobs' permissions, one script, and seven cases each (approved; the rule removed; the environment unreadable; no approval; only the other environment approved; rejected; the history unreadable);
  • the dry run's install: the line's parts (the falsifier's, plus --report), and that nothing from the venv runs but pip before the verdict. Nine verdict cases run over pip reports: the verified wheel passes; PyPI at, above and at the verified digest refuses; PyPI below is retried; TestPyPI older is retried; TestPyPI newer refuses; TestPyPI at another digest refuses; no opendox refuses.

validate.yml's floors are not moved. They permit a rise, and validate.yml is T095's single-writer file (openDox-code#75), as every other phase-3 test addition has left it. The whole suite at this PR's head is in proof 9.

pyproject.toml: readme = "README.md" (the holder's decision). Without it, the metadata carries no long description and twine check --strict refuses both files. README.md's four relative links are made absolute (SECURITY.md and three docs/ links), so they resolve from the PyPI page too.

  • pyproject.toml's single-writer order puts this edit after T072 (openDox-code#69) and T075 (openDox-code#73). The line sits in [project], away from both PRs' hunks.
  • version stays 0.0.0 here. The bump to 0.1.0 (RULED 5963162921) is openDox-code#79, the last phase-3 openDox-code landing before T087.

Proofs

Every run below is local and never pushed. The harness parses release.yml and runs each job's run steps exactly as written, under bash --noprofile --norc -eo pipefail, with the uses steps emulated. It refuses to emulate pypa/gh-action-pypi-publish and stops there, so nothing is uploaded.

1. The build job on this head's own tree. The tree is 7cc65f93, this branch merged with main 8e377823, which carries #69 and #73. One local-only commit sets version = "0.1.0". Round 6's verify step reads the same:

3. requirements: the local extra carries ['opendox[runtime]', 'pixeltable-pgserver<0.7,>=0.6.0']
5. web bundle: 42 of 42 tracked files are in the wheel
6. source tree: 118 of 118 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 2 of 2 are in the wheel's data directory
opendox[local]: 34 distributions, every requirement installed and satisfied
initdb (PostgreSQL) 16.14
postgres (PostgreSQL) 16.14
opendox generate-and-open --help names --local
wheel-sha256=8e179862e1f6678eee9cd292f177d48718c40bc796eb1620a48ef7c0a3e4e00f
JOB build: every run step passed

The full run, before #69 and #73 landed: #73's head 0457b383 (which contains #69's head 28e195b9), merged with this PR's fix-round commit 647d246e, with the same local-only version commit. The harness runs the steps of this head's release.yml (7cc65f93) over that tree. The release-commit and environment steps run separately (4 and 5 below). Every other step passed:

version 0.1.0: pyproject.toml declares it
Checking dist/opendox-0.1.0-py3-none-any.whl: PASSED
Checking dist/opendox-0.1.0.tar.gz: PASSED
1. dist/: ['opendox-0.1.0-py3-none-any.whl', 'opendox-0.1.0.tar.gz']
2. metadata: Name opendox, Version 0.1.0
3. requirements: the local extra carries ['opendox[runtime]', 'pixeltable-pgserver<0.7,>=0.6.0']
4. console scripts: {'opendox': 'opendox.cli:main', 'opendox-runtime': 'opendox.runtime.cli:main'}
5. web bundle: 42 of 42 tracked files are in the wheel
6. source tree: 117 of 117 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 2 of 2 are in the wheel's data directory
opendox 0.1.0 imports from .../runner-temp/fresh/lib/python3.12/site-packages/opendox
opendox[local]: 34 distributions, every requirement installed and satisfied
initdb (PostgreSQL) 16.14
postgres (PostgreSQL) 16.14
opendox generate-and-open --help names --local
wheel-sha256=3221e83d6b44457616b0f5db9689a7062475f5a77cd221b4eaf7dc92df1f34b7
sdist-sha256=3fdee8308c85abc85ad12ebeba0a1af5513eafa5fe3aa68d8e3c9d3b42ee9e01
JOB build: every run step passed

The wheel's digest is the same as the previous run's (3221e83d…), since SOURCE_DATE_EPOCH fixes its bytes. The sdist's contents are identical (diff -r of the two unpacked trees is empty), but its digest differs, because setuptools stamps the generated PKG-INFO and the gzip header with the build time. The workflow carries the digests of the one build it verified, so nothing depends on the sdist being reproducible.

The installed opendox --help still prints the carve-era usage: ideation-dashboard text. That is T084's to fix (openDox-code#77), not this PR's.

2. The same job at this PR's head 2b0083b9 (main 66ff7257 plus this branch, without #69 or #73), with the same local version commit. The verify step refuses, naming exactly what #69 and #73 add:

3. requirements: the local extra carries []
5. web bundle: 41 of 42 tracked files are in the wheel
6. source tree: 116 of 117 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 0 of 2 are in the wheel's data directory
::error::the `local` extra carries [], not opendox[runtime] and the bundled server's package, pixeltable-pgserver
::error::the wheel lacks web files: ['opendox/web/vendor/.gitkeep']
::error::the wheel lacks tracked files: ['opendox/web/vendor/.gitkeep']
::error::the wheel lacks migrations: [...0001_identity_and_coordination.sql', ...0002_migration_state.sql']
JOB build: FAILED at step 9 (verify the artifacts)

3. The version gate refuses 0.2.0 against 0.1.0, v0.1.0, 0.1.0+local, 1!2.0, banana, 0.1.0"; print(1) #, the 0.0.0 placeholder, the pre-release 0.2.0rc1 and the development release 0.2.0.dev1. It passes 0.1.0 against 0.1.0, and the post-release 0.1.0.post1. tests/test_release_workflow.py holds each case.

4. The release-commit step against the live opensoft/openDox main, which pins 047bb4fa394f3e1bf42466062a67ef18e99f8d6a today. openDox-code main has since moved past it (now 1130e996), which is the case T095 will make at the cut. The run below is fix round 3's single step. Round 5 splits it in two with the same scripts, and the pin step's live run follows the block.

The step's script was extracted from this head's release.yml and run with:

  • env -i;
  • an empty HOME;
  • GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1;
  • GIT_TRACE_CURL on.

The only token given was the GH_TOKEN the compare uses, and git never reads it:

== main, pinned
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is the commit opensoft/openDox main pins for opensoft/openDox-code
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is on main (ahead), dispatched on refs/heads/main
rc=0
== tag v0.1.0, pinned
... is on main (ahead), dispatched on refs/tags/v0.1.0
rc=0
== main, an unpinned commit
::error::this run is at 9a49040500000000000000000000000000000000, and the openDox root pins 047bb4fa394f3e1bf42466062a67ef18e99f8d6a; a release publishes only the pinned commit. ...
rc=1
== tag nightly
::error::a release dispatched on a tag names v0.1.0, and this run names refs/tags/nightly
rc=1
curl-trace (main): git requests=3 authorization headers=0
curl-trace (tag):  git requests=3 authorization headers=0

The three requests are GET /opensoft/openDox.git/info/refs?service=git-upload-pack and two POST /opensoft/openDox.git/git-upload-pack. None of them carries an Authorization header.

At this head, the pypi job's pin step (the same script as the build job's) was run the same way, with env -i, no token and no python but the system's python3:

== the pypi job's pin step, GITHUB_SHA=047bb4fa
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is the commit opensoft/openDox main pins for opensoft/openDox-code
rc=0; git requests=3 authorization=0
== the pypi job's pin step, GITHUB_SHA=22222222
::error::this run is at 2222222222222222222222222222222222222222, and the openDox root pins 047bb4fa394f3e1bf42466062a67ef18e99f8d6a; a release publishes only the pinned ...
rc=1; git requests=3 authorization=0

A commit that is not on main, such as #73's head, reads behind on the compare call, and the step refuses it.

5. The environment step today refuses both environments with gh: Not Found (HTTP 404), then ::error::the testpypi environment cannot be read (it does not exist yet, ...), and the same for pypi. Pointed at this repository's copilot environment, which has no protection rule, it prints ::error::the copilot environment names no required reviewer, ....

6. The publish jobs' digest check passes on the build job's artifact. It refuses a wheel with one changed byte and a third file in dist/, and the harness stops at each publish action.

7. The index checks, live. The one JSON script, extracted from the pypi job, was run against both indexes:

== TestPyPI sampleproject 1.2.0: sampleproject-1.2.0-py2.py3-none-any.whl sampleproject-1.2.0.tar.gz
TestPyPI serves exactly the verified files: {...}
rc=0
== TestPyPI sampleproject 1.2.0, the wheel's digest wrong
::error::after 2 reads, TestPyPI serves {...}
rc=1
== PyPI sampleproject 4.0.0: sampleproject-4.0.0-py3-none-any.whl sampleproject-4.0.0.tar.gz
PyPI serves exactly the verified files: {...}
rc=0
== PyPI sampleproject 4.0.0, the wheel's digest wrong
::error::after 2 reads, PyPI serves {...}
rc=1

testpypi-install.

  • The install step's line, pointed at a local HTTP simple index that serves the built wheel, with --extra-index-url https://pypi.org/simple/, installs opendox[local] and runs opendox --help.
  • Both run against TestPyPI itself only at the dry run.

8. Linters.

  • actionlint 1.7.12: no findings, over both workflow files.
  • zizmor 1.30.1, online audits, --persona=pedantic: No findings to report.

9. The whole suite at d91fd5b9 (main 8e377823 merged in), in a venv with the test extra, as validate.yml installs it, the bundled PostgreSQL included:

  • tests/: 3013 passed, 11 skipped;
  • tests_runtime/: 631 passed, 166 skipped.

In the earlier suite venv, which lacked the local extra's pixeltable-pgserver, 5 tests failed and 2 errored at this head, each with the local install's PostgreSQL server is not installed. They fail the same way at main 8e377823 in that venv, so the failures are environmental.

At b75205d4 (rounds 7 and 8 change release.yml and the test file only), tests/ reads 3023 passed, 11 skipped, and tests/test_release_workflow.py alone reads 93 passed. main has since gained 90ac7033 (#72, T073), which touches src/ and tests only: no pyproject.toml, no workflow and no lock. Mutants, each run against that file:

mutant result
the pre-release refusal removed 2 failed (round 3)
the URL redirect removed 7 failed (each case refuses instead of reaching GitHub; round 3)
the credential helper left in place 1 failed (round 3)
the token-backed contents/ API read restored 8 failed (round 3)
no skip-existing on the TestPyPI upload 1 failed, 56 passed (round 4)
testpypi-install back at a 20-minute timeout 1 failed, 56 passed (round 4)
no per-try timeout 300 on pip 1 failed, 56 passed (round 4)
the index check compares names, not digests 2 failed, 55 passed (round 4)
no PyPI check after the upload 6 failed, 51 passed (round 4)
no pin re-check in the pypi job 3 failed, 59 passed (round 5)
the pypi job's re-check ignores the commit 2 failed, 60 passed (round 5)
markers dropped from the requirement comparison 4 failed, 78 passed (round 6)
a direct URL ignored 1 failed, 82 passed (round 6; the changed-URL case was added for it)
a preflight that accepts any held file 4 failed, 78 passed (round 6)
no preflight in the pypi job 7 failed, 75 passed (round 6)
the PyPI upload unbounded; at 30 minutes; the pypi job at 30; PyPI's JSON check at 10 each fails the timeout test (round 7)
the verdict ignores the host; ignores the digest; the venv's interpreter run before it; no --report each fails (round 8)
any environment's approval counts; a rejected review counts; the rule not re-read; no approval check in pypi each fails (round 9)

10. The approval check, live. The step's script ran with env -i and a read token against a real run of OpsxFactory, whose github-administration environment is reviewer-protected:

  • for github-administration, the environment that run deployed to, it passes: a required reviewer is named, and this run's deployment was approved by brettheap;
  • for endpoint-verification, which that run did not deploy to, it refuses: holds no approval of a deployment to endpoint-verification;
  • against this repository's pypi, which does not exist yet, it refuses: the pypi environment cannot be read.

11. The verdict, on real pip reports. In fresh pip 24.0 venvs, a two-index install of sampleproject resolved 4.0.0 from files.pythonhosted.org: PyPI's file, which is the risk itself. A TestPyPI-only install resolved 1.2.0 from test-files.pythonhosted.org. With each report's name rewritten to opendox, the step's verdict script refused the first (... which is not the verified wheel, rc 2) and passed the second (opendox 1.2.0 is the verified wheel, from test-files.pythonhosted.org, rc 0).

Copilot's findings at de3e3249

  • The repository token and the public root (r4171040914). The pin is now read anonymously over git, as above, so the read depends on no token.
    • You cite GitHub's documentation that the token is limited to the workflow's repository. The estate also has evidence that the token reads a public repository of the same org over git: openxFactory's openreposhape-pin-gate.yml run 37085426729 checks out public opensoft/openRepoShape with the default github.token (Contents: read, Metadata: read), and succeeds.
    • The REST call under that token was not measured, and the change makes the question moot.
  • Pre-releases and the unversioned install (r4171040935). The version gate refuses them, through Version.is_prerelease. VERSION_CASES holds 0.2.0rc1 and 0.2.0.dev1 (both refused) and 0.1.0.post1 (accepted).

Copilot's findings at 7cc65f93 and 74b87375

  • A partial upload is not recoverable ("previously missed" at 7cc65f93): both uploads set skip-existing, and PyPI's served digests are now checked after its upload too (round 4, 74b87375).
  • The retry budget exceeds the job timeout ("previously missed" at 7cc65f93): testpypi-install is at 75 minutes, its budget in full, with each pip try cut off after 300 s. A test recomputes the budget from the steps (round 4).
  • The pin can move before the PyPI upload (r4171232529, at 74b87375): both publish jobs run the build job's pin step again, right before their upload (round 5, 994a39d7).

Copilot's findings at 5cdb2ded

  • Requirement markers were dropped before the comparison (r4173500546): markers are now compared whole, removing only setuptools' extra clause, and direct URLs are compared too (round 6, d7e9ff7b, d91fd5b9).
  • skip-existing on PyPI could make a mixed release (r4173500563): a preflight before each upload refuses a file the build job did not verify, and a verified name at another digest, before anything is uploaded (round 6).

Copilot's findings at d91fd5b9 and 960d549b

  • The upload step had no bound of its own (r4173861533), and the test assumed one (r4173861563). Every step after the build now declares its timeout, and the test reads each bound from the workflow (round 7, 960d549b).
  • The dry run could install an opendox that PyPI serves (r4173890172). The install line stays as written and adds --report. The report must show the verified TestPyPI wheel before anything from the install runs (round 8, b75205d4).

Copilot's review at b75205d4

"Needs a closer look", with no findings and one item it had missed before: the dry run's comment said its job holds no token, but its checkout uses the job's GITHUB_TOKEN. The comment now says only the install step receives no token (f59e7105).

The refresh of 2026-10-04: main ca9e1bd5 merged, the head is 7ee2b425

This PR stays DRAFT, and it lands last among phase 3's shipped-package changes. The refresh leaves only a final main merge for later.

Copilot's finding at f59e7105

  • The reviewer check goes stale before the uploads (r4174341950). Each publish job re-reads its environment's rule and this run's review history, right before its preflight, pin check and upload (round 9, b3907858).

What Brett configures, once, before the first dispatch

  1. pypi.org. Under Publishing, add a pending publisher (GitHub). PyPI project name: opendox. Owner: opensoft. Repository name: openDox-code. Workflow name: release.yml. Environment name: pypi.
  2. test.pypi.org (its own account). The same pending publisher, with environment name testpypi.
  3. GitHub, opensoft/openDox-code, Settings, Environments. Create pypi and testpypi, each with Required reviewers set to Brett. Leave "Prevent self-review" off if he both dispatches and approves. Optionally, limit deployment refs to main and tags v*. The build job refuses until both environments have a reviewer, so this must be done before the first dispatch.

A pending publisher does not reserve the project name, and opendox is free on both indexes today, so the first publish should follow the configuration closely.

At the cut, after T087 has pinned the version bump, T089 has passed and AT-R1 has passed (T095, T096), on Brett's publish word:

  1. Dispatch release with version = 0.1.0, on main, or on the tag v0.1.0 that the holder creates at the pinned commit.
  2. Approve testpypi.
  3. When testpypi-install passes, approve pypi.

A job that fails after an upload can be re-run from the same run. It reuses its artifacts, and skip-existing uploads only what is missing. A second dispatch of the same version builds new files, while TestPyPI keeps the first ones under the same names, so its JSON check refuses unless the two builds are byte-identical. After the publish verifies, T099's last step is a small openDox root PR that replaces the root README's "Where opendox comes from" paragraph with the PyPI install line (openxFactory#1220).

Version

pyproject.toml's version is 0.0.0 here. Release 1's version is 0.1.0, RULED 5963162921, "0.1.0 (Recommended)": the first public release, with the API still pre-1.0. The bump is openDox-code#79.

🤖 Generated with Claude Code

Summary by Sourcery

Establish a gated, provenance-checked release pipeline that safely publishes verified openDox packages to TestPyPI and PyPI through trusted publishing.

New Features:

  • Add a manually triggered, reviewer-gated release workflow that builds, verifies, and publishes openDox distributions to TestPyPI and PyPI using trusted publishing.
  • Validate the published package through the unversioned opendox[local] installation path before the production upload.

Enhancements:

  • Enforce release provenance, version consistency, artifact contents, dependency metadata, file digests, index state, and post-upload availability throughout the release process.
  • Pin release tooling and GitHub Actions to verified versions and use reproducible, isolated build and installation environments.

Build:

  • Add a hash-locked release-tool dependency set for Python 3.12 Linux builds.
  • Include README metadata and absolute documentation links suitable for PyPI project pages.

Deployment:

  • Introduce staged TestPyPI and PyPI deployment jobs with OIDC trusted publishing, required environment approvals, retry handling, and safe partial-upload recovery.

Tests:

  • Add hermetic coverage for workflow structure, release pinning, approvals, version validation, artifact verification, digest checks, index propagation, timeout budgets, and TestPyPI installation verdicts.

…blishing (plan 034)

RULED openxFactory#656 comment 5962754358, item 1: "Publish to PyPI at the
cut (Recommended)". Release 1's ruled install line is
`pip install "opendox[local]"` (#1144 10.3, batch H's addendum,
5850003126), and today pypi.org/pypi/opendox/json answers 404. Release 1's
version is 0.1.0 (RULED 5963162921), bumped in a separate last landing.

.github/workflows/release.yml, workflow_dispatch only, with a `version`
input that must equal pyproject.toml's version (normalized PEP 440, not
the 0.0.0 placeholder):

- build: refuses unless the dispatched commit is the one opensoft/openDox
  main's contracts/code-pin.yaml pins, and is on main (5963162921); refuses
  unless both environments, testpypi and pypi, name a required reviewer;
  installs the release tools from a hash lock; builds the sdist and the
  wheel with `python -m build`; runs `twine check --strict`; verifies the
  artifacts before any upload (exactly the two files, the metadata's name
  and version, the `local` extra, the `opendox` console script, every
  tracked web file, every tracked file under src/, the migrations);
  installs the wheel with its `local` extra into a fresh venv and runs
  `opendox --help`. It holds no token.
- testpypi: the dry run, in the `testpypi` environment.
- testpypi-install: TestPyPI serves the verified digests; then T099's
  falsifier, `opendox[local]` from TestPyPI into a fresh venv, and
  `opendox --help`.
- pypi: the publish, in the `pypi` environment.

Both uploads use pypa/gh-action-pypi-publish by trusted publishing (OIDC),
with `id-token: write` on those two jobs only, and each checks the files'
digests against the build job's first. Every action is pinned by full
commit SHA. No token and no secret appear anywhere.

.github/release-tools-cpython312-linux.txt: build 1.6.1, twine 7.0.0 and
setuptools 84.0.0 with their dependencies, hash-locked by uv.

pyproject.toml: `readme = "README.md"`, so the metadata carries a long
description and `twine check --strict` passes. pyproject.toml's
single-writer order puts this edit after T072 (openDox-code#69) and T075
(openDox-code#73). The version stays 0.0.0 here.

The artifact checks pass only once openDox-code#69 (T072, the `local`
extra and the migrations) and #73 (T075, the 42nd web file) have landed.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 23:47
@sourcery-ai

sourcery-ai Bot commented Oct 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a manually gated four-stage release workflow that builds and thoroughly verifies openDox, validates the TestPyPI installation, and publishes the exact verified artifacts to TestPyPI and PyPI via SHA-pinned actions and OIDC trusted publishing. It also adds a hash-locked release-tool environment and PyPI-compatible README metadata.

Sequence diagram for trusted openDox publishing

sequenceDiagram
    actor Maintainer
    participant GitHub as GitHub Actions
    participant Build as build job
    participant TestPyPI
    participant Install as testpypi-install
    participant PyPI

    Maintainer->>GitHub: workflow_dispatch(version)
    GitHub->>Build: verify pinned commit and environments
    Build->>Build: build and verify sdist and wheel
    Build->>Build: record artifact SHA256 digests
    Build-->>GitHub: upload dist artifact
    GitHub->>TestPyPI: request reviewer approval
    Maintainer->>TestPyPI: approve deployment
    TestPyPI->>TestPyPI: publish via OIDC trusted publishing
    TestPyPI-->>Install: release available
    Install->>TestPyPI: verify JSON digests
    Install->>Install: pip install opendox[local]
    Install->>Install: opendox --help
    GitHub->>PyPI: request reviewer approval
    Maintainer->>PyPI: approve deployment
    PyPI->>PyPI: publish same artifacts via OIDC
Loading

Flow diagram for the gated openDox PyPI release

flowchart LR
    Dispatch[workflow_dispatch with version] --> Build[build and verify]
    Build --> TestPyPI[Publish exact artifacts to TestPyPI]
    TestPyPI --> Install[Install opendox local from TestPyPI]
    Install --> PyPI[Publish exact artifacts to PyPI]
    TestPyPI --> ReviewTest{testpypi reviewer approval}
    ReviewTest --> Install
    Install --> ReviewPyPI{pypi reviewer approval}
    ReviewPyPI --> PyPI
    Build --> Checks[ pinned commit, version, metadata, wheel, sdist, fresh venv ]
    Checks --> TestPyPI
    PyPI --> OIDC[OIDC trusted publishing]
Loading

File-Level Changes

Change Details Files
Add a manually dispatched, OIDC-based release pipeline that builds, validates, dry-runs, and publishes identical artifacts to TestPyPI and PyPI.
  • Require the dispatched commit to match the openDox root pin and be present on this repository’s main branch.
  • Require reviewer-protected testpypi and pypi environments before building.
  • Validate normalized release versions, build reproducibly with hash-locked tools, and run strict metadata checks.
  • Verify wheel contents against tracked source, web assets, migrations, extras, and console scripts.
  • Install and exercise the built wheel from a fresh virtual environment.
  • Publish through sequential TestPyPI and PyPI jobs using trusted publishing and digest verification.
  • Add retrying TestPyPI JSON and installation checks before the production publish.
.github/workflows/release.yml
Provide a reproducible, hash-locked toolchain for release artifact construction.
  • Lock build, twine, setuptools, and transitive dependencies for CPython 3.12 on Linux.
  • Install release tooling with binary-only, hash-verified pip installation in an isolated virtual environment.
.github/release-tools-cpython312-linux.txt
Make the package metadata suitable for a strict PyPI release.
  • Declare README.md as the project readme so built distributions include a Markdown long description.
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Artifact dependency validation and TestPyPI polling contain release-blocking correctness gaps, and the workflow lacks committed regression coverage.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Adds a reviewer-gated, OIDC-based release pipeline for publishing verified openDox distributions to TestPyPI and PyPI.

Changes:

  • Builds, validates, smoke-tests, and publishes release artifacts.
  • Adds hash-locked release tooling.
  • Declares README.md as package metadata.
File Description
.github/​workflows/​release.yml Implements staged trusted publishing and artifact verification.
.github/​release-tools-cpython312-linux.txt Pins release tools and dependencies by hash.
pyproject.toml Adds the PyPI long description source.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
brettheap and others added 2 commits October 3, 2026 00:15
…re, the release tag, the install line as written, and tests (Copilot review)

Copilot's review of openDox-code#78 at 4173dbc, and of openxFactory#1220
at 00a5bdae, with the holder's ruling on the split (T101):

- The artifact checks compare the wheel's requirements with
  pyproject.toml's, for the base install and every extra, as normalized
  requirements, and require the `local` extra to carry opendox[runtime] and
  pixeltable-pgserver (r4170775660).
- The fresh venv walks the whole requirement closure of opendox[local]
  from the installed metadata, extras and markers included, and runs the
  bundled server's `initdb --version` and `postgres --version` through
  opendox.runtime.bundle.server_binaries() (r4170775660).
- The version gate refuses an epoch, whose `!` a wheel's file name escapes
  (r4170775696).
- The release-commit step never requires main's head: the commit must
  equal the openDox root's pin and be on main, and a dispatch on a tag must
  name v<version> (the holder's ruling).
- testpypi-install runs the falsifier's install line as written,
  unversioned, then requires the installed version to be the one uploaded,
  each try in a fresh venv (openxFactory#1220, r4170767305).
- tests/test_release_workflow.py: 39 hermetic cases that run the steps'
  own scripts, with a stand-in gh, over a hand-built wheel and sdist
  (r4170775717). validate.yml's floors are not moved here: they permit a
  rise, and validate.yml is T095's single-writer file (openDox-code#75).

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 00:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

TestPyPI propagation can fail prematurely, and the release documentation contains inaccurate credential claims and broken PyPI links.

Review effort: Balanced
Findings: 1 Medium severity · 2 Low severity

Open (3)
Resolved since last review (3)

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread pyproject.toml
brettheap and others added 2 commits October 3, 2026 00:55
… wording is exact, and README's links resolve on PyPI (Copilot review)

Copilot's review of openDox-code#78 at 2b0083b:

- r4170898255: the JSON read kept a partial or stale answer as final. It
  now retries, up to 20 reads 15 s apart, until TestPyPI serves exactly
  the build job's two files and digests, and only then refuses.
- r4170898293: the header said "no token". It now says no PyPI token and
  no stored secret, and names the one other credential, GitHub's own
  GITHUB_TOKEN, which the build job passes to gh for its two API reads,
  with contents: read and actions: read.
- r4170898319: README.md's four relative links (SECURITY.md and two docs)
  are absolute, so they resolve from the PyPI page that `readme` makes of
  README.md.

And openxFactory#1220's r4170908474: a dispatch runs at the head of the
ref it names, so the header and the release-commit refusal say to
dispatch on the tag v<version> at the pinned commit once main has moved.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 00:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The cross-repository pin gate uses a repository-scoped token that cannot read the root repository, blocking releases.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (3)

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
…-releases (Copilot review)

Copilot's review at de3e324 (r4171040914, r4171040935):

- The openDox root's contracts/code-pin.yaml is read by an anonymous
  depth-1 git fetch of opensoft/openDox main (public), with every
  credential helper cleared and no prompt, in a repository of its own
  under RUNNER_TEMP. The read of another repository no longer depends on
  this job's repository-scoped GITHUB_TOKEN. The step's one gh call is the
  compare with this repository's main.
- The version gate refuses a pre-release or a development release: the
  dry run installs the unversioned "opendox[local]", which pip resolves to
  a final release once one exists, so it could never install what was
  uploaded.
- tests/test_release_workflow.py: the release-commit cases read a
  stand-in root through git's url.insteadOf in the environment, with
  GIT_ALLOW_PROTOCOL=file so no case can reach the network; a root with no
  pin and an unreachable root refuse; a structural case holds the
  anonymous read; the version gate refuses 0.2.0rc1 and 0.2.0.dev1 and
  passes 0.1.0.post1. 44 cases.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 01:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Partial uploads are not safely recoverable, and the TestPyPI retry budget exceeds the job timeout.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Enable skip-existing for recoverable TestPyPI uploads

.github/​workflows/​release.yml:534

A partial TestPyPI upload is not recoverable as written. This pinned action defaults skip-existing to false and invokes Twine over both files, so if the wheel uploads before the sdist fails, a rerun stops on the immutable duplicate wheel instead of uploading the missing sdist. Enable skip-existing; the following JSON gate already verifies that the remote files and digests are exactly the build outputs.

This issue also appears on line 667 of the same file.

Medium severity Align job timeout with advertised retry budgets

.github/​workflows/​release.yml:540

The advertised retry budgets cannot fit this job timeout. If the first 19 JSON reads consume their 30-second timeout and the 20th succeeds, that gate takes 885 seconds; the nine 30-second waits before a successful tenth install attempt then bring the minimum to 19m15s before checkout/setup, venv creation, or any pip runtime. The job can therefore be cancelled before completing the promised retries.

…cked after the upload, and job timeouts that fit the retries (Copilot review)

Copilot's review at 7cc65f9 ("Needs a closer look", two items it had
missed before):

- Both pypa/gh-action-pypi-publish steps set skip-existing: a re-run after
  a partial upload uploads the rest instead of stopping on the file the
  index already holds. An index keeps each file name once, so a skipped
  file could differ from the verified one; each upload is therefore
  followed by the check that the index serves exactly the build job's two
  digests. TestPyPI's check already followed its upload, and the pypi job
  now runs the same script against PyPI after its upload. The one script
  reads INDEX, JSON_BASE, READS and PAUSE from its step's env, and catches
  a read timeout too.
- testpypi-install's timeout is 75 minutes, its budget in full: the JSON
  step (20 reads, each up to 30 s, 15 s apart: 15 min), the install (10
  tries, each cut off after 300 s, 30 s apart: 55 min), and setup (5 min).
  The pypi job's is 30 minutes (the upload, the JSON step, the download).
- tests/test_release_workflow.py (57 cases): skip-existing on both
  uploads; the two index checks are one script, and PyPI's follows the
  upload; the index check against a local server (exact, lagging, another
  sdist, a third file, a refused read); each job's timeout recomputed
  from its steps.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 01:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The root pin can change after its early validation but before the irreversible PyPI upload.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Comment thread .github/workflows/release.yml Outdated
brettheap and others added 2 commits October 3, 2026 14:05
… (Copilot review)

Copilot's review at 74b8737 (r4171232529): the pin was checked only in
the build job, and the run then waits on approvals (and, before PyPI, on
TestPyPI), so a pin the openDox root moved meanwhile would not stop the
irreversible upload.

- The build job's release-commit step splits in two: the ref and main
  check (a tag must be v<version>; the compare with this repository's
  main, through the API), and the pin check (the anonymous git read of
  opensoft/openDox main's contracts/code-pin.yaml, run with python3).
- testpypi and pypi each run that pin step again, the same script, as the
  step right before their upload; it needs no token.
- tests/test_release_workflow.py (62 cases): the release-commit cases run
  both build steps; the pin step carries no gh call and no env, and the
  ref step's one gh call is the compare; each publish job's step before
  its upload is the build job's pin step; a moved pin refuses there and an
  unchanged one passes, in each job.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Requirement-marker validation is lossy, and PyPI duplicate handling can modify an unrecoverable release before detecting mismatched artifacts.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (1)

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
…ad (Copilot review)

Copilot's review at 5cdb2de (r4173500546, r4173500563):

- The requirement comparison keeps each requirement's marker, less only
  setuptools' extra-ownership clause (`and extra == "X"`, read from the
  parsed marker), and a direct URL; an extra under a top-level `or`, or
  nested, is refused.
- Before each upload, the index must hold no file of this version but a
  verified one at its verified digest (a 404 is no file yet), so
  skip-existing can never make a mixed release. The same script in both
  publish jobs, before the pin's re-check.
- tests/test_release_workflow.py (82 cases): marker cases (an extra's or a
  base requirement's marker changed or dropped, a direct URL, the extra's
  clause first, an extra under an or); the preflight's place and the
  index cases.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap and others added 2 commits October 3, 2026 15:32
…kflow

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…arison needs)

The artifact tests' pyproject gains a direct-URL requirement, and a case
whose wheel names another URL for it; with the URL dropped from the
comparison, that case passes, so it holds the URL half of r4173500546.
83 cases.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 16:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The production upload lacks the timeout assumed by its retry-budget test, so post-upload verification can be cut short.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (2)

Comment thread .github/workflows/release.yml
Comment thread tests/test_release_workflow.py Outdated
Copilot AI balanced review requested due to automatic review settings October 3, 2026 16:11
… test reads each bound (Copilot review)

Copilot's review at d91fd5b (r4173861533, r4173861563): the PyPI upload
had no bound of its own, so it could take the time the JSON check after
it is owed, and the test assumed a 10-minute upload instead of reading it.

- Every step of testpypi, testpypi-install and pypi declares
  timeout-minutes (the uploads 10, each JSON check 16, the install 60, the
  pin step 3, here and in the build job, where it stays the same step), and
  each job's timeout covers the sum: testpypi 25, testpypi-install 90,
  pypi 40. A cut-off upload stays recoverable through the preflight and
  skip-existing.
- test_each_job_timeout_covers_the_retries_it_promises reads every bound
  from the workflow: each step declares one, each job covers the sum, each
  upload is at most 10 minutes, and each JSON check and the install cover
  their scripts' retry budgets. Mutants: the PyPI upload unbounded, at 30
  minutes, the pypi job at 30, PyPI's JSON step at 10: each fails.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The TestPyPI installation can select and execute an unverified opendox distribution from PyPI.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread .github/workflows/release.yml
Copilot AI balanced review requested due to automatic review settings October 3, 2026 18:20
…eel before running it (Copilot review)

Copilot's review at 960d549 (r4173890172): with PyPI as the extra index,
pip merges candidates from both, and opendox is not reserved on PyPI
before its first release, so the unversioned install line could resolve
an opendox PyPI serves; the step checked only the version string, and
ran the venv's interpreter to read it.

- The install line stays T099's falsifier as written (unversioned,
  wheels only) and gains --report. This job's own python3, never the
  venv's, reads the report before anything from the install runs: the
  one opendox installed must come from test-files.pythonhosted.org, at
  VERSION, with the verified wheel's sha256. An older opendox (an index
  that lags) is tried again; any other opendox (another host, another
  digest, VERSION or above) refuses at once. opendox --help runs only
  after that.
- tests/test_release_workflow.py (93 cases): the install line's parts
  and that nothing from the venv runs but pip before the verdict; nine
  verdict cases over pip reports (the verified wheel; PyPI at, above and
  below the version, and at the verified digest; TestPyPI older, newer,
  at another digest; no opendox). Mutants: the host ignored, the digest
  ignored, the venv's interpreter run before the verdict, no --report:
  each fails.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Irreversible package publication and cross-repository release gates warrant final human validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Low severity Correct inaccurate claim about credentials used by the job

.github/​workflows/​release.yml:799

This credential claim is inaccurate: the job explicitly grants contents: read, and the preceding actions/checkout uses that job's automatic GITHUB_TOKEN (even though persist-credentials: false removes it afterward). Clarify that only the install/run step receives no token; otherwise the documented threat model understates the credentials used by this job.

Copilot AI balanced review requested due to automatic review settings October 3, 2026 18:27
…iew)

Copilot's review at b75205d, an item it had missed before: the comment
said the testpypi-install job holds no token, but its checkout uses the
job's GITHUB_TOKEN (contents: read), without persisting it. The comment
now says the install step receives no token, no OIDC grant and no secret,
and that the token is used only by the checkout.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Environment reviewer protection is checked too early to guarantee approval remains required when publication occurs.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Comment thread .github/workflows/release.yml
Copilot AI balanced review requested due to automatic review settings October 3, 2026 18:38
…ilot review)

Copilot's review at f59e710 (r4174341950): the build job's check that
both environments ask a reviewer goes stale while the run waits; a rule
removed meanwhile would let a publish job start with nobody approving it.

- Each publish job, right before its preflight, pin check and upload,
  re-reads its environment: it must still name a required reviewer, and
  this run's review history (actions/runs/<id>/approvals) must hold an
  approved review of a deployment to it. Either missing refuses, and
  nothing is uploaded. The publish jobs gain actions: read for those two
  reads of this repository's API; the header says so. Budgets: testpypi
  24 of 25 minutes, pypi 40 of 45.
- tests/test_release_workflow.py (108 cases): the step's place, env and
  permissions in both jobs, one script; seven cases in each job (approved;
  the rule removed; the environment unreadable; no approval; only the other
  environment approved; rejected; the history unreadable). Mutants: any
  environment's approval counts, a rejected review counts, the rule not
  re-read, no check in pypi: each fails.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It performs irreversible external publication whose live OIDC, environment, and index behavior cannot be fully validated by repository tests.

Review effort: Balanced
Findings: None

Resolved since last review (1)

brettheap and others added 2 commits October 4, 2026 13:12
…kflow

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 13:45
@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The irreversible publishing workflow and external environment configuration warrant final human validation despite extensive safeguards.

Review effort: Balanced
Findings: None

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants