mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge commit 'd7356ef76e26cd461278de18cd471d7cb2682043'
Change-source: pulse-maintainer
This commit is contained in:
@@ -78,6 +78,62 @@ jobs:
|
||||
--diagnostic release-continuity-evidence/release-diagnostic.json \
|
||||
--github-output "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Reject orphaned stable publication tags
|
||||
id: frontier
|
||||
if: ${{ !cancelled() && steps.release.outputs.referenceable == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
evidence=release-continuity-evidence
|
||||
gh api --paginate --slurp \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||||
"repos/${REPOSITORY}/git/matching-refs/tags/v?per_page=100" \
|
||||
> "${evidence}/stable-refs.json"
|
||||
gh api --paginate --slurp \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
||||
"repos/${REPOSITORY}/releases?per_page=100" \
|
||||
> "${evidence}/releases.json"
|
||||
|
||||
fetch_registry_tags() {
|
||||
local registry_name="$1" auth_url="$2" registry_url="$3" image="$4" output="$5"
|
||||
local token
|
||||
token="$(curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors "${auth_url}")"
|
||||
token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"${token}")"
|
||||
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
"${registry_url}/v2/${image}/tags/list?n=10000" \
|
||||
| jq --arg name "${registry_name}/${image}" '.name = $name' > "${output}"
|
||||
}
|
||||
owner="${GITHUB_REPOSITORY_OWNER,,}"
|
||||
for image in pulse pulse-control-plane; do
|
||||
file_image="${image//-/_}"
|
||||
fetch_registry_tags \
|
||||
ghcr.io \
|
||||
"https://ghcr.io/token?service=ghcr.io&scope=repository:${owner}/${image}:pull" \
|
||||
https://ghcr.io "${owner}/${image}" \
|
||||
"${evidence}/ghcr-${file_image}-tags.json"
|
||||
done
|
||||
# The server image is public on both registries. The control-plane
|
||||
# image is publicly enumerable only on GHCR, so an unauthenticated
|
||||
# continuity audit must not depend on Docker Hub credentials.
|
||||
fetch_registry_tags \
|
||||
docker.io \
|
||||
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:rcourtman/pulse:pull" \
|
||||
https://registry-1.docker.io rcourtman/pulse \
|
||||
"${evidence}/docker-pulse-tags.json"
|
||||
python3 scripts/release_control/release_continuity.py frontier \
|
||||
--release-json "${evidence}/release.json" \
|
||||
--stable-refs-json "${evidence}/stable-refs.json" \
|
||||
--releases-json "${evidence}/releases.json" \
|
||||
--registry-tags-json "${evidence}/ghcr-pulse-tags.json" \
|
||||
--registry-tags-json "${evidence}/docker-pulse-tags.json" \
|
||||
--registry-tags-json "${evidence}/ghcr-pulse_control_plane-tags.json" \
|
||||
--diagnostic "${evidence}/frontier-diagnostic.json"
|
||||
|
||||
- name: Bind the release activation marker
|
||||
id: activation
|
||||
# A structurally valid tag/id/SHA remains safe lookup data even when
|
||||
@@ -114,7 +170,7 @@ jobs:
|
||||
|
||||
- name: Verify immutable release and build provenance
|
||||
id: packet
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -127,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Authenticate every published release asset
|
||||
id: assets
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
env:
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -136,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Verify exact-version container identities
|
||||
id: containers
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -158,7 +214,7 @@ jobs:
|
||||
|
||||
- name: Verify stable container discovery aliases
|
||||
id: aliases
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
env:
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
EXPECTED_SERVER_DIGEST: ${{ steps.activation.outputs.server_image_digest }}
|
||||
@@ -171,7 +227,7 @@ jobs:
|
||||
|
||||
- name: Verify exact-version Helm identity
|
||||
id: helm
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
if: ${{ always() && steps.release.outcome == 'success' && steps.frontier.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -202,6 +258,7 @@ jobs:
|
||||
CONTROL_PLANE_IMAGE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }}
|
||||
HELM_CHART_DIGEST: ${{ steps.activation.outputs.helm_chart_digest }}
|
||||
RELEASE_RESULT: ${{ steps.release.outcome }}
|
||||
FRONTIER_RESULT: ${{ steps.frontier.outcome }}
|
||||
ACTIVATION_RESULT: ${{ steps.activation.outcome }}
|
||||
PACKET_RESULT: ${{ steps.packet.outcome }}
|
||||
ASSET_RESULT: ${{ steps.assets.outcome }}
|
||||
@@ -216,7 +273,7 @@ jobs:
|
||||
if [ "${TRIGGER_SCHEDULE}" = '17 */6 * * *' ]; then
|
||||
mode=release_lock
|
||||
fi
|
||||
for diagnostic in release activation; do
|
||||
for diagnostic in release frontier activation; do
|
||||
path="release-continuity-evidence/${diagnostic}-diagnostic.json"
|
||||
if [ ! -s "${path}" ]; then
|
||||
jq -n \
|
||||
@@ -227,6 +284,7 @@ jobs:
|
||||
done
|
||||
jq -n \
|
||||
--slurpfile release_diagnostic release-continuity-evidence/release-diagnostic.json \
|
||||
--slurpfile frontier_diagnostic release-continuity-evidence/frontier-diagnostic.json \
|
||||
--slurpfile activation_diagnostic release-continuity-evidence/activation-diagnostic.json \
|
||||
--arg schema_version "1" \
|
||||
--arg checked_at "${checked_at}" \
|
||||
@@ -248,6 +306,7 @@ jobs:
|
||||
--arg control_plane_image_digest "${CONTROL_PLANE_IMAGE_DIGEST}" \
|
||||
--arg helm_chart_digest "${HELM_CHART_DIGEST}" \
|
||||
--arg release_result "${RELEASE_RESULT}" \
|
||||
--arg frontier_result "${FRONTIER_RESULT}" \
|
||||
--arg activation_result "${ACTIVATION_RESULT}" \
|
||||
--arg packet_result "${PACKET_RESULT}" \
|
||||
--arg asset_result "${ASSET_RESULT}" \
|
||||
@@ -281,10 +340,12 @@ jobs:
|
||||
},
|
||||
diagnostics: {
|
||||
release_identity: $release_diagnostic[0],
|
||||
stable_publication_frontier: $frontier_diagnostic[0],
|
||||
activation_binding: $activation_diagnostic[0]
|
||||
},
|
||||
checks: {
|
||||
release_resolution: $release_result,
|
||||
stable_publication_frontier: $frontier_result,
|
||||
activation_binding: $activation_result,
|
||||
immutable_packet_and_provenance: $packet_result,
|
||||
authenticated_assets: $asset_result,
|
||||
|
||||
@@ -161,12 +161,23 @@ func TestSecurityScanRevalidatesLatestStableDelivery(t *testing.T) {
|
||||
`"repos/${REPOSITORY}/releases/latest"`,
|
||||
`scripts/release_control/release_continuity.py release`,
|
||||
"release-diagnostic.json",
|
||||
"Reject orphaned stable publication tags",
|
||||
`"repos/${REPOSITORY}/git/matching-refs/tags/v?per_page=100"`,
|
||||
`"repos/${REPOSITORY}/releases?per_page=100"`,
|
||||
"https://ghcr.io/token?service=ghcr.io&scope=repository:",
|
||||
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:",
|
||||
`"${registry_url}/v2/${image}/tags/list?n=10000"`,
|
||||
`--registry-tags-json "${evidence}/ghcr-pulse-tags.json"`,
|
||||
`--registry-tags-json "${evidence}/docker-pulse-tags.json"`,
|
||||
`scripts/release_control/release_continuity.py frontier`,
|
||||
"frontier-diagnostic.json",
|
||||
"Bind the release activation marker",
|
||||
`!cancelled()`,
|
||||
`steps.release.outputs.referenceable == 'true'`,
|
||||
`scripts/release_control/release_continuity.py activation`,
|
||||
"activation-diagnostic.json",
|
||||
`steps.activation.outcome == 'success'`,
|
||||
`steps.frontier.outcome == 'success'`,
|
||||
`./scripts/verify-github-release-integrity.sh`,
|
||||
`./scripts/validate-published-release.sh`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
@@ -177,6 +188,7 @@ func TestSecurityScanRevalidatesLatestStableDelivery(t *testing.T) {
|
||||
`stable_container_aliases: $alias_result`,
|
||||
`activation_binding: $activation_result`,
|
||||
`release_identity: $release_diagnostic[0]`,
|
||||
`stable_publication_frontier: $frontier_diagnostic[0]`,
|
||||
`CONVERGENCE_RUN_ID: ${{ github.event.workflow_run.id }}`,
|
||||
`TRIGGER_SCHEDULE: ${{ github.event.schedule }}`,
|
||||
`mode=release_lock`,
|
||||
@@ -229,6 +241,7 @@ func TestSecurityScanRevalidatesLatestStableDelivery(t *testing.T) {
|
||||
step := workflowStepBlock(t, workflowJobBlock(t, workflow, "release-continuity"), stepName)
|
||||
for _, admission := range []string{
|
||||
`steps.release.outcome == 'success'`,
|
||||
`steps.frontier.outcome == 'success'`,
|
||||
`steps.activation.outcome == 'success'`,
|
||||
} {
|
||||
if !strings.Contains(step, admission) {
|
||||
|
||||
@@ -29,6 +29,10 @@ IMMUTABLE_REPLACEMENT_ACTION = (
|
||||
"Do not edit or repair the advertised release in place; restore the last known-good "
|
||||
"stable target if needed, then publish a corrected replacement through convergence."
|
||||
)
|
||||
ORPHAN_REPLACEMENT_ACTION = (
|
||||
"Do not advertise or repair the orphaned version in place; inspect the failed "
|
||||
"release run and publish a corrected replacement through convergence."
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -86,6 +90,27 @@ RELEASE_RULES = {
|
||||
}
|
||||
|
||||
|
||||
FRONTIER_RULES = {
|
||||
"frontier_payload_invalid": (
|
||||
"GitHub did not return complete stable-tag and release inventories.",
|
||||
"Inspect the matching-refs and releases API responses before retrying.",
|
||||
),
|
||||
"stable_tag_without_release": (
|
||||
"A stable source tag newer than the advertised release has no GitHub Release packet.",
|
||||
ORPHAN_REPLACEMENT_ACTION,
|
||||
),
|
||||
"newer_stable_release_not_advertised": (
|
||||
"A published stable release is newer than the advertised latest release.",
|
||||
"Inspect the latest-release selection and customer aliases; do not move them without "
|
||||
"an immutable activation packet and successful convergence.",
|
||||
),
|
||||
"registry_stable_tag_beyond_latest": (
|
||||
"A public container registry exposes a stable version newer than the advertised release.",
|
||||
ORPHAN_REPLACEMENT_ACTION,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
ACTIVATION_RULES = {
|
||||
"activation_asset_invalid": (
|
||||
"The release does not list one uploaded, digest-bound activation marker.",
|
||||
@@ -270,6 +295,185 @@ def release_is_referenceable(payload: Any, failures: list[Violation]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def stable_version(tag: str) -> tuple[int, int, int]:
|
||||
if STABLE_TAG.fullmatch(tag) is None:
|
||||
raise ValueError(f"invalid stable tag {tag!r}")
|
||||
parts = tuple(int(part) for part in tag.removeprefix("v").split("."))
|
||||
return (parts[0], parts[1], parts[2])
|
||||
|
||||
|
||||
def inventory_list(payload: Any) -> list[Any] | None:
|
||||
"""Flatten gh api's ordinary or --slurp pagination representation."""
|
||||
|
||||
if not isinstance(payload, list):
|
||||
return None
|
||||
if payload and all(isinstance(page, list) for page in payload):
|
||||
return [item for page in payload for item in page]
|
||||
return payload
|
||||
|
||||
|
||||
def frontier_violations(
|
||||
latest_release: Any,
|
||||
stable_refs_payload: Any,
|
||||
releases_payload: Any,
|
||||
registry_payloads: list[Any],
|
||||
) -> tuple[list[Violation], dict[str, Any]]:
|
||||
refs = inventory_list(stable_refs_payload)
|
||||
releases = inventory_list(releases_payload)
|
||||
latest_tag = latest_release.get("tag_name") if isinstance(latest_release, dict) else None
|
||||
identity: dict[str, Any] = {
|
||||
"advertised_tag": latest_tag,
|
||||
"newer_stable_tags": "",
|
||||
"orphaned_stable_tags": "",
|
||||
"unadvertised_published_stable_tags": "",
|
||||
"registry_stable_tags_beyond_latest": "",
|
||||
}
|
||||
if (
|
||||
not isinstance(latest_tag, str)
|
||||
or STABLE_TAG.fullmatch(latest_tag) is None
|
||||
or refs is None
|
||||
or releases is None
|
||||
):
|
||||
return (
|
||||
[
|
||||
violation(
|
||||
"frontier_payload_invalid",
|
||||
"$",
|
||||
"stable latest release plus tag and release arrays",
|
||||
{
|
||||
"latest_release": type(latest_release).__name__,
|
||||
"stable_refs": type(stable_refs_payload).__name__,
|
||||
"releases": type(releases_payload).__name__,
|
||||
},
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
],
|
||||
identity,
|
||||
)
|
||||
|
||||
stable_tags: set[str] = set()
|
||||
for ref in refs:
|
||||
if not isinstance(ref, dict):
|
||||
return (
|
||||
[
|
||||
violation(
|
||||
"frontier_payload_invalid",
|
||||
"stable_refs",
|
||||
"array of Git ref objects",
|
||||
ref,
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
],
|
||||
identity,
|
||||
)
|
||||
ref_name = ref.get("ref")
|
||||
if isinstance(ref_name, str):
|
||||
tag = ref_name.removeprefix("refs/tags/")
|
||||
if STABLE_TAG.fullmatch(tag):
|
||||
stable_tags.add(tag)
|
||||
|
||||
releases_by_tag: dict[str, dict[str, Any]] = {}
|
||||
for release in releases:
|
||||
if not isinstance(release, dict):
|
||||
return (
|
||||
[
|
||||
violation(
|
||||
"frontier_payload_invalid",
|
||||
"releases",
|
||||
"array of GitHub Release objects",
|
||||
release,
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
],
|
||||
identity,
|
||||
)
|
||||
release_tag = release.get("tag_name")
|
||||
if isinstance(release_tag, str) and STABLE_TAG.fullmatch(release_tag):
|
||||
releases_by_tag[release_tag] = release
|
||||
|
||||
latest_version = stable_version(latest_tag)
|
||||
newer_tags = sorted(
|
||||
(tag for tag in stable_tags if stable_version(tag) > latest_version),
|
||||
key=stable_version,
|
||||
)
|
||||
orphaned = [tag for tag in newer_tags if tag not in releases_by_tag]
|
||||
unadvertised = [
|
||||
tag
|
||||
for tag in newer_tags
|
||||
if tag in releases_by_tag
|
||||
and releases_by_tag[tag].get("draft") is False
|
||||
and isinstance(releases_by_tag[tag].get("published_at"), str)
|
||||
and bool(releases_by_tag[tag]["published_at"])
|
||||
]
|
||||
registry_tags: dict[str, set[str]] = {}
|
||||
for registry in registry_payloads:
|
||||
if (
|
||||
not isinstance(registry, dict)
|
||||
or not isinstance(registry.get("name"), str)
|
||||
or not isinstance(registry.get("tags"), list)
|
||||
or not all(isinstance(tag, str) for tag in registry["tags"])
|
||||
):
|
||||
return (
|
||||
[
|
||||
violation(
|
||||
"frontier_payload_invalid",
|
||||
"registries",
|
||||
"array of registry tag-list objects",
|
||||
registry,
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
],
|
||||
identity,
|
||||
)
|
||||
registry_name = registry["name"]
|
||||
for raw_tag in registry["tags"]:
|
||||
tag = raw_tag if raw_tag.startswith("v") else f"v{raw_tag}"
|
||||
if STABLE_TAG.fullmatch(tag) and stable_version(tag) > latest_version:
|
||||
registry_tags.setdefault(tag, set()).add(registry_name)
|
||||
identity.update(
|
||||
{
|
||||
"newer_stable_tags": ",".join(newer_tags),
|
||||
"orphaned_stable_tags": ",".join(orphaned),
|
||||
"unadvertised_published_stable_tags": ",".join(unadvertised),
|
||||
"registry_stable_tags_beyond_latest": ",".join(
|
||||
sorted(registry_tags, key=stable_version)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
failures = [
|
||||
violation(
|
||||
"stable_tag_without_release",
|
||||
f"refs/tags/{tag}",
|
||||
"one GitHub Release object",
|
||||
None,
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
for tag in orphaned
|
||||
]
|
||||
failures.extend(
|
||||
violation(
|
||||
"newer_stable_release_not_advertised",
|
||||
f"releases/{tag}",
|
||||
f"not newer than {latest_tag}",
|
||||
tag,
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
for tag in unadvertised
|
||||
)
|
||||
failures.extend(
|
||||
violation(
|
||||
"registry_stable_tag_beyond_latest",
|
||||
f"registries/{tag}",
|
||||
f"no public stable container tag newer than {latest_tag}",
|
||||
",".join(sorted(registry_tags[tag])),
|
||||
FRONTIER_RULES,
|
||||
)
|
||||
for tag in sorted(registry_tags, key=stable_version)
|
||||
)
|
||||
return failures, identity
|
||||
|
||||
|
||||
def activation_violations(
|
||||
payload: Any, expected_release: dict[str, Any], activation_bytes: bytes
|
||||
) -> list[Violation]:
|
||||
@@ -541,6 +745,29 @@ def validate_activation(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def validate_frontier(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
latest_release = read_json(args.release_json)
|
||||
stable_refs = read_json(args.stable_refs_json)
|
||||
releases = read_json(args.releases_json)
|
||||
registries = [read_json(path) for path in args.registry_tags_json]
|
||||
except ValueError as exc:
|
||||
failures = [
|
||||
violation(
|
||||
"frontier_payload_invalid", "$", "valid JSON inventories", str(exc), FRONTIER_RULES
|
||||
)
|
||||
]
|
||||
identity: dict[str, Any] = {}
|
||||
else:
|
||||
failures, identity = frontier_violations(latest_release, stable_refs, releases, registries)
|
||||
|
||||
write_diagnostic(args.diagnostic, "stable_publication_frontier", identity, failures)
|
||||
if failures:
|
||||
report_failures(failures, FRONTIER_RULES)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -555,6 +782,13 @@ def parse_args() -> argparse.Namespace:
|
||||
activation.add_argument("--activation-json", type=Path, required=True)
|
||||
activation.add_argument("--diagnostic", type=Path, required=True)
|
||||
activation.add_argument("--github-output", type=Path, required=True)
|
||||
|
||||
frontier = commands.add_parser("frontier")
|
||||
frontier.add_argument("--release-json", type=Path, required=True)
|
||||
frontier.add_argument("--stable-refs-json", type=Path, required=True)
|
||||
frontier.add_argument("--releases-json", type=Path, required=True)
|
||||
frontier.add_argument("--registry-tags-json", type=Path, action="append", required=True)
|
||||
frontier.add_argument("--diagnostic", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -562,7 +796,9 @@ def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "release":
|
||||
return validate_release(args)
|
||||
return validate_activation(args)
|
||||
if args.command == "activation":
|
||||
return validate_activation(args)
|
||||
return validate_frontier(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -67,6 +67,9 @@ class ReleaseContinuityTest(unittest.TestCase):
|
||||
command: str,
|
||||
release: object,
|
||||
activation: object | None = None,
|
||||
stable_refs: object | None = None,
|
||||
releases: object | None = None,
|
||||
registries: list[object] | None = None,
|
||||
) -> tuple[subprocess.CompletedProcess[str], dict[str, object], str]:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
@@ -83,13 +86,32 @@ class ReleaseContinuityTest(unittest.TestCase):
|
||||
str(release_path),
|
||||
"--diagnostic",
|
||||
str(diagnostic),
|
||||
"--github-output",
|
||||
str(output),
|
||||
]
|
||||
if command != "frontier":
|
||||
args.extend(["--github-output", str(output)])
|
||||
if command == "activation":
|
||||
activation_path = root / "activation.json"
|
||||
activation_path.write_bytes(encoded(activation))
|
||||
args.extend(["--activation-json", str(activation_path)])
|
||||
if command == "frontier":
|
||||
stable_refs_path = root / "stable-refs.json"
|
||||
releases_path = root / "releases.json"
|
||||
stable_refs_path.write_text(json.dumps(stable_refs), encoding="utf-8")
|
||||
releases_path.write_text(json.dumps(releases), encoding="utf-8")
|
||||
args.extend(
|
||||
[
|
||||
"--stable-refs-json",
|
||||
str(stable_refs_path),
|
||||
"--releases-json",
|
||||
str(releases_path),
|
||||
]
|
||||
)
|
||||
for index, registry in enumerate(
|
||||
registries or [{"name": "registry.example/pulse", "tags": []}]
|
||||
):
|
||||
registry_path = root / f"registry-{index}.json"
|
||||
registry_path.write_text(json.dumps(registry), encoding="utf-8")
|
||||
args.extend(["--registry-tags-json", str(registry_path)])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
cwd=ROOT,
|
||||
@@ -272,6 +294,102 @@ class ReleaseContinuityTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_frontier_accepts_tags_at_or_behind_advertised_release(self) -> None:
|
||||
release = valid_release()
|
||||
result, diagnostic, output = self.run_command(
|
||||
"frontier",
|
||||
release,
|
||||
stable_refs=[
|
||||
{"ref": "refs/tags/v6.4.1"},
|
||||
{"ref": "refs/tags/v6.4.2"},
|
||||
{"ref": "refs/tags/v6.4.3-rc.1"},
|
||||
],
|
||||
releases=[release],
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(diagnostic["status"], "success")
|
||||
self.assertEqual(diagnostic["identity"]["newer_stable_tags"], "")
|
||||
self.assertEqual(output, "")
|
||||
|
||||
def test_frontier_rejects_newer_stable_tag_without_release_packet(self) -> None:
|
||||
release = valid_release()
|
||||
result, diagnostic, _ = self.run_command(
|
||||
"frontier",
|
||||
release,
|
||||
stable_refs=[[{"ref": "refs/tags/v6.4.2"}, {"ref": "refs/tags/v6.4.3"}]],
|
||||
releases=[[release]],
|
||||
)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(
|
||||
[item["code"] for item in diagnostic["violations"]],
|
||||
["stable_tag_without_release"],
|
||||
)
|
||||
self.assertEqual(diagnostic["identity"]["orphaned_stable_tags"], "v6.4.3")
|
||||
self.assertIn("orphaned version", result.stderr)
|
||||
|
||||
def test_frontier_rejects_public_registry_version_beyond_latest(self) -> None:
|
||||
release = valid_release()
|
||||
result, diagnostic, _ = self.run_command(
|
||||
"frontier",
|
||||
release,
|
||||
stable_refs=[{"ref": "refs/tags/v6.4.2"}],
|
||||
releases=[release],
|
||||
registries=[
|
||||
{
|
||||
"name": "docker.io/rcourtman/pulse",
|
||||
"tags": ["latest", "6.4", "6.4.2", "v6.4.3"],
|
||||
},
|
||||
{
|
||||
"name": "ghcr.io/rcourtman/pulse",
|
||||
"tags": ["v6.4.3", "6.4.3-rc.1"],
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(
|
||||
[item["code"] for item in diagnostic["violations"]],
|
||||
["registry_stable_tag_beyond_latest"],
|
||||
)
|
||||
self.assertEqual(
|
||||
diagnostic["identity"]["registry_stable_tags_beyond_latest"],
|
||||
"v6.4.3",
|
||||
)
|
||||
self.assertIn("docker.io/rcourtman/pulse", diagnostic["violations"][0]["actual"])
|
||||
self.assertIn("ghcr.io/rcourtman/pulse", diagnostic["violations"][0]["actual"])
|
||||
|
||||
def test_frontier_rejects_published_stable_release_beyond_latest(self) -> None:
|
||||
release = valid_release()
|
||||
newer = {**release, "id": 67890, "tag_name": "v6.4.3"}
|
||||
result, diagnostic, _ = self.run_command(
|
||||
"frontier",
|
||||
release,
|
||||
stable_refs=[{"ref": "refs/tags/v6.4.3"}],
|
||||
releases=[release, newer],
|
||||
)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertEqual(
|
||||
[item["code"] for item in diagnostic["violations"]],
|
||||
["newer_stable_release_not_advertised"],
|
||||
)
|
||||
|
||||
def test_frontier_allows_in_progress_draft_beyond_latest(self) -> None:
|
||||
release = valid_release()
|
||||
draft = {
|
||||
**release,
|
||||
"id": 67890,
|
||||
"tag_name": "v6.4.3",
|
||||
"draft": True,
|
||||
"published_at": None,
|
||||
}
|
||||
result, diagnostic, _ = self.run_command(
|
||||
"frontier",
|
||||
release,
|
||||
stable_refs=[{"ref": "refs/tags/v6.4.3"}],
|
||||
releases=[release, draft],
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(diagnostic["status"], "success")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user