Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 94 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,106 @@ name: Release
on:
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: Existing published Release tag to build and publish
required: true
type: string
skip_upgrade_acceptance:
description: Publish for new databases only, without database upgrade acceptance
type: boolean
default: false

permissions:
contents: read

concurrency:
group: release-${{ github.event.release.tag_name || github.ref }}
group: release-${{ inputs.release_tag || github.event.release.tag_name || github.ref }}
cancel-in-progress: false

jobs:
resolve-release:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
tag: ${{ steps.release.outputs.tag }}
source_sha: ${{ steps.release.outputs.source_sha }}
steps:
- name: Resolve the published Release and pin its source
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
SKIP_UPGRADE_ACCEPTANCE: ${{ inputs.skip_upgrade_acceptance || false }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
python - <<'PY'
import json
import os
import re
import urllib.parse
import urllib.request
from pathlib import Path

tag = os.environ["RELEASE_TAG"]
if not re.fullmatch(r"(?:powercontext-v|v)?\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?", tag):
raise SystemExit("Invalid PowerContext Release tag")
manual = os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch"
skip = os.environ["SKIP_UPGRADE_ACCEPTANCE"] == "true"
if manual and os.environ["GITHUB_REF"] != "refs/heads/" + os.environ["DEFAULT_BRANCH"]:
raise SystemExit("Run manual publication from the default branch; select the target with release_tag")
if skip and not manual:
raise SystemExit("Skipping upgrade acceptance requires explicit manual dispatch")

def get(path):
request = urllib.request.Request(
f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}/{path}",
headers={"Authorization": f"Bearer {os.environ['GH_TOKEN']}",
"Accept": "application/vnd.github+json"},
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)

quoted_tag = urllib.parse.quote(tag, safe="")
release = get(f"releases/tags/{quoted_tag}")
if release["tag_name"] != tag or release["draft"] or not release["published_at"]:
raise SystemExit("Select an existing published GitHub Release")
notice = (
"Upgrade acceptance was skipped for this release. "
"Use only new databases; upgrades of existing databases are not supported."
)
if skip and notice not in (release["body"] or ""):
raise SystemExit("Add this exact notice to the GitHub Release before skipping upgrades: " + notice)
commit = get(f"commits/{urllib.parse.quote('refs/tags/' + tag, safe='')}")["sha"]
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise SystemExit("Release tag did not resolve to a commit")
if not manual and commit != os.environ["GITHUB_SHA"]:
raise SystemExit("Release tag changed since the published event")
event = Path(os.environ["RUNNER_TEMP"]) / "release-metadata" / "event.json"
event.parent.mkdir()
event.write_text(json.dumps({"release": release}, ensure_ascii=False), encoding="utf-8")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"tag={tag}\nsource_sha={commit}\n")
PY
- name: Preserve the Release notes used for validation
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: release-metadata
path: ${{ runner.temp }}/release-metadata/event.json
if-no-files-found: error
overwrite: true
retention-days: 30

release-build:
needs: resolve-release
uses: ./.github/workflows/upgrade-release.yml
with:
release: true
selection: automatic
source_ref: ${{ needs.resolve-release.outputs.source_sha }}
release_tag: ${{ needs.resolve-release.outputs.tag }}
skip_upgrade_acceptance: ${{ github.event_name == 'workflow_dispatch' && inputs.skip_upgrade_acceptance }}

pypi-publish:
runs-on: ubuntu-latest
Expand All @@ -37,7 +123,7 @@ jobs:
release-assets:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: release-build
needs: [resolve-release, release-build]
permissions:
contents: write
steps:
Expand All @@ -50,21 +136,21 @@ jobs:
- name: Upload GitHub Release assets
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
with:
tag_name: ${{ github.event.release.tag_name }}
tag_name: ${{ needs.resolve-release.outputs.tag }}
files: dist/*
fail_on_unmatched_files: true

release-verify:
needs: [pypi-publish, release-assets]
needs: [resolve-release, pypi-publish, release-assets]
permissions:
contents: read
uses: ./.github/workflows/release-verify.yml
with:
release_tag: ${{ github.event.release.tag_name }}
release_tag: ${{ needs.resolve-release.outputs.tag }}
release_package: powercontext

deploy-website:
needs: pypi-publish
needs: [resolve-release, pypi-publish]
permissions:
contents: read
pages: write
Expand All @@ -78,6 +164,8 @@ jobs:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.resolve-release.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false

- name: Set up the environment
Expand Down
135 changes: 131 additions & 4 deletions .github/workflows/upgrade-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ on:
description: Enforce release metadata and expose accepted publication artifacts
type: boolean
default: false
source_ref:
description: Pinned release commit; empty uses the calling workflow checkout
type: string
default: ''
release_tag:
description: Published Release tag selected by the release workflow
type: string
default: ''
skip_upgrade_acceptance:
description: Explicit manual publication for new databases without upgrade acceptance
type: boolean
default: false

permissions:
contents: read

env:
PC_UPGRADE_SOURCE_SHA: ${{ inputs.source_ref || github.sha }}

jobs:
prepare:
name: Prepare upgrade artifacts and matrix
Expand All @@ -26,16 +41,34 @@ jobs:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.source_ref }}
fetch-depth: 0
persist-credentials: false

- name: Require an explicit manual release for the upgrade bypass
env:
SKIP_UPGRADE_ACCEPTANCE: ${{ inputs.skip_upgrade_acceptance }}
RELEASE: ${{ inputs.release }}
run: |
if [ "$SKIP_UPGRADE_ACCEPTANCE" = true ]; then
test "$GITHUB_EVENT_NAME" = workflow_dispatch
test "$RELEASE" = true
fi

- name: Set up the environment
uses: ./.github/actions/setup-python-env

- name: Download the frozen Release metadata
if: inputs.release
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
with:
name: release-metadata
path: ${{ runner.temp }}/release-metadata/

- name: Verify package version from Release tag
if: inputs.release
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
python - <<'PY'
import os
Expand Down Expand Up @@ -77,7 +110,7 @@ jobs:
- name: Verify synchronized release references and release notes
if: inputs.release
env:
VERSION: ${{ github.event.release.tag_name }}
VERSION: ${{ inputs.release_tag }}
run: make release-check

- name: Run upgrade gate regressions
Expand All @@ -92,19 +125,21 @@ jobs:
uvx --from twine twine check dist/*

- name: Freeze distributions and resolve the source matrix once
if: ${{ !inputs.skip_upgrade_acceptance }}
id: prepare
env:
RELEASE: ${{ inputs.release }}
SELECTION: ${{ inputs.selection }}
run: |
args=()
if [ "$RELEASE" = true ]; then
args+=(--release --release-event "$GITHUB_EVENT_PATH")
args+=(--release --release-event "$RUNNER_TEMP/release-metadata/event.json")
fi
uv run python scripts/parallel_upgrade_release_gate.py prepare \
--output "$RUNNER_TEMP/upgrade-prepared" --selection "$SELECTION" "${args[@]}"

- name: Share the exact prepared inputs
if: ${{ !inputs.skip_upgrade_acceptance }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: upgrade-prepared
Expand All @@ -116,9 +151,99 @@ jobs:
overwrite: true
retention-days: 30

- name: Validate distributions without claiming upgrade acceptance
if: inputs.skip_upgrade_acceptance
env:
PYTHONPATH: scripts
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
uv run python - <<'PY'
import json
import os
import subprocess
from pathlib import Path
from run_upgrade_release_gate import stage_distributions
from upgrade_release_gate import ROOT, load_policy, require, sha256, wheel_version

policy = load_policy(ROOT)
require(policy["mode"] == "upgrade-required", "The bypass must not declare supported upgrades")
output = Path(os.environ["RUNNER_TEMP"]) / "release-without-upgrades"
output.mkdir()
wheel, files = stage_distributions(Path("dist"), output / "validated-dist", release=True)
require(wheel_version(wheel) == policy["target_version"], "Wheel version differs from release policy")
evidence = {
"release_tag": os.environ["RELEASE_TAG"],
"source_sha": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"workflow_run_id": os.environ["GITHUB_RUN_ID"],
"actor": os.environ["GITHUB_ACTOR"],
"upgrade_acceptance": "skipped",
"supported_use": "new-databases-only",
"artifacts": {path.name: sha256(path) for path in files},
}
(output / "publication.json").write_text(json.dumps(evidence, indent=2) + "\n")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as summary:
summary.write("## Upgrade acceptance skipped\n\n")
summary.write("Published use is limited to new databases. Existing database upgrades are unsupported.\n")
summary.write(f"\nTag: `{evidence['release_tag']}`; source: `{evidence['source_sha']}`.\n")
PY

- name: Verify a clean installation and new-database Server, MCP and Memory behavior
if: inputs.skip_upgrade_acceptance
run: |
verification="$RUNNER_TEMP/release-fresh-install"
uv venv "$verification"
wheels=("$RUNNER_TEMP"/release-without-upgrades/validated-dist/*.whl)
uv pip install --python "$verification/bin/python" "${wheels[0]}[cli,server]"
uv pip check --python "$verification/bin/python"
version="$("$verification/bin/python" -c 'from importlib.metadata import version; print(version("powercontext"))')"
"$verification/bin/python" scripts/ci_release_smoke.py --version "$version"

- name: Bind successful fresh-install verification to the uploaded bytes
if: inputs.skip_upgrade_acceptance
env:
PYTHONPATH: scripts
run: |
uv run python - <<'PY'
import json
import os
from pathlib import Path
from upgrade_release_gate import require, sha256

output = Path(os.environ["RUNNER_TEMP"]) / "release-without-upgrades"
record = output / "publication.json"
evidence = json.loads(record.read_text())
actual = {path.name: sha256(path) for path in (output / "validated-dist").iterdir()}
require(actual == evidence["artifacts"], "Distributions changed during fresh-install verification")
evidence["fresh_install_smoke"] = "passed"
record.write_text(json.dumps(evidence, indent=2) + "\n")
PY

- name: Preserve the explicit upgrade bypass record
if: always() && inputs.skip_upgrade_acceptance
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: release-without-upgrades
path: |
${{ runner.temp }}/release-without-upgrades/publication.json
${{ runner.temp }}/release-without-upgrades/sdist-build.log
if-no-files-found: warn
overwrite: true
retention-days: 30

- name: Upload distributions after fresh-install verification only
if: success() && inputs.skip_upgrade_acceptance
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: release-dists
path: ${{ runner.temp }}/release-without-upgrades/validated-dist/*
if-no-files-found: error
overwrite: true
retention-days: 30

cases:
name: Upgrade ${{ matrix.source_version }} / ${{ matrix.backend }} / ${{ matrix.configuration }}
needs: prepare
if: ${{ !inputs.skip_upgrade_acceptance }}
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
Expand All @@ -131,6 +256,7 @@ jobs:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.source_ref }}
persist-credentials: false
- name: Set up the environment
uses: ./.github/actions/setup-python-env
Expand Down Expand Up @@ -171,13 +297,14 @@ jobs:
collect:
name: Verify the selected upgrade matrix
needs: [prepare, cases]
if: always() && needs.prepare.result == 'success'
if: always() && needs.prepare.result == 'success' && !inputs.skip_upgrade_acceptance
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ inputs.source_ref }}
persist-credentials: false
- name: Set up the environment
uses: ./.github/actions/setup-python-env
Expand Down
Loading
Loading