mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Keep convergence evidence off sealed releases
Change-source: pulse-maintainer
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user