ci: add release backfill and dmg diagnostics (#1570)

This commit is contained in:
Carlos Florêncio
2026-07-02 11:25:05 +01:00
committed by GitHub
parent e99f81394e
commit a3bbb999ba
6 changed files with 450 additions and 14 deletions
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
label="${1:-failure}"
platform="${DESKTOP_DIAGNOSTICS_PLATFORM:?DESKTOP_DIAGNOSTICS_PLATFORM is required}"
rust_target="${DESKTOP_DIAGNOSTICS_RUST_TARGET:?DESKTOP_DIAGNOSTICS_RUST_TARGET is required}"
diag_dir="$GITHUB_WORKSPACE/dist/desktop-diagnostics"
bundle_root="$GITHUB_WORKSPACE/apps/desktop/src-tauri/target/${rust_target}/release/bundle"
mkdir -p "$diag_dir"
if [ ! -d "$bundle_root" ]; then
bundle_root="$GITHUB_WORKSPACE/apps/desktop/src-tauri/target/release/bundle"
fi
{
echo "label=$label"
echo "platform=$platform"
echo "rust_target=$rust_target"
echo "date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo
echo "## system"
uname -a || true
sw_vers || true
echo
echo "## disk"
df -h || true
echo
echo "## hdiutil info"
hdiutil info || true
echo
echo "## mounts"
mount || true
echo
echo "## related processes"
ps aux | grep -E 'hdiutil|diskimages|bundle_dmg|pnpm|cargo' | grep -v grep || true
echo
echo "## bundle files"
if [ -d "$bundle_root" ]; then
find "$bundle_root" -maxdepth 5 -print || true
else
echo "Missing bundle root: $bundle_root"
fi
} > "$diag_dir/macos-${platform}-${label}.txt"
if [ -f "$bundle_root/dmg/bundle_dmg.sh" ]; then
cp "$bundle_root/dmg/bundle_dmg.sh" "$diag_dir/bundle_dmg-${platform}-${label}.sh" || true
fi
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Contract tests for the maintainer release workflow."""
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "release.yml"
DIAGNOSTICS_PATH = REPO_ROOT / ".github" / "scripts" / "collect-macos-desktop-diagnostics.sh"
WORKFLOW = WORKFLOW_PATH.read_text()
DIAGNOSTICS = DIAGNOSTICS_PATH.read_text()
def step_block(name: str) -> str:
marker = f" - name: {name}"
start = WORKFLOW.find(marker)
if start == -1:
raise AssertionError(f"step not found: {name}")
next_step = re.search(r"\n - (?:name|uses): ", WORKFLOW[start + 1 :])
end = len(WORKFLOW) if next_step is None else start + 1 + next_step.start()
return WORKFLOW[start:end]
def job_block(name: str) -> str:
marker = f" {name}:"
start = WORKFLOW.find(marker)
if start == -1:
raise AssertionError(f"job not found: {name}")
next_job = re.search(r"\n [a-zA-Z0-9_-]+:\n", WORKFLOW[start + 1 :])
end = len(WORKFLOW) if next_job is None else start + 1 + next_job.start()
return WORKFLOW[start:end]
class ReleaseWorkflowContractTest(unittest.TestCase):
def test_backfill_tag_input_uses_existing_tag_without_recreating_it(self) -> None:
self.assertIn("backfill_tag:", WORKFLOW)
self.assertIn("BACKFILL_TAG: ${{ inputs.backfill_tag }}", WORKFLOW)
self.assertIn("Backfill existing release tag:", step_block("Compute next version"))
self.assertIn("backfill_tag is set; the 'bump' input", WORKFLOW)
self.assertIn("backfill_tag cannot be used: no existing release tags found.", WORKFLOW)
self.assertIn("must be the latest release tag", WORKFLOW)
self.assertIn("(expected ${next})", WORKFLOW)
self.assertIn('BACKFILL_REF="refs/tags/$BACKFILL_TAG"', WORKFLOW)
self.assertIn('echo "ref=$BACKFILL_REF" >> "$GITHUB_OUTPUT"', WORKFLOW)
self.assertIn('echo "ref=refs/tags/$TAG" >> "$GITHUB_OUTPUT"', WORKFLOW)
self.assertNotIn("ref: ${{ needs.prepare.outputs.tag }}", WORKFLOW)
for path in (
"apps/cli/package.json",
"apps/desktop/package.json",
"apps/desktop/src-tauri/tauri.conf.json",
"apps/desktop/src-tauri/Cargo.toml",
"apps/desktop/src-tauri/Cargo.lock",
):
self.assertIn(path, WORKFLOW)
for name in (
"Bump version + generate CHANGELOG (in working tree)",
"Create release PR + squash-merge + tag",
):
self.assertIn("inputs.backfill_tag == ''", step_block(name))
def test_backfill_tag_still_runs_build_and_publish_jobs(self) -> None:
for name in (
"build-web",
"build-bundles",
"build-desktop",
"docker-amd64",
"docker-arm64",
"docker-manifest",
"docker-universal-amd64",
"docker-universal-arm64",
"docker-universal-manifest",
"publish-release",
"publish-npm",
"update-homebrew-tap",
):
block = job_block(name)
self.assertIn("if: ${{ !inputs.dry_run", block)
self.assertNotIn("inputs.backfill_tag == ''", block)
def test_macos_dmg_build_has_retry_timeout_and_diagnostics(self) -> None:
build = step_block("Build Tauri desktop app")
self.assertIn("timeout-minutes: 70", build)
self.assertIn("TAURI_BUNDLE_ATTEMPTS:", build)
self.assertIn("run_with_timeout", build)
self.assertIn("collect_macos_desktop_diagnostics", build)
self.assertIn("DESKTOP_DIAGNOSTICS_SCRIPT", build)
self.assertIn("terminate_process_tree_with_signal", build)
self.assertIn("else\n status=$?", build)
self.assertIn("for attempt in", build)
helper = step_block("Prepare macOS desktop diagnostics helper")
self.assertIn("continue-on-error: true", helper)
self.assertIn("github.workflow_sha", helper)
self.assertIn("DESKTOP_DIAGNOSTICS_SCRIPT=$helper", helper)
collect = step_block("Collect macOS desktop diagnostics")
self.assertIn("if: failure() && startsWith(matrix.platform, 'macos-')", collect)
self.assertIn("DESKTOP_DIAGNOSTICS_SCRIPT", collect)
upload = step_block("Upload macOS desktop diagnostics")
self.assertIn("if: failure() && startsWith(matrix.platform, 'macos-')", upload)
self.assertIn("dist/desktop-diagnostics/**", upload)
self.assertIn("/release/bundle/dmg/**", upload)
self.assertIn("hdiutil info", DIAGNOSTICS)
self.assertIn("df -h", DIAGNOSTICS)
self.assertIn("bundle_dmg.sh", DIAGNOSTICS)
self.assertIn('cp "$bundle_root/dmg/bundle_dmg.sh"', DIAGNOSTICS)
self.assertIn("|| true", DIAGNOSTICS)
if __name__ == "__main__":
unittest.main()
@@ -7,12 +7,16 @@ on:
- ".github/workflows/**"
- ".github/scripts/lint-action-pinning.py"
- ".github/scripts/lint-action-pinning_test.py"
- ".github/scripts/collect-macos-desktop-diagnostics.sh"
- ".github/scripts/release-workflow-contract_test.py"
pull_request:
branches: [main]
paths:
- ".github/workflows/**"
- ".github/scripts/lint-action-pinning.py"
- ".github/scripts/lint-action-pinning_test.py"
- ".github/scripts/collect-macos-desktop-diagnostics.sh"
- ".github/scripts/release-workflow-contract_test.py"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -36,5 +40,8 @@ jobs:
- name: Test action-pinning lint
run: python3 .github/scripts/lint-action-pinning_test.py
- name: Test release workflow contract
run: python3 .github/scripts/release-workflow-contract_test.py
- name: Verify action refs
run: python3 .github/scripts/lint-action-pinning.py
+245 -14
View File
@@ -52,6 +52,11 @@ on:
description: "Build desktop artifacts only (no PR, tag, publish, npm, or tap update)"
type: boolean
default: false
backfill_tag:
description: "Existing release tag to rebuild/publish (for recovery; leave empty for normal release)"
required: false
default: ""
type: string
# Default to read-only at the workflow level. Each job opts into the writes
# it actually needs.
@@ -109,11 +114,104 @@ jobs:
- name: Compute next version
id: compute
env:
BACKFILL_TAG: ${{ inputs.backfill_tag }}
BUMP: ${{ inputs.bump }}
DRY_RUN: ${{ inputs.dry_run }}
DESKTOP_VALIDATION_ONLY: ${{ inputs.desktop_validation_only }}
CURRENT_REF: ${{ github.sha }}
run: |
set -euo pipefail
if [ -n "$BACKFILL_TAG" ]; then
echo "::notice::backfill_tag is set; the 'bump' input ($BUMP) is ignored."
if [ "$DRY_RUN" = "true" ]; then
echo "backfill_tag cannot be combined with dry_run." >&2
exit 1
fi
if [ "$DESKTOP_VALIDATION_ONLY" = "true" ]; then
echo "backfill_tag cannot be combined with desktop_validation_only." >&2
exit 1
fi
case "$BACKFILL_TAG" in
refs/tags/*) BACKFILL_TAG="${BACKFILL_TAG#refs/tags/}" ;;
esac
if [[ ! "$BACKFILL_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "backfill_tag must be a SemVer release tag like v1.2.3." >&2
exit 1
fi
BACKFILL_REF="refs/tags/$BACKFILL_TAG"
if ! git rev-parse --verify --quiet "${BACKFILL_REF}^{commit}" >/dev/null; then
echo "backfill_tag does not exist: $BACKFILL_TAG" >&2
exit 1
fi
NEXT="${BACKFILL_TAG#v}"
LATEST_RELEASE_TAG="$(git tag --list 'v*' | python3 -c "
import sys, re
best = None
best_tag = ''
for t in (l.strip() for l in sys.stdin if l.strip()):
s = t.lstrip('v')
m3 = re.match(r'^(\d+)\.(\d+)\.(\d+)$', s)
m2 = re.match(r'^(\d+)\.(\d+)$', s)
if m3:
v = tuple(int(x) for x in m3.groups())
tag = f'v{v[0]}.{v[1]}.{v[2]}'
elif m2:
v = (int(m2.group(1)), int(m2.group(2)), 0)
tag = f'v{v[0]}.{v[1]}.{v[2]}'
else:
continue
if best is None or v > best:
best = v
best_tag = tag
print(best_tag)
")"
if [ -z "$LATEST_RELEASE_TAG" ]; then
echo "backfill_tag cannot be used: no existing release tags found." >&2
exit 1
fi
if [ "$LATEST_RELEASE_TAG" != "$BACKFILL_TAG" ]; then
echo "backfill_tag must be the latest release tag ($LATEST_RELEASE_TAG) because backfill publishes mutable channels." >&2
exit 1
fi
BACKFILL_REF="$BACKFILL_REF" BACKFILL_TAG="$BACKFILL_TAG" NEXT="$NEXT" node <<'NODE'
const { execFileSync } = require("node:child_process");
const tag = process.env.BACKFILL_TAG;
const ref = process.env.BACKFILL_REF;
const next = process.env.NEXT;
const show = (path) => execFileSync("git", ["show", `${ref}:${path}`], { encoding: "utf8" });
const versions = new Map([
["apps/cli/package.json", JSON.parse(show("apps/cli/package.json")).version],
["apps/desktop/package.json", JSON.parse(show("apps/desktop/package.json")).version],
["apps/desktop/src-tauri/tauri.conf.json", JSON.parse(show("apps/desktop/src-tauri/tauri.conf.json")).version],
["apps/desktop/src-tauri/Cargo.toml", show("apps/desktop/src-tauri/Cargo.toml").match(/^version = "([^"]+)"/m)?.[1]],
[
"apps/desktop/src-tauri/Cargo.lock",
show("apps/desktop/src-tauri/Cargo.lock").match(/\[\[package\]\]\nname = "kandev-desktop"\nversion = "([^"]+)"/)?.[1],
],
]);
for (const [path, version] of versions) {
if (version !== next) {
console.error(`backfill_tag version mismatch: ${tag} points to ${path} version ${version ?? "<missing>"} (expected ${next}).`);
process.exit(1);
}
}
NODE
TAG="$BACKFILL_TAG"
echo "Backfill existing release tag: $TAG"
echo "Next version: $NEXT"
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "ref=$BACKFILL_REF" >> "$GITHUB_OUTPUT"
exit 0
fi
PKG_VERSION="$(node -p "require('./apps/cli/package.json').version")"
# Find latest git tag, normalizing legacy two-part tags (vM.m → M.m.0)
@@ -164,7 +262,7 @@ jobs:
if [ "$DESKTOP_VALIDATION_ONLY" = "true" ]; then
echo "ref=$CURRENT_REF" >> "$GITHUB_OUTPUT"
else
echo "ref=$TAG" >> "$GITHUB_OUTPUT"
echo "ref=refs/tags/$TAG" >> "$GITHUB_OUTPUT"
fi
# Sanity: tag must not already exist
@@ -178,7 +276,7 @@ jobs:
fi
- name: Bump version + generate CHANGELOG (in working tree)
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only }}
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only && inputs.backfill_tag == '' }}
env:
NEXT: ${{ steps.compute.outputs.version }}
TAG: ${{ steps.compute.outputs.tag }}
@@ -216,7 +314,7 @@ jobs:
git-cliff --tag "$TAG" -o CHANGELOG.md
- name: Create release PR + squash-merge + tag
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only }}
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only && inputs.backfill_tag == '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEXT: ${{ steps.compute.outputs.version }}
@@ -482,6 +580,34 @@ jobs:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- name: Prepare macOS desktop diagnostics helper
if: startsWith(matrix.platform, 'macos-')
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
helper="$RUNNER_TEMP/collect-macos-desktop-diagnostics.sh"
mkdir -p "$(dirname "$helper")"
if [ -f "$GITHUB_WORKSPACE/.github/scripts/collect-macos-desktop-diagnostics.sh" ]; then
cp "$GITHUB_WORKSPACE/.github/scripts/collect-macos-desktop-diagnostics.sh" "$helper"
else
encoded_helper="$RUNNER_TEMP/collect-macos-desktop-diagnostics.sh.b64"
gh api \
"repos/${{ github.repository }}/contents/.github/scripts/collect-macos-desktop-diagnostics.sh?ref=${{ github.workflow_sha }}" \
--jq .content > "$encoded_helper"
HELPER="$helper" ENCODED_HELPER="$encoded_helper" python3 - <<'PY'
import base64
import os
from pathlib import Path
encoded = Path(os.environ["ENCODED_HELPER"]).read_text()
Path(os.environ["HELPER"]).write_bytes(base64.b64decode(encoded))
PY
fi
chmod +x "$helper"
echo "DESKTOP_DIAGNOSTICS_SCRIPT=$helper" >> "$GITHUB_ENV"
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
version: "9.15.9"
@@ -640,8 +766,13 @@ jobs:
}
- name: Build Tauri desktop app
timeout-minutes: 70
env:
CI: "true"
DESKTOP_DIAGNOSTICS_PLATFORM: ${{ matrix.platform }}
DESKTOP_DIAGNOSTICS_RUST_TARGET: ${{ matrix.rust_target }}
TAURI_BUNDLE_ATTEMPTS: ${{ startsWith(matrix.platform, 'macos-') && '2' || '1' }}
TAURI_BUILD_TIMEOUT_SECONDS: ${{ startsWith(matrix.platform, 'macos-') && '1800' || '0' }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
@@ -676,10 +807,110 @@ jobs:
fi
pnpm -C apps --filter @kandev/desktop build:vite
cd apps/desktop
pnpm tauri build \
--features desktop-runtime \
--target "${{ matrix.rust_target }}" \
--bundles "${{ matrix.tauri_bundles }}"
collect_macos_desktop_diagnostics() {
local label="${1:-failure}"
if [[ "${{ matrix.platform }}" != macos-* ]]; then
return 0
fi
bash "${DESKTOP_DIAGNOSTICS_SCRIPT:-$GITHUB_WORKSPACE/.github/scripts/collect-macos-desktop-diagnostics.sh}" "$label" || true
}
terminate_process_tree() {
local pid="$1"
terminate_process_tree_with_signal "$pid" TERM
sleep 5
terminate_process_tree_with_signal "$pid" KILL
}
terminate_process_tree_with_signal() {
local pid="$1"
local signal="$2"
local child
while IFS= read -r child; do
[ -n "$child" ] || continue
terminate_process_tree_with_signal "$child" "$signal"
done < <(pgrep -P "$pid" 2>/dev/null || true)
kill "-$signal" "$pid" 2>/dev/null || true
}
run_with_timeout() {
local timeout_seconds="$1"
shift
if [ "$timeout_seconds" -le 0 ]; then
"$@"
return $?
fi
"$@" &
local command_pid=$!
local start_seconds
start_seconds="$(date +%s)"
while kill -0 "$command_pid" 2>/dev/null; do
local now_seconds
now_seconds="$(date +%s)"
if [ $((now_seconds - start_seconds)) -ge "$timeout_seconds" ]; then
echo "::warning::Command timed out after ${timeout_seconds}s: $*"
collect_macos_desktop_diagnostics "timeout"
terminate_process_tree "$command_pid"
wait "$command_pid" 2>/dev/null || true
return 124
fi
sleep 10
done
wait "$command_pid"
}
run_tauri_build() {
run_with_timeout "$TAURI_BUILD_TIMEOUT_SECONDS" \
pnpm tauri build \
--features desktop-runtime \
--target "${{ matrix.rust_target }}" \
--bundles "${{ matrix.tauri_bundles }}"
}
status=0
for attempt in $(seq 1 "$TAURI_BUNDLE_ATTEMPTS"); do
echo "Tauri desktop build attempt $attempt/$TAURI_BUNDLE_ATTEMPTS"
if run_tauri_build; then
status=0
break
else
status=$?
fi
echo "::warning::Tauri desktop build attempt $attempt/$TAURI_BUNDLE_ATTEMPTS failed with exit code $status."
collect_macos_desktop_diagnostics "attempt-${attempt}"
if [ "$attempt" -lt "$TAURI_BUNDLE_ATTEMPTS" ]; then
sleep 20
fi
done
exit "$status"
- name: Collect macOS desktop diagnostics
if: failure() && startsWith(matrix.platform, 'macos-')
env:
DESKTOP_DIAGNOSTICS_PLATFORM: ${{ matrix.platform }}
DESKTOP_DIAGNOSTICS_RUST_TARGET: ${{ matrix.rust_target }}
run: |
set -euo pipefail
bash "${DESKTOP_DIAGNOSTICS_SCRIPT:-$GITHUB_WORKSPACE/.github/scripts/collect-macos-desktop-diagnostics.sh}" failure || true
- name: Upload macOS desktop diagnostics
if: failure() && startsWith(matrix.platform, 'macos-')
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: desktop-diagnostics-${{ matrix.platform }}
path: |
dist/desktop-diagnostics/**
apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/dmg/**
apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos/**
apps/desktop/src-tauri/target/release/bundle/dmg/**
apps/desktop/src-tauri/target/release/bundle/macos/**
if-no-files-found: ignore
include-hidden-files: true
- name: Sign Windows desktop artifacts
if: matrix.platform == 'windows-x64' && env.WINDOWS_SIGNING_ENABLED == 'true'
@@ -813,7 +1044,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
@@ -884,7 +1115,7 @@ jobs:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
@@ -997,7 +1228,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
@@ -1049,7 +1280,7 @@ jobs:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
@@ -1152,7 +1383,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
fetch-depth: 0
- name: Generate release notes
@@ -1258,7 +1489,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
@@ -1290,7 +1521,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
- name: Update Homebrew tap formula
env:
@@ -0,0 +1,33 @@
# 0029: Release Backfill and Desktop Diagnostics
**Status:** accepted
**Date:** 2026-07-01
**Area:** infra, workflow
## Context
The release workflow can create and push a version tag before all platform artifacts have been built and published. If a later artifact job fails, a normal rerun from `workflow_dispatch` cannot safely resume the release because the tag already exists and the prepare job is intentionally non-idempotent for version bumping and tag creation.
The first desktop macOS failure mode we observed happened after Rust compilation completed, while Tauri was bundling the DMG. The GitHub Actions log ended at `bundle_dmg.sh` with little nested stderr. A Linux CI container image cannot remove this class of failure because DMG packaging must run on native macOS runners.
## Decision
Add a maintainer-only release backfill mode to `.github/workflows/release.yml`. The `backfill_tag` input accepts the current highest existing `vX.Y.Z` release tag, verifies that the tag exists, and verifies that all release-critical manifests at that tag match the version. It then uses the existing tag as the build and publish ref. In this mode the workflow skips version bumping, changelog generation, release PR creation, and tag creation, while still running the build, GitHub release, npm publish, and Homebrew update jobs.
Add macOS desktop packaging guardrails to the Tauri build job. macOS DMG builds now run with a bounded timeout and a retry, and failed macOS desktop builds upload diagnostics that include disk state, `hdiutil info`, related packaging processes, bundle directory listings, and Tauri's generated `bundle_dmg.sh` when present.
Keep desktop installer builds on native OS runners. Linux container images remain useful for Linux CI and e2e dependency stability, but they are not the mechanism for macOS DMG recovery.
## Consequences
Release recovery becomes explicit and repeatable for tag-only or partially published current releases. Publishing scripts can keep their existing idempotent behavior, so rerunning a backfill is safe when some assets or packages already exist. Older tags are rejected because this workflow still updates mutable channels such as Docker tags, npm dist-tags, and the Homebrew formula.
The release workflow has more branching logic, and maintainers must choose between normal release, desktop validation, dry run, and tag backfill modes intentionally. The workflow contract is covered by a small CI test so later YAML changes do not silently remove the recovery path or macOS diagnostics.
macOS package failures should now leave actionable artifacts instead of only the final Tauri command line. The retry may add time to a failing macOS job, but the step-level timeout caps total time.
## Alternatives Considered
1. **Rerun the normal release workflow.** Rejected because the existing tag makes prepare fail before artifact rebuild and publish jobs can run.
2. **Create a separate duplicate backfill workflow.** Rejected because it would duplicate the large release build/publish graph and increase drift risk.
3. **Use a custom Linux Docker image for release packaging.** Rejected for macOS DMG recovery because native DMG packaging requires macOS runners. This can still help Linux jobs that fail while downloading build dependencies, but it does not address the observed release failure.
+1
View File
@@ -34,3 +34,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 0026 | [Tauri desktop shell over native runtime](0026-tauri-desktop-shell.md) | accepted | frontend, backend, cli, infra | 2026-06-23 |
| 0027 | [Replayable schema migrations across SQLite and Postgres](0027-replayable-schema-migrations.md) | accepted | backend | 2026-06-24 |
| 0028 | [Backend-owned task-create last-used preferences](0028-task-create-last-used-source-of-truth.md) | accepted | backend, frontend | 2026-06-29 |
| 0029 | [Release backfill and desktop diagnostics](0029-release-backfill-and-desktop-diagnostics.md) | accepted | infra, workflow | 2026-07-01 |