mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Make private Pro publication block v6 releases
This commit is contained in:
@@ -1151,3 +1151,120 @@ jobs:
|
||||
with:
|
||||
tag: ${{ needs.prepare.outputs.tag }}
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
|
||||
# Customer-facing v6 public releases must not outrun the private Pulse Pro
|
||||
# runtime path. The public release is the immutable source tag; this job
|
||||
# dispatches the private build against that exact tag, waits for the R2 and
|
||||
# Docker publication workflow to pass, then dispatches the pulse-pro live
|
||||
# promotion workflow and waits for the signed packet to update the license
|
||||
# broker. A failure here fails the release pipeline instead of leaving paid
|
||||
# customers on a stale private manifest.
|
||||
publish_private_pro_runtime:
|
||||
needs:
|
||||
- prepare
|
||||
- validate_release_assets
|
||||
if: ${{ always() && needs.prepare.result == 'success' && needs.validate_release_assets.result == 'success' && needs.prepare.outputs.historical_asset_backfill_only != 'true' && github.event.inputs.draft_only != 'true' && startsWith(needs.prepare.outputs.version, '6.') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- name: Dispatch and verify private Pro runtime publication
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${GH_TOKEN:-}" ]]; then
|
||||
echo "::error::WORKFLOW_PAT is required to dispatch private Pro publication workflows."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_workflow() {
|
||||
local repo="$1"
|
||||
local workflow="$2"
|
||||
local branch="$3"
|
||||
local started_at="$4"
|
||||
local label="$5"
|
||||
local timeout_seconds="$6"
|
||||
local deadline=$((SECONDS + timeout_seconds))
|
||||
local run_id=""
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
if [[ -z "${run_id}" ]]; then
|
||||
run_id="$(
|
||||
gh run list \
|
||||
--repo "${repo}" \
|
||||
--workflow "${workflow}" \
|
||||
--event workflow_dispatch \
|
||||
--branch "${branch}" \
|
||||
--limit 50 \
|
||||
--json databaseId,createdAt \
|
||||
--jq "map(select(.createdAt >= \"${started_at}\")) | sort_by(.createdAt) | reverse | .[0].databaseId // \"\""
|
||||
)"
|
||||
if [[ -n "${run_id}" ]]; then
|
||||
echo "Watching ${label} run ${run_id} in ${repo}."
|
||||
else
|
||||
echo "Waiting for ${label} workflow run to appear..."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${run_id}" ]]; then
|
||||
run_state="$(
|
||||
gh run view "${run_id}" \
|
||||
--repo "${repo}" \
|
||||
--json status,conclusion,url \
|
||||
--jq '[.status, (.conclusion // ""), .url] | @tsv'
|
||||
)"
|
||||
status="$(awk -F '\t' '{print $1}' <<<"${run_state}")"
|
||||
conclusion="$(awk -F '\t' '{print $2}' <<<"${run_state}")"
|
||||
url="$(awk -F '\t' '{print $3}' <<<"${run_state}")"
|
||||
echo "${label}: status=${status} conclusion=${conclusion:-pending} ${url}"
|
||||
if [[ "${status}" == "completed" ]]; then
|
||||
if [[ "${conclusion}" == "success" ]]; then
|
||||
echo "[OK] ${label} completed successfully: ${url}"
|
||||
return 0
|
||||
fi
|
||||
echo "::error::${label} failed with conclusion=${conclusion}: ${url}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Timed out waiting for ${label} after ${timeout_seconds}s."
|
||||
return 1
|
||||
}
|
||||
|
||||
allow_ga_publish=false
|
||||
if [[ "${IS_PRERELEASE}" != "true" ]]; then
|
||||
allow_ga_publish=true
|
||||
fi
|
||||
|
||||
r2_prefix="${TAG}-pro-$(date -u '+%Y%m%d')-${GITHUB_RUN_ID}"
|
||||
build_started_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
echo "Dispatching private Pro build for ${TAG} with R2 prefix ${r2_prefix}."
|
||||
gh workflow run build-pro-release.yml \
|
||||
--repo rcourtman/pulse-enterprise \
|
||||
--ref main \
|
||||
-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 allow_stable_ga_publish="${allow_ga_publish}"
|
||||
wait_for_workflow rcourtman/pulse-enterprise "Build Pro Release" main "${build_started_at}" "private Pro build" 7200
|
||||
|
||||
promote_started_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
echo "Dispatching live paid-runtime promotion for ${TAG} with R2 prefix ${r2_prefix}."
|
||||
gh workflow run promote-paid-runtime-release.yml \
|
||||
--repo rcourtman/pulse-pro \
|
||||
--ref main \
|
||||
-f version="${VERSION}" \
|
||||
-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" 3600
|
||||
|
||||
@@ -70,11 +70,23 @@ Cloud, and self-hosted production users.
|
||||
4. A private Pro build from a moving branch is valid only as an internal proof
|
||||
artifact. It is not valid customer guidance and must not update the live
|
||||
paid-download manifest or private Docker customer tag.
|
||||
5. Customer-facing private Pro archive or Docker promotion must use the generated
|
||||
5. Customer-facing private Pro archive and Docker publication is part of the
|
||||
public v6 release pipeline. After `validate-release-assets.yml` succeeds for
|
||||
a non-draft v6 release, `create-release.yml` must dispatch
|
||||
`rcourtman/pulse-enterprise` `Build Pro Release` against the exact public
|
||||
tag with `upload_to_r2=true`, `publish_docker_image=true`, and an R2 prefix
|
||||
derived by the release run, then wait for that workflow to succeed.
|
||||
6. The public v6 release pipeline must then dispatch `rcourtman/pulse-pro`
|
||||
`Promote Paid Runtime Release` with the same version and R2 prefix, and
|
||||
wait for the signed packet to promote the live paid-download broker. A failed
|
||||
private build or failed live promotion fails the public release workflow;
|
||||
private Pro RC/GA advancement must not depend on an operator noticing a
|
||||
checklist item after the public RC has shipped.
|
||||
7. Customer-facing private Pro archive or Docker promotion must use the generated
|
||||
paid-runtime proof packet from the Pro release workflow. The canonical command
|
||||
is `scripts/promote_paid_runtime_release_packet.sh --release-dir <proof-packet-dir> --admin-token-file <explicit-token-file> --execute-live`
|
||||
from `repos/pulse-pro`; GA promotions also require `--allow-ga-prefix`.
|
||||
6. The promotion command is the release gate for the live paid-download broker:
|
||||
8. The promotion command is the release gate for the live paid-download broker:
|
||||
it validates the proof packet signatures, installs the exact manifest on
|
||||
`pulse-license`, runs the live customer-path proof, and restores the previous
|
||||
remote manifest if the gate fails. Do not send customer instructions from a
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# Paid Runtime Automatic Private Pro Release Gate
|
||||
|
||||
Date: 2026-06-15
|
||||
Owner: paid-runtime-build-attribution-alerting
|
||||
Evidence tier: test-proof
|
||||
|
||||
## Trigger
|
||||
|
||||
A paid customer reported that the private Pulse Pro v6 download links stopped at
|
||||
`6.0.0-rc.4` even though public v6 RCs had advanced past RC4.
|
||||
|
||||
## Finding
|
||||
|
||||
The live `pulse-license` private Pro release manifest still pointed at
|
||||
`6.0.0-rc.4` with prefix `v6.0.0-rc.4-pro-20260507`. Public releases
|
||||
`v6.0.0-rc.5` and `v6.0.0-rc.6` existed, but `rcourtman/pulse-enterprise`
|
||||
had no later `Build Pro Release` workflow-dispatch run after the corrected RC4
|
||||
customer-facing Pro publish on 2026-05-07.
|
||||
|
||||
The previous policy and checklist required a generated proof packet plus
|
||||
`scripts/promote_paid_runtime_release_packet.sh`, but that path was still a
|
||||
manual post-release operation. Public RC publication could therefore advance
|
||||
without automatically building or promoting the matching private Pro runtime.
|
||||
|
||||
## Decision
|
||||
|
||||
For every non-draft v6 public release, the public release workflow owns the
|
||||
private Pro runtime publication handoff:
|
||||
|
||||
1. After public asset validation succeeds, dispatch `rcourtman/pulse-enterprise`
|
||||
`Build Pro Release` against the exact public tag and version.
|
||||
2. Require `upload_actions_artifact=false`, `upload_to_r2=true`, and
|
||||
`publish_docker_image=true`.
|
||||
3. Derive the private R2 prefix from the public release workflow run.
|
||||
4. Wait for the private Pro R2/Docker publication workflow to succeed.
|
||||
5. Dispatch `rcourtman/pulse-pro` `Promote Paid Runtime Release` with the same
|
||||
version and R2 prefix.
|
||||
6. Wait for the live paid-download broker promotion to succeed.
|
||||
|
||||
A failed private build or failed live promotion fails the public release
|
||||
workflow. Private Pro RC/GA advancement must not depend on an operator noticing
|
||||
a checklist item after the public RC has shipped.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `repos/pulse/.github/workflows/create-release.yml` now has a
|
||||
`publish_private_pro_runtime` job gated on non-draft v6 releases after
|
||||
`validate_release_assets`.
|
||||
- The job dispatches `rcourtman/pulse-enterprise` `Build Pro Release`, waits for
|
||||
completion, dispatches `rcourtman/pulse-pro` `Promote Paid Runtime Release`,
|
||||
and waits for completion without `continue-on-error`.
|
||||
- `repos/pulse-pro/.github/workflows/promote-paid-runtime-release.yml` downloads
|
||||
the signed R2 proof packet, verifies its version, then runs
|
||||
`scripts/promote_paid_runtime_release_packet.sh --release-dir <proof-packet-dir> --execute-live`.
|
||||
- Release policy, deployment-installability ownership docs, Pro operations docs,
|
||||
the Pro upgrade runbook, and the Pro launch checklist now describe the
|
||||
automatic path.
|
||||
- The paid-runtime distribution validator now requires the promotion workflow
|
||||
and rejects non-blocking promotion drift.
|
||||
- The legacy license email now repeats the private Pulse Pro runtime handoff for
|
||||
v6 paid features, including the Linux/Proxmox LXC archive guard.
|
||||
|
||||
## Proof
|
||||
|
||||
- `go test ./scripts/installtests -run 'TestCreateReleasePublishesPrivateProRuntime|TestInstallShSmokeWorkflowPresent|TestPublishHelmChartReachableViaWorkflowCall' -count=1`
|
||||
- `python3 scripts/validate_paid_runtime_distribution.py`
|
||||
- `python3 -m unittest scripts.tests.test_validate_paid_runtime_distribution`
|
||||
- `go test . -run 'Test.*LicenseEmail|TestV6LicenseEmailIncludesPrivateDownloadPage' -count=1`
|
||||
- YAML parse checks for `.github/workflows/create-release.yml` and
|
||||
`.github/workflows/promote-paid-runtime-release.yml`
|
||||
- `git diff --check` in `repos/pulse` and `repos/pulse-pro`
|
||||
|
||||
## Residual
|
||||
|
||||
This record fixes the future release process and prevents another silent private
|
||||
Pro runtime lag. It does not itself publish a new private Pro artifact for the
|
||||
already-shipped public RC6 line; that is a separate live release operation.
|
||||
@@ -6501,7 +6501,7 @@
|
||||
},
|
||||
{
|
||||
"id": "paid-runtime-build-attribution-alerting",
|
||||
"summary": "Confirm active paid licenses cannot silently run on the public community runtime: Pulse must detect non-Pro or unknown runtime identity for paid installs, surface a clear private-runtime warning in-product, preserve runtime-build telemetry for support, and expose Pro/community/unknown runtime status in admin support views.",
|
||||
"summary": "Confirm active paid licenses cannot silently run on the public community runtime or on a stale private Pro release line: Pulse must detect non-Pro or unknown runtime identity for paid installs, surface a clear private-runtime warning in-product, preserve runtime-build telemetry for support, expose Pro/community/unknown runtime status in admin support views, and block future v6 public release success when matching private Pro R2/Docker publication or live broker promotion fails.",
|
||||
"owner": "project-owner",
|
||||
"blocking_level": "release-ready",
|
||||
"minimum_evidence_tier": "managed-runtime-exercise",
|
||||
@@ -6520,6 +6520,12 @@
|
||||
"path": "docs/release-control/v6/internal/records/paid-runtime-build-attribution-alerting-2026-05-07.md",
|
||||
"kind": "file",
|
||||
"evidence_tier": "managed-runtime-exercise"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/records/paid-runtime-build-attribution-alerting-automatic-private-pro-release-2026-06-15.md",
|
||||
"kind": "file",
|
||||
"evidence_tier": "test-proof"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -534,14 +534,22 @@ TLS floor in the dynamic config.
|
||||
can keep the normal service setup while installing the private Pulse Pro
|
||||
runtime.
|
||||
Customer-facing private Pro RC/GA promotion is part of that same boundary:
|
||||
after the `pulse-enterprise` Pro release workflow publishes private archives,
|
||||
the private Docker image, and the paid-runtime proof packet, the operator must
|
||||
run `scripts/promote_paid_runtime_release_packet.sh --release-dir <proof-packet-dir> --admin-token-file <explicit-token-file> --execute-live`
|
||||
from `repos/pulse-pro` before sending customer instructions. That command is
|
||||
the canonical live-broker promotion path because it validates the signed proof
|
||||
packet, installs the exact manifest on `pulse-license`, runs the customer-path
|
||||
live proof, and restores the previous remote manifest if the gate fails. GA
|
||||
promotions also require `--allow-ga-prefix`.
|
||||
for every non-draft v6 public release, `create-release.yml` must call the
|
||||
private `rcourtman/pulse-enterprise` `Build Pro Release` workflow after
|
||||
`validate_release_assets` succeeds, pass the exact public tag/version, set
|
||||
`upload_to_r2=true` and `publish_docker_image=true`, wait for that workflow
|
||||
to succeed, then call the private `rcourtman/pulse-pro`
|
||||
`Promote Paid Runtime Release` workflow with the same version and R2 prefix.
|
||||
The promotion workflow downloads the signed proof packet and runs
|
||||
`scripts/promote_paid_runtime_release_packet.sh --release-dir <proof-packet-dir> --execute-live`
|
||||
from `repos/pulse-pro`. That command is the canonical live-broker promotion
|
||||
path because it validates the signed proof packet, installs the exact
|
||||
manifest on `pulse-license`, runs the customer-path live proof, and restores
|
||||
the previous remote manifest if the gate fails. GA promotions also require
|
||||
`--allow-ga-prefix`. A failed private build or failed live promotion must
|
||||
fail the public release workflow; future private Pro publication must not
|
||||
depend on an operator noticing a manual checklist step after the public RC
|
||||
has shipped.
|
||||
The repo-root VERSION file is part of the same governed boundary and must
|
||||
not drift as an
|
||||
unowned release-cut switch: changing the version string for a new RC or
|
||||
|
||||
@@ -1002,14 +1002,15 @@ func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The three release-pipeline downstream workflows (install-sh-smoke,
|
||||
// promote-floating-tags, publish-helm-chart) all share the same root cause:
|
||||
// v6 rc.1 → rc.5 silently broke because GitHub's `release: published` webhook
|
||||
// doesn't fire when create-release.yml's draft → PATCH(draft=false) promotion
|
||||
// path is used, and `workflow_run` chains don't fire when their upstream
|
||||
// fails. The fix in each case is a workflow_call entry from create-release.yml
|
||||
// after validate_release_assets succeeds. The tests below pin the trigger
|
||||
// declarations and resolver logic so the regression class can't return.
|
||||
// The release-pipeline downstream workflows and private Pro publication path
|
||||
// share the same root cause: v6 rc.1 -> rc.6 silently broke because GitHub's
|
||||
// `release: published` webhook doesn't fire when create-release.yml's draft ->
|
||||
// PATCH(draft=false) promotion path is used, `workflow_run` chains don't fire
|
||||
// when their upstream fails, and the private Pro path was left as a manual
|
||||
// checklist step. The fix is explicit post-release orchestration after
|
||||
// validate_release_assets succeeds. The tests below pin the trigger
|
||||
// declarations, resolver logic, and private Pro dispatch contract so the
|
||||
// regression class can't return.
|
||||
|
||||
func TestInstallShSmokeWorkflowPresent(t *testing.T) {
|
||||
assertFileContainsAll(t, repoFile(".github", "workflows", "install-sh-smoke.yml"),
|
||||
@@ -1072,6 +1073,47 @@ func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
job := workflowJobBlock(t, workflow, "publish_private_pro_runtime")
|
||||
|
||||
for _, needle := range []string{
|
||||
`needs.validate_release_assets.result == 'success'`,
|
||||
`github.event.inputs.draft_only != 'true'`,
|
||||
`startsWith(needs.prepare.outputs.version, '6.')`,
|
||||
`GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}`,
|
||||
`r2_prefix="${TAG}-pro-$(date -u '+%Y%m%d')-${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 allow_stable_ga_publish="${allow_ga_publish}"`,
|
||||
`wait_for_workflow rcourtman/pulse-enterprise "Build Pro Release" main "${build_started_at}" "private Pro build"`,
|
||||
`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(job, needle) {
|
||||
t.Fatalf("publish_private_pro_runtime missing required contract: %s", needle)
|
||||
}
|
||||
}
|
||||
if strings.Contains(job, "continue-on-error: true") {
|
||||
t.Fatal("publish_private_pro_runtime must fail the release pipeline when private Pro publication or promotion fails")
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user