Pin deployment defaults and verify Helm docs downloads

This commit is contained in:
rcourtman 2026-04-22 06:05:06 +01:00
parent 9c4bb4a90e
commit 1841c032f6
8 changed files with 146 additions and 11 deletions

View file

@ -107,8 +107,12 @@ jobs:
- name: Install helm-docs
run: |
cd /tmp
wget https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_x86_64.tar.gz
tar -xzf helm-docs_1.14.2_Linux_x86_64.tar.gz
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

View file

@ -2,7 +2,7 @@ version: '3.8'
services:
pulse:
image: ${PULSE_IMAGE:-rcourtman/pulse:latest}
image: ${PULSE_IMAGE:-rcourtman/pulse:6.0.0}
container_name: pulse
restart: unless-stopped
logging:

View file

@ -84,6 +84,8 @@ server-side update execution surfaces.
62. `tests/integration/scripts/relay-mobile-token-helper.go`
63. `tests/integration/tests/helpers.ts`
64. `tests/integration/tests/runtime-defaults.ts`
65. `docker-compose.yml`
66. `scripts/install-docker.sh`
## Shared Boundaries
@ -96,7 +98,7 @@ server-side update execution surfaces.
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, and `.github/workflows/update-demo-server.yml`
3. Add or change shell installer, Windows installer, container-agent installer, or auto-update script behavior through `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh`
3. Add or change shell installer, Docker bootstrap installer, Windows installer, container-agent installer, repo-root compose defaults, or auto-update script behavior through `scripts/install.sh`, `scripts/install-docker.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, `docker-compose.yml`, and `scripts/pulse-auto-update.sh`
4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
@ -149,6 +151,9 @@ server-side update execution surfaces.
in `deploy/helm/pulse/README.md` output must be regenerated from the same
chart metadata and release line so published Helm docs, chart version
badges, and packaged archive metadata all describe the identical cut.
External helper binaries fetched by governed release workflows are part of
the same supply-chain boundary and must be checksum-verified before they are
executed.
8. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go`
9. Add or change the canonical hosted staging smoke operator path through `scripts/run_hosted_staging_smoke.sh`, `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs`, `tests/integration/scripts/hosted-mobile-token-runtime.mjs`, `tests/integration/scripts/hosted-tenant-runtime.mjs`, and `tests/integration/scripts/relay-mobile-token-helper.go`
@ -209,6 +214,12 @@ so release automation, packaged chart metadata, and chart-runtime smoke no
longer depend on unowned `deploy/helm/pulse/` files while the governed
release workflows package and publish those artifacts.
That same lane also owns version-pinned Docker bootstrap defaults. The repo
root `docker-compose.yml` sample and `scripts/install-docker.sh` must default
to the governed `VERSION` cut instead of floating `:latest`, so self-hosted
operators only move to a newer image when they choose a newer explicit tag or
override `PULSE_IMAGE`.
`internal/updates/` is the live deployment and upgrade planner. It owns
deployment-type detection, update-plan generation, adapter selection, server
update sequencing, and rollback-aware update state for supported Pulse

View file

@ -2389,6 +2389,7 @@
".github/workflows/release-dry-run.yml",
".github/workflows/update-demo-server.yml",
"cmd/pulse-control-plane/main.go",
"docker-compose.yml",
"Dockerfile",
"docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md",
"docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md",
@ -2419,6 +2420,7 @@
"scripts/hot-dev-bg.sh",
"scripts/hot-dev.sh",
"scripts/install-container-agent.sh",
"scripts/install-docker.sh",
"scripts/install.ps1",
"scripts/install.sh",
"scripts/pulse-auto-update.sh",
@ -2448,7 +2450,10 @@
"verification": {
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [],
"exact_files": [
"scripts/installtests/build_release_assets_test.go",
"scripts/installtests/install_docker_sh_test.go"
],
"require_explicit_path_policy_coverage": true,
"path_policies": [
{
@ -2589,7 +2594,9 @@
"label": "deployment script runtime proof",
"match_prefixes": [],
"match_files": [
"docker-compose.yml",
"scripts/install-container-agent.sh",
"scripts/install-docker.sh",
"scripts/install.ps1",
"scripts/pulse-auto-update.sh"
],

View file

@ -4,8 +4,31 @@
set -euo pipefail
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
DOCKER_IMAGE_REPO="${DOCKER_IMAGE_REPO:-rcourtman/pulse}"
DEFAULT_PULSE_IMAGE="${DOCKER_IMAGE_REPO}:latest"
CANONICAL_DEFAULT_PULSE_VERSION="6.0.0"
resolve_default_pulse_version() {
if [ -n "${PULSE_IMAGE_VERSION:-}" ]; then
printf '%s' "${PULSE_IMAGE_VERSION}"
return
fi
for candidate in "${PULSE_VERSION_FILE:-}" "${SCRIPT_DIR}/../VERSION" "${PWD}/VERSION"; do
if [ -n "${candidate}" ] && [ -f "${candidate}" ]; then
local version
version=$(tr -d '\r\n' < "${candidate}" 2>/dev/null || true)
if [ -n "${version}" ]; then
printf '%s' "${version}"
return
fi
fi
done
printf '%s' "${CANONICAL_DEFAULT_PULSE_VERSION}"
}
DEFAULT_PULSE_IMAGE="${DOCKER_IMAGE_REPO}:$(resolve_default_pulse_version)"
PULSE_IMAGE="${PULSE_IMAGE:-${DEFAULT_PULSE_IMAGE}}"
PULSE_PORT="${PULSE_PORT:-7655}"

View file

@ -199,6 +199,58 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
}
}
func TestDeploymentDefaultsPinVersionedImagesAndHelmDocsChecksum(t *testing.T) {
versionBytes, err := os.ReadFile(repoFile("VERSION"))
if err != nil {
t.Fatalf("read VERSION: %v", err)
}
version := strings.TrimSpace(string(versionBytes))
if version == "" {
t.Fatal("VERSION is empty")
}
composeBytes, err := os.ReadFile(repoFile("docker-compose.yml"))
if err != nil {
t.Fatalf("read docker-compose.yml: %v", err)
}
compose := string(composeBytes)
if !strings.Contains(compose, "image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}") {
t.Fatalf("docker-compose.yml must pin the governed release version:\n%s", compose)
}
if strings.Contains(compose, ":latest") {
t.Fatalf("docker-compose.yml must not default to a floating latest tag:\n%s", compose)
}
installDockerBytes, err := os.ReadFile(repoFile("scripts", "install-docker.sh"))
if err != nil {
t.Fatalf("read install-docker.sh: %v", err)
}
installDocker := string(installDockerBytes)
if !strings.Contains(installDocker, `CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`) {
t.Fatalf("install-docker.sh must pin the governed release version:\n%s", installDocker)
}
if strings.Contains(installDocker, ":latest") {
t.Fatalf("install-docker.sh must not default to a floating latest tag:\n%s", installDocker)
}
helmPagesBytes, err := os.ReadFile(repoFile(".github", "workflows", "helm-pages.yml"))
if err != nil {
t.Fatalf("read helm-pages.yml: %v", err)
}
helmPages := string(helmPagesBytes)
required := []string{
`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 --`,
}
for _, needle := range required {
if !strings.Contains(helmPages, needle) {
t.Fatalf("helm-pages.yml missing checksum-verified helm-docs install step: %s", needle)
}
}
}
func TestDeployDemoWorkflowFailsClosedForPreviewAndVerifiesFrontendParity(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "deploy-demo-server.yml"))
if err != nil {

View file

@ -8,8 +8,22 @@ import (
"testing"
)
func currentReleaseVersion(t *testing.T) string {
t.Helper()
content, err := os.ReadFile(repoFile("VERSION"))
if err != nil {
t.Fatalf("read VERSION: %v", err)
}
version := strings.TrimSpace(string(content))
if version == "" {
t.Fatal("VERSION is empty")
}
return version
}
func TestInstallDockerScriptUsesConfiguredImageRepoDefault(t *testing.T) {
workDir := t.TempDir()
version := currentReleaseVersion(t)
runInstallDockerScript(t, workDir, "DOCKER_IMAGE_REPO=example/pulse-enterprise")
composePath := filepath.Join(workDir, "docker-compose.yml")
@ -18,11 +32,11 @@ func TestInstallDockerScriptUsesConfiguredImageRepoDefault(t *testing.T) {
t.Fatalf("read docker-compose.yml: %v", err)
}
composeText := string(composeContent)
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:latest}") {
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:"+version+"}") {
t.Fatalf("docker-compose.yml missing configured image default:\n%s", composeText)
}
if strings.Contains(composeText, "rcourtman/pulse:latest") {
t.Fatalf("docker-compose.yml still hardcodes upstream image:\n%s", composeText)
if strings.Contains(composeText, ":latest") {
t.Fatalf("docker-compose.yml must not default to a floating latest tag:\n%s", composeText)
}
envPath := filepath.Join(workDir, ".env")
@ -31,13 +45,14 @@ func TestInstallDockerScriptUsesConfiguredImageRepoDefault(t *testing.T) {
t.Fatalf("read .env: %v", err)
}
envText := string(envContent)
if !strings.Contains(envText, "PULSE_IMAGE=example/pulse-enterprise:latest") {
if !strings.Contains(envText, "PULSE_IMAGE=example/pulse-enterprise:"+version) {
t.Fatalf(".env missing configured image default:\n%s", envText)
}
}
func TestInstallDockerScriptPrefersExplicitPulseImage(t *testing.T) {
workDir := t.TempDir()
version := currentReleaseVersion(t)
runInstallDockerScript(
t,
workDir,
@ -51,7 +66,7 @@ func TestInstallDockerScriptPrefersExplicitPulseImage(t *testing.T) {
t.Fatalf("read docker-compose.yml: %v", err)
}
composeText := string(composeContent)
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:latest}") {
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:"+version+"}") {
t.Fatalf("docker-compose.yml lost configured default image:\n%s", composeText)
}
@ -66,6 +81,22 @@ func TestInstallDockerScriptPrefersExplicitPulseImage(t *testing.T) {
}
}
func TestRepoDockerComposeDefaultPinsCurrentVersion(t *testing.T) {
version := currentReleaseVersion(t)
content, err := os.ReadFile(repoFile("docker-compose.yml"))
if err != nil {
t.Fatalf("read docker-compose.yml: %v", err)
}
text := string(content)
if !strings.Contains(text, "image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}") {
t.Fatalf("repo docker-compose.yml must pin the current release version:\n%s", text)
}
if strings.Contains(text, ":latest") {
t.Fatalf("repo docker-compose.yml must not default to a floating latest tag:\n%s", text)
}
}
func runInstallDockerScript(t *testing.T, workDir string, envVars ...string) {
t.Helper()

View file

@ -402,6 +402,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn('git checkout -B "$REQUIRED_BRANCH" "origin/$REQUIRED_BRANCH"', helm_pages)
self.assertIn('git pull --rebase origin "$REQUIRED_BRANCH"', helm_pages)
self.assertIn('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("git pull --rebase origin main", helm_pages)
self.assertNotIn("git push origin main", helm_pages)
self.assertNotIn("kind load docker-image", helm_pages)