Promote qualified release payloads directly

This commit is contained in:
rcourtman 2026-08-21 14:47:52 +01:00
parent 962df694e9
commit 74bd06a953
8 changed files with 261 additions and 286 deletions

View file

@ -1271,7 +1271,7 @@ jobs:
return 1
fi
sleep 30
sleep 5
done
echo "::error::Timed out waiting for ${label} after ${timeout_seconds}s."

View file

@ -2,308 +2,236 @@ name: Release Helm Chart to GitHub Pages
run-name: Release Helm Chart ${{ inputs.chart_version }}
# Called only from release-convergence.yml while it holds the global customer-
# promotion lease. This workflow mutates the public Pages index and must never
# expose a direct dispatch path that bypasses that lease.
# promotion lease. Chart lint, packaging, OCI publication, and the exact-
# candidate install/upgrade smoke complete before activation. This workflow
# verifies and promotes that immutable source-run artifact after activation.
on:
workflow_call:
inputs:
chart_version:
description: "Chart version (e.g., 6.2.0-rc.9)."
description: "Exact chart version (e.g., 6.3.0-rc.5)."
required: true
type: string
source_release_run_id:
description: "Exact create-release.yml run containing the qualified chart."
required: true
type: string
target_commitish:
description: "Exact activated release source commit."
required: true
type: string
permissions:
actions: read
contents: write
jobs:
release:
runs-on: ubuntu-24.04
steps:
- name: Determine chart version
id: version
run: |
VERSION="${{ inputs.chart_version }}"
if [ -z "$VERSION" ]; then
echo "::error::chart_version input is required"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Chart version: $VERSION"
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
fetch-tags: true
- name: Validate release line policy
id: release_line
env:
RELEASE_TAG: ${{ steps.version.outputs.release_tag }}
run: |
set -euo pipefail
python3 scripts/release_control/validate_artifact_release_line.py \
--tag "${RELEASE_TAG}" \
--purpose "Helm pages release" \
--github-output "$GITHUB_OUTPUT"
- name: Require activated GitHub release
- name: Require activated GitHub release and source run
id: identity
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.version.outputs.release_tag }}
VERSION: ${{ inputs.chart_version }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
run: |
set -euo pipefail
release_state="$(gh release view "${RELEASE_TAG}" --json isDraft,publishedAt,tagName \
--jq '[.tagName, (.isDraft | tostring), (.publishedAt // "")] | @tsv')"
actual_tag="$(awk -F '\t' '{print $1}' <<<"${release_state}")"
is_draft="$(awk -F '\t' '{print $2}' <<<"${release_state}")"
published_at="$(awk -F '\t' '{print $3}' <<<"${release_state}")"
if [ "${actual_tag}" != "${RELEASE_TAG}" ] || [ "${is_draft}" != "false" ] || [ -z "${published_at}" ]; then
echo "::error::Helm Pages refuses to advertise inactive release ${RELEASE_TAG}."
if [[ -z "${VERSION}" ]] || \
[[ ! "${SOURCE_RELEASE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${TARGET_COMMITISH}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Helm Pages requires an exact version, source run ID, and commit SHA."
exit 1
fi
- name: Check out validated release tag
release_tag="v${VERSION}"
release_state="$(
gh api "repos/${{ github.repository }}/releases/tags/${release_tag}" \
--jq '[.tag_name, .target_commitish, (.draft | tostring), (.published_at // "")] | @tsv'
)"
actual_tag="$(awk -F '\t' '{print $1}' <<<"${release_state}")"
actual_commit="$(awk -F '\t' '{print $2}' <<<"${release_state}")"
is_draft="$(awk -F '\t' '{print $3}' <<<"${release_state}")"
published_at="$(awk -F '\t' '{print $4}' <<<"${release_state}")"
if [[ "${actual_tag}" != "${release_tag}" ]] || \
[[ "${actual_commit}" != "${TARGET_COMMITISH}" ]] || \
[[ "${is_draft}" != "false" ]] || \
[[ -z "${published_at}" ]]; then
echo "::error::Helm Pages refuses to advertise inactive or mismatched release ${release_tag}."
exit 1
fi
marker="$(mktemp)"
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${marker}" \
"https://github.com/${{ github.repository }}/releases/download/${release_tag}/release-activation.json"
jq -e \
--arg tag "${release_tag}" \
--arg target_commitish "${TARGET_COMMITISH}" \
--arg source_release_run_id "${SOURCE_RELEASE_RUN_ID}" \
'.schema_version == 1 and .tag == $tag and .target_commitish == $target_commitish and .source_release_run_id == $source_release_run_id' \
"${marker}" >/dev/null
rm -f "${marker}"
source_run="$(
gh api "repos/${{ github.repository }}/actions/runs/${SOURCE_RELEASE_RUN_ID}" \
--jq '[.path, .event, .head_sha] | @tsv'
)"
run_path="$(awk -F '\t' '{print $1}' <<<"${source_run}")"
run_event="$(awk -F '\t' '{print $2}' <<<"${source_run}")"
run_sha="$(awk -F '\t' '{print $3}' <<<"${source_run}")"
if [[ "${run_path}" != ".github/workflows/create-release.yml" ]] || \
[[ "${run_event}" != "workflow_dispatch" ]] || \
[[ "${run_sha}" != "${TARGET_COMMITISH}" ]]; then
echo "::error::Source release run ${SOURCE_RELEASE_RUN_ID} does not own the activated ${release_tag} chart."
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
- name: Download immutable qualified chart
env:
RELEASE_TAG: ${{ steps.version.outputs.release_tag }}
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.identity.outputs.version }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
run: |
set -euo pipefail
git checkout --detach "refs/tags/${RELEASE_TAG}"
mkdir -p dist
gh run download "${SOURCE_RELEASE_RUN_ID}" \
--repo "${{ github.repository }}" \
--name "pulse-chart-${VERSION}" \
--dir dist
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm
- name: Set up Helm
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.15.2
- name: Install helm-docs
run: |
cd /tmp
HELM_DOCS_VERSION="1.14.2"
HELM_DOCS_ARCHIVE="helm-docs_${HELM_DOCS_VERSION}_Linux_x86_64.tar.gz"
HELM_DOCS_SHA256="a8cf72ada34fad93285ba2a452b38bdc5bd52cc9a571236244ec31022928d6cc"
wget "https://github.com/norwoodj/helm-docs/releases/download/v${HELM_DOCS_VERSION}/${HELM_DOCS_ARCHIVE}"
printf '%s %s\n' "$HELM_DOCS_SHA256" "$HELM_DOCS_ARCHIVE" | sha256sum --check --
tar -xzf "$HELM_DOCS_ARCHIVE"
sudo mv helm-docs /usr/local/bin/
helm-docs --version
- name: Generate chart documentation
- name: Verify immutable chart identity
env:
VERSION: ${{ steps.identity.outputs.version }}
CHART_PATH: dist/pulse-${{ steps.identity.outputs.version }}.tgz
run: |
set -euo pipefail
cd deploy/helm/pulse
helm-docs
cd ../../..
python3 - <<'PY'
import os
import pathlib
import re
import tarfile
- name: Update Chart.yaml version
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
python3 scripts/sync_chart_release_metadata.py \
--chart deploy/helm/pulse/Chart.yaml \
--version "$VERSION" \
--repo "${{ github.repository }}"
chart_path = pathlib.Path(os.environ["CHART_PATH"])
version = os.environ["VERSION"]
if not chart_path.is_file():
raise SystemExit(f"missing immutable qualified chart: {chart_path}")
with tarfile.open(chart_path, "r:gz") as archive:
for member in archive.getmembers():
parts = pathlib.PurePosixPath(member.name).parts
if member.name.startswith("/") or any(part in {"", ".", ".."} for part in parts):
raise SystemExit(f"unsafe chart member: {member.name}")
stream = archive.extractfile(archive.getmember("pulse/Chart.yaml"))
if stream is None:
raise SystemExit("qualified chart has no readable pulse/Chart.yaml")
metadata = stream.read().decode("utf-8")
- name: Validate Helm chart
run: |
# Strict linting
helm lint deploy/helm/pulse --strict
def value(key: str) -> str:
match = re.search(rf"(?m)^{re.escape(key)}:\s*[\"']?([^\"'\s]+)", metadata)
return match.group(1) if match else ""
# Template validation with minimal values
helm template pulse deploy/helm/pulse --set persistence.enabled=false > /dev/null
if value("name") != "pulse" or value("version") != version or value("appVersion") != version:
raise SystemExit("qualified chart metadata does not match the activated release")
PY
sha256sum "${CHART_PATH}"
helm show chart "${CHART_PATH}"
# Template validation with common overrides
helm template pulse deploy/helm/pulse \
--set ingress.enabled=true \
--set ingress.hosts[0].host=pulse.example.com \
--set agent.enabled=true > /dev/null
echo "✓ Chart validation passed"
- name: Smoke test with kind
run: |
set -euo pipefail
cleanup() {
kind delete cluster --name pulse-test >/dev/null 2>&1 || true
}
diagnose() {
echo "::group::helm status"
helm status pulse || true
echo "::endgroup::"
echo "::group::kubectl get all"
kubectl get all -A || true
echo "::endgroup::"
echo "::group::kubectl describe pods"
kubectl describe pods -A || true
echo "::endgroup::"
echo "::group::pod logs"
pods=$(kubectl get pods -A -o name 2>/dev/null || true)
for pod in $pods; do
echo "### ${pod}"
kubectl logs --all-containers=true --tail=200 "$pod" || true
done
echo "::endgroup::"
echo "::group::events"
kubectl get events -A --sort-by=.lastTimestamp || kubectl get events -A || true
echo "::endgroup::"
cleanup
}
trap 'diagnose' ERR
# Install kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# Create cluster
kind create cluster --name pulse-test --wait 5m
# Install chart
helm install pulse deploy/helm/pulse \
--set persistence.enabled=false \
--set server.secretEnv.create=true \
--set server.secretEnv.data.API_TOKENS=test-token \
--wait --timeout 5m --debug
# Verify deployment
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=pulse --timeout=180s || (kubectl describe pods -l app.kubernetes.io/name=pulse && exit 1)
kubectl get pods -l app.kubernetes.io/name=pulse
# Test upgrade
helm upgrade pulse deploy/helm/pulse \
--set persistence.enabled=false \
--set server.secretEnv.create=true \
--set server.secretEnv.data.API_TOKENS=test-token \
--wait --timeout 5m --debug
# Cleanup
trap - ERR
cleanup
echo "✓ Smoke test passed"
- name: Run chart-releaser
uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0
- name: Check out current Pages index
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
charts_dir: deploy/helm
config: cr.yaml
skip_existing: true
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CR_RELEASE_NAME_TEMPLATE: "helm-chart-{{ .Version }}"
CR_MAKE_RELEASE_LATEST: false
ref: gh-pages
path: gh-pages
fetch-depth: 1
persist-credentials: true
- name: Ensure chart release and pages index
- name: Publish chart release and merge Pages index
env:
GITHUB_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ steps.version.outputs.release_tag }}
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.identity.outputs.version }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
run: |
set -euo pipefail
chart="pulse-${VERSION}.tgz"
chart_path="dist/${chart}"
chart_release="helm-chart-${VERSION}"
chart_release_url="https://github.com/${{ github.repository }}/releases/download/${chart_release}"
CHART="pulse-${VERSION}.tgz"
CHART_PATH="dist/${CHART}"
CHART_RELEASE="helm-chart-${VERSION}"
CHART_RELEASE_URL="https://github.com/${REPOSITORY}/releases/download/${CHART_RELEASE}"
RELEASE_SHA="$(git rev-list -n 1 "${RELEASE_TAG}")"
mkdir -p dist
helm package deploy/helm/pulse \
--version "${VERSION}" \
--app-version "${VERSION}" \
--destination dist
if gh release view "${CHART_RELEASE}" >/dev/null 2>&1; then
gh release upload "${CHART_RELEASE}" "${CHART_PATH}" --clobber
if gh release view "${chart_release}" >/dev/null 2>&1; then
gh release upload "${chart_release}" "${chart_path}" --clobber
else
gh release create "${CHART_RELEASE}" "${CHART_PATH}" \
--target "${RELEASE_SHA}" \
gh release create "${chart_release}" "${chart_path}" \
--target "${TARGET_COMMITISH}" \
--title "Helm chart ${VERSION}" \
--notes "Helm chart for Pulse ${VERSION}." \
--prerelease \
--latest=false
fi
gh release edit "${CHART_RELEASE}" --prerelease --latest=false
gh release edit "${chart_release}" --prerelease --latest=false
workdir="$(mktemp -d)"
cleanup() {
git worktree remove -f "${workdir}/gh-pages" >/dev/null 2>&1 || true
rm -rf "${workdir}"
}
git -C gh-pages config user.name "$GITHUB_ACTOR"
git -C gh-pages config user.email "$GITHUB_ACTOR@users.noreply.github.com"
index_work="$(mktemp -d)"
cleanup() { rm -rf "${index_work}"; }
trap cleanup EXIT
git fetch origin gh-pages
git worktree add "${workdir}/gh-pages" origin/gh-pages
git -C "${workdir}/gh-pages" checkout -B gh-pages origin/gh-pages
index_work="${workdir}/index"
mkdir -p "${index_work}"
cp "${CHART_PATH}" "${index_work}/${CHART}"
if [ -f "${workdir}/gh-pages/index.yaml" ]; then
cp "${workdir}/gh-pages/index.yaml" "${index_work}/index.yaml"
cp "${chart_path}" "${index_work}/${chart}"
if [[ -f gh-pages/index.yaml ]]; then
cp gh-pages/index.yaml "${index_work}/index.yaml"
helm repo index "${index_work}" \
--url "${CHART_RELEASE_URL}" \
--url "${chart_release_url}" \
--merge "${index_work}/index.yaml"
else
helm repo index "${index_work}" --url "${CHART_RELEASE_URL}"
helm repo index "${index_work}" --url "${chart_release_url}"
fi
cp "${index_work}/index.yaml" "${workdir}/gh-pages/index.yaml"
cp "${index_work}/index.yaml" gh-pages/index.yaml
if ! grep -q "version: ${VERSION}" "${workdir}/gh-pages/index.yaml"; then
echo "::error::Helm pages index is missing version ${VERSION}"
if ! grep -q "version: ${VERSION}" gh-pages/index.yaml; then
echo "::error::Helm Pages index is missing version ${VERSION}."
exit 1
fi
git -C "${workdir}/gh-pages" add index.yaml
if git -C "${workdir}/gh-pages" diff --cached --quiet; then
echo "Helm pages index already contains ${VERSION}"
git -C gh-pages add index.yaml
if git -C gh-pages diff --cached --quiet; then
echo "Helm Pages index already contains ${VERSION}."
else
git -C "${workdir}/gh-pages" commit -m "Update Helm chart index for ${VERSION}"
git -C "${workdir}/gh-pages" push origin HEAD:gh-pages
git -C gh-pages commit -m "Update Helm chart index for ${VERSION}"
git -C gh-pages push origin HEAD:gh-pages
fi
- name: Verify public Pages chart
env:
VERSION: ${{ steps.identity.outputs.version }}
run: |
set -euo pipefail
public_repo="https://rcourtman.github.io/Pulse"
public_ready=false
for attempt in $(seq 1 12); do
public_index="${workdir}/public-index.yaml"
if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
-o "$public_index" "${public_repo}/index.yaml" && \
grep -q "version: ${VERSION}" "$public_index"; then
for attempt in $(seq 1 30); do
public_index="$(mktemp)"
if curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors \
-o "${public_index}" "${public_repo}/index.yaml" && \
grep -q "version: ${VERSION}" "${public_index}"; then
helm repo remove pulse-public >/dev/null 2>&1 || true
helm repo add pulse-public "$public_repo" --force-update
helm repo add pulse-public "${public_repo}" --force-update
helm repo update pulse-public
if helm show chart pulse-public/pulse --version "$VERSION" >/dev/null; then
if helm show chart pulse-public/pulse --version "${VERSION}" >/dev/null; then
public_ready=true
rm -f "${public_index}"
break
fi
fi
echo "Public Helm repository has not exposed ${VERSION} yet (attempt ${attempt}/12)."
sleep 5
rm -f "${public_index}"
echo "Public Helm repository has not exposed ${VERSION} yet (${attempt}/30)."
sleep 2
done
if [ "$public_ready" != "true" ]; then
if [[ "${public_ready}" != "true" ]]; then
echo "::error::Public Helm repository did not expose chart ${VERSION}."
exit 1
fi
echo "[OK] Public Helm repository serves pulse ${VERSION}."
- name: Mark Helm chart release as pre-release (avoid latest override)
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
TAG="helm-chart-${{ steps.version.outputs.version }}"
gh release edit "$TAG" --prerelease --latest=false || echo "No helm chart release to edit for $TAG"

View file

@ -100,7 +100,7 @@ jobs:
echo "::error::${label} failed with conclusion=${conclusion}: ${url}"
return 1
fi
sleep 30
sleep 5
done
echo "::error::Timed out waiting for ${label} after ${timeout_seconds}s."

View file

@ -67,7 +67,8 @@ jobs:
run: |
set -euo pipefail
marker_url="https://github.com/${{ github.repository }}/releases/download/${TAG}/release-activation.json"
for attempt in $(seq 1 2100); do
max_attempts=10500
for attempt in $(seq 1 "${max_attempts}"); do
release_state="$(
gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \
--jq '[.id, .tag_name, .target_commitish, (.draft | tostring), (.published_at // "")] | @tsv' \
@ -104,7 +105,7 @@ jobs:
if [ "${original_status}" != "completed" ]; then
rm -f "${marker}"
echo "Original convergence owner ${activation_owner_run_id} is ${original_status:-unavailable}; successor adoption is not yet allowed."
sleep 10
sleep 2
continue
fi
echo "[OK] Adopting committed ${TAG} from completed convergence owner ${activation_owner_run_id}."
@ -117,8 +118,8 @@ jobs:
exit 0
fi
rm -f "${marker}"
echo "Activation marker for ${TAG} is not committed yet (${attempt}/2100)."
sleep 10
echo "Activation marker for ${TAG} is not committed yet (${attempt}/${max_attempts})."
sleep 2
done
echo "::error::Timed out waiting for the verified activation marker for ${TAG}."
exit 1
@ -286,6 +287,8 @@ jobs:
secrets: inherit
with:
chart_version: ${{ inputs.version }}
source_release_run_id: ${{ inputs.source_release_run_id }}
target_commitish: ${{ inputs.target_commitish }}
promote_private_pro_runtime:
name: Converge paid-runtime broker

View file

@ -565,6 +565,21 @@ upgrade, update, release, or artifact-selection behavior.
architecture-bound payload unit; cross-archive deduplication must allow
those three files to differ while continuing to reject drift in every
universal agent, installer, and script input.
The container payload must validate the three Windows no-extension aliases
against their exact `.exe` targets and then omit them, because the image
recreates those aliases deterministically and the immutable payload
manifest remains regular-file-only.
Helm Pages convergence must promote the immutable chart artifact produced
and qualified by the exact create-release run. It must bind that artifact
to the activated source run, tag, commit, and activation marker, and must
not repeat chart packaging or the pre-activation kind install/upgrade smoke.
Release-to-convergence and cross-repository child-run observation should
use short bounded polls so GitHub indexing cannot add tens of seconds after
a required exact run or activation marker has already completed.
Paid-runtime convergence must retain the download-page browser audit, live
private broker/image proof, and public-runtime mismatch proof, but may run
the independent browser and public-boundary checks as a sibling of the
lease-bound broker mutation so customer-path evidence is not serialized.
This credential-free lane may run against the isolated rootless
Docker daemon owned by the low-priority PVE build identity; it must not gain
host-Docker access, signing keys, registry login, or package-write authority.

View file

@ -204,6 +204,9 @@ func TestReleaseContainerContextTreatsServerSignaturesAsArchitectureBound(t *tes
"bin/pulse-agent-linux-amd64": "shared-agent",
"bin/pulse-agent-linux-amd64.sig": "shared-agent-minisign",
"bin/pulse-agent-linux-amd64.sshsig": "shared-agent-sshsig",
"bin/pulse-agent-windows-amd64.exe": "shared-windows-amd64-agent",
"bin/pulse-agent-windows-arm64.exe": "shared-windows-arm64-agent",
"bin/pulse-agent-windows-386.exe": "shared-windows-386-agent",
"scripts/install-container-agent.sh": "shared-container-installer",
"scripts/install-docker.sh": "shared-docker-installer",
"scripts/install.sh": "shared-agent-installer",
@ -226,6 +229,14 @@ func TestReleaseContainerContextTreatsServerSignaturesAsArchitectureBound(t *tes
t.Fatalf("write %s to %s archive: %v", name, arch, err)
}
}
for _, windowsArch := range []string{"amd64", "arm64", "386"} {
name := "bin/pulse-agent-windows-" + windowsArch
target := "pulse-agent-windows-" + windowsArch + ".exe"
header := &tar.Header{Name: name, Mode: 0o777, Typeflag: tar.TypeSymlink, Linkname: target}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatalf("write %s alias to %s archive: %v", name, arch, err)
}
}
if err := tarWriter.Close(); err != nil {
t.Fatalf("close %s tar stream: %v", arch, err)
}
@ -256,6 +267,12 @@ func TestReleaseContainerContextTreatsServerSignaturesAsArchitectureBound(t *tes
if _, err := os.Stat(filepath.Join(outputDir, "arm64", "scripts", "install.sh")); !os.IsNotExist(err) {
t.Fatalf("prepared context retained duplicate universal payload: %v", err)
}
for _, windowsArch := range []string{"amd64", "arm64", "386"} {
aliasPath := filepath.Join(outputDir, "amd64", "bin", "pulse-agent-windows-"+windowsArch)
if _, err := os.Lstat(aliasPath); !os.IsNotExist(err) {
t.Fatalf("prepared context retained recreated Windows alias %s: %v", aliasPath, err)
}
}
writeArchive("arm64", true)
cmd = exec.Command(repoFile("scripts", "prepare-release-container-context.sh"), releaseDir, version, outputDir)
@ -1479,27 +1496,31 @@ func TestDeploymentDefaultsPinVersionedImagesAndHelmDocsChecksum(t *testing.T) {
required := []string{
`workflow_call:`,
`chart_version:`,
`Require activated GitHub release`,
`gh release view "${RELEASE_TAG}" --json isDraft,publishedAt,tagName`,
`Helm Pages refuses to advertise inactive release ${RELEASE_TAG}.`,
`HELM_DOCS_VERSION="1.14.2"`,
`HELM_DOCS_ARCHIVE="helm-docs_${HELM_DOCS_VERSION}_Linux_x86_64.tar.gz"`,
`HELM_DOCS_SHA256="a8cf72ada34fad93285ba2a452b38bdc5bd52cc9a571236244ec31022928d6cc"`,
`sha256sum --check --`,
`name: Ensure chart release and pages index`,
`gh release create "${CHART_RELEASE}" "${CHART_PATH}"`,
`source_release_run_id:`,
`target_commitish:`,
`Require activated GitHub release and source run`,
`release-activation.json`,
`.github/workflows/create-release.yml`,
`gh run download "${SOURCE_RELEASE_RUN_ID}"`,
`--name "pulse-chart-${VERSION}"`,
`qualified chart metadata does not match the activated release`,
`name: Publish chart release and merge Pages index`,
`gh release create "${chart_release}" "${chart_path}"`,
`helm repo index "${index_work}"`,
`git -C "${workdir}/gh-pages" push origin HEAD:gh-pages`,
`git -C gh-pages push origin HEAD:gh-pages`,
`grep -q "version: ${VERSION}"`,
`helm show chart pulse-public/pulse --version "$VERSION"`,
`helm show chart pulse-public/pulse --version "${VERSION}"`,
}
for _, needle := range required {
if !strings.Contains(helmPages, needle) {
t.Fatalf("helm-pages.yml missing checksum-verified helm-docs install step: %s", needle)
t.Fatalf("helm-pages.yml missing immutable chart promotion step: %s", needle)
}
}
for _, forbidden := range []string{
"workflow_run:",
`Smoke test with kind`,
`Install helm-docs`,
`helm package deploy/helm/pulse`,
`git checkout -B "$REQUIRED_BRANCH"`,
`git push origin HEAD:"$REQUIRED_BRANCH"`,
} {

View file

@ -85,6 +85,20 @@ for arch in amd64 arm64; do
fi
validate_archive_entries "${archive}"
tar --no-same-owner --no-same-permissions -xzf "${archive}" -C "${output_dir}/${arch}"
for windows_arch in amd64 arm64 386; do
alias_path="${output_dir}/${arch}/bin/pulse-agent-windows-${windows_arch}"
expected_target="pulse-agent-windows-${windows_arch}.exe"
if [[ ! -L "${alias_path}" ]] || \
[[ "$(readlink "${alias_path}")" != "${expected_target}" ]] || \
[[ ! -f "${output_dir}/${arch}/bin/${expected_target}" ]]; then
echo "Error: ${archive} has an invalid Windows agent alias ${alias_path}." >&2
exit 1
fi
# runtime_prebuilt recreates these aliases after copying the immutable
# .exe payloads. Keep the manifest-covered context symlink-free rather
# than weakening its canonical-file rule.
rm -f "${alias_path}"
done
for required in \
bin/pulse \
bin/pulse.sig \

View file

@ -458,21 +458,16 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertLess(marker_upload, committed)
self.assertLess(committed, readback)
for workflow_path, refusal in (
(
".github/workflows/helm-pages.yml",
"Helm Pages refuses to advertise inactive release",
),
(
".github/workflows/promote-floating-tags.yml",
"Floating-tag promotion refuses inactive release",
),
):
with self.subTest(workflow_path=workflow_path):
guarded_workflow = read(workflow_path)
self.assertIn("Require activated GitHub release", guarded_workflow)
self.assertIn("--json isDraft,publishedAt,tagName", guarded_workflow)
self.assertIn(refusal, guarded_workflow)
helm_pages = read(".github/workflows/helm-pages.yml")
self.assertIn("Require activated GitHub release", helm_pages)
self.assertIn("release-activation.json", helm_pages)
self.assertIn("source_release_run_id", helm_pages)
self.assertIn("Helm Pages refuses to advertise inactive", helm_pages)
floating_tags = read(".github/workflows/promote-floating-tags.yml")
self.assertIn("Require activated GitHub release", floating_tags)
self.assertIn("--json isDraft,publishedAt,tagName", floating_tags)
self.assertIn("Floating-tag promotion refuses inactive release", floating_tags)
def test_each_post_commit_surface_failure_is_retriable_convergence_debt(self) -> None:
release_workflow = read(".github/workflows/create-release.yml")
@ -692,7 +687,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
)
await_commit = workflow_job_block(convergence, "await_activation_commit")
self.assertIn("timeout-minutes: 360", await_commit)
self.assertIn("seq 1 2100", await_commit)
self.assertIn('max_attempts=10500', await_commit)
self.assertIn('sleep 2', await_commit)
self.assertIn("source_release_run_id", await_commit)
self.assertIn("EXPECTED_CONVERGENCE_RUN_ID: ${{ github.run_id }}", await_commit)
self.assertIn('original_status}" != "completed"', await_commit)
@ -1785,7 +1781,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertNotIn("go build", preview_deploy)
self.assertNotIn("scp ", preview_deploy)
self.assertIn("validate_artifact_release_line.py", helm)
self.assertIn("validate_artifact_release_line.py", helm_pages)
self.assertIn(".github/workflows/create-release.yml", helm_pages)
self.assertIn("release-activation.json", helm_pages)
self.assertIn('gh run download "${SOURCE_RELEASE_RUN_ID}"', helm_pages)
self.assertIn("release_branch_for_version", artifact_validator)
self.assertIn("matching prerelease tag", artifact_validator)
self.assertIn("previous stable tag", artifact_validator)
@ -1797,39 +1795,35 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn('--set image.pullPolicy=Never', qualifier_workflow)
self.assertNotIn("needs.docker_build.result", release_workflow)
self.assertNotIn("needs.helm_smoke.result", release_workflow)
self.assertIn('--github-output "$GITHUB_OUTPUT"', helm_pages)
self.assertIn('qualified chart metadata does not match the activated release', helm_pages)
self.assertIn("workflow_call:", helm_pages)
self.assertNotIn("workflow_run:", helm_pages)
self.assertIn('git checkout --detach "refs/tags/${RELEASE_TAG}"', helm_pages)
self.assertIn('gh run download "${SOURCE_RELEASE_RUN_ID}"', helm_pages)
self.assertIn('--name "pulse-chart-${VERSION}"', helm_pages)
self.assertIn("release-activation.json", helm_pages)
self.assertIn(".github/workflows/create-release.yml", helm_pages)
self.assertNotIn('git pull --rebase origin "$REQUIRED_BRANCH"', helm_pages)
self.assertNotIn('git push origin HEAD:"$REQUIRED_BRANCH"', helm_pages)
self.assertIn('HELM_DOCS_VERSION="1.14.2"', helm_pages)
self.assertIn('HELM_DOCS_ARCHIVE="helm-docs_${HELM_DOCS_VERSION}_Linux_x86_64.tar.gz"', helm_pages)
self.assertIn(
'HELM_DOCS_SHA256="a8cf72ada34fad93285ba2a452b38bdc5bd52cc9a571236244ec31022928d6cc"',
helm_pages,
)
self.assertIn('printf \'%s %s\\n\' "$HELM_DOCS_SHA256" "$HELM_DOCS_ARCHIVE" | sha256sum --check --', helm_pages)
self.assertNotIn("HELM_DOCS_VERSION", helm_pages)
self.assertNotIn("helm package deploy/helm/pulse", helm_pages)
self.assertNotIn("git pull --rebase origin main", helm_pages)
self.assertNotIn("git push origin main", helm_pages)
self.assertNotIn("kind load docker-image", helm_pages)
self.assertIn("Ensure chart release and pages index", helm_pages)
self.assertIn('gh release create "${CHART_RELEASE}" "${CHART_PATH}"', helm_pages)
self.assertIn("Publish chart release and merge Pages index", helm_pages)
self.assertIn('gh release create "${chart_release}" "${chart_path}"', helm_pages)
self.assertIn('helm repo index "${index_work}"', helm_pages)
self.assertIn('git -C "${workdir}/gh-pages" push origin HEAD:gh-pages', helm_pages)
self.assertIn('git -C gh-pages push origin HEAD:gh-pages', helm_pages)
self.assertIn('grep -q "version: ${VERSION}"', helm_pages)
self.assertIn('helm show chart pulse-public/pulse --version "$VERSION"', helm_pages)
self.assertIn("helm status pulse || true", helm_pages)
self.assertIn("kubectl describe pods -A || true", helm_pages)
self.assertIn("kubectl get events -A --sort-by=.lastTimestamp || kubectl get events -A || true", helm_pages)
self.assertIn('helm show chart pulse-public/pulse --version "${VERSION}"', helm_pages)
self.assertNotIn("helm status pulse || true", helm_pages)
self.assertNotIn("kubectl describe pods", helm_pages)
self.assertIn("release-convergence.yml/dispatches", release_workflow)
self.assertIn("Release Activation Commit Verdict", release_workflow)
self.assertNotIn('gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}"', release_workflow)
self.assertNotIn('TARGET="preview-v6"', release_workflow)
self.assertIn("sync_chart_release_metadata.py", helm)
self.assertIn("sync_chart_release_metadata.py", helm_pages)
self.assertNotIn("sync_chart_release_metadata.py", helm_pages)
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm)
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm_pages)
self.assertIn('git checkout --detach "refs/tags/${RELEASE_TAG}"', helm)
self.assertIn("Verify public GHCR chart read", helm)
self.assertIn("helm registry logout ghcr.io || true", helm)