mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Promote qualified release payloads directly
This commit is contained in:
parent
962df694e9
commit
74bd06a953
8 changed files with 261 additions and 286 deletions
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -1271,7 +1271,7 @@ jobs:
|
|||
return 1
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "::error::Timed out waiting for ${label} after ${timeout_seconds}s."
|
||||
|
|
|
|||
394
.github/workflows/helm-pages.yml
vendored
394
.github/workflows/helm-pages.yml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
11
.github/workflows/release-convergence.yml
vendored
11
.github/workflows/release-convergence.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"`,
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue