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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
done
cd "$RELEASE_OUTPUT/anonymous"
sha256sum --check SHA256SUMS
- name: Open an immutable catalog promotion draft
- name: Open the next reviewed promotion draft
run: |
# Promotion data starts from main; tooling stays at the reviewed workflow commit.
git switch --detach origin/main
Expand Down
37 changes: 20 additions & 17 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Release operations

Version 0.3.5 pairs the supervisor and analyzer through `PIG_*` settings and
targets schema revision 5. Follow the [0.3.5 preparation](releases/0.3.5.md)
Version 0.3.6 pairs the supervisor and analyzer through `PIG_*` settings and
targets schema revision 5. Follow the [0.3.6 preparation](releases/0.3.6.md)
for candidate selection and installation prerequisites.
The [stable catalog](catalog/stable.json)
selects published releases. Local tests are not installation acceptance. Versions 0.3.0
Expand Down Expand Up @@ -113,7 +113,7 @@ check can pass. No customer secret belongs in this repository or its evidence.

### Analyzer schema requirements

The 0.3.5 requirements accept starting schema revisions 0–4 and target revision 5.
The 0.3.6 requirements accept starting schema revisions 0–4 and target revision 5.
The worker also accepts the target revision for retries and configuration rotation.
The image must advertise `alembic-migrations-v1` and `storage-readiness-v1`.
Worker CI runs installation and recovery tests on PostgreSQL 15–18 before customer GHCR image
Expand Down Expand Up @@ -159,23 +159,26 @@ administrator fixes package visibility.
Both OCI charts must pull anonymously and match their package checksums. The
workflow then creates the immutable source tag and GitHub release, anonymously
downloads every asset, and compares exact bytes and `SHA256SUMS`. It opens a
**draft catalog promotion PR**; it never merges that PR. A partial existing GitHub
**draft manifest-retention PR**; it never merges a PR. A partial existing GitHub
release with missing or mismatched assets fails closed and needs operator repair
or a new version, without overwriting already published bytes.

The promotion uses two commits: immutable manifest first, then a stable-index
entry pointing at that commit. Review the immutable URLs and evidence before
merging. If PR creation is interrupted after the branch push, rerun publication:
it verifies the existing manifest bytes, digest, and reachable immutable commit,
then creates or locates the PR without rewriting the branch. A mismatched branch
or closed, unmerged PR requires operator review.
If organization policy blocks Actions from creating PRs, the operator can open a
draft PR from the published `release/catalog-VERSION` branch to `main`, then rerun
publication. The retry verifies the branch and reuses that PR; organization-wide
Actions permissions do not need to change.
Merging makes the release eligible for automatic updates, including
major versions. Do not squash away or delete the manifest commit referenced by
its URL. Keep releases, tags, and those commits reachable and protected.
Promotion uses two reviewed PRs. The first merges the immutable manifest into
`main` without selecting the release. Rerun publication after that merge to open
the stable catalog PR, whose manifest URL references the retained commit on
`main`. Either PR can use the repository's normal squash or rebase merge method.
Review the immutable URLs and evidence before merging the stable catalog PR;
that merge makes the release eligible for automatic updates, including major
versions. Keep releases, tags, and referenced commits reachable and protected.

If PR creation is interrupted after a branch push, rerun publication. It verifies
the existing manifest bytes, digest, and reachable immutable commit, then creates
or locates the matching PR without rewriting the branch. A mismatched branch or
closed, unmerged PR requires operator review. If organization policy blocks
Actions from creating PRs, the operator can open a draft PR from the published
`release/manifest-VERSION` or `release/catalog-VERSION` branch to `main`, then
rerun publication. The retry verifies the branch and reuses that PR;
organization-wide Actions permissions do not need to change.

## Scope of acceptance

Expand Down
4 changes: 2 additions & 2 deletions charts/pig-supervisor/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: pig-supervisor
description: PIG release supervision for one customer deployment in one existing namespace
type: application
version: 0.3.5
appVersion: 0.3.5
version: 0.3.6
appVersion: 0.3.6
4 changes: 2 additions & 2 deletions charts/pig-trace-analyzer/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: pig-trace-analyzer
description: Manual PIG analyzer deployment with native cloud object storage
type: application
version: 0.3.5
appVersion: 0.3.5
version: 0.3.6
appVersion: 0.3.6
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pig-supervisor"
version = "0.3.5"
version = "0.3.6"
requires-python = ">=3.11"
dependencies = ["httpx>=0.28,<1", "pydantic>=2.11,<3", "packaging>=24", "PyYAML>=6,<7"]
[project.scripts]
Expand Down
56 changes: 56 additions & 0 deletions releases/0.3.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Prepare PIG 0.3.6

PIG 0.3.6 pairs the supervisor and analyzer through the `PIG_*` environment
contract. The analyzer targets schema revision 5 for cloud host enrollment and
trace provenance, with PostgreSQL 15–18 and controller protocol 1.

## Select candidate artifacts

1. Build the supervisor candidate from the merged 0.3.6 source using the
[candidate workflow](../.github/workflows/candidate.yml). Record its source
commit and image digest. Verify that `--version` reports `0.3.6` and its OCI
revision matches that commit.
2. Select an analyzer image published from merged application source. Execute
`deployment-capabilities` against the [requirements](requirements/0.3.6.json).
The image must read `PIG_*` settings and report schema revision 5. Its deployment
verification must permit hosted policy to disable analysis; canonical analysis
acceptance still requires analysis permission and repository access.
3. Verify anonymous image pulls and the candidate's capabilities following
[release operations](../RELEASING.md#candidate-and-acceptance).

## Upgrade an installation

Supervisor 0.3.4 supplies `INSTRUCTION_HUB_*` environment variables, which cannot
configure the 0.3.6 analyzer. The supervisor updates its own image after worker
verification, so this transition requires a coordinated bootstrap:

1. Pause the installation with `spec.release.paused: true` before changing its
selected release. Confirm there is no migration in progress.
2. Scale the supervisor Deployment to zero and wait for its Pod to terminate.
A paused supervisor still reconciles the current worker configuration; stop it
before switching the environment contract.
3. Select the verified stable release and resume the installation while the
supervisor is stopped. Remove `spec.release.pinnedVersion` to follow stable
automatically, or set an explicit version when the installation requires a pin.
4. Upgrade the Helm-managed supervisor to the verified 0.3.6 chart and immutable
image digest. Preserve the installation's namespace, ingress, and other Helm
values. Confirm the new supervisor is ready and has acquired leadership.
5. Confirm the supervisor completes preflight, migration to schema 5, worker
rollout, hosted verification, and controller convergence. Check the observed
release and both running image digests against the manifest.

Keep the installation's analysis policy unchanged. Successful deployment
verification proves hosted connectivity and required infrastructure checks;
canonical trace analysis and Dashboard acknowledgement are separate acceptance
results. Schema-5 recovery requires a compatible image or forward repair.

## Record acceptance and publish

Collect the required reports or obtain a version-specific owner decision using
[release operations](../RELEASING.md#candidate-and-acceptance). Bind the acceptance
record to the exact source, images, and requirements. Record omitted checks and
avoid advertising untested rollback paths. Azure and GCP remain experimental.

Publication merges the immutable manifest before opening the stable catalog PR.
Follow [publication and promotion](../RELEASING.md#publication-and-promotion),
then verify public catalog consumption before updating production.
14 changes: 14 additions & 0 deletions releases/requirements/0.3.6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"kubernetesMin": "1.30.0",
"postgresMin": 15,
"postgresMax": 18,
"operatorCapacityRequirements": [],
"capacityConfirmationMaxAgeHours": 24,
"storageBackends": ["s3", "azureBlob", "gcs"],
"capabilities": ["native-storage-v1", "migration-ledger-v1", "installation-identity-v1", "alembic-migrations-v1", "storage-readiness-v1", "controller-self-update-v1", "crd-update-v1"],
"recoveryMaxAgeHours": null,
"destructiveMigration": false,
"schemaFrom": [0, 1, 2, 3, 4],
"schemaTo": 5,
"controllerProtocol": 1
}
87 changes: 52 additions & 35 deletions supervisor/pig_supervisor/promotion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Resume catalog promotion without rewriting an already published manifest commit."""
"""Promote release manifests through reviewed commits retained on main."""

import json
import re
Expand All @@ -11,21 +11,27 @@


def promote_draft(root: Path, manifest: Path) -> str:
"""Verify/reuse a pushed promotion, then create or locate its draft PR."""
"""Open the manifest PR, then its stable-selection PR after the manifest merges."""
from .publication import promote_catalog

payload = manifest.read_bytes()
release = Release.model_validate_json(payload)
if release.artifacts is None:
raise ValueError("published release is missing its artifact inventory")
version = release.version
branch = f"release/catalog-{version}"
manifest_path = f"catalog/releases/{version}.json"

def run(*args: str) -> str:
return subprocess.check_output(args, cwd=root, text=True).strip()

def verify(ref: str) -> None:
def read_manifest(ref: str) -> bytes:
return subprocess.check_output(["git", "show", f"{ref}:{manifest_path}"], cwd=root)

def verify_manifest(ref: str) -> None:
if read_manifest(ref) != payload:
raise ValueError("existing promotion manifest bytes differ from accepted artifacts")

def verify_catalog(ref: str) -> None:
catalog = json.loads(run("git", "show", f"{ref}:catalog/stable.json"))
entries = [entry for entry in catalog["releases"] if entry["version"] == version]
if len(entries) != 1 or entries[0]["sha256"] != sha256(payload).hexdigest():
Expand All @@ -36,47 +42,57 @@ def verify(ref: str) -> None:
)
if url is None:
raise ValueError("existing promotion must reference an immutable manifest commit")
commit = url[1]
run("git", "merge-base", "--is-ancestor", commit, ref)
for revision in (commit, ref):
existing = subprocess.check_output(["git", "show", f"{revision}:{manifest_path}"], cwd=root)
if existing != payload:
raise ValueError("existing promotion manifest bytes differ from accepted artifacts")
run("git", "merge-base", "--is-ancestor", url[1], "origin/main")
verify_manifest(url[1])
verify_manifest(ref)

run("git", "fetch", "origin", "main")
main_catalog = json.loads(run("git", "show", "origin/main:catalog/stable.json"))
already_promoted = any(entry["version"] == version for entry in main_catalog["releases"])
manifest_merged = bool(run("git", "ls-tree", "--name-only", "origin/main", "--", manifest_path))
if manifest_merged:
verify_manifest("origin/main")
catalog_phase = manifest_merged
branch = f"release/{'catalog' if catalog_phase else 'manifest'}-{version}"
changed_path = "catalog/stable.json" if catalog_phase else manifest_path
promotion_commit = None
if already_promoted:
verify("origin/main")
verify_catalog("origin/main")
else:
remote = run("git", "ls-remote", "--heads", "origin", f"refs/heads/{branch}")
if remote:
run("git", "fetch", "origin", f"refs/heads/{branch}")
verify("FETCH_HEAD")
promotion_commit = run("git", "rev-parse", "FETCH_HEAD")
base = run("git", "merge-base", "origin/main", "FETCH_HEAD")
changed = set(run("git", "diff", "--name-only", base, "FETCH_HEAD").splitlines())
catalog = json.loads(run("git", "show", "FETCH_HEAD:catalog/stable.json"))
catalog["releases"] = [entry for entry in catalog["releases"] if entry["version"] != version]
if changed != {manifest_path, "catalog/stable.json"} or catalog != json.loads(
run("git", "show", f"{base}:catalog/stable.json")
):
if changed != {changed_path}:
raise ValueError("existing promotion contains unrelated changes")
verify_manifest("FETCH_HEAD")
if catalog_phase:
verify_catalog("FETCH_HEAD")
catalog = json.loads(run("git", "show", "FETCH_HEAD:catalog/stable.json"))
catalog["releases"] = [entry for entry in catalog["releases"] if entry["version"] != version]
if catalog != json.loads(run("git", "show", f"{base}:catalog/stable.json")):
raise ValueError("existing promotion contains unrelated catalog changes")
else:
run("git", "switch", "--detach", "origin/main")
path = root / manifest_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
run("git", "add", manifest_path)
run("git", "commit", "-m", f"Add immutable manifest for PIG {version}")
commit = run("git", "rev-parse", "HEAD")
promote_catalog(root, path, commit)
run("git", "add", "catalog/stable.json")
run("git", "commit", "-m", f"Select PIG {version} in the stable catalog")
if catalog_phase:
# This commit is already on main, including after a squash merge.
commit = run("git", "log", "-1", "--format=%H", "origin/main", "--", manifest_path)
promote_catalog(root, root / manifest_path, commit)
else:
path = root / manifest_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
run("git", "add", changed_path)
message = (
f"Select PIG {version} in the stable catalog"
if catalog_phase
else f"Add immutable manifest for PIG {version}"
)
run("git", "commit", "-m", message)
promotion_commit = run("git", "rev-parse", "HEAD")
# An absent-ref lease also rejects a concurrently created branch, even
# if our commits happen to descend from it. Never rewrite published refs.
# An absent-ref lease rejects concurrent creation without rewriting it.
run("git", "push", f"--force-with-lease=refs/heads/{branch}:", "origin", f"HEAD:refs/heads/{branch}")

pulls = json.loads(
Expand All @@ -101,20 +117,21 @@ def verify(ref: str) -> None:
]
for pull in pulls:
if pull["state"] == "MERGED" and already_promoted:
run("git", "merge-base", "--is-ancestor", pull["headRefOid"], "origin/main")
verify(pull["headRefOid"])
return pull["url"]
if pull["state"] == "OPEN" and not already_promoted and pull["headRefOid"] == promotion_commit:
return pull["url"]
if pulls or already_promoted:
raise ValueError("existing promotion has no matching open or merged PR; inspect its review state")
with tempfile.TemporaryDirectory(prefix="pig-promotion-") as directory:
body = Path(directory) / "body.md"
if catalog_phase:
title = f"Promote PIG {version} to stable"
description = f"Select PIG {version} using the immutable manifest already merged into main. Merging this catalog entry makes the release eligible for automatic updates, including major upgrades."
else:
title = f"Retain the immutable manifest for PIG {version}"
description = f"Retain the accepted PIG {version} manifest on main. Both runtime images, chart packages, and source archive passed anonymous access checks. After this PR merges, rerun publication to create the stable catalog selection PR."
body.write_text(
f"Promote the accepted PIG {version} release. Both runtime images, chart packages, and source archive "
"passed anonymous access checks. Release-specific acceptance or owner sign-off is in "
f"releases/acceptance/{version}.json. Merging this catalog entry makes the release eligible for "
"automatic updates, including major upgrades.\n"
f"{description}\n\nRelease-specific acceptance or owner sign-off is in releases/acceptance/{version}.json.\n"
)
return run(
"gh",
Expand All @@ -128,7 +145,7 @@ def verify(ref: str) -> None:
"--head",
branch,
"--title",
f"Promote PIG {version} to stable",
title,
"--body-file",
str(body),
)
Loading
Loading