Skip to content

Publish openRepoShape to TestPyPI and PyPI as openreposhape, so one pipx install puts the tools and git-filter-repo on any machine (design first) #563

Closed

Description

@brettheap

Goal: publish openRepoShape as a Python package named openreposhape, so that one pipx install openreposhape puts the tools on any machine, with git-filter-repo alongside. The design comes first, then the code.

Decision. Brett Heap, 2026-10-14:

  • At 20:32:42Z: "should we deploy this to a py lib site? we did that with other py tools".
  • At 20:35:56Z, after lane 3 recommended a v1.1.0 issue with a design first: "file it in v1.1.0".

Why

  • One install command on every machine, on the host and in every container. Today a person installs the openRepoShape launcher with gh api … | bash -s -- --install (openRepoShape:5-11), and installs git-filter-repo separately on each machine.
  • adopt needs git-filter-repo. adopt-project.py:6611-6622 refuses without it. adopt-project.py:548-554 tells people how to install it and keeps it un-vendored on purpose. A package can declare it as a dependency.
  • The workBenches image could install a released version. Today it vendors two files of this repository at a pinned commit (devBenches/base-image/upstream-pin.yaml, block openreposhape), and ruling 33 adds git-filter-repo to its pip list. Once this ships, the image can run pip install openreposhape==<version> instead.
  • The precedent is openDox. It publishes opendox 0.1.0 to TestPyPI and PyPI from opensoft/openDox-code .github/workflows/release.yml:
    • a workflow_dispatch with a version input;
    • it builds once, publishes to TestPyPI, installs from TestPyPI into a fresh environment, then publishes the same bytes to PyPI;
    • it uses trusted publishing (id-token: write) through the testpypi and pypi environments, each with a deployment limit.

Constraints the design must keep

  1. "Clone it and run it" keeps working with python3 and git only. scripts/repo_shape.py:5-10 says the tools use the standard library only, on purpose, for an organisation with "no permission to pip install anything".
    • The package is a second way in, never a requirement.
    • The tools gain no runtime dependency.
    • git-filter-repo stays needed by adopt only.
  2. A pin keeps naming one commit of this repository.
    • A project's project.yaml pins the standard by repository, commit, and a sorted-ls-tree-r-v1 digest of that commit's tree (templates/assembly-root/project.yaml:74-81; scripts/repo_shape.py:76 and 929-944).
    • scaffold-project.py:870 reads that commit with git rev-parse HEAD in its own checkout.
    • A wheel has no checkout. So a packaged run must carry the commit and digest it was built from, and the build must refuse when they differ from the tag's own. scripts/repo_shape.py:954 already reads the same digest from the forge.
  3. Python 3.9 or newer, on Linux, macOS and Windows, as now (setup.sh:76; README.md:365).

Questions the design answers. Brett Heap approves the design on this issue before any packaging code lands.

  1. A thin package or a full one?
    • Thin: the wheel gives a Python command for the launcher's work and declares git-filter-repo. At run time it clones this repository at the commit it was built from, as the launcher already does (openRepoShape:31 and 356-378). Pins and digests do not change, and it still needs the network, as today. The launcher is 703 lines of bash, so the modes the package offers are re-stated in Python.
    • Full: the wheel ships the tools, templates/ and contracts/ as package data, with the commit and digest it was built from, and runs offline. Every tool that finds its files from Path(__file__) must then find them in the package. Examples: scripts/shape_materialize.py:3354 and 3646, scripts/validate-repository-naming.py:95, scripts/family.py:134, scripts/bump-leg.py:108, scripts/shape_advisory.py:105.
    • Lane 3 leans to thin first, because it changes nothing a pin means.
  2. Commands. Which console commands does the package give? One option is openRepoShape for the launcher's modes plus scaffold-project, adopt-project, update-shape and shape-doctor. The other is one command with subcommands. Either way, Windows gets native commands from the entry points.
  3. Finding git-filter-repo. pipx exposes only the package's own commands, not its dependencies'. So git filter-repo (adopt-project.py:7194) would not find the git-filter-repo the package installed. Either the tools put their own environment's scripts directory on PATH for that one call, or the docs say pipx install --include-deps.
  4. Versions. Does the package version equal the git tag (v1.1.0 → 1.1.0)? The packaging code comes after v1.0.0, so the first published version would be 1.1.0.
  5. Where it publishes. TestPyPI, then PyPI in the same release, as openDox does? Or TestPyPI only, until a later word?
  6. The documented way in. Does README keep the --install one-liner first and add pipx as a second way, or lead with pipx?

Brett Heap's own acts, since these are accounts and settings, never an agent's:

  • add the trusted publishers on pypi.org and test.pypi.org;
  • create the testpypi and pypi environments, with their deployment limits.

Not part of v1.0.0. v1.0.0 is tagged at bfed143 as it stands. On 2026-10-14 neither PyPI nor TestPyPI had a project named openreposhape: both JSON APIs answered 404.

Order: after the v1.0.0 tag and the workBenches re-pin (ruling 33).

Filed by lane openRepoShape-3 on Brett Heap's word above.

Activity

  1. added this to the v1.1.0 milestone on Oct 14, 2026
  2. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-3, session b30e8b49-f249-4668-adb1-1dbfa93c09e1@Eagle, 2026-10-15T11:48:51Z, for #563

    Logged in lanes/log/openRepoShape-3.md at 937984b0964feaa3dfcf8835f4561e992e782703.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#563:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#563"`, kept where the row or its body names `#563`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `563` as a whole token:
       none
    
  3. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    (lane openRepoShape-3, 2026-10-15T12:07:50Z)

    #563 design: openreposhape on TestPyPI and PyPI. There are six decisions, and each can be answered by its number. Line numbers are main at a35f1be1, except those marked #577:, which are the v1.0.1 installer's head 868c9bf8; main receives that code for v1.1.0. In short: a thin wheel carries the release's commit and digest, and runs the tools from a checkout at that commit. A pin means what it means today, and no tool gains a dependency.

    # Question Recommendation Alternative
    1 Thin or full Thin. A stamped commit and digest, and the tools run from a cached checkout at that commit Full (the tools, templates/ and contracts/ as package data), or the bash launcher shipped as a script
    2 Commands openRepoShape (the launcher's modes and flags) plus scaffold-project, adopt-project, update-shape and shape-doctor, with arguments passed through One command with subcommands
    3 git-filter-repo Depend on git-filter-repo==2.47.0. The package's commands put their own scripts directory first on PATH for the children they start pipx install --include-deps openreposhape in the docs
    4 Version The tag. v1.1.0 → 1.1.0, stamped at build; the first release is 1.1.0 A static version in pyproject.toml, checked against the tag (as openDox does)
    5 Where TestPyPI → an install check on three OSes → PyPI, in one dispatched run. Each upload waits for your approval TestPyPI only, and PyPI by a later dispatch
    6 README The one-line install stays first. pipx is the second way, and the first on native Windows pipx leads everywhere

    1. Thin

    Recommendation. The wheel holds a small stdlib-only package and one file of the tag's tree, setup-project.py, byte for byte. That file lets openRepoShape --preflight work before git is installed: --preflight returns before the git probe and the clone (setup-project.py:2201-2210).

    Every other run uses a checkout of this repository at the stamped commit. The checkout is:

    Before each run with no override, the command requires the checkout to be at the commit, to be clean, and to digest to the stamped value with its own scripts/repo_shape.py:926, never a copy. A scaffold then pins exactly that commit and digest (scaffold-project.py:870-872).

    • The ref is the stamped 40-hex commit, never the tag, because a tag can be moved.
      • $OPENREPOSHAPE_REF, set for one run, overrides it, as it overrides the launcher's record (#577:openRepoShape:38-47, 570-588).
      • $OPENREPOSHAPE_REPO still names a fork or a mirror.
      • --version prints the version, the tag, the commit, the digest and the checkout's path.
    • Re-stated in Python, from #577:openRepoShape:
      • the argument routing (683-777);
      • one mode per run (79-84);
      • --org from $OPENREPOSHAPE_ORG or a prompt (833-848);
      • the usage text for a run with no arguments (818-822);
      • the ref checks (514-532);
      • --doctor's clone and run (475-497, 601-680).
    • Not re-stated: setup.sh's section 0, which setup-project.py:633 already is ("setup.sh:157-206, in Python"); the preflight; and --install with its record, which pipx and the stamp replace. The launcher's own test cases drive the Python command, on the Windows job too, where the launcher's own tests skip.
    • The build refuses any tree that is not the tag's.
      • The run is dispatched on tag v<version>, which must be annotated and point at the run's commit, and the checkout must be clean.
      • Its tree_digest must equal the forge's tree_digest_from_gh (scripts/repo_shape.py:926, 953).
      • The wheel is built from git archive of the tag, plus the generated stamp openreposhape/_release.py.
      • Before the upload, every file in the wheel except the stamp and the metadata must equal the tag's own file, byte for byte, and the stamp must hold the values above.

    Alternatives.

    • Full costs more than package data.
      • Three tools read the standard's commit from its own git (scaffold-project.py:870, adopt-project.py:8263, scripts/family.py:1096). update-shape.py reads the standard's history (update-shape.py:388-436), and so does the doctor's currency row. A wheel with no .git therefore needs a second source of the tools' own commit, or a clone anyway.
      • Working offline gains little, because scaffold and adopt create and push GitHub repositories.
      • 13 of the 52 files under templates/ and contracts/ are dotfiles or sit under .github/. setuptools globs skip those unless told to; openDox's web/** once packed 41 of its 42 files for that reason (openDox pyproject.toml:319-328).
      • The Path(__file__) anchors (scaffold-project.py:76, scripts/shape_materialize.py:3354, 3646 and others) hold only if the tree's layout ships intact.
    • The bash launcher shipped as a script re-states nothing, and pipx exposes a wheel's scripts as commands (pipx 1.17.12, venv_inspect.py:139-163). But it gives native Windows nothing, because the launcher needs bash, and curl or gh. Its --install would also copy it out of the environment pipx manages.

    Later, the launcher can become a shim over this Python, as setup.sh became one over setup-project.py (#50). That is not part of this issue.

    2. Commands

    Recommendation. openRepoShape keeps the launcher's modes and flags: <Project>, --preflight, --doctor, --version and --help. Beside it, scaffold-project, adopt-project, update-shape and shape-doctor each run that file from the checkout, with every argument unchanged, in the directory where the command was typed.

    • These are the names that README, AGENTS.md and docs/cli.md already use, less ./ and .py, so docs/cli.md stays the one flag reference.
    • On Windows each one is a native .exe. That gives native Windows the openRepoShape command it lacks today (#577:README.md:155-158).
    • The packaged --install refuses, and names pipx upgrade openreposhape instead.

    One name, two installers, one directory. pipx places its commands in ~/.local/bin (pipx paths.py:19), which is where install.sh places the launcher.

    • pipx does not replace a file it did not place. It warns "File exists … Not modifying" (pipx commands/common.py:217-229).
    • In the other order, the launcher's --install would cp through pipx's symlink into the package's environment (#577:openRepoShape:401-408).
    • So the first PR makes --install, and with it install.sh, refuse a target that is a symlink. README says to use one installer or the other.

    bump-leg and family would take one line each, if you want them. I left them out because family is a generic name to put on a PATH.

    Alternative. One command with subcommands (openRepoShape adopt plan …). It needs a second grammar beside the launcher's flag modes and its positional <Project> (#577:openRepoShape:683-777), and a second spelling of every documented command.

    3. Finding git-filter-repo

    Recommendation. dependencies = ["git-filter-repo==2.47.0"]. That version:

    • is the one CI runs (.github/workflows/tests.yml:61-64, 184-185);
    • is the one install.sh installs (#577:install.sh:59-61);
    • is the one adopt's measurements name (adopt-project.py:4638-4639);
    • is the newest on PyPI: a py3-none-any wheel for Python 3.6 or newer, whose one console script is git-filter-repo = git_filter_repo:main.

    pipx exposes only the package's own commands (pipx pipx_metadata_file.py:120-121, 141-149). So every child that the package's commands start gets the package environment's scripts directory (sysconfig.get_path("scripts")) first on PATH, whenever a git-filter-repo is there.

    • adopt finds it with shutil.which and runs git filter-repo (adopt-project.py:6635-6645, 7218). git finds git-filter-repo on PATH, and the .exe form too: the Windows job installs it with pip and runs the adoption suites, which skip without it (tests.yml:174-185).
    • The doctor's machine row finds it the same way (shape-doctor.py:6046).
    • No tool changes, and adopt still refuses with filter-repo-missing wherever the tool is absent.

    The gap. A NEXT line pasted as python3 <checkout>/adopt-project.py execute … runs outside the package. On a machine whose only git-filter-repo is inside the package's environment, it refuses with filter-repo-missing before anything is created (adopt-project.py:8215). So FILTER_REPO_HINT (adopt-project.py:548-554) and README.md:747 gain the pipx route. They also drop the bare pip install, which Debian 12 and Ubuntu 23.04 and later refuse (#577:install.sh:219-224).

    Alternatives.

    • pipx install --include-deps openreposhape covers pasted lines too. But every reader must remember the flag, plain pip has no such step, and it collides with a git-filter-repo that install.sh or brew already placed.
    • A range (>=2.47,<3) in place of the pin would install an untested release on the day it appears.

    4. Versions

    Recommendation. The tag is the one source: vX.Y.Z → X.Y.Z.

    • The build writes the version into the stamp, with the tag, the commit and the digest. pyproject.toml reads it as dynamic = ["version"].
    • The committed stamp says 0.0.0, and the workflow refuses to publish that, as openDox refuses its placeholder (release.yml:331-332).
    • A build from a checkout reports in --version that it is unstamped, and follows main, as a launcher with no record does.
    • Only final releases are published. The first is 1.1.0: the v1.0.x line carries no packaging code, so it is never published.
    • Each version is uploaded once. A bad upload is fixed by the next patch, never by moving a tag.

    Alternative. A static version in pyproject.toml, bumped in the release commit and checked against the dispatched tag (openDox release.yml:295-337). That is one more line to change per release, and two sources that the check must keep in agreement.

    5. Where it publishes

    Recommendation. openDox's workflow, adapted (openDox release.yml:51-64): .github/workflows/release.yml, dispatch only, run on the tag with the version as its input. It has four jobs:

    1. build: the checks in 1, release tools from a hash lock, a wheel only, and the wheel's digest recorded.
    2. testpypi: environment testpypi, with trusted publishing. Only the two upload jobs get id-token: write.
    3. install check: install from TestPyPI with pipx into a fresh environment, on ubuntu, macOS and Windows, plus one leg on Python 3.9. --version must name the commit, --preflight must run, and --doctor over an empty repository must find git-filter-repo in its machine row.
    4. pypi: environment pypi. After the upload, PyPI must serve exactly the verified digest.

    The pypi job waits for your approval after the check passes. "TestPyPI first, PyPI on a later word" is therefore one run and one set of bytes.

    • One refusal more than openDox: the tag's commit must carry passing tests, tests-macos and tests-windows checks, the three-OS gate this repository lands by.
    • A dispatch on a branch builds and runs every check but cannot deploy, because both environments admit only v* tags. That is the rehearsal before 1.1.0.
    • TestPyPI holds git-filter-repo 2.23.0 only. So the install check takes git-filter-repo from PyPI, and openreposhape from TestPyPI by digest. It never takes a dependency from TestPyPI.
    • A wheel only: one pure file, byte-reproducible when built with the commit's time. openDox's sdist is not reproducible (release.yml:66-76). An sdist can come later.
    • In the release steps (#577:docs/release-notes/v1.0.1.md:76-134), v1.1.0's "Before tagging" gains one step after step 2, which pushes the tag: dispatch release.yml on the tag and approve both uploads. Step 3's gh release create then follows, so the notes never name a version that PyPI lacks.

    Alternative. TestPyPI only, and PyPI by a later dispatch. That costs a second dispatch and a second build. The second wheel must match the first byte for byte, or PyPI gets bytes that TestPyPI never checked. One run with a held approval needs neither.

    6. README

    Recommendation. install.sh's one line stays as step 3 (#577:README.md:228-241), and pipx install openreposhape follows it as the second way.

    • In the "Windows, natively" block (#577:README.md:112-158), pipx comes first. The two-command download stays after it, as the way that installs nothing.
    • The one line needs only curl and bash, installs git-filter-repo where pipx is present, and ends with the preflight (#577:install.sh:13-27).
    • pipx is itself an extra install on most machines: sudo apt-get install -y pipx on Debian and Ubuntu (#577:install.sh:231-233).
    • Your words on 2026-10-14T22:50:40Z were "now and after, we need a simple install process". Each way stays one line.
    • README's "No pip, no packaging" (#577:README.md:1438) becomes constraint 1's own words: the tools use the standard library only, and the package is a second way in, never a requirement.
    • AGENTS.md gets the one-liner's rule for pipx too: the person runs the install, and an agent only relays it.

    Alternative. pipx first everywhere. That is one spelling on three systems, but the first line then needs pipx installed already, and the install no longer ends with the preflight.

    Brett Heap's own acts

    1. pypi.org: a pending trusted publisher for the project openreposhape, with owner opensoft, repository openRepoShape, workflow release.yml and environment pypi. A pending publisher does not reserve the name until its first upload (docs.pypi.org). Both indexes answered 404 for openreposhape at 2026-10-15T11:52Z.
    2. test.pypi.org: the same, with environment testpypi. test.pypi.org needs its own account.
    3. GitHub, opensoft/openRepoShape: environments testpypi and pypi, each with you as the required reviewer and a deployment limit to tags v*, as openDox-code's two environments have today. This repository has only a copilot environment now.
    4. Each release: approve the two deployments. Optionally, add a ruleset on v* tags, which openDox's workflow recommends (release.yml:47-49).

    Lane acts after your approval

    In this order, after the workBenches re-pin, as this issue's Order says, and after the #577 carry to main, which brings install.sh and README's step 3:

    1. PR, the package: pyproject.toml; openreposhape/, stdlib only and added to SHIPPED in tests/test_repo_hygiene.py; the five commands, with the launcher's test cases run against them on all three OSes; a CI step that builds the wheel and installs it with pipx; and --install refusing a symlinked target.
    2. PR, the workflow: release.yml from openDox's, adapted as in 5, with its hash-locked release tools and a test that reads the workflow.
    3. PR, the docs: README (as in 6), AGENTS.md, the adopt hint and README.md:747, a regenerated docs/cli.md, and the handbook.
    4. After your acts: a dispatch on a branch as the rehearsal, which deploys nothing. Then v1.1.0's notes gain the dispatch step, and at the tag the lane dispatches on v1.1.0.
    5. Then workBenches installs openreposhape==1.1.0 with pip, in place of its vendored launcher and not beside it. pip writes its openRepoShape to /usr/local/bin, where the image copies the vendored launcher today (opensoft/workBenches devBenches/base-image/Dockerfile:217). git-filter-repo then arrives as the package's dependency, which covers ruling 33's need.

    Evidence: main a35f1be1; #577 head 868c9bf8; openDox-code c94878be; git-filter-repo's PyPI JSON and its 2.47.0 wheel; the source of pipx 1.17.12.

    Not yet verified. The first PR checks each of these before anything depends on it:

    • that GitHub's hosted ubuntu, macOS and Windows images still ship pipx, and that actions/setup-python still offers Python 3.9;
    • that git for Windows finds a git-filter-repo.exe put first on PATH from a pipx environment's Scripts directory (the Windows job's adoption suites suggest it, but no run log was read);
    • that a wheel built from git archive with SOURCE_DATE_EPOCH is byte-reproducible here;
    • that a console script named openRepoShape, with its capitals, behaves under pipx on macOS and Windows.

    Ruling 33's workBenches change adds git-filter-repo==2.47.0, the same version as decision 3, so a later pip install openreposhape in the image does not conflict with it.

  4. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    (lane openRepoShape-3, 2026-10-15T13:34:30Z) Approved. Brett Heap, at 2026-10-15T13:32:51Z: "approve all six for #563". The design above is the plan.

    What lane 3 does next, in order:

    1. The package PR. The writer starts now, on a branch stacked on install.sh and openRepoShape --install, carried into main from v1.0.1 (#577): the one line curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bash installs the newest release's command pinned to that release, installs git-filter-repo with pipx where it can, and ends with the preflight (#564) #587 (One-line install: curl -fsSL …/releases/latest/download/install.sh | bash installs the newest release pinned to it, with git-filter-repo, and checks the machine (v1.0.1) #564's carry into main), because the package re-states the v1.0.1 launcher. Its draft PR runs the three-OS CI, and it lands only after install.sh and openRepoShape --install, carried into main from v1.0.1 (#577): the one line curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bash installs the newest release's command pinned to that release, installs git-filter-repo with pipx where it can, and ends with the preflight (#564) #587.
      • It adds pyproject.toml and openreposhape/ (stdlib only), with the stamp, the cached checkout and its digest check.
      • It adds the five commands, and runs the launcher's test cases against openRepoShape on all three OSes.
      • It adds a CI job that builds the wheel and installs it with pipx, and makes --install refuse a symlinked target.
    2. The workflow PR: release.yml, from openDox's, adapted as in decision 5.
    3. The docs PR: README, AGENTS.md, the adopt hint and the handbook.
    4. A rehearsal dispatch on a branch, which deploys nothing.

    Before the first upload, Brett Heap's own acts:

    • the trusted publishers on pypi.org and test.pypi.org;
    • the testpypi and pypi environments.

    Nothing is uploaded before them, and the first upload is 1.1.0, at the v1.1.0 tag.

  5. 13 remaining items

  6. brettheap commented on Oct 17, 2026

    @brettheap
    ContributorAuthor

    (lane openRepoShape-3, 2026-10-17T21:55:11Z) Done: openreposhape 1.1.0 is on PyPI and TestPyPI, so pipx install openreposhape installs it.

    Lane: openreposhape-3 (openRepoShape-3)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions