From dbf15ec69dd0600da8da4a73e7ec419426a240ea Mon Sep 17 00:00:00 2001 From: Shmuel Osovski Date: Mon, 31 Aug 2026 21:00:05 +0300 Subject: [PATCH 1/3] release: set the 0.3.0 release date, repository URL, and pin the publish action CHANGELOG.md carried `unreleased (release date set at publication)` and CITATION.cff carried a TODO for the two fields that were unknowable before the repository was published. Both are knowable now. Also pin `pypa/gh-action-pypi-publish` to v1.14.2. It was the only action in the repository still on a mutable reference (`@release/v1`), and it is the single highest-privilege step we have: it runs with `id-token: write` inside the `release` environment and can upload to PyPI under our name. Everything else was pinned exactly during release prep; this was missed because the job had never run. check_versions --tag v0.3.0 and check_public_tree both pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TcdE6eeSDBh8xrgKCoGrkw --- .github/workflows/release.yml | 2 +- CHANGELOG.md | 2 +- CITATION.cff | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c5111e..fbe1c7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: - name: Publish to PyPI through trusted publishing if: vars.PUBLISH_TO_PYPI == 'true' - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@v1.14.2 with: packages-dir: dist/packages/ verbose: true diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc70ad..6e895a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to the public AKMS packages are documented here. The public history begins with the first curated release; earlier private development is intentionally not replayed. -## [0.3.0] — unreleased (release date set at publication) +## [0.3.0] — 2026-08-31 ### Added - First public research preview: `akms` (deterministic knowledge compiler, diff --git a/CITATION.cff b/CITATION.cff index 503b5d6..0a2bd55 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,4 @@ # To cite AKMS, use the metadata below. -# TODO: set `repository-code` and `date-released` at publication time — the -# public repository URL is not final until the maintainer publishes the tree. cff-version: 1.2.0 message: "If you use AKMS in your research, please cite it as below." title: "AKMS: Adaptive Knowledge Management System" @@ -10,6 +8,8 @@ authors: given-names: Shmuel email: shmuliko@technion.ac.il version: 0.3.0 +date-released: 2026-08-31 +repository-code: "https://github.com/CEmM2/AKMS" abstract: >- A deterministic global-local knowledge compiler: typed knowledge nodes, graph compilation, task-scoped projections, and structured evidence From 277a16e149c0022678ccabc87dc2e4cf5cbe3411 Mon Sep 17 00:00:00 2001 From: Shmuel Osovski Date: Mon, 31 Aug 2026 21:11:30 +0300 Subject: [PATCH 2/3] ci(release): publish each package from its own environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyPI enforces uniqueness on the (owner, repository, workflow, environment) tuple for pending trusted publishers. All four packages ship from one repo through one workflow, so with a single shared `release` environment only the first of them could be registered — the other three are rejected. That makes the current single-job layout unable to claim the four project names at all. A job may declare exactly one environment, so the publish step has to fan out. Split the workflow into build → publish (matrix, one environment per package) → github-release, passing the verified distributions between jobs as an artifact so every package publishes the exact bytes that were tested rather than a rebuild. Two things fall out of the split that are worth keeping on their own merits: - Each package's OIDC token is now scoped to an environment that can publish that package and nothing else, instead of one credential able to push all four. - Permissions are per-job. `contents: write` belongs only to the release job and `id-token: write` only to the publish jobs; the top level drops to `contents: read`. `github-release` needs `always()`, because `publish` is skipped whenever PUBLISH_TO_PYPI is unset and a skipped dependency would otherwise skip it too. It still refuses to run after a failed upload. The per-package globs are checked against real output rather than assumed: `uv build` was run for all four packages and each prefix claims exactly its own wheel and sdist, with all eight distributions claimed exactly once and no cross-matching between `akms-` and `akms_*`. The isolate step asserts the count of 2 at release time so a glob that silently stops matching fails loudly instead of publishing an empty directory. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TcdE6eeSDBh8xrgKCoGrkw --- .github/workflows/release.yml | 101 +++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbe1c7e..b18eadc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,19 +6,16 @@ on: - "v*" permissions: - contents: write - id-token: write + contents: read concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: - release: - name: Validate, build, and publish + build: + name: Validate and build runs-on: ubuntu-latest - environment: - name: release steps: - name: Check out tagged source uses: actions/checkout@v7 @@ -94,13 +91,99 @@ jobs: "from akms.orchestrator import run_pipeline; print('embedded runtime import: OK')" .venv-release/bin/python -m pytest tests/public_smoke/runtime -q - - name: Publish to PyPI through trusted publishing - if: vars.PUBLISH_TO_PYPI == 'true' + - name: Hand the verified distributions to the publish jobs + uses: actions/upload-artifact@v7.0.1 + with: + name: distributions + path: dist + if-no-files-found: error + retention-days: 7 + + # One publish job per package, each in its own environment. + # + # PyPI enforces uniqueness on the (owner, repository, workflow, environment) + # tuple for *pending* publishers, so four packages released from one repo by + # one workflow cannot share a single environment — only the first of them can + # be registered. A job may declare exactly one environment, so the publish + # step has to fan out rather than upload all four from one job. + # + # This is the better shape on its own terms anyway: each package's OIDC token + # is scoped to an environment that can publish that package and nothing else. + publish: + name: Publish ${{ matrix.package }} to PyPI + if: vars.PUBLISH_TO_PYPI == 'true' + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: pypi-${{ matrix.package }} + url: https://pypi.org/p/${{ matrix.package }} + strategy: + fail-fast: false + matrix: + include: + - package: akms + prefix: "akms-" + - package: akms-learn + prefix: "akms_learn-" + - package: akms-nodes-gen + prefix: "akms_nodes_gen-" + - package: akms-failure-memory + prefix: "akms_failure_memory-" + steps: + - name: Download the verified distributions + uses: actions/download-artifact@v8.0.1 + with: + name: distributions + path: dist + + - name: Isolate this package's distributions + shell: bash + run: | + set -euo pipefail + mkdir -p upload + cp dist/packages/${{ matrix.prefix }}* upload/ + # Exactly one wheel and one sdist. A prefix that silently matched + # nothing would publish an empty directory, and one that over-matched + # would push a sibling package into the wrong project. Both are quiet + # failures worth making loud. + found=$(find upload -type f | wc -l | tr -d ' ') + if [ "$found" -ne 2 ]; then + echo "expected 2 distributions for ${{ matrix.package }}, found ${found}:" >&2 + find upload -type f >&2 + exit 1 + fi + ls -l upload + + - name: Publish through trusted publishing uses: pypa/gh-action-pypi-publish@v1.14.2 with: - packages-dir: dist/packages/ + packages-dir: upload/ verbose: true + github-release: + name: Create GitHub release + needs: [build, publish] + # `publish` is skipped whenever PUBLISH_TO_PYPI is unset, and a skipped + # dependency would otherwise skip this job too. Run whenever the build + # succeeded and nothing actually failed, so a validation-only tag still + # cuts the release — but never publish a release after a failed upload. + if: >- + always() + && needs.build.result == 'success' + && !contains(needs.*.result, 'failure') + && !contains(needs.*.result, 'cancelled') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download the verified distributions + uses: actions/download-artifact@v8.0.1 + with: + name: distributions + path: dist + - name: Create GitHub release uses: softprops/action-gh-release@v3 with: From 308f6e246a44b2902c12711fcf8e2ae156cf544d Mon Sep 17 00:00:00 2001 From: Shmuel Osovski Date: Mon, 31 Aug 2026 21:53:31 +0300 Subject: [PATCH 3/3] ci(release): make publishing idempotent so the first release can go in waves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PyPI caps how many *pending* trusted publishers one account may hold, and the cap is lower than the four projects released here. Combined with the uniqueness constraint on (owner, repository, workflow, environment), that means the four names cannot all be pre-registered before the first release. They can be claimed in waves instead. A pending publisher converts into a normal publisher on first use, which frees its slot — so registering what fits, publishing, then registering the rest works, provided a second run does not choke on the distributions the first one already uploaded. `skip-existing` makes that safe. It also turns recovery from a partial upload, such as a network failure part-way through the matrix, into a re-run rather than a version bump. It cannot overwrite a release: PyPI rejects a duplicate filename whatever its contents, and this flag only swallows that rejection. The signal it costs — a loud failure when republishing an existing version — is already covered upstream by check_versions tying the tag to all four package versions. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TcdE6eeSDBh8xrgKCoGrkw --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b18eadc..fc18c96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,19 @@ jobs: with: packages-dir: upload/ verbose: true + # Makes a re-run idempotent. PyPI caps how many *pending* publishers + # one account may hold, which is fewer than the four projects here, + # so the first release has to publish in waves: register what fits, + # publish (which converts those pending publishers into normal ones + # and frees the slots), register the rest, run again. Without this, + # the second run fails on the packages the first one already + # uploaded. It also makes recovery from a partial upload — a network + # failure part-way through the matrix — a re-run rather than a + # version bump. + # + # This cannot overwrite anything: PyPI rejects a duplicate filename + # whatever its contents, and the flag only swallows that rejection. + skip-existing: true github-release: name: Create GitHub release