diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 9df88ef3bb..0a476d09b6 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -696,23 +696,25 @@ jobs: continue-on-error: true env: GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} + REQUIRED_BRANCH: ${{ needs.prepare.outputs.required_branch }} run: | - gh workflow run publish-docker.yml -f tag="${{ needs.prepare.outputs.tag }}" - echo "[OK] Docker publish workflow dispatched" + gh workflow run publish-docker.yml --ref "${REQUIRED_BRANCH}" -f tag="${{ needs.prepare.outputs.tag }}" + echo "[OK] Docker publish workflow dispatched from ${REQUIRED_BRANCH}" - name: Trigger demo server update if: ${{ github.event.inputs.draft_only != 'true' }} continue-on-error: true env: GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} + REQUIRED_BRANCH: ${{ needs.prepare.outputs.required_branch }} run: | if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then TARGET="preview-v6" else TARGET="stable" fi - gh workflow run update-demo-server.yml -f tag="${{ needs.prepare.outputs.tag }}" -f target="${TARGET}" - echo "[OK] Demo server update dispatched for ${TARGET}" + gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}" -f tag="${{ needs.prepare.outputs.tag }}" -f target="${TARGET}" + echo "[OK] Demo server update dispatched for ${TARGET} from ${REQUIRED_BRANCH}" - name: Summary run: | diff --git a/.github/workflows/update-demo-server.yml b/.github/workflows/update-demo-server.yml index f704228bb0..340c78e515 100644 --- a/.github/workflows/update-demo-server.yml +++ b/.github/workflows/update-demo-server.yml @@ -363,24 +363,24 @@ jobs: AUTH_USER="${DEMO_AUTH_USER:-demo}" AUTH_PASS="${DEMO_AUTH_PASS:-demo}" - NODES=$(ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \ + RESOURCES=$(ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \ 'AUTH_USER='"$(printf '%q' "$AUTH_USER")"' AUTH_PASS='"$(printf '%q' "$AUTH_PASS")"' DEMO_LOCAL_BASE_URL='"$(printf '%q' "$DEMO_LOCAL_BASE_URL")"' bash -s' <<'EOF' set -euo pipefail COOKIE_FILE="$HOME/.demo-cookies.txt" curl -fsS -c "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/login" -X POST \ -H "Content-Type: application/json" \ -d "{\"username\":\"${AUTH_USER}\",\"password\":\"${AUTH_PASS}\"}" > /dev/null - curl -fsS -b "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/state" | jq -r '.nodes | length' + curl -fsS -b "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/state" | jq -r '.resources | if type == "array" then length else 0 end' rm -f "$COOKIE_FILE" EOF ) - echo "Mock nodes detected: $NODES" + echo "Mock resources detected: $RESOURCES" - if [ "$NODES" -ge 1 ]; then + if [ "$RESOURCES" -ge 1 ]; then echo "Demo server successfully updated and verified." else - echo "::error::Demo server updated but mock mode may not be active" + echo "::error::Demo server updated but canonical mock resources are missing" exit 1 fi diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index abb6039f15..3a99951f0a 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -278,6 +278,11 @@ open on API-only reachability. That proof must treat the visible login controls as the readiness signal and must not block on Playwright `networkidle`, because the public demo shell can keep background activity alive after the page is already usable. +That same demo-update verification boundary also owns the canonical v6 mock +runtime state contract. `.github/workflows/update-demo-server.yml` must verify +mock-mode readiness through the unified `/api/state` `resources[]` collection, +not legacy `nodes`, because v6 intentionally strips per-type arrays from the +state payload. That same operator-proof boundary also now owns the canonical hosted staging smoke entrypoint. `scripts/run_hosted_staging_smoke.sh` must stay as the repo-tracked operator command that composes the hosted signup/billing eval pack @@ -293,6 +298,12 @@ that promotion metadata. Human-visible workflow inputs, summaries, and error messages must describe the path as a prerelease or preview flow rather than implying a near-ready release candidate, while machine-owned identifiers such as `rc`, `rc-to-ga-*`, and `v6.0.0-rc.1` remain the canonical internal keys. +That same downstream-dispatch boundary also owns release-ref fidelity. When +`.github/workflows/create-release.yml` fans out to governed post-publish +workflows such as Docker publication or demo updates, it must dispatch those +workflows on `needs.prepare.outputs.required_branch` rather than GitHub's +default-branch workflow definition, so prerelease automation cannot silently +fall back onto stale `main`-branch inputs or older demo verification logic. That same promotion-governance package also owns the dated rehearsal-record materialization path. The public recorder `scripts/release_control/record_rc_to_ga_rehearsal.py` and its internal module diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index f713f4dd1c..21a40322b4 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -223,6 +223,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn('REQUIRED_BRANCH: ${{ steps.branch_policy.outputs.required_branch }}', content) self.assertIn("resolve_release_promotion.py", content) self.assertIn("Rollback command:", content) + self.assertIn('gh workflow run publish-docker.yml --ref "${REQUIRED_BRANCH}"', content) + self.assertIn('gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}"', content) self.assertIn("rollback target and exact reinstall command recorded", policy) self.assertIn("rc-to-ga-rehearsal-summary", policy) self.assertIn("record_rc_to_ga_rehearsal.py --run-id ", policy) @@ -312,6 +314,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("DEMO_EXPECTED_HOSTNAME", demo) self.assertIn("Verify target host identity", demo) self.assertIn("Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.", demo) + self.assertIn(".resources | if type == \"array\" then length else 0 end", demo) + self.assertNotIn(".nodes | length", demo) + self.assertIn("Mock resources detected", demo) + self.assertIn("canonical mock resources are missing", demo) self.assertIn("Verify frontend parity", demo) self.assertIn("Verify public browser smoke", demo) self.assertIn("./scripts/run_demo_public_browser_smoke.sh", demo) @@ -336,7 +342,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("Refusing cross-line Helm pages release", helm_pages) self.assertIn('TARGET="preview-v6"', release_workflow) self.assertIn('TARGET="stable"', release_workflow) - self.assertIn('gh workflow run update-demo-server.yml -f tag="${{ needs.prepare.outputs.tag }}" -f target="${TARGET}"', release_workflow) + self.assertIn('gh workflow run update-demo-server.yml --ref "${REQUIRED_BRANCH}" -f tag="${{ needs.prepare.outputs.tag }}" -f target="${TARGET}"', release_workflow) self.assertIn("sync_chart_release_metadata.py", helm) self.assertIn("sync_chart_release_metadata.py", helm_pages) self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm)