Skip to content

[FEATURE] Branching strategy Phase 3 - PyPI and CodeArtifact publish workflows - #638

Open
John McCall (lowlydba) wants to merge 19 commits into
mainfrom
509-devops-branching-strategy---phase-3
Open

[FEATURE] Branching strategy Phase 3 - PyPI and CodeArtifact publish workflows#638
John McCall (lowlydba) wants to merge 19 commits into
mainfrom
509-devops-branching-strategy---phase-3

Conversation

@lowlydba

@lowlydba John McCall (lowlydba) commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 3 of the branching strategy (#509): the publish side of the pipeline Phase 2.B (#557) computed versions for. Internal builds go to CodeArtifact on every push to main; human-cut releases go to public PyPI.

What lands

Internal builds → CodeArtifact

main-publish.yaml, on push to main: detect-affected-packages (new composite action, using tj-actions/changed-files pinned by commit SHA rather than a hand-rolled git diff --name-only, for correct filename quoting and its dir_names-to-package-name folding) diffs the push against packages/** to find packages touched without a version bump (bumped packages release via the path below instead). Each affected package gets a .postN+main.<sha> version stamped in via compute-version, built, and published to CodeArtifact.

The same workflow doubles as a build-only smoke test on PRs touching the composite actions or itself: it discovers every package unconditionally instead of diffing (a PR touching .github/actions/** never touches packages/**, so the diff would always return zero and skip the job it's meant to exercise), builds, and stops short of publishing. Replaces compute-versions-dry-run.yaml.

Releases → PyPI

release-publish.yaml, on release: published: parses <package>-v<version> off the tag, builds, verifies the wheel matches (and, for overture-schema-pyspark, that the generated expressions are in it), and publishes via pypa/gh-action-pypi-publish with OIDC trusted publishing and attestations. Gated by the pypi-release environment's required reviewers, so a release only reaches PyPI after a human approves it there (Slack subscription to that environment's approval requests covers the notification side, no extra code).

release-trigger.yaml creates releases with an overture-release-publisher app installation token instead of GITHUB_TOKEN: GitHub doesn't fire release: published for releases created with the default token, so release-publish.yaml would never see them. Token scoped to contents: write only. App provisioning was tracked on #637 (now done).

A package's first version counts as a bump too: detect_version_bumps.py no longer skips before: null entries, so a brand-new package flows through release-trigger to PyPI on its first version, same as any other bump. detect_affected_packages.py's bumped set was widened to match (any package with a non-null after version), so the same push doesn't also queue it for an internal .postN build.

Mechanical follow-through: diff-package-versions action, deleted workflows, docs

package_versions.py diff was inlined at three call sites (detect-version-bumps, the new detect-affected-packages, and the pre-existing reusable-check-python-package-versions.yaml), each reimplementing the temp-file-then-$GITHUB_OUTPUT plumbing slightly differently. Pulled it into its own diff-package-versions action exposing count/diff outputs; the three consumers now just pipe diff into their own filter logic instead of each knowing the script's path and output convention.

package_versions.py itself moved from .github/workflows/scripts/ into diff-package-versions/, its only remaining caller, so every action in the repo now carries its own script(s) instead of reaching outside its directory.

Deletes compute-versions-dry-run.yaml and publish-python-packages.yaml, both superseded by the two workflows above.

docs/versioning.md gets a workflow-name reference table and the pypi-release approval-gate mechanics under the release walkthrough. CONTRIBUTING.md drops the "rolling out in phases" banner (this is the last phase) and corrects two release-flow descriptions that predated the approval gate.

Testing

detect-affected-packages's exclusion logic is tested locally against real repo commit ranges plus synthetic bumped/removed/new-package cases, including through the diff-package-versions wiring and after the script relocation. The uv version <post-release-string> --package <pkg> --frozen --dry-run stamping technique is confirmed to accept PEP 440 local-label strings. zizmor is clean on every touched/new workflow and action file. The workflows themselves can't run end to end until the external prerequisites below land.

Still open (tracked on #509, all external/manual)

  • PyPI Trusted Publisher config per package
  • pypi-release environment + required reviewers
  • p3-dev-builds-ca (vnext dev builds), blocked on ops-team#299
  • a resolver smoke test against real CodeArtifact once the app and PyPI config are live

Closes #509

…ish workflows

Adds the publish side of the versioning pipeline built in Phase 2.B:

- main-publish.yaml: on push to main, diffs changed packages/** files
  (via the new detect-affected-packages action) to find packages touched
  without a version bump, stamps a .postN+main.<sha> build via
  compute-version, and publishes to CodeArtifact. Also runs as a
  build-only smoke test on PRs touching the composite actions or itself,
  replacing compute-versions-dry-run.yaml.
- 
elease-publish.yaml: on 
elease: published, parses <package>-v<version>
  from the tag, builds, and publishes to PyPI via
  `pypa/gh-action-pypi-publish` (OIDC trusted publishing + attestations),
  gated by the pypi-release environment's required reviewers.
- 
elease-trigger.yaml: releases now get created with an
  overture-release-publisher app installation token instead of
  GITHUB_TOKEN, since GITHUB_TOKEN-created releases don't fire
  
elease: published for other workflows to pick up. App provisioning is
  tracked separately in #637 and has to happen before this path works.

Deletes compute-versions-dry-run.yaml and publish-python-packages.yaml,
both superseded by the two workflows above.

docs/versioning.md gets a workflow-name reference table and the
pypi-release approval-gate mechanics.

Still open, all external/manual, tracked on #509:
- provisioning overture-release-publisher (#637)
- PyPI Trusted Publisher config per package
- pypi-release environment + reviewers
- p3-dev-builds-ca, blocked on ops-team#299

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🗺️ Schema reference docs preview is live!

🌍 Preview https://staging.overturemaps.org/schema/pr/638/schema/index.html
🕐 Updated Aug 14, 2026 14:22 UTC
📝 Commit 3dd502d
🔧 env SCHEMA_PREVIEW true

Note

♻️ This preview updates automatically with each push to this PR.

Replaces the hand-rolled `git diff --name-only` + path-splitting with
tj-actions/changed-files' dir_names output, pinned by commit SHA. Fixes a
real gap in the old parser: git diff --name-only quotes non-ASCII/unusual
filenames, which naive line.split("/") didn't account for. Uses
�ll_modified_files (ACMRD) rather than �ll_changed_files (ACMR) so file
deletions still count as a package change, matching the old diff's behavior.

detect_affected_packages.py drops its subprocess/git plumbing entirely and
just reads the directory list from CHANGED_DIRS; the bump/removed
exclusion logic (a three-way set difference) stays in Python rather than
jq/bash, that's a different complexity class than this repo's existing
jq usage in enforce-change-type-label.yaml.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
…scripts run back to back

The action wasn't wrong, just awkward to read: python, jump to a JS
action, jump back to python. Steps 1 (version diff) and 2 (changed
dirs) don't depend on each other, only step 3 does, so nothing stops
them running adjacent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
package_versions.py diff was inlined at three call sites
(detect-version-bumps, detect-affected-packages, and

eusable-check-python-package-versions.yaml), each reimplementing the
temp-file-then-$GITHUB_OUTPUT plumbing slightly differently (one even
wrote to a different temp path and used heredoc-style output).

diff-package-versions is now the one place that knows how to run the
script and expose it as count/diff outputs. The three consumers just
pipe steps.diff.outputs.diff into their own filter logic:
detect_version_bumps.py and detect_affected_packages.py are unchanged,
they already read JSON from stdin. 
eusable-check-python-package-versions.yaml's
CodeArtifact existence check now reads the diff from an env var instead of
a temp file; its changed_packages output is compact JSON now instead of
pretty-printed, no consumer depends on the formatting.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
@lowlydba John McCall (lowlydba) added the automation 🦾 Change/enhance automation of a process label Aug 5, 2026
Last remaining dangling script: package_versions.py lived in
.github/workflows/scripts/ and was reached into by relative path.
Every other action already keeps its script(s) alongside its own
action.yml (detect-version-bumps, detect-affected-packages,
create-package-release); this was the one holdout, and after the last
refactor diff-package-versions is its only remaining caller anyway.

Moved it in, switched the reference to ${GITHUB_ACTION_PATH}, and
removed the now-empty scripts/ directory. No behavior change: git
commands inside the script still run with the repo root as CWD, only
the path used to invoke it changed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
…lease flow

Phase 3 was the last phase (#509); Phase 4 got folded into it instead of
staying a separate doc-polish pass (see #490). Drops the "rolling out in
phases" banner pointing at the tracking issue, since the flow it describes
is now fully implemented rather than in progress.

Also corrects two release-flow descriptions that predated the pypi-release
approval gate this phase added: a release no longer lands on PyPI
"immediately", it starts a maintainer-gated publish first.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
push never fires for fork PRs (only same-repo branch pushes), so a
required check relying solely on push would never run for external
contributors, a required status check that can never be satisfied by
the PRs GitHub Actions actually needs to gate.

Add pull_request with the same path filters; push stays for post-merge
validation on main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
John McCall (lowlydba) added a commit that referenced this pull request Aug 6, 2026
push never fires for fork PRs, so a required-status-check rule depending
on test-schema.yaml would deadlock every fork PR. Adds a pull_request
trigger alongside push, and switches the concurrency group's PR fallback
to github.event.pull_request.number so per-PR runs don't collide.

Cherry-picked from #638's test-schema.yaml hunk, that PR's scope is the
unrelated Phase 3 publish workflows and won't merge soon, this piece
can't wait on it since it's needed to verify the Build job actually
triggers and reports on a fork PR per #647's acceptance criteria.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
release-trigger.yaml assumes the narrow gha-releaser-secrets-reader OIDC
role and fetches the PEM from omf-github-terraform/releaser/pem instead
of reading GHA repo secrets, matching the safe-settings and
project-manager app pattern. Client ID is inlined (not sensitive) instead
of a secret. Companion Terraform PR wires the role and Secrets Manager
entry in omf-github-terraform.

Fixes #637.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
John McCall (lowlydba) added a commit that referenced this pull request Aug 11, 2026
…ion (#648)

* [CHORE](ci) Add all-Python-checks gate, resolve Build job-name collision

check-python-code.yaml's check matrix produces one status per cell with no
aggregator, so a required-status-check rule has to track six names and
misses any new Python version until updated. test-schema.yaml and
schema-pr-preview.yml both name their job Build, ambiguous once
test-schema.yaml gets a pull_request trigger and needs pinning itself.

Adds an all-python-checks-pass job to check-python-code.yaml (needs:
[check], if: always()) using lowlydba/are-we-good to roll the matrix's
per-cell results into one pass/fail. Renames schema-pr-preview.yml's build
job from Build to Build preview, leaving test-schema.yaml's Build as the
stable name #646 already plans to pin.

Fixes #647

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>

* [CHORE](ci) Add pull_request trigger to test-schema.yaml

push never fires for fork PRs, so a required-status-check rule depending
on test-schema.yaml would deadlock every fork PR. Adds a pull_request
trigger alongside push, and switches the concurrency group's PR fallback
to github.event.pull_request.number so per-PR runs don't collide.

Cherry-picked from #638's test-schema.yaml hunk, that PR's scope is the
unrelated Phase 3 publish workflows and won't merge soon, this piece
can't wait on it since it's needed to verify the Build job actually
triggers and reports on a fork PR per #647's acceptance criteria.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>

---------

Signed-off-by: John McCall <john@overturemaps.org>
detect_version_bumps.py no longer skips before:null entries; a
brand-new package now flows through release-trigger to PyPI on its
first version instead of sitting on CodeArtifact-only .postN builds.

detect_affected_packages.py's bumped set is widened to match (any
package with a non-null after version) so the same push doesn't also
queue an internal build for a package that just got its first release.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
Signed-off-by: John McCall <john@overturemaps.org>
@lowlydba
John McCall (lowlydba) marked this pull request as ready for review August 12, 2026 15:25
@lowlydba
John McCall (lowlydba) requested a review from a team as a code owner August 12, 2026 15:25
Copilot AI lite review requested due to automatic review settings August 12, 2026 15:25
…e's own prebuild script

overture-schema-pyspark ships generated validation expressions that
aren't committed to git. main-publish.yaml and release-publish.yaml
special-cased it directly (if: ... == 'overture-schema-pyspark'
generate step, plus a post-build wheel-unzip check), so both workflows
had to know about one package's build quirk.

Moves that into packages/overture-schema-pyspark/scripts/prebuild.sh,
invoked generically as "run the package's prebuild.sh if it has one".
Neither workflow references pyspark by name anymore, and the script's
own empty-output guard replaces the wheel-content check. This also
keeps the convention backend-agnostic ahead of #623's hatchling ->
uv_build migration, which drops hatchling's custom build-hook support
entirely.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
…-publish

Lets a package publish its current on-disk version to Test PyPI on
demand, exercising the build-and-publish pipeline without waiting on
a real release or touching the production index.

Each package gets its own pypi-release-<package> / test-pypi-<package>
Environment pair rather than one shared pypi-release/test-pypi pair: a
PyPI Trusted Publisher's identity is (repo, workflow filename,
environment), so a shared name across packages would let only one
package's project name ever bind to it (see #653).

Also replaces the post-build wheel-filename check with an upfront
`uv version --short` comparison against the release tag: uv has no
built-in check for this (astral-sh/uv#9653), and failing before the
build runs is cheaper than after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Implements Phase 3 of the branching strategy by adding dedicated publish workflows: internal .postN+main.<sha> builds to AWS CodeArtifact on no-bump merges to main, and human-approved GitHub Releases that publish released versions to public PyPI via Trusted Publishing.

Changes:

  • Add main-publish.yaml to detect “changed without version bump” packages and publish .postN builds to CodeArtifact on pushes to main (plus a PR smoke-test mode).
  • Add release-publish.yaml to build and publish a single released package to PyPI on release: published, gated via the pypi-release environment.
  • Factor version-diffing into a reusable diff-package-versions composite action, update consumers, and retire superseded workflows.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/versioning.md Documents the new workflow matrix and the Environment approval gate for PyPI publishing.
CONTRIBUTING.md Updates contributor-facing release flow descriptions to reflect the maintainer-gated PyPI publish.
.github/workflows/reusable-check-python-package-versions.yaml Switches to the new diff-package-versions action for version-diff plumbing.
.github/workflows/release-trigger.yaml Creates releases using an app token (and AWS Secrets Manager retrieval) so release: published downstream workflows fire.
.github/workflows/release-publish.yaml New workflow: builds and publishes the released package to public PyPI with OIDC + attestations and environment gating.
.github/workflows/publish-python-packages.yaml Deleted legacy publish workflow superseded by the new release/main publish split.
.github/workflows/main-publish.yaml New workflow: computes .postN versions and publishes internal builds to CodeArtifact for no-bump changes on main.
.github/workflows/compute-versions-dry-run.yaml Deleted dry-run workflow superseded by main-publish.yaml (PR smoke-test path) and real publish flows.
.github/actions/diff-package-versions/package_versions.py Updates script docs to reflect new location/invocation from the composite action.
.github/actions/diff-package-versions/action.yml New composite action exposing count/diff outputs for per-package version diffs.
.github/actions/detect-version-bumps/detect_version_bumps.py Treats newly added packages as releasable bumps (removed packages still skipped).
.github/actions/detect-version-bumps/action.yml Migrates to diff-package-versions output wiring (no temp file plumbing).
.github/actions/detect-affected-packages/detect_affected_packages.py New script: combines version diff + changed directories to find no-bump packages needing .postN builds.
.github/actions/detect-affected-packages/action.yml New composite action: changed-dir detection + version diff + filtering into an affected package matrix.
Suppressed comments (2)

.github/workflows/release-publish.yaml:79

  • github.event.release.target_commitish can be a branch name (e.g. main) rather than the immutable tag/commit for the release, which risks publishing a different commit than the one the release tag points to if the branch has moved. Check out the release tag itself to ensure the build exactly matches what was released.
          set -euo pipefail
          if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
            if [[ ! -d "packages/${INPUT_PACKAGE}" ]]; then
              echo "::error::No such package: packages/${INPUT_PACKAGE}"
              exit 1

.github/actions/detect-affected-packages/action.yml:62

  • all_modified_files does not include deletions, so removing a file from a package without a version bump would be missed even though it changes the built wheel. Use all_changed_files (with json: true) so deletions are included in the affected-package detection.
        # all_modified_files (not all_changed_files) so deletions count too: a
        # file removed from a package without a version bump still changes
        # the built wheel.
        CHANGED_DIRS: ${{ steps.changed-dirs.outputs.all_modified_files }}
        DIFF: ${{ steps.diff.outputs.diff }}

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

Comment thread .github/actions/detect-affected-packages/action.yml
Comment thread docs/versioning.md Outdated
Comment thread .github/workflows/release-trigger.yaml Outdated
Comment thread .github/workflows/release-publish.yaml
12 packages each needing their own pypi-release-<package> environment
(see prior commit, #653) makes per-environment required-reviewer
config unruly to maintain. The version-bump PR review is already the
approval; nothing further needs to gate the publish once a release
exists.

Environments stay, scoping only the Trusted Publisher identity (repo,
workflow filename, environment) that PyPI's OIDC matching requires.
Updates release-publish.yaml's comments, docs/versioning.md, and
CONTRIBUTING.md to match, and drops a stale token-scoping comment from
release-trigger.yaml that the `permission-contents: write` line already
says plainly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
… to the release tag

overture-releaser was never the app's real name; overture-release-publisher
is, per #637. Fix it everywhere it drifted in.

release-publish's checkout also used target_commitish, which can be a
branch name that moves past the release. Use tag_name instead so the
publish always builds the exact released commit.

Also drops the "explicitly scope the app token" comment above
permission-contents: write per feedback -- the field name already says that.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
'default' didn't say what it defaulted to. locked names what actually
distinguishes it from lowest-direct: it runs against the committed uv.lock.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Raised a few minor nittish review comments. Nothing big. I'm excited to merge this, once we do it seems we're in the business of publishing Python packages!

Comment thread .github/workflows/main-publish.yaml Outdated
Comment thread .github/workflows/main-publish.yaml Outdated
Comment thread .github/workflows/release-publish.yaml Outdated
Comment thread .github/actions/detect-affected-packages/detect_affected_packages.py Outdated
Comment thread .github/actions/detect-affected-packages/detect_affected_packages.py Outdated
Comment thread .github/workflows/main-publish.yaml Outdated
- Downscope main-publish's PR smoke test to the CodeArtifact read-only
  IAM role instead of the publish role.
- Validate the parsed package directory exists for both release-publish
  arms (release tag and workflow_dispatch), not just workflow_dispatch.
- Standardize on the actions/checkout v7.0.1 pin used elsewhere in the
  repo; main-publish/release-publish had drifted to a mix of v7.0.0/v7.0.1.
- Fix wording nit: "in-memory checkout" -> temporary checkout.
- detect_affected_packages.py: read version_diff + changed_dirs from a
  single stdin JSON payload instead of splitting across stdin and an env
  var, and raise a clear error instead of an IndexError on a malformed
  changed-directory path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Signed-off-by: John McCall <john@overturemaps.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation 🦾 Change/enhance automation of a process

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Devops] Branching Strategy - Phase 3

3 participants