From da4bd103c62299f7ebc7e3c5e258d860e8d01c0f Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Sat, 1 Aug 2026 18:34:45 -0700 Subject: [PATCH] fix(ci): support desktop release tags Signed-off-by: Brian Charbonneau --- .github/workflows/private-ca-release.yml | 47 +++++++++++++++++-- .../private-ca-desktop-release-lifecycle.md | 36 +++++++++----- scripts/normalize-private-ca-desktop-tag.sh | 19 ++++++++ scripts/test-private-ca-release-contract.sh | 27 +++++++++++ 4 files changed, 115 insertions(+), 14 deletions(-) create mode 100755 scripts/normalize-private-ca-desktop-tag.sh diff --git a/.github/workflows/private-ca-release.yml b/.github/workflows/private-ca-release.yml index b1c980a465..459c8119fd 100644 --- a/.github/workflows/private-ca-release.yml +++ b/.github/workflows/private-ca-release.yml @@ -38,7 +38,7 @@ jobs: set -euo pipefail release="$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/latest")" tag="$(jq -r '.tag_name' <<<"${release}")" - [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "not a stable tag: ${tag}" >&2; exit 1; } + tag="$(scripts/normalize-private-ca-desktop-tag.sh "${tag}")" existing="$(gh issue list --state all --search "[Buzz update] ${tag} available in:title" --json number --jq '.[0].number // empty')" [[ -z "${existing}" ]] || { echo "ticket already exists: #${existing}"; exit 0; } work="$(mktemp -d)"; trap 'rm -rf "${work}"' EXIT @@ -78,7 +78,7 @@ jobs: if [[ "${patch_status}" == conflict ]]; then labels+=(--label remediation-required) fi - gh issue create --title "[Buzz update] ${tag} available" "${labels[@]}" --body-file "${work}/issue.md" + gh issue create --title "[Buzz update] ${tag} available" "${labels[@]}" --assignee BrianInAz --body-file "${work}/issue.md" skip: if: >- @@ -97,6 +97,47 @@ jobs: gh issue comment "${{ github.event.issue.number }}" --body "Skipped by @BrianInAz; no private-CA package was built." gh issue close "${{ github.event.issue.number }}" + built: + if: >- + github.event_name == 'issues' && github.event.action == 'labeled' && + github.event.label.name == 'built' && github.actor == 'BrianInAz' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + issues: write + steps: + - name: Record the local package handoff + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + body="$(gh issue view "${ISSUE_NUMBER}" --json body --jq .body)" + grep -Eq '^$' <<<"${body}" || { echo "not a clean monitor-created ticket" >&2; exit 1; } + gh issue comment "${ISSUE_NUMBER}" --body "Built by @BrianInAz on the approved Apple Silicon Mac. Record the immutable artifact, checksum, private WSS gate, and local acceptance evidence before adding \`accepted\`." + + accepted: + if: >- + github.event_name == 'issues' && github.event.action == 'labeled' && + github.event.label.name == 'accepted' && github.actor == 'BrianInAz' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + issues: write + steps: + - name: Close an accepted release ticket + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + body="$(gh issue view "${ISSUE_NUMBER}" --json body --jq .body)" + grep -Eq '^$' <<<"${body}" || { echo "not a clean monitor-created ticket" >&2; exit 1; } + labels="$(gh issue view "${ISSUE_NUMBER}" --json labels --jq '.labels[].name')" + grep -Fxq built <<<"${labels}" || { echo "accepted requires the built lifecycle state" >&2; exit 1; } + gh issue comment "${ISSUE_NUMBER}" --body "Accepted by @BrianInAz after private WSS, state-preserving install, restart, reconnect, and history-restoration evidence was recorded." + gh issue close "${ISSUE_NUMBER}" + approval: if: >- github.event_name == 'issues' && github.event.action == 'labeled' && @@ -118,7 +159,7 @@ jobs: run: | set -euo pipefail body="$(gh issue view "${ISSUE_NUMBER}" --json body --jq .body)" - marker="$(grep -E '^$' <<<"${body}")" + marker="$(grep -E '^$' <<<"${body}")" [[ -n "${marker}" ]] || { echo "not a clean monitor-created ticket" >&2; exit 1; } tag="$(sed -E 's/.*tag=([^ ]+).*/\1/' <<<"${marker}")" source_sha="$(sed -E 's/.*source_sha=([^ ]+).*/\1/' <<<"${marker}")" diff --git a/docs/operations/private-ca-desktop-release-lifecycle.md b/docs/operations/private-ca-desktop-release-lifecycle.md index fcf38233d9..c7eb5f533f 100644 --- a/docs/operations/private-ca-desktop-release-lifecycle.md +++ b/docs/operations/private-ca-desktop-release-lifecycle.md @@ -61,14 +61,17 @@ that repository's normal `develop` then targeted `main` promotion flow. ## Durable task state -The private operational task is Beads `tailscale-vault-of5-213`, a child of -`tailscale-vault-of5-211.13`. Every stopping point records branch, SHA, tests, -URLs, blockers, and the exact next action there. +The authoritative cross-service process is tracked by Beads epic +`tailscale-vault-of5-213`. Buzz release-monitor repair and retirement work is +child `tailscale-vault-of5-213.1`. Every stopping point records branch, SHA, +tests, URLs, blockers, and the exact next action there. Historical delivery +evidence remains in the GitHub release tickets and must not be substituted for +the open retirement tracker. Before resuming, another agent must read, in order: 1. This document. -2. Beads `tailscale-vault-of5-213` and its parent. +2. Beads `tailscale-vault-of5-213` and child `tailscale-vault-of5-213.1`. 3. [BrianInAz/buzz#1](https://github.com/BrianInAz/buzz/issues/1). 4. The current branch, status, remotes, upstream activity, and CI results. @@ -77,14 +80,14 @@ directories or stale chat transcripts; request the approved plan. ## Daily release monitor -The fork workflow runs daily at 15:05 UTC and by manual dispatch. It observes -new stable desktop releases in `block/buzz`; prereleases require explicit -manual selection. +The fork workflow runs daily at 15:05 UTC and by manual dispatch. It accepts +both historical `vX.Y.Z` and current `desktop-vX.Y.Z` stable desktop release +tags from `block/buzz`. Relay tags, prereleases, and malformed tags fail closed. For a new immutable upstream release, it records the tag, source SHA, publication time, release notes, comparison link, private-patch applicability, -and any explicitly recorded upstream trust fix. It creates exactly one assigned -issue titled `[Buzz update] vX.Y.Z available` with labels: +and any explicitly recorded upstream trust fix. It creates exactly one issue +assigned to `BrianInAz`, titled `[Buzz update] available`, with labels: ```text buzz-update @@ -104,7 +107,17 @@ issue was generated by the monitor, the source tag still resolves to the recorded SHA, and the pinned patch applies cleanly. Public comments and issue text never become command input. `skip` closes the ticket without a build. A conflict or failed preflight applies `remediation-required` and publishes no -package. +package. `built` records the approved-Mac handoff but does not close the ticket. +`accepted` is valid only after `built`; it records the private WSS, install, +restart/reconnect, and history-restoration acceptance and then closes the +ticket. + +The monitor failure on +[run 30707522015](https://github.com/BrianInAz/buzz/actions/runs/30707522015) +triggered the tag-format correction: upstream's latest desktop release changed +to the `desktop-vX.Y.Z` namespace. After this correction is merged, a manual +dispatch must create exactly one unapproved `desktop-v0.5.3` decision ticket. +That dispatch does not authorize a package build or installation. ## Hosted validation and local package @@ -216,4 +229,5 @@ acceptance, rollback bundle, Beads evidence, and documentation are all green. - Official: `/Applications/Buzz.app/Contents/MacOS/buzz-desktop` (PID 41857, `xyz.block.buzz.app`) - Development: `target/debug/buzz-desktop` (PID 76852, `xyz.block.buzz.app.dev.fix-macos-private-ca-websocket`) - Rollback archive: `/Users/b/Applications/Buzz Rollback/v0.5.2/Buzz.app` - +- Lifecycle closeout: issue #3 is assigned to `BrianInAz`, labeled `built` and + `accepted`, and closed only after the evidence above is preserved. diff --git a/scripts/normalize-private-ca-desktop-tag.sh b/scripts/normalize-private-ca-desktop-tag.sh new file mode 100755 index 0000000000..87c1656897 --- /dev/null +++ b/scripts/normalize-private-ca-desktop-tag.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "$#" -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 64 +fi + +tag="$1" + +if [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || + [[ "${tag}" =~ ^desktop-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf '%s\n' "${tag}" + exit 0 +fi + +echo "not a stable Buzz Desktop tag: ${tag}" >&2 +exit 1 diff --git a/scripts/test-private-ca-release-contract.sh b/scripts/test-private-ca-release-contract.sh index a77046d132..b2a7785647 100755 --- a/scripts/test-private-ca-release-contract.sh +++ b/scripts/test-private-ca-release-contract.sh @@ -4,6 +4,7 @@ set -euo pipefail workflow=".github/workflows/private-ca-release.yml" local_builder="scripts/build-private-ca-macos.sh" +tag_parser="scripts/normalize-private-ca-desktop-tag.sh" if [[ ! -f "${workflow}" ]]; then echo "missing ${workflow}" >&2 @@ -44,6 +45,13 @@ require 'desktop/src-tauri/binaries' require 'gh issue close' require 'Local macOS package handoff' require 'scripts/build-private-ca-macos.sh' +require 'scripts/normalize-private-ca-desktop-tag.sh' +require '--assignee BrianInAz' +require 'ticket already exists' +require "github.event.label.name == 'built'" +require "github.event.label.name == 'accepted'" +require 'accepted requires the built lifecycle state' +require 'not a clean monitor-created ticket' forbid 'BUZZ_TEST_WSS_URL' forbid 'buzz.bjzy.me' @@ -61,6 +69,25 @@ if [[ ! -x "${local_builder}" ]]; then exit 1 fi +if [[ ! -x "${tag_parser}" ]]; then + echo "missing executable ${tag_parser}" >&2 + exit 1 +fi + +for tag in v0.5.2 desktop-v0.5.3; do + if [[ "$("${tag_parser}" "${tag}")" != "${tag}" ]]; then + echo "${tag_parser} must accept stable desktop tag ${tag}" >&2 + exit 1 + fi +done + +for tag in relay-v0.5.3 desktop-v0.5.3-rc.1 v0.5.3-beta.1 nonsense; do + if "${tag_parser}" "${tag}" >/dev/null 2>&1; then + echo "${tag_parser} must reject non-stable desktop tag ${tag}" >&2 + exit 1 + fi +done + for expected in 'set -euo pipefail' 'createUpdaterArtifacts": false' 'codesign --verify --deep --strict' 'hdiutil create'; do if ! grep -F -q -- "${expected}" "${local_builder}"; then echo "${local_builder} must contain: ${expected}" >&2