Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c6399c7
[FEATURE] Add release-trigger workflow and versioning docs (Phase 2.B)
lowlydba Jul 14, 2026
82d0c42
[DOCS] Document umbrella minor bump as escape hatch for non-core rele…
lowlydba Jul 14, 2026
742ab50
[DOCS] Annotate vnext release merge diagram with version bump and dra…
lowlydba Jul 14, 2026
4d1c242
[BUG] Fail release-trigger when umbrella major/minor goes backwards
lowlydba Jul 14, 2026
2b5e4d2
[FEATURE](devops) Rework Phase 2.B: per-package releases + towncrier …
lowlydba Jul 21, 2026
04158f2
[DOCS](contributing) Collapsible per-path gitGraphs, slim Opening a PR
lowlydba Jul 21, 2026
396174d
[DOCS](contributing) Note release/availability per path; tidy Opening…
lowlydba Jul 21, 2026
77a7b25
[REFACTOR](ci) Extract release-trigger Python into script files
lowlydba Jul 21, 2026
4e10447
[REFACTOR](ci) Add create-package-release action; require fragment on…
lowlydba Jul 21, 2026
c6b15e4
Merge remote-tracking branch 'origin/main' into 533-devops-branching-…
lowlydba Jul 21, 2026
69bbe1d
[DOCS](changelog) Add pyspark changelog keeper; align keeper wording …
lowlydba Jul 21, 2026
ceb4d3d
Update docs/versioning.md
lowlydba Jul 23, 2026
da328e8
Update docs/versioning.md
lowlydba Jul 23, 2026
3565b1b
Update packages/overture-schema/changelog.d/557.misc.md
lowlydba Jul 23, 2026
f14f5b1
Update packages/overture-schema-addresses-theme/changelog.d/README.md
lowlydba Jul 23, 2026
341feda
[FEATURE](devops) Human-owned patch releases + PEP 440 post internal …
lowlydba Jul 28, 2026
248ac09
[REFACTOR](ci) Drop dynamic-version handling; all packages are static…
lowlydba Jul 28, 2026
1e8bd85
Merge remote-tracking branch 'origin/main' into 533-devops-branching-…
lowlydba Jul 28, 2026
40462ea
[REFACTOR](ci) Diff package versions from git blobs instead of dual c…
lowlydba Jul 28, 2026
1d56761
[REFACTOR](ci) Replace github-script checks with plain git and bash
lowlydba Jul 28, 2026
a9563ce
[DOCS](versioning) Note verified resolver behavior and the >= vs > pi…
lowlydba Jul 28, 2026
0d38169
[DOCS](versioning) Block vnext internal builds on a dedicated dev rep…
lowlydba Jul 28, 2026
38ac277
[DOCS](versioning) Note the static dual-declaration alternative is no…
lowlydba Jul 28, 2026
9486930
[FEATURE](ci) Enforce major-bump cascade across workspace dependents
lowlydba Jul 28, 2026
dfb4bf8
[FEATURE](ci) Sequence internal builds per version from CodeArtifact
lowlydba Jul 28, 2026
7842f7c
[REFACTOR](deps) Declare static workspace dependency floors, drop mat…
lowlydba Jul 29, 2026
93ead4e
[BUG](ci) Sync the publish workflow from the committed lockfile
lowlydba Jul 29, 2026
40d56eb
[DOCS](changelog) Add copy-paste quick start for changelog fragments
lowlydba Jul 29, 2026
756625d
chore: resolve merge conflicts with main
Copilot Jul 29, 2026
2fb1432
fix: resolve merge conflict in reusable-check-python-package-versions…
Copilot Jul 29, 2026
a832f03
Merge branch 'main' into 533-devops-branching-strategy---phase-2b---r…
lowlydba Jul 30, 2026
9f99018
chore: merge main into branch, resolve modify/delete conflicts
Copilot Aug 4, 2026
e3b7155
[DOCS](changelog) Denote text language on keeper README code fences
lowlydba Aug 4, 2026
932b5de
[DOCS](ci) Fix stale major.minor.0 in create-package-release input de…
lowlydba Aug 4, 2026
a06aa69
[REFACTOR](ci) Wrap bump detection in a composite action
lowlydba Aug 4, 2026
20bed8d
[REFACTOR](ci) Reject non-plain versions with a clear error, stdlib only
lowlydba Aug 4, 2026
adb4125
[REFACTOR](ci) Rename current to before; surface diagnostics on unrea…
lowlydba Aug 4, 2026
85176cb
[REFACTOR](ci) Compose bump detection from the shared version diff
lowlydba Aug 4, 2026
7a82ddf
[REFACTOR](ci) Split diff and filter into discrete action steps
lowlydba Aug 4, 2026
88e3ac2
[FEATURE](ci) Enforce floor raises when a workspace dependency majors
lowlydba Aug 4, 2026
b1cac63
[REFACTOR](ci) Rename workflow to Publish GitHub release
lowlydba Aug 4, 2026
db67d72
[DOCS](versioning) Make the fragment consumption lifecycle explicit
lowlydba Aug 4, 2026
7a8e959
[DOCS](versioning) State the CI-never-writes-to-main principle
lowlydba Aug 4, 2026
11a1110
[FEATURE](deps) Take towncrier as a dev dependency
lowlydba Aug 4, 2026
5d5895c
[FEATURE](ci) Continue the legacy bare tag series for umbrella releases
lowlydba Aug 4, 2026
6836a3b
[DOCS](changelog) Require --version in every towncrier invocation
lowlydba Aug 5, 2026
205a3b5
Merge branch 'main' into 533-devops-branching-strategy---phase-2b---r…
lowlydba Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 57 additions & 87 deletions .github/actions/compute-version/action.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
name: Compute package version
description: >
Computes the version string for a package given branch context.
Computes the internal build version string for a package on a no-bump merge.

Contexts:
- `vnext`: `<last-published>+dev.<run_number>` (PEP 440 local version).
Falls back to `<major>.<minor>.0+dev.<run_number>` if never published.
Local versions are rejected by PyPI — only suitable for private indexes
like CodeArtifact.
- `main`: `<major>.<minor>.<next-patch>` — increments the highest
published patch for the same major.minor series.
- `main-bump`: `<major>.<minor>.0` — used when a major/minor bump commit
lands on main (patch resets to 0).
All three released version components (<major>.<minor>.<patch>) are
human-owned in pyproject.toml; releases publish that version as-is. This
action only versions the interim internal builds published to CodeArtifact
between releases, using a PEP 440 post-release so they order AFTER the
released version and are picked up by `>=<version>` specifiers:

- `main`: `<version>.post<N>+main.<shortsha>`
- `vnext`: `<version>.post<N>+vnext.<shortsha>`

`N` is a per-version sequence: the first internal build of a version is
`.post0` (communicating "identical to the release"), and each subsequent
build increments from the highest `.postN` already published to
CodeArtifact.

The `+main`/`+vnext` local label distinguishes the two build streams; local
labels are ignored during version comparison so ordering comes from
`.post<N>` alone. Local versions are rejected by public PyPI, which
guarantees these builds stay internal.

Prerequisites: repo must be checked out and `uv` must be available.

Expand All @@ -20,8 +29,8 @@ inputs:
required: true
context:
description: >
Branch context controlling the version formula.
Supported values: `vnext`, `main`, `main-bump`.
Branch context naming the build stream. Supported values: `main`,
`vnext`.
required: true
index_url:
description: >
Expand All @@ -45,88 +54,49 @@ runs:
PACKAGE: ${{ inputs.package }}
CONTEXT: ${{ inputs.context }}
INDEX_URL: ${{ inputs.index_url }}
RUN_NUMBER: ${{ github.run_number }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail

# --- Read seed version from pyproject.toml ---
SEED=$(cd "packages/${PACKAGE}" && uv version --short)
MAJOR_MINOR=$(echo "$SEED" | grep -oE '^[0-9]+\.[0-9]+')
echo "Seed version for ${PACKAGE}: ${SEED} (major.minor: ${MAJOR_MINOR})"

# --- Query CodeArtifact for the latest published version ---
# uv pip compile resolves the latest matching version from the index.
# We constrain to the current major.minor series for `main` context.
resolve_latest() {
local constraint="$1"
local output
# uv pip compile exits non-zero both when nothing matches (a normal
# "not published yet" result) and on real failures (network/auth/etc).
# Only treat the former as benign; anything else must surface and fail.
output=$(echo "$constraint" \
| uv pip compile - --index-url "$INDEX_URL" --no-deps --quiet 2>&1) || {
if echo "$output" | grep -qiE 'no solution found|could not find a version|not found in the package registry'; then
echo ""
return 0
fi
echo "ERROR: uv pip compile failed for '${constraint}':" >&2
echo "$output" >&2
exit 1
}
echo "$output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true
}

# --- Compute version based on context ---
case "$CONTEXT" in
vnext)
LATEST=$(resolve_latest "$PACKAGE")
if [ -n "$LATEST" ]; then
BASE="$LATEST"
echo "Latest published version: ${LATEST}"
else
# No published version at all — use the pyproject.toml seed
# as-is (not just its major.minor) so a patch already bumped
# there (e.g. baselining) isn't regressed.
BASE="$SEED"
echo "No published version found — falling back to seed version ${BASE}"
fi
VERSION="${BASE}+dev.${RUN_NUMBER}"
;;

main)
# Resolve the highest patch within the current major.minor series.
LATEST_IN_SERIES=$(resolve_latest "${PACKAGE}>=${MAJOR_MINOR}.0,<${MAJOR_MINOR}.99999")
SEED_PATCH=$(echo "$SEED" | grep -oE '[0-9]+$')
if [ -n "$LATEST_IN_SERIES" ]; then
CURRENT_PATCH=$(echo "$LATEST_IN_SERIES" | grep -oE '[0-9]+$')
NEXT_FROM_PUBLISHED=$((CURRENT_PATCH + 1))
echo "Latest in ${MAJOR_MINOR}.x series: ${LATEST_IN_SERIES} → next patch: ${NEXT_FROM_PUBLISHED}"
else
NEXT_FROM_PUBLISHED=0
echo "No published version in ${MAJOR_MINOR}.x series"
fi
# The pyproject.toml seed's patch acts as a floor so a manual
# bump there (e.g. baselining) is never regressed — CI only
# takes over incrementing once publishing has caught up to it.
if [ "$SEED_PATCH" -gt "$NEXT_FROM_PUBLISHED" ]; then
NEXT_PATCH=$SEED_PATCH
echo "Seed patch (${SEED_PATCH}) is ahead of published — using it as the baseline"
else
NEXT_PATCH=$NEXT_FROM_PUBLISHED
fi
VERSION="${MAJOR_MINOR}.${NEXT_PATCH}"
;;

main-bump)
VERSION="${MAJOR_MINOR}.0"
echo "Major/minor bump — patch resets to 0"
;;

main|vnext) ;;
*)
echo "::error::Unknown context '${CONTEXT}'. Supported: vnext, main, main-bump."
echo "::error::Unknown context '${CONTEXT}'. Supported: main, vnext."
exit 1
;;
esac

SEED=$(cd "packages/${PACKAGE}" && uv version --short)
SHORT_SHA=$(echo "$SHA" | cut -c1-7)

# Resolve the highest published build of the seed version. The `.*`
# prefix match includes the release itself and its post-releases.
# uv pip compile exits non-zero both when nothing matches (a normal
# "not published yet" result) and on real failures (network/auth/etc).
# Only treat the former as benign; anything else must surface and fail.
LATEST=""
if ! OUTPUT=$(echo "${PACKAGE}==${SEED}.*" \
| uv pip compile - --index-url "$INDEX_URL" --no-deps --quiet 2>&1); then
if ! echo "$OUTPUT" | grep -qiE 'no solution found|could not find a version|not found in the package registry'; then
echo "ERROR: uv pip compile failed for '${PACKAGE}==${SEED}.*':" >&2
echo "$OUTPUT" >&2
exit 1
fi
else
LATEST=$(echo "$OUTPUT" | grep -oE "==[0-9][^ ]*" | head -1 | cut -c3-)
fi

# First build of this version is .post0; otherwise increment the
# highest published .postN.
if [[ "$LATEST" =~ \.post([0-9]+) ]]; then
N=$(( ${BASH_REMATCH[1]} + 1 ))
echo "Latest published build: ${LATEST} -> next N: ${N}"
else
N=0
echo "No published .post build for ${SEED} (latest: '${LATEST:-none}') -> N: 0"
fi

VERSION="${SEED}.post${N}+${CONTEXT}.${SHORT_SHA}"

echo "Computed version for ${PACKAGE} (${CONTEXT}): ${VERSION}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
146 changes: 146 additions & 0 deletions .github/actions/create-package-release/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: Create package release
description: >
Creates a published GitHub Release for a single package from its towncrier
CHANGELOG.md.

Reads the `## [<version>]` section of `packages/<package>/CHANGELOG.md` for
the release notes (falling back to a pointer message if absent), fails if the
target tag already exists, and creates the release tagged
`<package>-v<version>` at `target`. Optionally also creates a bare vanity
tag (no second GitHub Release) to continue a legacy tag series.

Prerequisites: repo must be checked out and the GitHub CLI (`gh`) available
(both true on GitHub-hosted runners).

inputs:
package:
description: Package directory name under packages/ (e.g. overture-schema).
required: true
version:
description: Release version, major.minor.patch (e.g. 1.18.0).
required: true
tag:
description: Release tag (e.g. overture-schema-v1.18.0).
required: true
target:
description: Commit SHA the tag should point at.
required: true
latest:
description: Whether to mark this release as "Latest" (true/false).
required: false
default: "false"
vanity-tag:
description: >
Optional additional bare git tag to create at `target` (e.g. v1.18.0),
with no GitHub Release attached. A pre-existing tag warns instead of
failing; the vanity tag is cosmetic and must not fail a completed
release.
required: false
default: ""
dry-run:
description: >
When "true", resolve and print what would be released without creating
anything. Read-only: a pre-existing tag is reported as a warning, not a
failure.
required: false
default: "false"
token:
description: "Token used to create the release (needs contents: write)."
required: true

outputs:
release-url:
description: URL of the created release, or empty on a dry run.
value: ${{ steps.release.outputs.release-url }}

runs:
using: composite
steps:
- name: Create release
id: release
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
PACKAGE: ${{ inputs.package }}
VERSION: ${{ inputs.version }}
TAG: ${{ inputs.tag }}
TARGET: ${{ inputs.target }}
LATEST: ${{ inputs.latest }}
VANITY_TAG: ${{ inputs.vanity-tag }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
set -euo pipefail

tag_exists=false
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
tag_exists=true
fi

if [ "$tag_exists" = "true" ] && [ "$DRY_RUN" != "true" ]; then
echo "::error::Release ${TAG} already exists. A duplicate ${PACKAGE} ${VERSION} bump landed on main; investigate before re-releasing."
exit 1
fi

changelog="packages/${PACKAGE}/CHANGELOG.md"
notes=$(python3 "${GITHUB_ACTION_PATH}/extract_release_notes.py" "$VERSION" "$changelog")
if [ -z "$notes" ]; then
notes="Release ${VERSION} of \`${PACKAGE}\`. No changelog section was found; add towncrier fragments under \`packages/${PACKAGE}/changelog.d\` and run \`uv run towncrier build --config pyproject.toml --dir packages/${PACKAGE} --version ${VERSION}\`."
fi
printf '%s\n' "$notes" > "${RUNNER_TEMP}/notes.md"

latest_flag="--latest=false"
[ "$LATEST" = "true" ] && latest_flag="--latest"

if [ "$DRY_RUN" = "true" ]; then
[ "$tag_exists" = "true" ] && echo "::warning::Release ${TAG} already exists; a real run would fail here."
{
echo "## 🔍 Dry run: \`${PACKAGE}\` ${VERSION}"
echo ""
echo "Would create tag \`${TAG}\` at \`${TARGET}\` (${latest_flag})."
if [ -n "$VANITY_TAG" ]; then
echo "Would also create vanity tag \`${VANITY_TAG}\` (no release)."
fi
echo ""
echo "<details><summary>Notes</summary>"
echo ""
cat "${RUNNER_TEMP}/notes.md"
echo ""
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
echo "release-url=" >> "$GITHUB_OUTPUT"
exit 0
fi

gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$TARGET" \
--title "\`${PACKAGE}\` ${VERSION}" \
--notes-file "${RUNNER_TEMP}/notes.md" \
$latest_flag

url=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json url --jq .url)
echo "release-url=${url}" >> "$GITHUB_OUTPUT"

# Vanity tag: a bare ref continuing the legacy series, no second
# release. Cosmetic, so a collision warns rather than failing the
# already-published release.
if [ -n "$VANITY_TAG" ]; then
if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${VANITY_TAG}" >/dev/null 2>&1; then
echo "::warning::Vanity tag ${VANITY_TAG} already exists; leaving it untouched."
else
gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/tags/${VANITY_TAG}" \
-f sha="$TARGET" >/dev/null
echo "Created vanity tag ${VANITY_TAG} at ${TARGET}."
fi
fi

{
echo "## 📦 Released \`${PACKAGE}\` ${VERSION}"
echo ""
echo "Tag \`${TAG}\`: [published GitHub Release](${url})."
if [ -n "$VANITY_TAG" ]; then
echo ""
echo "Vanity tag \`${VANITY_TAG}\` continues the legacy series (no separate release)."
fi
} >> "$GITHUB_STEP_SUMMARY"
49 changes: 49 additions & 0 deletions .github/actions/create-package-release/extract_release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3

"""
Extract one package's release notes from its CHANGELOG.md.

Run by the `Publish GitHub release` workflow. Prints the changelog section for the
given version (the block from `## [<version>]` up to the next `## [` heading,
stripped). Prints nothing if the changelog or the section is absent, so the
caller can fall back to a default message.

Usage:
extract_release_notes.py <version> <changelog-path>
"""

from pathlib import Path
import sys


def extract(version: str, changelog: str) -> str:
"""Return the trimmed `## [<version>]` section, or "" if not found."""
path = Path(changelog)
if not path.is_file():
return ""

lines = path.read_text(encoding="utf-8").splitlines()
start = next(
(i for i, line in enumerate(lines) if line.startswith(f"## [{version}]")),
None,
)
if start is None:
return ""

end = next(
(j for j in range(start + 1, len(lines)) if lines[j].startswith("## [")),
len(lines),
)
return "\n".join(lines[start:end]).strip()


def main() -> None:
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <version> <changelog-path>", file=sys.stderr)
sys.exit(2)

sys.stdout.write(extract(sys.argv[1], sys.argv[2]))


if __name__ == "__main__":
main()
Loading
Loading