Files
pulse/scripts/installtests/build_release_assets_test.go
T
2026-08-07 12:27:36 +01:00

2241 lines
95 KiB
Go

package installtests
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
func TestBuildReleaseUsesV6InstallScripts(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "build-release.sh"))
if err != nil {
t.Fatalf("read build-release.sh: %v", err)
}
script := string(content)
required := []string{
`SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`,
`PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"`,
`cd "${PULSE_REPO_ROOT}"`,
`source "${SCRIPT_DIR}/release_asset_common.sh"`,
`RENDERED_INSTALLERS_DIR="${BUILD_DIR}/rendered-installers"`,
`go run ./scripts/render_installers.go \`,
// The published install.sh asset is the server installer (root install.sh).
// The rendered AGENT installer is shipped inside tarballs and Docker images
// at ./scripts/install.sh and served at the running server's /install.sh
// endpoint, but is intentionally not a top-level GitHub Releases asset:
// pulse-auto-update.sh, the root install.sh's own --rc/--version flows, and
// the README quickstart all expect releases/<tag>/install.sh
// to be the server installer that accepts --version vX.Y.Z.
`cp install.sh "$RELEASE_DIR/install.sh"`,
`[ -f "${RENDERED_INSTALLERS_DIR}/install.ps1" ] && cp "${RENDERED_INSTALLERS_DIR}/install.ps1" "$RELEASE_DIR/install.ps1"`,
`cp "$BUILD_DIR/pulse-agent-linux-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-arm64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-armv7" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-armv6" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-agent-linux-386" "$RELEASE_DIR/"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing required release asset copy: %s", needle)
}
}
// Sanity-check the opposite drift: the rendered AGENT installer must NOT be
// the published install.sh asset. Publishing it there shipped a broken LXC
// install + auto-update path across every v6 RC (rc.1 → rc.5).
if strings.Contains(script, `cp "${RENDERED_INSTALLERS_DIR}/install.sh" "$RELEASE_DIR/install.sh"`) {
t.Fatal("build-release.sh must not publish the rendered agent install.sh as the top-level release asset")
}
requiredScriptWiring := []string{
`agent_ldflags="$(./scripts/release_ldflags.sh agent --version "v${VERSION}" "${update_ldflags_args[@]}")"`,
`server_ldflags="$(./scripts/release_ldflags.sh server --version "v${VERSION}" --build-time "${build_time}" --git-commit "${git_commit}" "${license_ldflags_args[@]}" "${update_ldflags_args[@]}")"`,
`release_go_build_args=(-buildvcs=false -trimpath)`,
`"${release_go_build_args[@]}"`,
`RELEASE_PACKET_SBOM="pulse-v${VERSION}-release.sbom.spdx.json"`,
`pulse_release_prepare_signing_state "pulse-installer" "pulse-install"`,
`trap 'pulse_release_cleanup_signing_state' EXIT`,
`--installer-ssh-public-key "${PULSE_RELEASE_UPDATE_SSH_PUBLIC_KEY}"`,
`pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}"`,
`mapfile -t checksum_files < <(pulse_release_collect_checksum_files "${RELEASE_DIR}")`,
`pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"`,
}
for _, needle := range requiredScriptWiring {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing canonical ldflags wiring: %s", needle)
}
}
if builds, cleanBuilds := strings.Count(script, `env $build_env go build \`), strings.Count(script, `"${release_go_build_args[@]}"`); builds != cleanBuilds {
t.Fatalf("build-release.sh must disable automatic VCS stamping on every release go build: builds=%d clean_builds=%d", builds, cleanBuilds)
}
helperBytes, err := os.ReadFile(repoFile("scripts", "release_asset_common.sh"))
if err != nil {
t.Fatalf("read release_asset_common.sh: %v", err)
}
helper := string(helperBytes)
helperRequired := []string{
`: "${PULSE_SCRIPTS_DIR:=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"`,
`: "${PULSE_REPO_ROOT:=$(cd "${PULSE_SCRIPTS_DIR}/.." && pwd)}"`,
`go -C "${PULSE_REPO_ROOT}" run ./scripts/release_update_key.go "$@"`,
`pulse_release_go_run_update_key public-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}"`,
`pulse_release_go_run_update_key public-key-ssh --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key openssh-private-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
`pulse_release_go_run_update_key sign --private-key "${PULSE_UPDATE_SIGNING_KEY}" --file "${absolute_file}"`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT`,
`Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}`,
`ssh-keygen -q -Y sign`,
`"${resolved_tool}" "dir:${release_dir}" -o "spdx-json=${tmp_sbom}"`,
`if compgen -G "pulse-*.sbom.spdx.json" > /dev/null; then`,
`find . -maxdepth 1 -type f \( -name '*.sig' -o -name '*.sshsig' \) -delete`,
}
for _, needle := range helperRequired {
if !strings.Contains(helper, needle) {
t.Fatalf("release_asset_common.sh missing canonical release asset wiring: %s", needle)
}
}
}
func TestHelmChartShipsOpenShiftProfile(t *testing.T) {
read := func(parts ...string) string {
t.Helper()
content, err := os.ReadFile(repoFile(parts...))
if err != nil {
t.Fatalf("read %s: %v", filepath.Join(parts...), err)
}
return string(content)
}
values := read("deploy", "helm", "pulse", "values.yaml")
agent := read("deploy", "helm", "pulse", "templates", "agent.yaml")
deployment := read("deploy", "helm", "pulse", "templates", "deployment.yaml")
rbac := read("deploy", "helm", "pulse", "templates", "agent-rbac.yaml")
helmCI := read(".github", "workflows", "helm-ci.yml")
docs := read("docs", "KUBERNETES.md")
for _, required := range []string{
"openShift:",
"kubernetesAgent:",
"clusterID:",
"rbac:",
} {
if !strings.Contains(values, required) {
t.Fatalf("values.yaml missing OpenShift chart value %q", required)
}
}
for _, required := range []string{
`$openShiftAgent := and .Values.openShift.enabled .Values.openShift.kubernetesAgent.enabled`,
`- --enable-kubernetes`,
`- --enable-host=false`,
`"name" "PULSE_AGENT_ID"`,
`$dockerSocketEnabled := and .Values.agent.dockerSocket.enabled (not $openShiftAgent)`,
`"runAsNonRoot" true`,
`omit $openShiftSecurityContext "runAsUser" "runAsGroup"`,
} {
if !strings.Contains(agent, required) {
t.Fatalf("agent template missing OpenShift contract %q", required)
}
}
for _, required := range []string{
`.Values.openShift.enabled`,
`omit $openShiftContainerSecurityContext "runAsUser" "runAsGroup"`,
`"allowPrivilegeEscalation" false`,
} {
if !strings.Contains(deployment, required) {
t.Fatalf("server deployment missing OpenShift SCC contract %q", required)
}
}
for _, required := range []string{
"kind: ClusterRole",
"kind: ClusterRoleBinding",
`apiGroups: ["metrics.k8s.io"]`,
`resources: ["nodes", "pods"]`,
`apiGroups: ["discovery.k8s.io"]`,
`resources: ["endpointslices"]`,
`apiGroups: ["rbac.authorization.k8s.io"]`,
} {
if !strings.Contains(rbac, required) {
t.Fatalf("agent RBAC template missing read-only collector rule %q", required)
}
}
if strings.Contains(rbac, "- secrets") || strings.Contains(rbac, "- nodes/proxy") {
t.Fatal("OpenShift default role must not grant Secrets or direct kubelet proxy access")
}
for _, required := range []string{
"Render and verify the OpenShift profile",
"--show-only templates/agent-rbac.yaml",
`grep -Eq "runAs(User|Group):|fsGroup:"`,
`grep -q "/var/run/docker.sock"`,
} {
if !strings.Contains(helmCI, required) {
t.Fatalf("Helm CI missing OpenShift render assertion %q", required)
}
}
if !strings.Contains(docs, "--set openShift.enabled=true") ||
!strings.Contains(docs, "--set openShift.kubernetesAgent.enabled=true") ||
!strings.Contains(docs, "create secret generic pulse-server-env") ||
!strings.Contains(docs, "create secret generic pulse-agent-env") {
t.Fatal("Kubernetes guide must document the shipped OpenShift profile")
}
if strings.Contains(docs, "--set-string agent.secretEnv.data.PULSE_TOKEN") {
t.Fatal("OpenShift guide must not persist the agent token in Helm release values")
}
}
func shieldsBadgeMessage(value string) string {
return strings.ReplaceAll(value, "-", "--")
}
// TestAgentBuildCacheDoesNotResurrectPulseAgentPackage guards the repository's
// GHCR package list, which is a user-facing surface. A registry cache ref
// creates the package it points at, so pointing the agent_runtime build cache
// at ghcr.io/<owner>/pulse-agent recreated an empty package on every release.
// It then sat in the repo's Packages sidebar beside pulse, pulse-control-plane
// and pulse-chart/pulse, reading like a pullable agent image even though no
// workflow publishes it. Only images a release workflow actually pushes may
// own a package; the unified agent ships inside the main pulse image.
func TestAgentBuildCacheDoesNotResurrectPulseAgentPackage(t *testing.T) {
workflowDir := repoFile(".github", "workflows")
entries, err := os.ReadDir(workflowDir)
if err != nil {
t.Fatalf("read workflow dir: %v", err)
}
// Registry-qualified refs only: the release binaries are legitimately named
// pulse-agent-<os>-<arch> and must keep matching nothing here. Workflow
// expressions are collapsed first so an owner interpolated as
// ${{ github.repository_owner }} cannot hide the ref behind its spaces.
workflowExpr := regexp.MustCompile(`\$\{\{[^}]*\}\}`)
packageRef := regexp.MustCompile(`(?:ghcr\.io|docker\.io)/[^\s"']*/pulse-agent\b|(?:^|\s)rcourtman/pulse-agent\b`)
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || (!strings.HasSuffix(name, ".yml") && !strings.HasSuffix(name, ".yaml")) {
continue
}
content, err := os.ReadFile(filepath.Join(workflowDir, name))
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
for i, line := range strings.Split(string(content), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
if match := packageRef.FindString(workflowExpr.ReplaceAllString(line, "EXPR")); match != "" {
t.Fatalf("%s:%d targets the unpublished pulse-agent package (%q); no workflow may reference it, buildcache refs included", name, i+1, strings.TrimSpace(match))
}
}
}
release, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
for _, required := range []string{
`cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:agent-buildcache`,
`cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:agent-buildcache,mode=max`,
} {
if !strings.Contains(string(release), required) {
t.Fatalf("create-release.yml must cache the agent_runtime build under the published pulse package; missing %q", required)
}
}
values, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "values.yaml"))
if err != nil {
t.Fatalf("read values.yaml: %v", err)
}
agentBlock := topLevelYAMLBlock(t, string(values), "agent")
if !strings.Contains(agentBlock, "repository: rcourtman/pulse\n") {
t.Fatal("chart agent.image.repository must default to the published rcourtman/pulse image")
}
}
// topLevelYAMLBlock returns the lines of a top-level mapping key, from the key
// itself up to the next unindented key.
func topLevelYAMLBlock(t *testing.T, doc string, key string) string {
t.Helper()
lines := strings.Split(doc, "\n")
start := -1
for i, line := range lines {
if line == key+":" {
start = i
break
}
}
if start < 0 {
t.Fatalf("values.yaml missing top-level %q key", key)
}
for i := start + 1; i < len(lines); i++ {
line := lines[i]
if line == "" || strings.HasPrefix(line, " ") || strings.HasPrefix(line, "#") {
continue
}
return strings.Join(lines[start:i], "\n")
}
return strings.Join(lines[start:], "\n")
}
func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
validationContent, err := os.ReadFile(repoFile(".github", "workflows", "validate-release-assets.yml"))
if err != nil {
t.Fatalf("read validate-release-assets.yml: %v", err)
}
workflow := string(content)
validationWorkflow := string(validationContent)
required := []string{
`historical_asset_backfill_only:`,
`description: 'Repair an already-published release packet in place without rebuilding binaries'`,
`SYFT_VERSION="1.42.4"`,
`SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`,
`SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`,
`install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft`,
`release_upload_with_retry "${TAG}" release/*.sbom.spdx.json --clobber`,
`release/pulse-agent-linux-amd64`,
`release/pulse-agent-linux-arm64`,
`release/pulse-agent-linux-armv7`,
`release/pulse-agent-linux-armv6`,
`release/pulse-agent-linux-386`,
`release/pulse-agent-freebsd-amd64`,
`release/pulse-agent-freebsd-arm64`,
`release/pulse-agent-windows-amd64.exe`,
`release/pulse-agent-windows-arm64.exe`,
`release/pulse-agent-windows-386.exe`,
`release_upload_with_retry "${TAG}" release/install.sh --clobber`,
`if [ -f release/install.ps1 ]; then`,
`release_upload_with_retry "${TAG}" release/install.ps1 --clobber`,
`release_upload_with_retry "${TAG}" release/*.sig --clobber`,
`release_upload_with_retry "${TAG}" release/*.sshsig --clobber`,
`gh release upload "$@"`,
`gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s`,
`gh release upload failed after ${max_attempts} attempts`,
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
`subject-path: release/*`,
`gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate`,
`git push origin "refs/tags/${TAG}" --force`,
`--rawfile body "$NOTES_FILE"`,
`--input "$RELEASE_PAYLOAD"`,
`--expected-body-file "$NOTES_FILE"`,
`historical_asset_backfill_only=${HISTORICAL_ASSET_BACKFILL_ONLY}`,
`if: ${{ always() && needs.prepare.result == 'success' && needs.build_release_candidate.result == 'success' && needs.create_release.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' }}`,
`candidate_manifest_artifact: ${{ needs.build_release_candidate.outputs.manifest_artifact_name }}`,
`if: ${{ needs.prepare.outputs.historical_asset_backfill_only == 'true' }}`,
`permissions:`,
`issues: write`,
`statuses: write`,
`ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$RELEASE_JSON_FILE")`,
`ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$RELEASE_JSON_FILE")`,
`Draft release ${RELEASE_ID} is bound to tag ${ACTUAL_RELEASE_TAG}, expected ${TAG}.`,
`Draft release ${RELEASE_ID} target_commitish is ${ACTUAL_TARGET_COMMITISH}, expected ${HEAD_SHA}.`,
`./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"`,
`./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"`,
// End-to-end install.sh smoke must run downstream of
// validate_release_assets on every release that is not a
// historical asset backfill. Without this wiring the smoke
// workflow exists but never actually protects a release —
// exactly the regression class that let rc.1 → rc.5 ship with
// broken install.sh.
`uses: ./.github/workflows/install-sh-smoke.yml`,
`install_sh_smoke:`,
`needs.validate_release_assets.result == 'success'`,
`needs.prepare.outputs.historical_asset_backfill_only != 'true'`,
`repository: ${{ github.repository }}`,
`asset_source: staged`,
`release_id: ${{ needs.create_release.outputs.release_id }}`,
// Helm chart publish must be called explicitly from create-release
// because the draft→PATCH(draft=false) publish path does NOT fire
// the `release: published` webhook (GitHub-documented quirk). v6
// rc.1 → rc.5 published successfully but never produced a Helm
// chart on the GitHub Pages index, breaking
// `helm install pulse pulse/pulse --version 6.0.0-rc.X`.
`uses: ./.github/workflows/publish-helm-chart.yml`,
`publish_helm_chart:`,
`chart_version: ${{ needs.prepare.outputs.version }}`,
`app_version: ${{ needs.prepare.outputs.version }}`,
`uses: ./.github/workflows/helm-pages.yml`,
`publish_helm_pages:`,
// Mutable image aliases have one explicit owner at the activation
// barrier; no implicit workflow_run may race that call.
`uses: ./.github/workflows/promote-floating-tags.yml`,
`promote_floating_tags:`,
`tag: ${{ needs.prepare.outputs.tag }}`,
`prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}`,
// Draft-only mode stops after staged validation and skips the
// customer activation sequence.
`needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true'`,
`activate_release:`,
`needs.promote_private_pro_runtime.result == 'success'`,
`Publish the fully staged release`,
`'{draft: false, make_latest: $make_latest}'`,
`returning ${TAG} to draft quarantine`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("create-release.yml missing required installer upload step: %s", needle)
}
}
publishedReleaseGuard := `needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true'`
for _, job := range []string{"install_sh_smoke", "publish_helm_chart", "publish_helm_pages"} {
block := workflowJobBlock(t, workflow, job)
if !strings.Contains(block, publishedReleaseGuard) {
t.Fatalf("create-release.yml job %s must skip historical backfill and draft-only runs before invoking downstream workflow_call", job)
}
}
readinessJob := workflowJobBlock(t, workflow, "release_readiness")
if !strings.Contains(readinessJob, publishedReleaseGuard) {
t.Fatal("release_readiness must skip historical backfill and draft-only runs")
}
floatingJob := workflowJobBlock(t, workflow, "promote_floating_tags")
if !strings.Contains(floatingJob, `needs.release_readiness.result == 'success'`) {
t.Fatal("floating-tag promotion must run only after the immutable readiness barrier")
}
if !strings.Contains(workflow, `draft: true`) {
t.Fatal("create-release.yml must validate the release while it remains staged as a draft")
}
createJob := workflowJobBlock(t, workflow, "create_release")
if strings.Contains(createJob, `draft=false`) || strings.Contains(createJob, `Publish release`) {
t.Fatal("create_release must stage assets without crossing the customer publication boundary")
}
if strings.Contains(workflow, `provenance: false`) {
t.Fatal("create-release.yml must not disable release-image provenance")
}
validationRequired := []string{
`statuses: write`,
`curl --fail-with-body --silent --show-error -X POST`,
`"context": "Release Asset Validation"`,
`--arg tag "${{ steps.context.outputs.tag }}"`,
`--arg target_commitish "${{ steps.context.outputs.target_commitish }}"`,
`{body: $body, tag_name: $tag, target_commitish: $target_commitish}`,
`{draft: true, tag_name: $tag, target_commitish: $target_commitish}`,
`Validation release body update detached release tag`,
`Validation release body update changed target_commitish`,
`Validate release body integrity`,
`--validate-body-file "$RELEASE_BODY_FILE"`,
`--expected-body-file "$CLEAN_BODY_FILE"`,
`Quarantine malformed release body`,
`The release was quarantined as a draft without deleting its assets.`,
}
for _, needle := range validationRequired {
if !strings.Contains(validationWorkflow, needle) {
t.Fatalf("validate-release-assets.yml missing required status publication contract: %s", needle)
}
}
}
func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if isPrereleaseVersion(version) {
t.Skip("current release is a prerelease")
}
previous, ok := previousStablePatchVersion(version)
if !ok {
t.Skip("current release is not a stable patch release")
}
releaseBranch := requiredReleaseBranchForVersion(t, version)
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a stable patch release",
"`v"+previous+"`",
"Proxmox monitoring is more authoritative",
"TrueNAS monitoring uses the supported JSON-RPC transport",
"QNAP and Unraid installs now fail early",
"not Authenticode-signed",
"Unknown Publisher",
"`no-mobile-impact`",
"rollback target for this patch release is `v"+previous+"`",
)
assertFileContainsAll(t, changelogPath,
"Version: `v"+version+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: stable patch hotfix from `"+releaseBranch+"`",
"Proxmox and PBS monitoring",
"TrueNAS uses its supported JSON-RPC transport",
"Unified Agent installs on constrained QNAP and Unraid roots",
"`v6.1.2`-only",
"Unknown Publisher",
"Mobile decision: `no-mobile-impact`",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+version+"-informational",
"AppVersion-"+version+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
func TestCurrentStableMinorReleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if isPrereleaseVersion(version) {
t.Skip("current release is a prerelease")
}
parts, valid := parseStableVersion(version)
if !valid || parts[1] == 0 || parts[2] != 0 {
t.Skip("current release is not a stable minor release")
}
previous, ok := previousStableForPrereleaseVersion(version + "-rc.1")
if !ok {
t.Fatal("stable minor release has no earlier stable rollback packet")
}
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a stable minor release",
"stable `v"+previous+"`",
"## Highlights",
"Operational Trust",
"Actions provides a dedicated inbox",
"existing Pulse Mobile candidate",
"not Authenticode-signed",
"The rollback target is `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
"Version: `v"+version+"`",
"Previous stable: `v"+previous+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: owner-approved exact-SHA stable cutoff from `main`",
"Mobile decision: `existing-mobile-build-compatible`",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+version+"-informational",
"AppVersion-"+version+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"`rollback_version=v"+previous+"`",
"The exact stable `main` SHA must pass the no-publication dry run before the same SHA is dispatched through the single-build publish workflow.",
"The stable server cut is classified `existing-mobile-build-compatible`.",
"explicit version-bound decision",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
func TestCurrentSupportPrereleasePacketTracksInstallMetadata(t *testing.T) {
version := currentReleaseVersion(t)
if !isPrereleaseVersion(version) {
t.Skip("current release is stable")
}
previous, ok := previousStableForPrereleaseVersion(version)
if !ok {
t.Skip("current prerelease does not have a previous stable patch")
}
releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md")
changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md")
assertFileContainsAllNormalized(t, releaseNotesPath,
"`v"+version+"` is a release candidate",
"## Highlights",
"stable `v"+previous+"`",
"supersedes `v6.2.0-rc.8`",
"Alert delivery preserves configured ntfy metadata and acknowledgement state",
"Certificate-expiry monitoring, QNAP RAID bitmap parsing, real vCenter tags",
"Agent install and update paths reject older served binaries",
"Provider-hosted restore writes and automatic agent identity matching",
"Phone and narrow-screen layouts retain workload identity",
"Tenant resource stores close cleanly during offboarding and shutdown",
"Targeted regressions cover the alert, monitoring, agent-install",
"iOS build 12 is distributed through the TestFlight public beta link",
"Android versionCode 9 remains available through Play open testing",
"No public mobile-store rollout is part of this RC",
"not yet Authenticode-signed",
"No unsigned-Windows exception applies to any `v6.2.0` release",
"rollback target is stable `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
"Version: `v"+version+"`",
"Previous candidate: `v6.2.0-rc.8`",
"Previous stable: `v"+previous+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: exact-SHA single-build release candidate from `main`",
"This changelog describes the changes since `v6.2.0-rc.8`",
"Certificate-validity monitoring and alerting",
"Shared generation-bound resource views across API requests",
"Canonical agent auto-registration identity matching",
"Preserved configured ntfy metadata, provider incidents",
"Refused to serve agent binaries older than the server",
"Closed the outstanding CodeQL findings",
"Windows signing decision: Authenticode through SignPath is the mandatory signing backend",
"Mobile decision: `existing-mobile-build-compatible`",
"iOS build 12 is distributed through the TestFlight public beta link",
"Android versionCode 9 remains on Play open testing",
"No public store rollout is part of this candidate",
)
assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
"current v6 support release candidate packet",
)
assertFileContainsAll(t, repoFile("docs", "UPGRADE_v6.md"),
"docs/releases/RELEASE_NOTES_v"+version+".md",
"docs/releases/V6_CHANGELOG_v"+version+".md",
"current v6 support release candidate packet",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "Chart.yaml"),
"version: "+version,
`appVersion: "`+version+`"`,
"raw.githubusercontent.com/rcourtman/Pulse/v"+version+"/docs/images/pulse-logo.svg",
"blob/v"+version+"/docs/KUBERNETES.md",
)
assertFileContainsAll(t, repoFile("deploy", "helm", "pulse", "README.md"),
"Version-"+shieldsBadgeMessage(version)+"-informational",
"AppVersion-"+shieldsBadgeMessage(version)+"-informational",
"Autogenerated from chart metadata using [helm-docs v1.14.2]",
)
assertFileContainsAll(t, repoFile("docker-compose.yml"),
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
)
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
)
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active support prerelease `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"This support prerelease keeps `rollback_version=v"+previous+"`, publishes a versioned public GitHub prerelease plus versioned Docker and Helm artifacts, and does not move stable/latest install pointers or stable semver aliases.",
"For the active support prerelease `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"` until the next governed stable cut moves them forward.",
"The `v"+version+"` server cut is classified `existing-mobile-build-compatible`.",
"The changes since RC8 do not alter mobile relay payloads, pairing, approvals, authentication, or onboarding contracts; no additional companion upload or public store rollout is part of RC9.",
)
}
func TestBackfillReleaseWorkflowRepairsPublishedAssetsWithoutRebuilds(t *testing.T) {
scriptBytes, err := os.ReadFile(repoFile("scripts", "backfill-release-assets.sh"))
if err != nil {
t.Fatalf("read backfill-release-assets.sh: %v", err)
}
script := string(scriptBytes)
scriptRequired := []string{
`SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`,
`PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"`,
`cd "${PULSE_REPO_ROOT}"`,
`source "${SCRIPT_DIR}/release_asset_common.sh"`,
`gh release view "${TAG}" -R "${REPO}" --json isDraft,tagName`,
`Error: ${TAG} is still a draft release; use the normal release pipeline instead of historical backfill.`,
`gh release download "${TAG}" -R "${REPO}" --dir "${RELEASE_DIR}" --clobber`,
`pulse_release_prepare_signing_state "pulse-installer" "pulse-install"`,
`pulse_release_generate_packet_sbom "${PAYLOAD_DIR}" "${RELEASE_PACKET_SBOM}"`,
`pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"`,
`gh release upload "${TAG}" "${RELEASE_DIR}/checksums.txt" --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sha256 --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sig --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}"/*.sshsig --clobber`,
`gh release upload "${TAG}" "${RELEASE_DIR}/${RELEASE_PACKET_SBOM}" --clobber`,
}
for _, needle := range scriptRequired {
if !strings.Contains(script, needle) {
t.Fatalf("backfill-release-assets.sh missing required historical backfill step: %s", needle)
}
}
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "backfill-release-assets.yml"))
if err != nil {
t.Fatalf("read backfill-release-assets.yml: %v", err)
}
workflow := string(workflowBytes)
workflowRequired := []string{
`name: Backfill Release Assets`,
`workflow_dispatch:`,
`contents: write`,
`runs-on: ubuntu-24.04`,
`uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3`,
`uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0`,
`SYFT_VERSION="1.42.4"`,
`SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`,
`SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`,
`./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"`,
`PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"`,
}
for _, needle := range workflowRequired {
if !strings.Contains(workflow, needle) {
t.Fatalf("backfill-release-assets.yml missing required release-repair step: %s", needle)
}
}
}
func TestReleaseValidationRequiresSignedSidecars(t *testing.T) {
localValidatorBytes, err := os.ReadFile(repoFile("scripts", "validate-release.sh"))
if err != nil {
t.Fatalf("read validate-release.sh: %v", err)
}
localValidator := string(localValidatorBytes)
localRequired := []string{
`"pulse-v${PULSE_VERSION}-release.sbom.spdx.json"`,
`release_sbom="pulse-${PULSE_TAG}-release.sbom.spdx.json"`,
`error "checksums.txt is missing ${release_sbom}"`,
`success "Release SBOM is listed in checksums.txt"`,
`info "Validating SSH signature sidecars..."`,
`if [ ! -s "checksums.txt.sshsig" ]; then`,
`error "Missing or empty checksums.txt.sshsig"`,
`if [ ! -s "${filename}.sshsig" ]; then`,
`error "Missing or empty ${filename}.sshsig"`,
`success "SSH signature sidecars validated"`,
`validate_download_binary_headers() {`,
`http_header_value "X-Checksum-Sha256"`,
`http_header_value "X-Signature-Ed25519"`,
`http_header_value "X-Signature-SSHSIG"`,
`url="http://127.0.0.1:${HOST_PORT}/${script_name}"`,
`^# Pulse Unified Agent Installer`,
`--token-file`,
`TokenFile`,
`Install script endpoints returned required signature headers`,
`Download endpoints returned binaries with checksum and signature headers for all platforms/architectures`,
`Offline self-heal: download endpoint works with checksum and signature headers without outbound network`,
// Server installer identity guard — see the rc.1 → rc.5 regression where
// the rendered agent installer shipped as the top-level install.sh asset
// for 30 days before anyone noticed. Removing any of these unpins the asset.
`Validating install.sh is the Pulse server installer`,
`grep -qE '^# Pulse Installer Script'`,
`grep -qE '^[[:space:]]*--version\)'`,
`Pulse Unified Agent Installer`,
`bash "$install_sh_path" --help`,
`Install specific version (e.g.`,
// README key drift guard — across v6 rc.2 → rc.5 the README pinned a
// stale ed25519 key that did not verify install.sh.sshsig, so anyone
// following the secure-install path saw "Could not verify signature".
// validate-release.sh must extract the README's pinned key and actually
// run ssh-keygen -Y verify against the signed installer.
`Validating README pinned signature key matches install.sh.sshsig`,
`grep -oE "ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer" "$readme_path"`,
`ssh-keygen -Y verify \`,
`README's pinned signature key does not verify install.sh.sshsig`,
}
readmeBytes, err := os.ReadFile(repoFile("README.md"))
if err != nil {
t.Fatalf("read README.md: %v", err)
}
readme := string(readmeBytes)
// Lock in the actual signing key documented to customers. This is the public
// counterpart of PULSE_UPDATE_SIGNING_KEY and matches what install.sh and
// scripts/pulse-auto-update.sh have embedded. A future edit cannot silently
// regress to the stale Ds21c5 key without tripping this assertion.
const correctReadmeKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
if !strings.Contains(readme, correctReadmeKey) {
t.Fatalf("README.md must pin the correct pulse-installer ed25519 key for install.sh signature verification")
}
const staleReadmeKey = "Ds21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV"
if strings.Contains(readme, staleReadmeKey) {
t.Fatalf("README.md still references the stale pulse-installer key Ds21c5...; rc.2 → rc.5 shipped this drift")
}
// Format drift guard — ssh-keygen -Y verify -f expects an allowed_signers
// file whose FIRST field is the principal. The docs shipped the key in
// authorized_keys order (principal last, parsed as a comment), so the
// documented verification failed against a perfectly good signature.
// Reported by a customer against v6.0.5 on 2026-07-13.
const allowedSignersLine = `pulse-installer namespaces="pulse-install" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer`
if !strings.Contains(readme, allowedSignersLine) {
t.Fatalf("README.md verification snippet must publish the key as an allowed_signers line (principal first), not authorized_keys order")
}
installDocsBytes, err := os.ReadFile(repoFile("docs", "INSTALL.md"))
if err != nil {
t.Fatalf("read docs/INSTALL.md: %v", err)
}
installDocs := string(installDocsBytes)
if !strings.Contains(installDocs, correctReadmeKey) {
t.Fatalf("docs/INSTALL.md must pin the correct pulse-installer ed25519 key")
}
if strings.Contains(installDocs, staleReadmeKey) {
t.Fatalf("docs/INSTALL.md still references the stale pulse-installer key Ds21c5...")
}
if !strings.Contains(installDocs, allowedSignersLine) {
t.Fatalf("docs/INSTALL.md verification snippet must publish the key as an allowed_signers line (principal first), not authorized_keys order")
}
for _, needle := range localRequired {
if !strings.Contains(localValidator, needle) {
t.Fatalf("validate-release.sh missing signed sidecar validation: %s", needle)
}
}
if strings.Contains(localValidator, `url="http://127.0.0.1:${HOST_PORT}/download/${script_name}"`) {
t.Fatal("validate-release.sh must smoke-test /install.sh and /install.ps1, not non-existent /download/install.* routes")
}
publishedValidatorBytes, err := os.ReadFile(repoFile("scripts", "validate-published-release.sh"))
if err != nil {
t.Fatalf("read validate-published-release.sh: %v", err)
}
publishedValidator := string(publishedValidatorBytes)
publishedRequired := []string{
`RELEASE_SBOM="pulse-${TAG}-release.sbom.spdx.json"`,
`echo "Failed to download ${RELEASE_SBOM} for ${TAG}" >&2`,
`echo "${RELEASE_SBOM} is empty for ${TAG}" >&2`,
`CHECKSUMS_SIG_PATH="${TMP_DIR}/checksums.txt.sshsig"`,
`"${BASE_URL}/checksums.txt.sshsig"`,
`echo "Failed to download checksums.txt.sshsig for ${TAG}" >&2`,
`sshsig_path="${TMP_DIR}/${filename}.sshsig"`,
`"${artifact_url}.sshsig"`,
`echo "Failed to download ${filename}.sshsig" >&2`,
`Published release assets for ${TAG} match checksums.txt, *.sha256 files, and required *.sshsig sidecars.`,
}
for _, needle := range publishedRequired {
if !strings.Contains(publishedValidator, needle) {
t.Fatalf("validate-published-release.sh missing signed sidecar validation: %s", needle)
}
}
contractBytes, err := os.ReadFile(repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"))
if err != nil {
t.Fatalf("read deployment-installability contract: %v", err)
}
contract := string(contractBytes)
contractRequired := []string{
"`scripts/validate-release.sh`",
"`scripts/validate-published-release.sh`",
"`scripts/backfill-release-assets.sh`",
"`.github/workflows/backfill-release-assets.yml`",
"`scripts/validate-release.sh`, and",
"`scripts/release_asset_common.sh`",
"must derive the embedded update trust root",
"standalone SPDX JSON SBOM",
"already-published packet",
"derived integrity assets",
"and fail validation if",
"published artifact or",
"`checksums.txt` is missing its `.sshsig` sidecar",
"release-packet SBOM is absent",
"download endpoints must return checksum and signature headers",
"must disable Go's automatic VCS stamping",
"`-buildvcs=false`",
}
for _, needle := range contractRequired {
if !strings.Contains(contract, needle) {
t.Fatalf("deployment-installability contract missing signed sidecar validation requirement: %s", needle)
}
}
}
func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
dockerRequired := []string{
`FROM --platform=linux/amd64 node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-builder`,
`FROM --platform=linux/amd64 golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS backend-builder`,
`FROM backend-builder AS release-assets-builder`,
`FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS agent_runtime`,
`FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS pulse-runtime-base`,
`FROM pulse-runtime-base AS hosted_runtime`,
`FROM pulse-runtime-base AS runtime`,
`COPY scripts/release_ldflags.sh ./scripts/release_ldflags.sh`,
`COPY scripts/release_update_key.go ./scripts/release_update_key.go`,
`COPY scripts/render_installers.go ./scripts/render_installers.go`,
`ARG PULSE_LICENSE_PUBLIC_KEY_SHA256`,
`--mount=type=secret,id=pulse_license_public_key,required=false`,
`--mount=type=secret,id=pulse_update_signing_key,required=false`,
`ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
`LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"`,
`EXPECTED_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256#SHA256:}"`,
`mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256.`,
`UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"`,
`mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY.`,
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
`./scripts/release_ldflags.sh agent --version "${VERSION}"`,
`-buildvcs=false`,
`go run ./scripts/render_installers.go --source-dir ./scripts --output-dir /app/rendered-installers`,
`--allow-empty-installer-ssh-public-key`,
`ssh-keygen -q -Y sign -f "${OPENSSH_SIGNING_KEY}" -n pulse-install`,
`COPY --from=release-assets-builder /app/rendered-installers/install.sh /opt/pulse/scripts/install.sh`,
`COPY --from=release-assets-builder /app/pulse-agent-* /opt/pulse/bin/`,
}
for _, needle := range dockerRequired {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile missing canonical release ldflags usage: %s", needle)
}
}
hostedStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS hosted_runtime`)
runtimeStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS runtime`)
if hostedStart == -1 || runtimeStart == -1 || hostedStart > runtimeStart {
t.Fatal("Dockerfile must define hosted_runtime from pulse-runtime-base before the full runtime stage")
}
hostedStage := dockerfile[hostedStart:runtimeStart]
if strings.Contains(hostedStage, "rendered-installers") || strings.Contains(hostedStage, "/opt/pulse/bin") {
t.Fatalf("hosted_runtime target must not depend on installer rendering or embedded agent artifacts:\n%s", hostedStage)
}
if strings.Contains(dockerfile, `FROM --platform=linux/amd64 node:20-alpine AS frontend-builder`) ||
strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.26.5-alpine AS backend-builder`) ||
strings.Contains(dockerfile, `FROM alpine:3.20 AS agent_runtime`) ||
strings.Contains(dockerfile, `FROM alpine:3.20 AS pulse-runtime-base`) {
t.Fatal("Dockerfile base images must be pinned by immutable @sha256 digests")
}
if builds, cleanBuilds := strings.Count(dockerfile, " go build \\"), strings.Count(dockerfile, "-buildvcs=false"); builds != cleanBuilds {
t.Fatalf("Dockerfile release go builds must all disable automatic VCS stamping: builds=%d clean_builds=%d", builds, cleanBuilds)
}
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "deploy-demo-server.yml"))
if err != nil {
t.Fatalf("read deploy-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
workflowRequired := []string{
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
`-buildvcs=false`,
`demo-stable`,
`workflow_dispatch:`,
`target:`,
}
for _, needle := range workflowRequired {
if !strings.Contains(workflow, needle) {
t.Fatalf("deploy-demo-server workflow missing canonical release ldflags usage: %s", needle)
}
}
if strings.Contains(workflow, `preview-v6`) || strings.Contains(workflow, `demo-preview-v6`) {
t.Fatal("deploy-demo-server workflow must not keep a separate v6 preview demo target after GA")
}
}
func TestDockerRuntimeShipsPinnedAppriseCLI(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
required := []string{
`ARG APPRISE_VERSION=1.12.0`,
`AS apprise-builder`,
`python3 -m venv /opt/apprise`,
`/opt/apprise/bin/pip install --no-cache-dir "apprise==${APPRISE_VERSION}"`,
`COPY --from=apprise-builder /opt/apprise /opt/apprise`,
`ln -s /opt/apprise/bin/apprise /usr/local/bin/apprise`,
`apprise --version | grep -F "Apprise v${APPRISE_VERSION}"`,
}
for _, needle := range required {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile missing pinned Apprise runtime contract: %s", needle)
}
}
if strings.Count(dockerfile, `apprise --version | grep -F "Apprise v${APPRISE_VERSION}"`) < 2 {
t.Fatal("Dockerfile must verify the pinned Apprise CLI in both its build and runtime stages")
}
}
func TestAgentRuntimeImageDefaultsToUnifiedHostAndDockerMonitoring(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
required := []string{
`mkdir -p /var/lib/pulse-agent`,
`PULSE_DISABLE_AUTO_UPDATE=true`,
`PULSE_ENABLE_HOST=true`,
`PULSE_ENABLE_DOCKER=true`,
`PULSE_AGENT_ID_FILE=/var/lib/pulse-agent/agent-id`,
`PULSE_STATE_DIR=/var/lib/pulse-agent`,
`VOLUME ["/var/lib/pulse-agent"]`,
`ENTRYPOINT ["/usr/local/bin/pulse-agent"]`,
}
for _, needle := range required {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile agent_runtime missing unified host and Docker contract: %s", needle)
}
}
if strings.Contains(dockerfile, `PULSE_ENABLE_HOST=false`) {
t.Fatal("agent_runtime must not silently force every deployment into workload-only mode")
}
if strings.Contains(dockerfile, `ENTRYPOINT ["/usr/local/bin/pulse-agent", "--enable-docker", "--enable-host=false"]`) {
t.Fatal("agent_runtime must not hard-code module flags in ENTRYPOINT; env defaults keep user args overridable")
}
}
func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) {
candidateWorkflowPath := repoFile(".github", "workflows", "build-release-candidate.yml")
assertFileContainsAll(t, candidateWorkflowPath,
`require_macos_signing:`,
`require_windows_signing:`,
`sign-macos-agent:`,
`codesign --force --timestamp --options runtime`,
`xcrun notarytool submit`,
`--output-format json > notarization-result.json`,
`result.get('status') != 'Accepted'`,
`codesign --verify --deep --strict --verbose=2`,
`sign-windows-agent:`,
`windows_signing_backend:`,
`signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2`,
`github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}`,
`SIGNPATH_API_TOKEN`,
`SIGNPATH_ORGANIZATION_ID`,
`SIGNPATH_PROJECT_SLUG`,
`SIGNPATH_SIGNING_POLICY_SLUG`,
`SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`,
`SIGNPATH_EXPECTED_CERTIFICATE_SUBJECT`,
`signtool sign`,
`signtool verify /pa /v`,
`windows-signing-evidence.json`,
`signerThumbprint`,
`PULSE_AGENT_NATIVE_BINARIES_DIR:`,
)
candidateWorkflow, err := os.ReadFile(candidateWorkflowPath)
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
if strings.Contains(string(candidateWorkflow), `spctl --assess --type execute`) {
t.Fatal("bare command-line Mach-O binaries must not use Gatekeeper app assessment after notarization")
}
assertFileContainsAll(t, repoFile(".github", "workflows", "create-release.yml"),
`require_macos_signing: true`,
`require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}`,
`unsigned_windows_exception:`,
`unsigned_windows_reason:`,
`windows_signing_backend: signpath`,
)
assertFileContainsAll(t, repoFile(".github", "workflows", "release-dry-run.yml"),
`Definitive Dry-Run Verdict`,
`require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2') && inputs.unsigned_windows_exception) }}`,
`require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success`,
`require_result "stable demo no-mutation verification" "$DEMO_RESULT" success`,
)
assertFileContainsAll(t, repoFile("scripts", "release_control", "resolve_release_promotion.py"),
`version not in {"6.1.0", "6.1.1", "6.1.2"}`,
`unsigned_windows_reason is required`,
`not Authenticode-signed`,
`require_windows_signing = not is_prerelease and not unsigned_windows_exception`,
)
assertFileContainsAll(t, repoFile("scripts", "build-release.sh"),
`PULSE_AGENT_NATIVE_BINARIES_DIR`,
`native_targets=()`,
`PULSE_REQUIRE_MACOS_SIGNING:-false`,
`native_targets+=(darwin-amd64 darwin-arm64)`,
`PULSE_REQUIRE_WINDOWS_SIGNING:-false`,
`native_targets+=(windows-amd64 windows-arm64 windows-386)`,
`Applied required platform-native signed Unified Agent binaries.`,
`required native signing is enabled but PULSE_AGENT_NATIVE_BINARIES_DIR is empty.`,
)
}
func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
createReleaseBytes, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
candidateWorkflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "build-release-candidate.yml"))
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
createRelease := string(createReleaseBytes) + "\n" + string(candidateWorkflowBytes)
createReleaseRequired := []string{
`provenance: mode=max`,
`sbom: true`,
`secrets: |`,
`id: license_key_cache`,
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`Validate installer signing key pins`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`install.sh scripts/pulse-auto-update.sh release/pulse-auto-update.sh`,
`does not trust the configured release signing key.`,
`DOCKER_BUILDKIT: 1`,
`--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY`,
`--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY`,
`--build-arg PULSE_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256}"`,
`--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"`,
`id-token: write`,
`attestations: write`,
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
}
for _, needle := range createReleaseRequired {
if !strings.Contains(createRelease, needle) {
t.Fatalf("create-release.yml missing attested secret-safe release build contract: %s", needle)
}
}
if strings.Contains(createRelease, `PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`) {
t.Fatal("create-release.yml must not pass the license public key through docker build args")
}
publishBytes, err := os.ReadFile(repoFile(".github", "workflows", "publish-docker.yml"))
if err != nil {
t.Fatalf("read publish-docker.yml: %v", err)
}
publish := string(publishBytes)
publishRequired := []string{
`provenance: mode=max`,
`sbom: true`,
`secrets: |`,
`id: license_key_cache`,
`id: build_control_plane_image`,
`file: deploy/provider-msp/Dockerfile.control-plane`,
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`subject-name: docker.io/rcourtman/pulse`,
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse`,
`subject-name: docker.io/rcourtman/pulse-control-plane`,
`subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane`,
`rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}`,
`ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}`,
// pulse-agent ships as release-asset binaries, not as a Docker
// image (see commit dropping the agent image publish steps).
// The agent attestation subject-names intentionally do not
// appear in publish-docker.yml.
`push-to-registry: true`,
`create-storage-record: false`,
`id-token: write`,
`attestations: write`,
}
for _, needle := range publishRequired {
if !strings.Contains(publish, needle) {
t.Fatalf("publish-docker.yml missing attested secret-safe publish contract: %s", needle)
}
}
if strings.Contains(publish, `PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`) {
t.Fatal("publish-docker.yml must not pass the license public key through docker build args")
}
}
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)
}
chartBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "Chart.yaml"))
if err != nil {
t.Fatalf("read Helm Chart.yaml: %v", err)
}
chart := string(chartBytes)
chartRequired := []string{
"version: " + version,
`appVersion: "` + version + `"`,
"https://raw.githubusercontent.com/rcourtman/Pulse/v" + version + "/docs/images/pulse-logo.svg",
"https://github.com/rcourtman/Pulse/blob/v" + version + "/docs/KUBERNETES.md",
}
for _, needle := range chartRequired {
if !strings.Contains(chart, needle) {
t.Fatalf("Helm Chart.yaml must pin the governed release version, missing %s:\n%s", needle, chart)
}
}
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(chart, "v"+previous) {
t.Fatalf("Helm Chart.yaml must not retain the previous stable patch tag v%s:\n%s", previous, chart)
}
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(chart, "v"+previous) {
t.Fatalf("Helm Chart.yaml must not retain the previous prerelease tag v%s:\n%s", previous, chart)
}
chartReadmeBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "README.md"))
if err != nil {
t.Fatalf("read Helm README.md: %v", err)
}
chartReadme := string(chartReadmeBytes)
badgeVersion := shieldsBadgeMessage(version)
chartReadmeRequired := []string{
"![Version: " + version + "](https://img.shields.io/badge/Version-" + badgeVersion + "-informational?style=flat-square)",
"![AppVersion: " + version + "](https://img.shields.io/badge/AppVersion-" + badgeVersion + "-informational?style=flat-square)",
}
for _, needle := range chartReadmeRequired {
if !strings.Contains(chartReadme, needle) {
t.Fatalf("Helm README.md must reflect the governed release version, missing %s:\n%s", needle, chartReadme)
}
}
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(chartReadme, previous) {
t.Fatalf("Helm README.md must not retain the previous stable patch version %s:\n%s", previous, chartReadme)
}
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(chartReadme, previous) {
t.Fatalf("Helm README.md must not retain the previous prerelease version %s:\n%s", previous, chartReadme)
}
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{
`workflow_call:`,
`chart_version:`,
`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}"`,
`helm repo index "${index_work}"`,
`git -C "${workdir}/gh-pages" push origin HEAD:gh-pages`,
`grep -q "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)
}
}
for _, forbidden := range []string{
"workflow_run:",
`git checkout -B "$REQUIRED_BRANCH"`,
`git push origin HEAD:"$REQUIRED_BRANCH"`,
} {
if strings.Contains(helmPages, forbidden) {
t.Fatalf("helm-pages.yml must be an awaited exact-tag staging job; found forbidden %q", forbidden)
}
}
}
func TestHelmChartDoesNotPublishRetiredExplorePrepassMonitoring(t *testing.T) {
chartDir := repoFile("deploy", "helm", "pulse")
err := filepath.WalkDir(chartDir, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
switch filepath.Ext(path) {
case ".yaml", ".json", ".md":
default:
return nil
}
content, readErr := os.ReadFile(path)
if readErr != nil {
return readErr
}
text := string(content)
for _, forbidden := range []string{
"prometheusRule",
"pulse_ai_explore",
"Explore pre-pass",
"explore_runs_total",
} {
if strings.Contains(text, forbidden) {
t.Fatalf("helm chart file %s must not publish retired Assistant explore-prepass monitoring %q", path, forbidden)
}
}
return nil
})
if err != nil {
t.Fatalf("walk helm chart: %v", err)
}
}
func TestDeployDemoWorkflowFailsClosedForStableAndVerifiesFrontendParity(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "deploy-demo-server.yml"))
if err != nil {
t.Fatalf("read deploy-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
required := []string{
`DEMO_EXPECTED_HOSTNAME: ${{ vars.DEMO_EXPECTED_HOSTNAME }}`,
`DEMO_LOCAL_BASE_URL: ${{ vars.DEMO_LOCAL_BASE_URL }}`,
`[ -n "$DEMO_EXPECTED_HOSTNAME" ] || { echo "::error::DEMO_EXPECTED_HOSTNAME is required in the selected demo environment."; exit 1; }`,
`[ -n "$DEMO_LOCAL_BASE_URL" ] || { echo "::error::DEMO_LOCAL_BASE_URL is required in the selected demo environment."; exit 1; }`,
`ENVIRONMENT_NAME="demo-stable"`,
`options:`,
` - stable`,
`Capture expected frontend entry asset`,
`Verify target host identity`,
`uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4`,
`ping: ${{ secrets.DEMO_SERVER_HOST }}`,
`bash .github/scripts/check-demo-reachability.sh`,
`bash .github/scripts/setup-demo-ssh.sh`,
`SERVICE_NAME="pulse"`,
`Unsupported demo target: ${TARGET}`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Verify frontend parity`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
`extract_entry_asset()`,
`<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"`,
`Remote service is serving $REMOTE_ASSET but the build expected $EXPECTED_ASSET.`,
`Public demo is serving $PUBLIC_ASSET but the build expected $EXPECTED_ASSET.`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("deploy-demo-server workflow missing stable isolation or frontend parity proof: %s", needle)
}
}
for _, forbidden := range []string{`pulse-v6-preview`, `preview-v6`, `demo-preview-v6`} {
if strings.Contains(workflow, forbidden) {
t.Fatalf("deploy-demo-server workflow must not retain retired v6 preview target %s", forbidden)
}
}
}
func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
if err != nil {
t.Fatalf("read update-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
required := []string{
`- name: Tailscale`,
`uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4`,
`oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}`,
`oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}`,
`tags: tag:infra`,
`version: '1.94.2'`,
`ping: ${{ secrets.DEMO_SERVER_HOST }}`,
`bash .github/scripts/check-demo-reachability.sh`,
`workflow_call:`,
`verify_only:`,
`release_id:`,
`repos/${{ github.repository }}/releases/${RELEASE_ID}/assets?per_page=100`,
`Accept: application/octet-stream`,
`--archive "/tmp/${tarball}" --disable-auto-updates`,
`Refuse mutation during verification-only checks`,
`uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh`,
`Verify target host identity`,
`bash .github/scripts/setup-demo-ssh.sh`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Prepare demo host storage`,
`KEEP_BACKUPS=2`,
`Removing demo backup to restore install headroom: %s`,
`Pruning demo volatile runtime stores to restore install headroom.`,
`sudo find "$CONFIG_DIR" -xdev -type f`,
`-name "metrics.db"`,
`Removing demo volatile store: %s`,
`Demo host does not have enough free space to back up $CONFIG_DIR before install.`,
`Restore demo runtime configuration`,
`resolve_config_dir`,
`set_env_value DEMO_MODE true`,
`set_env_value PULSE_MOCK_MODE true`,
`ensure_demo_fixture_entitlement`,
`"demo_fixtures"`,
`del(.integrity)`,
`Demo fixture entitlement ensured in governed demo billing state.`,
`/api/license/runtime-capabilities`,
`Mock mode enabled`,
`Demo server mock mode did not enable after entitlement sync`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("update-demo-server workflow missing governed network path: %s", needle)
}
}
}
func TestDemoSshSetupHelperHandlesIpLiteralTargets(t *testing.T) {
helperBytes, err := os.ReadFile(repoFile(".github", "scripts", "setup-demo-ssh.sh"))
if err != nil {
t.Fatalf("read demo ssh setup helper: %v", err)
}
helper := string(helperBytes)
required := []string{
`is_ip_literal()`,
`ipaddress.ip_address(sys.argv[1])`,
`host_needs_dns=false`,
`Demo SSH host is an IP literal; skipping DNS resolution wait.`,
`[ "$host_needs_dns" = "true" ] && ! getent hosts "$DEMO_SERVER_HOST"`,
`ssh-keyscan -T 10 -H "$DEMO_SERVER_HOST"`,
`MAX_SSH_SETUP_ATTEMPTS="${DEMO_SSH_SETUP_ATTEMPTS:-3}"`,
`Demo network preflight passed, but ssh-keyscan did not return host keys.`,
}
for _, needle := range required {
if !strings.Contains(helper, needle) {
t.Fatalf("demo ssh setup helper missing guarded IP/hostname behavior: %s", needle)
}
}
tmpDir := t.TempDir()
fakeBin := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatalf("create fake bin: %v", err)
}
getentMarker := filepath.Join(tmpDir, "getent-called")
if err := os.WriteFile(filepath.Join(fakeBin, "getent"), []byte("#!/bin/sh\n: > \"$GETENT_MARKER\"\nexit 1\n"), 0o755); err != nil {
t.Fatalf("write fake getent: %v", err)
}
if err := os.WriteFile(filepath.Join(fakeBin, "ssh-keyscan"), []byte("#!/bin/sh\nprintf '100.109.163.95 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemo\\n'\n"), 0o755); err != nil {
t.Fatalf("write fake ssh-keyscan: %v", err)
}
homeDir := filepath.Join(tmpDir, "home")
cmd := exec.Command("bash", repoFile(".github", "scripts", "setup-demo-ssh.sh"))
cmd.Env = append(os.Environ(),
"DEMO_SERVER_HOST=100.109.163.95",
"DEMO_SERVER_SSH_KEY=fake-private-key",
"GETENT_MARKER="+getentMarker,
"HOME="+homeDir,
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("demo ssh setup helper failed for IP literal: %v\n%s", err, output)
}
if _, err := os.Stat(getentMarker); !os.IsNotExist(err) {
t.Fatalf("demo ssh setup helper must not require getent hosts for IP literals; stat err=%v", err)
}
knownHosts, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "known_hosts"))
if err != nil {
t.Fatalf("read generated known_hosts: %v", err)
}
if !strings.Contains(string(knownHosts), "ssh-ed25519") {
t.Fatalf("known_hosts missing captured key: %s", knownHosts)
}
if !strings.Contains(string(output), "Demo SSH host is an IP literal; skipping DNS resolution wait.") {
t.Fatalf("helper output did not report IP literal path: %s", output)
}
}
func TestDemoReachabilityHelperSeparatesTailnetAndSshTransportProof(t *testing.T) {
helperBytes, err := os.ReadFile(repoFile(".github", "scripts", "check-demo-reachability.sh"))
if err != nil {
t.Fatalf("read demo reachability helper: %v", err)
}
helper := string(helperBytes)
for _, needle := range []string{
`tailscale status --json`,
`tailscale ping --c 3 --timeout 10s "$DEMO_SERVER_HOST"`,
`nc -z -w 5 "$DEMO_SERVER_HOST" "$TCP_PORT"`,
`Runner Tailscale DNS:`,
`Runner Tailscale tags:`,
`Demo peer is not present in the runner peer map yet.`,
`Verify sshd and the host firewall on tailscale0.`,
} {
if !strings.Contains(helper, needle) {
t.Fatalf("demo reachability helper missing diagnostic contract: %s", needle)
}
}
tmpDir := t.TempDir()
fakeBin := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatalf("create fake bin: %v", err)
}
tailscaleScript := `#!/bin/sh
if [ "$1" = "status" ]; then
printf '%s\n' '{"BackendState":"Running","Self":{"TailscaleIPs":["100.100.100.1"]},"Peer":{"demo":{"TailscaleIPs":["100.109.163.95"],"Online":true,"Active":true,"Relay":"lhr"}}}'
exit 0
fi
if [ "$1" = "ping" ]; then
echo 'pong from demo'
exit 0
fi
exit 1
`
if err := os.WriteFile(filepath.Join(fakeBin, "tailscale"), []byte(tailscaleScript), 0o755); err != nil {
t.Fatalf("write fake tailscale: %v", err)
}
if err := os.WriteFile(filepath.Join(fakeBin, "nc"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write fake nc: %v", err)
}
cmd := exec.Command("bash", repoFile(".github", "scripts", "check-demo-reachability.sh"))
cmd.Env = append(os.Environ(),
"DEMO_SERVER_HOST=100.109.163.95",
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("demo reachability helper failed: %v\n%s", err, output)
}
for _, needle := range []string{"Tailscale backend: Running", "Demo peer state: online=True active=True relay=lhr", "Demo SSH transport is reachable over Tailscale."} {
if !strings.Contains(string(output), needle) {
t.Fatalf("demo reachability output missing %q: %s", needle, output)
}
}
}
func TestDemoPublicBrowserSmokeWaitsForVisibleLoginUI(t *testing.T) {
scriptBytes, err := os.ReadFile(repoFile("scripts", "demo_public_browser_smoke.cjs"))
if err != nil {
t.Fatalf("read demo public browser smoke script: %v", err)
}
script := string(scriptBytes)
required := []string{
`waitUntil: 'domcontentloaded'`,
`getByLabel('Username').waitFor({ state: 'visible', timeout: 120000 })`,
`getByLabel('Password').waitFor({ state: 'visible', timeout: 120000 })`,
`getByRole('button', { name: 'Sign in to Pulse' }).waitFor({ state: 'visible', timeout: 120000 })`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("demo public browser smoke missing visible-login readiness proof: %s", needle)
}
}
if strings.Contains(script, `waitUntil: 'networkidle'`) {
t.Fatal("demo public browser smoke still depends on networkidle instead of visible login readiness")
}
}
func TestDockerfileStagesShippedDocsForEmbeddedFrontendBuild(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
required := []string{
`COPY docs/ /app/docs/`,
`COPY SECURITY.md TERMS.md /app/`,
}
for _, needle := range required {
if !strings.Contains(dockerfile, needle) {
t.Fatalf("Dockerfile missing shipped-doc build input: %s", needle)
}
}
dockerignoreBytes, err := os.ReadFile(repoFile(".dockerignore"))
if err != nil {
t.Fatalf("read .dockerignore: %v", err)
}
dockerignore := string(dockerignoreBytes)
requiredAllowlist := []string{
`!docs/`,
`!docs/**`,
`!SECURITY.md`,
`!TERMS.md`,
}
for _, needle := range requiredAllowlist {
if !strings.Contains(dockerignore, needle) {
t.Fatalf(".dockerignore missing shipped-doc allowlist entry: %s", needle)
}
}
}
func TestDockerfileStampsTelemetryDeploymentMethod(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}
if !strings.Contains(string(dockerfileBytes), `ENV PULSE_DEPLOYMENT_METHOD=container_other`) {
t.Fatal("Dockerfile must stamp the closed fallback deployment method for container images")
}
}
func TestReleaseUpdateKeyFingerprintUsesCanonicalRawPublicKeyHash(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
cmd := exec.Command("go", "run", "./scripts/release_update_key.go", "fingerprint", "--private-key", base64.StdEncoding.EncodeToString(privateKey))
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release_update_key.go fingerprint failed: %v\n%s", err, output)
}
sum := sha256.Sum256(publicKey)
expected := "SHA256:" + base64.StdEncoding.EncodeToString(sum[:])
if got := strings.TrimSpace(string(output)); got != expected {
t.Fatalf("fingerprint mismatch: got %q want %q", got, expected)
}
}
func TestReleaseUpdateKeyPublicKeySSHAcceptsPublicKey(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
cmd := exec.Command("go", "run", "./scripts/release_update_key.go", "public-key-ssh", "--public-key", base64.StdEncoding.EncodeToString(publicKey), "--comment", "pulse-installer")
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release_update_key.go public-key-ssh failed: %v\n%s", err, output)
}
sshPublicKey, err := ssh.NewPublicKey(publicKey)
if err != nil {
t.Fatalf("derive SSH public key: %v", err)
}
expected := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))) + " pulse-installer"
if got := strings.TrimSpace(string(output)); got != expected {
t.Fatalf("SSH public key mismatch: got %q want %q", got, expected)
}
}
func TestReleaseAssetCommonRunsUpdateKeyThroughModulePath(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not installed")
}
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go not installed")
}
cmd := exec.Command("bash", "-lc", "source ./scripts/release_asset_common.sh; pulse_release_go_run_update_key")
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected release_update_key.go usage failure, got success:\n%s", output)
}
text := string(output)
if !strings.Contains(text, "release_update_key.go public-key") {
t.Fatalf("expected release_update_key.go usage output, got:\n%s", output)
}
if strings.Contains(text, "use of internal package") {
t.Fatalf("release helper invoked update key outside module import boundary:\n%s", output)
}
}
func TestReleaseAssetCommonRejectsUnexpectedUpdateSigningPublicKey(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not installed")
}
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go not installed")
}
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
unexpectedPublicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate unexpected public key: %v", err)
}
cmd := exec.Command("bash", "-lc", "source ./scripts/release_asset_common.sh; pulse_release_prepare_signing_state pulse-installer pulse-install")
cmd.Dir = repoFile()
cmd.Env = append(os.Environ(),
"PULSE_UPDATE_SIGNING_KEY="+base64.StdEncoding.EncodeToString(privateKey),
"PULSE_UPDATE_SIGNING_PUBLIC_KEY="+base64.StdEncoding.EncodeToString(unexpectedPublicKey),
)
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected release_asset_common.sh to reject a mismatched signing public key:\n%s", output)
}
if !strings.Contains(string(output), "does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY") {
t.Fatalf("expected mismatched signing public key error, got:\n%s", output)
}
}
// TestBuildReleasePackagesPulseMcpForAllPlatforms pins the
// distribution path for pulse-mcp: each Pulse release must build
// the MCP adapter for the same multi-OS matrix as the unified
// agent and emit per-platform tarballs/zips, bare binaries (for
// /releases/latest/download/ redirect compatibility), and the
// install-mcp.sh script into RELEASE_DIR. Drift in any of those
// strings means an integrator following the published install
// path hits a 404 on the release endpoint instead of a working
// binary.
func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "build-release.sh"))
if err != nil {
t.Fatalf("read build-release.sh: %v", err)
}
script := string(content)
required := []string{
// Build loop wires through ./cmd/pulse-mcp.
`-o "$output_path" \
./cmd/pulse-mcp`,
// Per-platform packaging follows the pulse-agent shape
// exactly so the upload step's glob does not need
// special cases.
`tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-amd64`,
`tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-arm64`,
`zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-mcp-windows-amd64.exe"`,
// Bare-binary copies for the /releases/latest/download/
// redirect that install-mcp.sh fetches by default.
`cp "$BUILD_DIR/pulse-mcp-linux-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-darwin-amd64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-darwin-arm64" "$RELEASE_DIR/"`,
`cp "$BUILD_DIR/pulse-mcp-windows-amd64.exe" "$RELEASE_DIR/"`,
// The installer scripts themselves must reach
// RELEASE_DIR so the GitHub Releases asset upload can
// publish them as the canonical curl-pipe-bash entry
// point.
`cp scripts/install-mcp.sh "$RELEASE_DIR/install-mcp.sh"`,
`[ -f scripts/install-mcp.ps1 ] && cp scripts/install-mcp.ps1 "$RELEASE_DIR/install-mcp.ps1"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("build-release.sh missing pulse-mcp distribution wiring: %s", needle)
}
}
// install-mcp.sh and install-mcp.ps1 must both exist as
// shipped scripts; the build pipeline references them, so
// missing-file drift breaks release builds rather than
// quietly ships an installer that 404s.
if _, err := os.Stat(repoFile("scripts", "install-mcp.sh")); err != nil {
t.Fatalf("scripts/install-mcp.sh missing: %v", err)
}
if _, err := os.Stat(repoFile("scripts", "install-mcp.ps1")); err != nil {
t.Fatalf("scripts/install-mcp.ps1 missing: %v", err)
}
// install-mcp.sh's install-dir resolution and SHA256
// verification are load-bearing: dropping either silently
// turns the installer into "curl | bash with no integrity
// check," which is the failure mode the hook is here to
// prevent. Pin the touchstones.
mcpScript, err := os.ReadFile(repoFile("scripts", "install-mcp.sh"))
if err != nil {
t.Fatalf("read install-mcp.sh: %v", err)
}
for _, needle := range []string{
`detect_platform()`,
`choose_install_dir()`,
`PULSE_MCP_NO_VERIFY`,
`checksums.txt`,
`sha256 mismatch`,
} {
if !strings.Contains(string(mcpScript), needle) {
t.Fatalf("install-mcp.sh missing required helper or guard: %s", needle)
}
}
mcpPowerShell, err := os.ReadFile(repoFile("scripts", "install-mcp.ps1"))
if err != nil {
t.Fatalf("read install-mcp.ps1: %v", err)
}
for _, needle := range []string{
`function Resolve-Architecture`,
`PULSE_MCP_NO_VERIFY`,
`checksums.txt`,
`Get-FileHash -Path $tmp -Algorithm SHA256`,
`sha256 mismatch`,
} {
if !strings.Contains(string(mcpPowerShell), needle) {
t.Fatalf("install-mcp.ps1 missing required helper or guard: %s", needle)
}
}
}
// The release-pipeline downstream workflows and private Pro publication path
// share one customer boundary. Exact-version artifacts are staged behind a
// draft, verified, and only then activated; GitHub publication is the final
// notification rather than the trigger for a long tail of publication work.
// The tests below pin that barrier so the staggered-release regression class
// cannot return.
func TestInstallShSmokeWorkflowPresent(t *testing.T) {
assertFileContainsAll(t, repoFile(".github", "workflows", "install-sh-smoke.yml"),
// Inputs and triggers.
`name: install.sh Smoke (Release Assets)`,
`workflow_call:`,
`workflow_dispatch:`,
`asset_source:`,
`release_id:`,
// Staged cuts use authenticated draft assets; manual verification can
// still pull from the public release URL.
`repos/${REPO}/releases/${RELEASE_ID}/assets?per_page=100`,
`repos/${REPO}/releases/assets/${asset_id}`,
`Accept: application/octet-stream`,
`releases/download/${TAG}`,
`install.sh.sshsig`,
`pulse-${TAG}-linux-amd64.tar.gz`,
// README key extraction + ssh-keygen verify against the asset.
`grep -oE 'ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer' README.md`,
`ssh-keygen -Y verify \`,
`-I pulse-installer \`,
`-n pulse-install \`,
`-s install.sh.sshsig < install.sh`,
// Server-installer identity assertions, mirroring validate-release.sh.
`grep -qE '^# Pulse Installer Script' install.sh`,
`grep -q 'Pulse Unified Agent Installer' install.sh`,
`grep -qE '^[[:space:]]*--version\)' install.sh`,
// End-to-end install in a privileged systemd container.
`jrei/systemd-debian:12`,
`bash install.sh --archive /smoke/${tarball} --disable-auto-updates`,
`systemctl is-active pulse`,
// curl --retry handles its own poll loop instead of a bash for-loop.
`--retry 30 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:7655/api/health`,
// Authoritative version check via /api/version (not /api/health).
`curl -fsS http://127.0.0.1:7655/api/version`,
`Installed version mismatch. Expected`,
)
}
func TestPromoteFloatingTagsReachableViaWorkflowCall(t *testing.T) {
workflowPath := repoFile(".github", "workflows", "promote-floating-tags.yml")
assertFileContainsAll(t, workflowPath,
`workflow_call:`,
`tag:`,
`description: "Release tag (e.g., v6.0.0). Required for workflow_call."`,
`prerelease:`,
`type: boolean`,
`TAG="${INPUT_TAG}"`,
`for image in pulse pulse-control-plane; do`,
`"rcourtman/${image}:rc"`,
`"ghcr.io/${OWNER}/${image}:latest"`,
)
content, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("read promote-floating-tags.yml: %v", err)
}
if strings.Contains(string(content), "workflow_run:") {
t.Fatal("floating aliases must have one explicit activation owner, not an implicit workflow_run trigger")
}
publishBytes, err := os.ReadFile(repoFile(".github", "workflows", "publish-docker.yml"))
if err != nil {
t.Fatalf("read publish-docker.yml: %v", err)
}
publishWorkflow := string(publishBytes)
for _, mutableTag := range []string{
`rcourtman/pulse:latest`,
`ghcr.io/{0}/pulse:latest`,
`rcourtman/pulse-control-plane:latest`,
`ghcr.io/{0}/pulse-control-plane:latest`,
} {
if strings.Contains(publishWorkflow, mutableTag) {
t.Fatalf("publish-docker.yml must stage exact-version images without moving mutable alias %q", mutableTag)
}
}
}
func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) {
workflowPath := repoFile(".github", "workflows", "publish-helm-chart.yml")
assertFileContainsAll(t, workflowPath,
`workflow_call:`,
`chart_version:`,
`description: "Chart version (e.g., 6.0.0-rc.5). Required for workflow_call."`,
`required: true`,
`type: string`,
`app_version:`,
// Chart-version resolver prefers inputs over release-event tag.
`if [ -n "${INPUT_CHART_VERSION}" ]; then`,
`RELEASE_TAG="${RELEASE_TAG_NAME}"`,
`name: Verify public GHCR chart read`,
`helm registry logout ghcr.io || true`,
`helm show chart`,
`oci://ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse`,
`--version "${{ steps.versions.outputs.chart_version }}"`,
)
content, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("read publish-helm-chart.yml: %v", err)
}
workflow := string(content)
for _, forbidden := range []string{
`versions/latest/restore`,
`-f visibility=public`,
`Package visibility configuration attempted`,
} {
if strings.Contains(workflow, forbidden) {
t.Fatalf("publish-helm-chart.yml must verify chart readability instead of masking GHCR visibility API failures; found %q", forbidden)
}
}
}
func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
createBytes, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
candidateBytes, err := os.ReadFile(repoFile(".github", "workflows", "build-release-candidate.yml"))
if err != nil {
t.Fatalf("read build-release-candidate.yml: %v", err)
}
validationBytes, err := os.ReadFile(repoFile(".github", "workflows", "validate-release-assets.yml"))
if err != nil {
t.Fatalf("read validate-release-assets.yml: %v", err)
}
createWorkflow := string(createBytes)
candidateWorkflow := string(candidateBytes)
validationWorkflow := string(validationBytes)
createJob := workflowJobBlock(t, createWorkflow, "create_release")
backendJob := workflowJobBlock(t, createWorkflow, "backend_tests")
integrationJob := workflowJobBlock(t, createWorkflow, "integration_tests")
validationJob := workflowJobBlock(t, createWorkflow, "validate_release_assets")
privateStageJob := workflowJobBlock(t, createWorkflow, "stage_private_pro_runtime")
readinessJob := workflowJobBlock(t, createWorkflow, "release_readiness")
privatePromotionJob := workflowJobBlock(t, createWorkflow, "promote_private_pro_runtime")
activationJob := workflowJobBlock(t, createWorkflow, "activate_release")
for _, needle := range []string{
`./scripts/build-release.sh "${{ inputs.version }}"`,
`scripts/validate-release.sh "${{ inputs.version }}" --skip-docker`,
`scripts/release_candidate_manifest.py create`,
`compression-level: 0`,
`retention-days: 1`,
} {
if !strings.Contains(candidateWorkflow, needle) {
t.Fatalf("build-release-candidate.yml missing single-build contract: %s", needle)
}
}
for _, needle := range []string{
`Download immutable release candidate`,
`scripts/release_candidate_manifest.py verify-local`,
`needs.build_release_candidate.outputs.artifact_name`,
} {
if !strings.Contains(createJob, needle) {
t.Fatalf("create_release missing candidate promotion contract: %s", needle)
}
}
if strings.Contains(createJob, "scripts/build-release.sh") {
t.Fatal("create_release must promote the verified candidate instead of rebuilding release assets")
}
if !strings.Contains(backendJob, "- frontend_checks") || !strings.Contains(integrationJob, "- frontend_checks") {
t.Fatal("backend and integration jobs must consume the shared verified frontend bundle")
}
if strings.Contains(integrationJob, "- backend_tests") {
t.Fatal("integration tests must run in parallel with backend tests")
}
if !strings.Contains(integrationJob, `tests/66-organization-sharing-approval-ui.spec.ts`) {
t.Fatal("integration release gate missing current organization-sharing coverage")
}
if strings.Contains(integrationJob, `tests/03-multi-tenant.spec.ts`) {
t.Fatal("integration release gate must not target the quarantined multi-tenant spec")
}
if strings.Contains(validationJob, "- publish_docker") {
t.Fatal("release asset digest validation must run in parallel with Docker publication")
}
if !strings.Contains(privateStageJob, "- create_release") || strings.Contains(privateStageJob, "- validate_release_assets") {
t.Fatal("private Pro staging must start after draft creation without waiting for asset validation")
}
for _, dependency := range []string{
"- create_release",
"- publish_docker",
"- validate_release_assets",
"- install_sh_smoke",
"- publish_helm_chart",
"- publish_helm_pages",
"- stage_private_pro_runtime",
} {
if !strings.Contains(readinessJob, dependency) {
t.Fatalf("immutable release readiness missing dependency: %s", dependency)
}
}
for _, dependency := range []string{"- release_readiness", "- stage_private_pro_runtime"} {
if !strings.Contains(privatePromotionJob, dependency) {
t.Fatalf("private Pro live promotion missing staging dependency: %s", dependency)
}
}
for _, dependency := range []string{
"- release_readiness",
"- update_stable_demo",
"- promote_floating_tags",
"- promote_private_pro_runtime",
} {
if !strings.Contains(activationJob, dependency) {
t.Fatalf("release activation missing readiness dependency: %s", dependency)
}
}
if !strings.Contains(activationJob, `'{draft: false, make_latest: $make_latest}'`) {
t.Fatal("release activation must be the job that crosses the draft publication boundary")
}
for _, needle := range []string{
`inputs.candidate_manifest_artifact != ''`,
`scripts/release_candidate_manifest.py verify-release`,
`inputs.candidate_manifest_artifact == ''`,
} {
if !strings.Contains(validationWorkflow, needle) {
t.Fatalf("validate-release-assets.yml missing fast digest contract: %s", needle)
}
}
}
func TestReleaseCutGatesCriticalFrontendAndWindowsRuntimeProof(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
workflow := string(content)
frontendJob := workflowJobBlock(t, workflow, "frontend_checks")
windowsJob := workflowJobBlock(t, workflow, "windows_install_command_smoke")
smokeJob := workflowJobBlock(t, workflow, "release_smoke")
createJob := workflowJobBlock(t, workflow, "create_release")
verdictJob := workflowJobBlock(t, workflow, "release_verdict")
for _, needle := range []string{
`npm --prefix frontend-modern run type-check`,
`npm --prefix frontend-modern test`,
} {
if !strings.Contains(frontendJob, needle) {
t.Fatalf("frontend release gate missing %s", needle)
}
}
for _, needle := range []string{
`runs-on: windows-2025`,
`agentInstallCommand.windows.test.ts`,
} {
if !strings.Contains(windowsJob, needle) {
t.Fatalf("Windows install-command release gate missing %s", needle)
}
}
for _, needle := range []string{
`tests/95-release-smoke.spec.ts`,
`release-smoke-failures-${{ github.sha }}`,
`tests/integration/test-results/`,
} {
if !strings.Contains(smokeJob, needle) {
t.Fatalf("release render smoke missing %s", needle)
}
}
if !strings.Contains(createJob, `needs.windows_install_command_smoke.result == 'success'`) {
t.Fatal("release assembly must fail closed on the Windows install-command smoke")
}
if !strings.Contains(verdictJob, `require_result "Windows install command smoke" "$WINDOWS_INSTALL_COMMAND_RESULT" success`) {
t.Fatal("definitive release verdict must report the Windows install-command smoke")
}
}
func TestCreateReleasePublishesPrivateProRuntime(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
if err != nil {
t.Fatalf("read create-release.yml: %v", err)
}
workflow := string(content)
stageJob := workflowJobBlock(t, workflow, "stage_private_pro_runtime")
promotionJob := workflowJobBlock(t, workflow, "promote_private_pro_runtime")
for _, needle := range []string{
`needs.create_release.result == 'success'`,
`github.event.inputs.draft_only != 'true'`,
`startsWith(needs.prepare.outputs.version, '6.')`,
`GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}`,
`--json createdAt`,
`r2_prefix="${TAG}-pro-${run_created_date}-${GITHUB_RUN_ID}"`,
`gh workflow run build-pro-release.yml`,
`--repo rcourtman/pulse-enterprise`,
`-f pulse_ref="${TAG}"`,
`-f version="${VERSION}"`,
`-f upload_actions_artifact=false`,
`-f upload_to_r2=true`,
`-f publish_docker_image=true`,
`-f docker_image=license.pulserelay.pro/pulse-pro`,
`-f r2_prefix="${r2_prefix}"`,
`-f reuse_existing_packet=true`,
`-f allow_stable_ga_publish="${allow_ga_publish}"`,
`wait_for_workflow rcourtman/pulse-enterprise "Build Pro Release" main "${build_started_at}" "private Pro build"`,
`echo "r2_prefix=${r2_prefix}" >> "$GITHUB_OUTPUT"`,
} {
if !strings.Contains(stageJob, needle) {
t.Fatalf("stage_private_pro_runtime missing required contract: %s", needle)
}
}
for _, needle := range []string{
`needs.stage_private_pro_runtime.result == 'success'`,
`R2_PREFIX: ${{ needs.stage_private_pro_runtime.outputs.r2_prefix }}`,
`gh workflow run promote-paid-runtime-release.yml`,
`--repo rcourtman/pulse-pro`,
`-f r2_prefix="${R2_PREFIX}"`,
`-f allow_ga_prefix="${allow_ga_publish}"`,
`wait_for_workflow rcourtman/pulse-pro "Promote Paid Runtime Release" main "${promote_started_at}" "private Pro live promotion"`,
`echo "::error::${label} failed with conclusion=${conclusion}: ${url}"`,
} {
if !strings.Contains(promotionJob, needle) {
t.Fatalf("promote_private_pro_runtime missing required contract: %s", needle)
}
}
if strings.Contains(stageJob, "continue-on-error: true") || strings.Contains(promotionJob, "continue-on-error: true") {
t.Fatal("private Pro staging and promotion must fail the release pipeline on error")
}
}
func TestHelmAgentRuntimePointsAtRealImage(t *testing.T) {
// The helm chart's agent.enabled=true workload used to default to
// ghcr.io/rcourtman/pulse-agent — an image that was never published.
// The chart now points at the main rcourtman/pulse image and uses an
// arch-resolved /usr/local/bin/pulse-agent symlink baked into the
// runtime stage. This test pins:
// 1. values.yaml uses the main image
// 2. values.yaml has the command override
// 3. the agent template renders the command
// 4. the Dockerfile creates the symlink for every supported arch
// 5. validate-release.sh asserts the symlink exists in the published image
// Reverting any one of these unwires the chart back to ImagePullBackOff.
valuesBytes, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "values.yaml"))
if err != nil {
t.Fatalf("read values.yaml: %v", err)
}
values := string(valuesBytes)
if !strings.Contains(values, "repository: rcourtman/pulse\n") {
t.Fatal("agent.image.repository must default to rcourtman/pulse (single-image agent + server)")
}
// Match the actual config value, not casual mentions in surrounding
// comments that explain why the default changed.
if strings.Contains(values, "repository: ghcr.io/rcourtman/pulse-agent") {
t.Fatal("agent.image.repository must not reference the never-published ghcr.io/rcourtman/pulse-agent image")
}
if !strings.Contains(values, "- /usr/local/bin/pulse-agent") {
t.Fatal("agent.command must default to /usr/local/bin/pulse-agent so the main image's server ENTRYPOINT is overridden")
}
agentTemplate, err := os.ReadFile(repoFile("deploy", "helm", "pulse", "templates", "agent.yaml"))
if err != nil {
t.Fatalf("read agent.yaml: %v", err)
}
tmpl := string(agentTemplate)
if !strings.Contains(tmpl, "{{- if .Values.agent.command }}") {
t.Fatal("agent.yaml template must conditionally render command from .Values.agent.command")
}
if !strings.Contains(tmpl, "command:\n {{- toYaml .Values.agent.command | nindent 12 }}") {
t.Fatal("agent.yaml template must render command via toYaml so list values pass through correctly")
}
assertFileContainsAll(t, repoFile("Dockerfile"),
`ln -s /opt/pulse/bin/pulse-agent-linux-arm64 /usr/local/bin/pulse-agent`,
`ln -s /opt/pulse/bin/pulse-agent-linux-armv7 /usr/local/bin/pulse-agent`,
`ln -s /opt/pulse/bin/pulse-agent-linux-amd64 /usr/local/bin/pulse-agent`,
)
assertFileContainsAll(t, repoFile("scripts", "validate-release.sh"),
`Validating /usr/local/bin/pulse-agent arch-resolved symlink`,
`[ -L /usr/local/bin/pulse-agent ]`,
`/usr/local/bin/pulse-agent target is not executable`,
)
}
func repoFile(parts ...string) string {
root := filepath.Join("..", "..")
segments := append([]string{root}, parts...)
return filepath.Join(segments...)
}
// assertFileContainsAll reads the file at path and fails the test if any of
// the required substrings is missing. The standard pinning-test shape in
// this package.
func assertFileContainsAll(t *testing.T, path string, required ...string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := string(content)
for _, needle := range required {
if !strings.Contains(s, needle) {
t.Fatalf("%s missing required substring: %s", path, needle)
}
}
}
func assertFileContainsAllNormalized(t *testing.T, path string, required ...string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := normalizedInstallTestWhitespace(string(content))
for _, needle := range required {
if !strings.Contains(s, normalizedInstallTestWhitespace(needle)) {
t.Fatalf("%s missing required normalized substring: %s", path, needle)
}
}
}
func normalizedInstallTestWhitespace(text string) string {
return strings.Join(strings.Fields(text), " ")
}
func workflowJobBlock(t *testing.T, workflow, job string) string {
t.Helper()
startMarker := "\n " + job + ":\n"
start := strings.Index(workflow, startMarker)
if start == -1 {
t.Fatalf("workflow missing job %s", job)
}
start += 1
rest := workflow[start+len(" "+job+":\n"):]
end := len(rest)
for _, line := range strings.Split(rest, "\n") {
if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") {
candidate := strings.Index(rest, "\n"+line)
if candidate >= 0 {
end = candidate
break
}
}
}
return workflow[start : start+len(" "+job+":\n")+end]
}