Keep convergence evidence off sealed releases

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-01 11:20:44 +01:00
parent b1feb10b3f
commit a34f3c752b
7 changed files with 290 additions and 106 deletions
@@ -29,11 +29,11 @@ on:
required: true
type: string
pulse_owner_asset_name:
description: "Unique immutable convergence-owner evidence asset."
description: "Owner evidence path in the exact Pulse lease commit."
required: true
type: string
pulse_owner_asset_sha256:
description: "SHA-256 digest of the immutable convergence-owner evidence."
description: "SHA-256 digest of the commit-bound owner evidence."
required: true
type: string
@@ -67,6 +67,56 @@ jobs:
echo "::error::Private Pro staging did not return an R2 prefix."
exit 1
fi
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[1-9][0-9]*)?$ ]] || \
[[ ! "${PULSE_LEASE_SHA}" =~ ^[0-9a-f]{40}$ ]] || \
[[ ! "${PULSE_CONVERGENCE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${PULSE_OWNER_ASSET_NAME}" =~ ^release-convergence-owner-${PULSE_CONVERGENCE_RUN_ID}-[1-9][0-9]*\.json$ ]] || \
[[ ! "${PULSE_OWNER_ASSET_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Paid-runtime promotion requires an exact release, convergence owner, and lease."
exit 1
fi
release_json="$(mktemp)"
marker="$(mktemp)"
owner_record="$(mktemp)"
cleanup_owner_proof() {
rm -f "${release_json}" "${marker}" "${owner_record}"
}
trap cleanup_owner_proof EXIT
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > "${release_json}"
jq -e --arg tag "${TAG}" \
'.tag_name == $tag and .draft == false and .immutable == true and (.published_at | type == "string" and length > 0)' \
"${release_json}" >/dev/null
observed_lease_sha="$(
gh api \
"repos/${GITHUB_REPOSITORY}/git/ref/heads/release-customer-promotion-lock" \
--jq '.object.sha'
)"
if [[ "${observed_lease_sha}" != "${PULSE_LEASE_SHA}" ]]; then
echo "::error::Paid-runtime lease is ${observed_lease_sha:-absent}, expected ${PULSE_LEASE_SHA}."
exit 1
fi
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${marker}" \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/release-activation.json"
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${owner_record}" \
"https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${PULSE_LEASE_SHA}/${PULSE_OWNER_ASSET_NAME}"
printf '%s %s\n' "${PULSE_OWNER_ASSET_SHA256}" "${owner_record}" | sha256sum --check --
activation_owner_run_id="$(jq -r '.convergence_run_id' "${marker}")"
activation_marker_sha256="$(sha256sum "${marker}" | awk '{print $1}')"
jq -e \
--arg tag "${TAG}" \
--arg r2_prefix "${R2_PREFIX}" \
--arg activation_owner_run_id "${activation_owner_run_id}" \
--arg activation_marker_sha256 "${activation_marker_sha256}" \
--arg convergence_run_id "${PULSE_CONVERGENCE_RUN_ID}" \
--arg owner_asset_name "${PULSE_OWNER_ASSET_NAME}" \
'.schema_version == 2 and .tag == $tag and .r2_prefix == $r2_prefix and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .owner_asset_name == $owner_asset_name' \
"${owner_record}" >/dev/null
cleanup_owner_proof
trap - EXIT
echo "[OK] Paid-runtime mutation is bound to immutable ${TAG} convergence run ${PULSE_CONVERGENCE_RUN_ID}."
wait_for_workflow() {
local repo="$1"
+8 -65
View File
@@ -207,8 +207,8 @@ jobs:
lock_sha: ${{ steps.acquire.outputs.lock_sha }}
superseded: ${{ steps.admit.outputs.superseded }}
desired_tag: ${{ steps.admit.outputs.desired_tag }}
owner_asset_name: ${{ steps.owner.outputs.owner_asset_name }}
owner_asset_sha256: ${{ steps.owner.outputs.owner_asset_sha256 }}
owner_asset_name: ${{ steps.acquire.outputs.owner_asset_name }}
owner_asset_sha256: ${{ steps.acquire.outputs.owner_asset_sha256 }}
server_image_digest: ${{ needs.await_activation_commit.outputs.server_image_digest }}
control_plane_image_digest: ${{ needs.await_activation_commit.outputs.control_plane_image_digest }}
helm_chart_digest: ${{ needs.await_activation_commit.outputs.helm_chart_digest }}
@@ -224,6 +224,12 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
RELEASE_ID: ${{ inputs.release_id }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
R2_PREFIX: ${{ inputs.r2_prefix }}
ACTIVATION_OWNER_RUN_ID: ${{ needs.await_activation_commit.outputs.activation_owner_run_id }}
ACTIVATION_MARKER_SHA256: ${{ needs.await_activation_commit.outputs.activation_marker_sha256 }}
run: |
set -euo pipefail
scripts/release_control/customer_promotion_lease.sh acquire "${TAG}"
@@ -272,69 +278,6 @@ jobs:
echo "[OK] ${TAG} is the monotonic ${IS_PRERELEASE} channel convergence target."
fi
- name: Bind this lease as the active convergence successor
id: owner
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
RELEASE_ID: ${{ inputs.release_id }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
R2_PREFIX: ${{ inputs.r2_prefix }}
ACTIVATION_OWNER_RUN_ID: ${{ needs.await_activation_commit.outputs.activation_owner_run_id }}
ACTIVATION_MARKER_SHA256: ${{ needs.await_activation_commit.outputs.activation_marker_sha256 }}
LEASE_SHA: ${{ steps.acquire.outputs.lock_sha }}
run: |
set -euo pipefail
owner_dir="$(mktemp -d)"
verified_record="$(mktemp)"
cleanup() { rm -rf "${owner_dir}"; rm -f "${verified_record}"; }
trap cleanup EXIT
owner_asset_name="release-convergence-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${LEASE_SHA}.json"
owner_record="${owner_dir}/${owner_asset_name}"
jq -n \
--arg tag "${TAG}" \
--arg target_commitish "${TARGET_COMMITISH}" \
--arg release_id "${RELEASE_ID}" \
--arg source_release_run_id "${SOURCE_RELEASE_RUN_ID}" \
--arg r2_prefix "${R2_PREFIX}" \
--arg activation_owner_run_id "${ACTIVATION_OWNER_RUN_ID}" \
--arg activation_marker_sha256 "${ACTIVATION_MARKER_SHA256}" \
--arg convergence_run_id "${GITHUB_RUN_ID}" \
--arg lease_sha "${LEASE_SHA}" \
--arg owner_asset_name "${owner_asset_name}" \
'{
schema_version: 1,
tag: $tag,
target_commitish: $target_commitish,
release_id: $release_id,
source_release_run_id: $source_release_run_id,
r2_prefix: $r2_prefix,
activation_owner_run_id: $activation_owner_run_id,
activation_marker_sha256: $activation_marker_sha256,
convergence_run_id: $convergence_run_id,
lease_sha: $lease_sha,
owner_asset_name: $owner_asset_name
}' > "${owner_record}"
owner_asset_sha256="$(sha256sum "${owner_record}" | awk '{print $1}')"
echo "owner_asset_name=${owner_asset_name}" >> "$GITHUB_OUTPUT"
echo "owner_asset_sha256=${owner_asset_sha256}" >> "$GITHUB_OUTPUT"
gh release upload "${TAG}" "${owner_record}"
curl -fsSL --retry 8 --retry-delay 2 --retry-all-errors \
-o "${verified_record}" \
"https://github.com/${{ github.repository }}/releases/download/${TAG}/${owner_asset_name}"
printf '%s %s\n' "${owner_asset_sha256}" "${verified_record}" | sha256sum --check --
jq -e \
--arg tag "${TAG}" \
--arg activation_owner_run_id "${ACTIVATION_OWNER_RUN_ID}" \
--arg activation_marker_sha256 "${ACTIVATION_MARKER_SHA256}" \
--arg convergence_run_id "${GITHUB_RUN_ID}" \
--arg lease_sha "${LEASE_SHA}" \
--arg owner_asset_name "${owner_asset_name}" \
'.schema_version == 1 and .tag == $tag and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .lease_sha == $lease_sha and .owner_asset_name == $owner_asset_name' \
"${verified_record}" >/dev/null
echo "[OK] Bound convergence run ${GITHUB_RUN_ID} to lease ${LEASE_SHA} for ${TAG}."
promote_floating_tags:
name: Converge Docker aliases
needs: acquire_customer_promotion_lease
+10 -10
View File
@@ -28,12 +28,12 @@ on:
default: ''
type: string
convergence_owner_asset_name:
description: 'Unique immutable owner record for the active convergence lease.'
description: 'Owner record stored in the exact convergence lease commit.'
required: false
default: ''
type: string
convergence_owner_asset_sha256:
description: 'SHA-256 digest of the immutable convergence owner record.'
description: 'SHA-256 digest of the commit-bound convergence owner record.'
required: false
default: ''
type: string
@@ -238,20 +238,21 @@ jobs:
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
[[ ! "${CONVERGENCE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${LEASE_SHA}" =~ ^[0-9a-f]{40}$ ]] || \
[[ "${OWNER_ASSET_NAME}" != "release-convergence-owner-${CONVERGENCE_RUN_ID}-"*"-${LEASE_SHA}.json" ]] || \
[[ ! "${OWNER_ASSET_NAME}" =~ ^release-convergence-owner-${CONVERGENCE_RUN_ID}-[1-9][0-9]*\.json$ ]] || \
[[ ! "${OWNER_ASSET_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Stable demo mutation requires an exact stable tag, convergence owner, and lease."
exit 1
fi
release_state="$(
gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json isDraft,isPrerelease,publishedAt,tagName \
--jq '[.tagName, (.isDraft | tostring), (.isPrerelease | tostring), (.publishedAt // "")] | @tsv'
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
--jq '[.tag_name, (.draft | tostring), (.prerelease | tostring), (.immutable // false | tostring), (.published_at // "")] | @tsv'
)"
if [ "$(awk -F '\t' '{print $1}' <<<"${release_state}")" != "${TAG}" ] || \
[ "$(awk -F '\t' '{print $2}' <<<"${release_state}")" != "false" ] || \
[ "$(awk -F '\t' '{print $3}' <<<"${release_state}")" != "false" ] || \
[ -z "$(awk -F '\t' '{print $4}' <<<"${release_state}")" ]; then
echo "::error::Stable demo mutation refuses inactive or prerelease tag ${TAG}."
[ "$(awk -F '\t' '{print $4}' <<<"${release_state}")" != "true" ] || \
[ -z "$(awk -F '\t' '{print $5}' <<<"${release_state}")" ]; then
echo "::error::Stable demo mutation refuses mutable, inactive, or prerelease tag ${TAG}."
exit 1
fi
marker="$(mktemp)"
@@ -276,16 +277,15 @@ jobs:
fi
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${owner_record}" \
"https://github.com/${{ github.repository }}/releases/download/${TAG}/${OWNER_ASSET_NAME}"
"https://raw.githubusercontent.com/${{ github.repository }}/${LEASE_SHA}/${OWNER_ASSET_NAME}"
printf '%s %s\n' "${OWNER_ASSET_SHA256}" "${owner_record}" | sha256sum --check --
jq -e \
--arg tag "${TAG}" \
--arg activation_owner_run_id "${activation_owner_run_id}" \
--arg activation_marker_sha256 "${activation_marker_sha256}" \
--arg convergence_run_id "${CONVERGENCE_RUN_ID}" \
--arg lease_sha "${LEASE_SHA}" \
--arg owner_asset_name "${OWNER_ASSET_NAME}" \
'.schema_version == 1 and .tag == $tag and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .lease_sha == $lease_sha and .owner_asset_name == $owner_asset_name' \
'.schema_version == 2 and .tag == $tag and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .owner_asset_name == $owner_asset_name' \
"${owner_record}" >/dev/null
rm -f "${marker}" "${owner_record}"
echo "[OK] Stable demo mutation is bound to committed ${TAG} convergence run ${CONVERGENCE_RUN_ID}."
@@ -1729,11 +1729,14 @@ artifact-selection behaviour.
run is completed, a fresh manual convergence dispatch from fixed workflow
code may adopt the same immutable tag, source run, target commit, release ID,
R2 prefix, and activation-marker digest. Adoption happens only after lease
acquisition and publishes a unique immutable owner record whose asset name
includes the new run ID, run attempt, and lease SHA. Downstream demo and paid
broker mutation must verify the exact passed owner filename and SHA-256. A
clobbered constant owner record is forbidden because cached prior bytes could
authorize stale ownership.
acquisition and writes a unique owner record into the exact lease commit.
The record name includes the new run ID and run attempt; downstream demo and
paid broker mutation must read it from the passed lease commit and verify the
exact filename and SHA-256. A unique evidence tag retains that exact commit
after the active lease ref is released. The release remains sealed after
publication, while every successor gets immutable, run-scoped Git evidence.
Reading a floating ref or a clobbered constant record is forbidden because
cached prior bytes could authorize stale ownership.
A support-only private Pro prerelease image is a narrower exception for
customer verification of an already-fixed defect. It may dispatch the private
`Build Pro Release` workflow with `publish_docker_image=true`,
@@ -3137,7 +3137,8 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
for _, needle := range []string{
`"repos/${GITHUB_REPOSITORY}/git/refs"`,
`Bootstrapped absent customer-promotion lease ref`,
`git push origin "${lock_commit}:${LOCK_REF}"`,
`git push --atomic origin`,
`"${lock_commit}:${owner_ref}"`,
} {
if !strings.Contains(leaseScript, needle) {
t.Fatalf("customer-promotion lease missing absent-ref bootstrap contract: %s", needle)
@@ -25,18 +25,81 @@ acquire() {
return 1
fi
: "${TARGET_COMMITISH:?TARGET_COMMITISH is required}"
: "${RELEASE_ID:?RELEASE_ID is required}"
: "${SOURCE_RELEASE_RUN_ID:?SOURCE_RELEASE_RUN_ID is required}"
: "${R2_PREFIX:?R2_PREFIX is required}"
: "${ACTIVATION_OWNER_RUN_ID:?ACTIVATION_OWNER_RUN_ID is required}"
: "${ACTIVATION_MARKER_SHA256:?ACTIVATION_MARKER_SHA256 is required}"
if [[ ! "${TARGET_COMMITISH}" =~ ^[0-9a-f]{40}$ ]] || \
[[ ! "${RELEASE_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${SOURCE_RELEASE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${ACTIVATION_OWNER_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${ACTIVATION_MARKER_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Customer-promotion owner identity is malformed."
return 1
fi
export GIT_AUTHOR_NAME="${GITHUB_ACTOR}"
export GIT_AUTHOR_EMAIL="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"
export GIT_COMMITTER_NAME="${GIT_AUTHOR_NAME}"
export GIT_COMMITTER_EMAIL="${GIT_AUTHOR_EMAIL}"
local lock_message="Release customer promotion lock run=${GITHUB_RUN_ID} attempt=${GITHUB_RUN_ATTEMPT} owner=${tag}"
local lock_commit
lock_commit="$(printf '%s\n' "${lock_message}" | git commit-tree "$(git rev-parse 'HEAD^{tree}')" -p HEAD)"
local owner_dir owner_record owner_asset_name owner_asset_sha256 owner_ref
local owner_blob lock_index lock_tree lock_commit
owner_dir="$(mktemp -d)"
lock_index="$(mktemp)"
owner_asset_name="release-convergence-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json"
owner_ref="refs/tags/${owner_asset_name%.json}"
owner_record="${owner_dir}/${owner_asset_name}"
jq -n \
--arg tag "${tag}" \
--arg target_commitish "${TARGET_COMMITISH}" \
--arg release_id "${RELEASE_ID}" \
--arg source_release_run_id "${SOURCE_RELEASE_RUN_ID}" \
--arg r2_prefix "${R2_PREFIX}" \
--arg activation_owner_run_id "${ACTIVATION_OWNER_RUN_ID}" \
--arg activation_marker_sha256 "${ACTIVATION_MARKER_SHA256}" \
--arg convergence_run_id "${GITHUB_RUN_ID}" \
--arg owner_asset_name "${owner_asset_name}" \
'{
schema_version: 2,
tag: $tag,
target_commitish: $target_commitish,
release_id: $release_id,
source_release_run_id: $source_release_run_id,
r2_prefix: $r2_prefix,
activation_owner_run_id: $activation_owner_run_id,
activation_marker_sha256: $activation_marker_sha256,
convergence_run_id: $convergence_run_id,
owner_asset_name: $owner_asset_name
}' > "${owner_record}"
owner_asset_sha256="$(sha256sum "${owner_record}" | awk '{print $1}')"
# Store the owner record inside the lease commit itself. The exact lock SHA
# therefore addresses both the active lease and immutable evidence for its
# owner without trying to append an asset to an already-published release.
owner_blob="$(git hash-object -w "${owner_record}")"
GIT_INDEX_FILE="${lock_index}" git read-tree HEAD
GIT_INDEX_FILE="${lock_index}" git update-index --add --cacheinfo \
"100644,${owner_blob},${owner_asset_name}"
lock_tree="$(GIT_INDEX_FILE="${lock_index}" git write-tree)"
lock_commit="$(printf '%s\n' "${lock_message}" | git commit-tree "${lock_tree}" -p HEAD)"
local deadline=$((SECONDS + timeout_seconds))
while (( SECONDS < deadline )); do
if git push origin "${lock_commit}:${LOCK_REF}" >/dev/null 2>&1; then
echo "lock_sha=${lock_commit}" >> "${GITHUB_OUTPUT}"
# The unique evidence tag retains the exact commit after the active lock ref
# is released. Atomic creation prevents either ref from existing alone.
if git push --atomic origin \
"${lock_commit}:${LOCK_REF}" \
"${lock_commit}:${owner_ref}" >/dev/null 2>&1; then
{
echo "lock_sha=${lock_commit}"
echo "owner_asset_name=${owner_asset_name}"
echo "owner_asset_sha256=${owner_asset_sha256}"
} >> "${GITHUB_OUTPUT}"
rm -rf -- "${owner_dir}"
rm -f -- "${lock_index}"
echo "[OK] Acquired global customer-promotion lease ${lock_commit}."
return 0
fi
@@ -92,6 +155,8 @@ acquire() {
sleep 30
done
rm -rf -- "${owner_dir}"
rm -f -- "${lock_index}"
echo "::error::Timed out acquiring the global customer-promotion lease."
return 1
}
@@ -3,12 +3,14 @@
from __future__ import annotations
import hashlib
import json
import os
from pathlib import PurePosixPath
from pathlib import Path, PurePosixPath
import re
import subprocess
import tempfile
import unittest
import json
import yaml
from yaml.constructor import ConstructorError
@@ -664,7 +666,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
"inputs.verify_only != true",
"release-activation.json",
".convergence_run_id == $convergence_run_id",
"Stable demo mutation refuses inactive or prerelease tag",
"Stable demo mutation refuses mutable, inactive, or prerelease tag",
"https://raw.githubusercontent.com/${{ github.repository }}/${LEASE_SHA}/${OWNER_ASSET_NAME}",
".immutable // false",
".schema_version == 2",
):
self.assertIn(needle, demo_mutation)
self.assertIn("tag: ${{ inputs.tag }}", demo_caller)
@@ -686,6 +691,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
"pulse_owner_asset_name: $pulse_owner_asset_name",
"pulse_owner_asset_sha256: $pulse_owner_asset_sha256",
"return_run_details: true",
".immutable == true",
"https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${PULSE_LEASE_SHA}/${PULSE_OWNER_ASSET_NAME}",
".schema_version == 2",
"Paid-runtime mutation is bound to immutable",
):
self.assertIn(needle, paid_dispatch)
@@ -703,7 +712,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertNotIn("release-customer-promotion-lock", release_workflow)
self.assertIn("customer_promotion_lease.sh acquire", lease)
self.assertIn("refs/heads/release-customer-promotion-lock", lease_script)
self.assertIn("git push origin \"${lock_commit}:${LOCK_REF}\"", lease_script)
self.assertIn("git push --atomic origin", lease_script)
self.assertIn('"${lock_commit}:${owner_ref}"', lease_script)
self.assertIn('"repos/${GITHUB_REPOSITORY}/git/refs"', lease_script)
self.assertIn("Bootstrapped absent customer-promotion lease ref", lease_script)
self.assertIn("--force-with-lease=\"${LOCK_REF}:${observed_sha}\"", lease_script)
@@ -715,6 +725,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("no global customer pointer will move backward", lease)
self.assertIn("customer_promotion_lease.sh release", release_lease)
self.assertIn("--force-with-lease=\"${LOCK_REF}:${lock_sha}\"", lease_script)
self.assertIn('schema_version: 2', lease_script)
self.assertIn('git hash-object -w "${owner_record}"', lease_script)
self.assertIn('GIT_INDEX_FILE="${lock_index}" git update-index', lease_script)
self.assertNotIn('gh release upload "${TAG}" "${owner_record}"', lease)
helm_pages = workflow_job_block(convergence, "publish_helm_pages")
convergence_verdict = workflow_job_block(convergence, "convergence_verdict")
@@ -854,6 +868,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
convergence = read(".github/workflows/release-convergence.yml")
await_commit = workflow_job_block(convergence, "await_activation_commit")
lease = workflow_job_block(convergence, "acquire_customer_promotion_lease")
lease_script = read("scripts/release_control/customer_promotion_lease.sh")
original_owner = "100"
successor_owner = "200"
@@ -870,18 +885,117 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
)
self.assertTrue(successor_may_adopt)
self.assertIn("Adopting committed ${TAG} from completed convergence owner", await_commit)
self.assertIn("Bind this lease as the active convergence successor", lease)
self.assertIn("activation_owner_run_id", lease)
self.assertIn("activation_marker_sha256", lease)
self.assertIn("release-convergence-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${LEASE_SHA}.json", lease)
self.assertNotIn("#release-convergence-owner.json", lease)
self.assertIn("owner_asset_sha256", lease)
self.assertIn("sha256sum --check --", lease)
self.assertIn("ACTIVATION_OWNER_RUN_ID", lease)
self.assertIn("ACTIVATION_MARKER_SHA256", lease)
self.assertIn(
"release-convergence-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json",
lease_script,
)
self.assertIn("owner_asset_sha256", lease_script)
self.assertIn("git commit-tree", lease_script)
self.assertNotIn("gh release upload", lease)
stale_name = f"release-convergence-owner-{original_owner}-5-{'a' * 40}.json"
successor_name = f"release-convergence-owner-{successor_owner}-1-{'b' * 40}.json"
stale_name = f"release-convergence-owner-{original_owner}-5.json"
successor_name = f"release-convergence-owner-{successor_owner}-1.json"
self.assertNotEqual(stale_name, successor_name)
def test_customer_promotion_lease_commit_contains_exact_owner_evidence(self) -> None:
script = REPO_ROOT / "scripts" / "release_control" / "customer_promotion_lease.sh"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
remote = root / "remote.git"
checkout = root / "checkout"
output = root / "github-output"
subprocess.run(["git", "init", "--bare", str(remote)], check=True, capture_output=True)
subprocess.run(["git", "init", str(checkout)], check=True, capture_output=True)
for key, value in (
("user.name", "Pulse Test"),
("user.email", "pulse-test@example.invalid"),
):
subprocess.run(
["git", "config", key, value], cwd=checkout, check=True
)
(checkout / "README").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "add", "README"], cwd=checkout, check=True)
subprocess.run(["git", "commit", "-m", "base"], cwd=checkout, check=True, capture_output=True)
subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=checkout, check=True)
subprocess.run(
["git", "push", "origin", "HEAD:refs/heads/release-customer-promotion-lock"],
cwd=checkout,
check=True,
capture_output=True,
)
env = os.environ.copy()
env.update(
{
"GH_TOKEN": "test-token",
"GITHUB_REPOSITORY": "rcourtman/Pulse",
"GITHUB_RUN_ID": "200",
"GITHUB_RUN_ATTEMPT": "3",
"GITHUB_ACTOR": "pulse-test",
"GITHUB_ACTOR_ID": "1234",
"GITHUB_OUTPUT": str(output),
"TARGET_COMMITISH": "a" * 40,
"RELEASE_ID": "321",
"SOURCE_RELEASE_RUN_ID": "100",
"R2_PREFIX": "v6.5.0-pro-test",
"ACTIVATION_OWNER_RUN_ID": "150",
"ACTIVATION_MARKER_SHA256": "b" * 64,
}
)
result = subprocess.run(
[str(script), "acquire", "v6.5.0"],
cwd=checkout,
env=env,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
outputs = dict(
line.split("=", 1)
for line in output.read_text(encoding="utf-8").splitlines()
)
self.assertRegex(outputs["lock_sha"], r"^[0-9a-f]{40}$")
self.assertEqual(
outputs["owner_asset_name"],
"release-convergence-owner-200-3.json",
)
record_text = subprocess.run(
[
"git",
"show",
f"{outputs['lock_sha']}:{outputs['owner_asset_name']}",
],
cwd=checkout,
text=True,
capture_output=True,
check=True,
).stdout
record = json.loads(record_text)
self.assertEqual(record["schema_version"], 2)
self.assertEqual(record["convergence_run_id"], "200")
self.assertEqual(record["activation_owner_run_id"], "150")
self.assertNotIn("lease_sha", record)
self.assertEqual(
outputs["owner_asset_sha256"],
hashlib.sha256(record_text.encode()).hexdigest(),
)
retained = subprocess.run(
[
"git",
"ls-remote",
"origin",
"refs/tags/release-convergence-owner-200-3",
],
cwd=checkout,
text=True,
capture_output=True,
check=True,
).stdout.split()[0]
self.assertEqual(retained, outputs["lock_sha"])
def test_private_dispatches_wait_for_the_exact_created_run(self) -> None:
release_workflow = read(".github/workflows/create-release.yml")
private_promotion_workflow = read(
@@ -1498,7 +1612,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertNotIn("--enforce-prerelease-observation-window", dry_run_workflow)
self.assertNotIn("--enforce-prerelease-observation-window", dry_run_helper)
self.assertIn("render_release_body.py", content)
self.assertIn('--promotion-channel "${{ needs.prepare.outputs.release_stage }}"', content)
self.assertIn('WORKFLOW_OUTPUT_3: ${{ needs.prepare.outputs.release_stage }}', content)
self.assertIn('--promotion-channel "${WORKFLOW_OUTPUT_3}"', content)
self.assertIn(
"needs.prepare.outputs.release_stage != 'alpha' && needs.prepare.outputs.release_stage != 'beta'",
content,
@@ -1565,8 +1680,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("statuses: write", validation_workflow)
self.assertIn("curl --fail-with-body --silent --show-error -X POST", validation_workflow)
self.assertIn('"context": "Release Asset Validation"', validation_workflow)
self.assertIn('--arg tag "${{ steps.context.outputs.tag }}"', validation_workflow)
self.assertIn('--arg target_commitish "${{ steps.context.outputs.target_commitish }}"', validation_workflow)
self.assertIn('WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}', validation_workflow)
self.assertIn(
'WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}',
validation_workflow,
)
self.assertIn('--arg tag "${WORKFLOW_OUTPUT_4}"', validation_workflow)
self.assertIn('--arg target_commitish "${WORKFLOW_OUTPUT_5}"', validation_workflow)
self.assertIn("{body: $body, tag_name: $tag, target_commitish: $target_commitish}", validation_workflow)
self.assertIn("{draft: true, tag_name: $tag, target_commitish: $target_commitish}", validation_workflow)
self.assertIn("Validation release body update detached release tag", validation_workflow)
@@ -1606,8 +1726,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
'ACTUAL_TARGET_COMMITISH=$(jq -r \'.target_commitish // empty\' "$RELEASE_JSON_FILE")',
content,
)
self.assertIn('./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"', content)
self.assertIn('./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"', content)
self.assertIn('WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}', content)
self.assertIn('./scripts/backfill-release-assets.sh --tag "${WORKFLOW_OUTPUT_1}" --repo "${{ github.repository }}"', content)
self.assertIn('./scripts/validate-published-release.sh "${WORKFLOW_OUTPUT_1}" "${{ github.repository }}"', content)
self.assertIn("PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", content)
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", content)
self.assertIn(
@@ -1616,8 +1737,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
- name: Validate published release packet
env:
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
WORKFLOW_OUTPUT_1: ${{ needs.prepare.outputs.tag }}
run: |
./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"
./scripts/validate-published-release.sh "${WORKFLOW_OUTPUT_1}" "${{ github.repository }}"
"""
),
normalize_ws(content),