mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
ci(release): gate OSS desktop auto-update promotion (#5398)
## Summary Separate OSS desktop artifact publication from fleet-wide auto-update promotion. - retain the exact generated updater manifest as `updater-manifest.json` on each immutable `desktop-vX.Y.Z` release - stop the tag-triggered build from mutating `buzz-desktop-latest/latest.json` - add a `main`-only manual promotion workflow with one global concurrency group - validate stable semver, release/tag commit identity, draft/prerelease state, exact platform set, signatures, version-bound asset URLs, asset existence, monotonicity, idempotent retries, and a final stale-state check before writing - document the operator flow and pin the split with focused contract tests ## Safety behavior Publishing a versioned GitHub release no longer exposes it through the in-app updater. Operators can install and test those exact signed/notarized artifacts, then manually run **Promote OSS Desktop Auto-Update** with the stable version. Promotion rejects downgrades. A same-version retry succeeds only when the rolling and candidate manifests are byte-identical. The workflow re-reads the current rolling version immediately before its only write and records the actor, source tag commit, previous version, manifest digest, and run URL. ## Verification Verified at commit `39caf1603be06bb476905225ec55f7bbbe86b237`: ```text scripts/test-oss-desktop-promotion.sh OSS desktop promotion contract passed scripts/test-release-ref-contract.sh release ref contract passed git diff --check origin/main...HEAD (clean) ``` The repository pre-push hook also passed `branch-skew` for the exact pushed head; package suites were correctly skipped because this change only touches release workflows, scripts, and documentation. Originating conversation: Buzz channel `separate-publish-step-release`, thread `8857ce8bbe928e891165eddcf06c666cf6eae16181c3f02a6d8c396d8a536026`. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -78,6 +78,10 @@ jobs:
|
||||
run: scripts/test-release-ref-contract.sh
|
||||
- name: Desktop release candidate contract
|
||||
run: scripts/test-desktop-release-candidate.sh
|
||||
- name: OSS desktop promotion contract
|
||||
run: |
|
||||
scripts/test-oss-desktop-promotion.sh
|
||||
scripts/test-oss-desktop-promotion-behavior.sh
|
||||
- name: Mobile release contract
|
||||
run: |
|
||||
scripts/test-mobile-release-contract.sh
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Promote OSS Desktop Auto-Update
|
||||
run-name: Promote desktop-v${{ inputs.version }} to auto-update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Stable desktop version to promote (X.Y.Z)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: oss-desktop-auto-update-promotion
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Require the reviewed workflow from main
|
||||
env:
|
||||
DISPATCH_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [ "$DISPATCH_REF" != "refs/heads/main" ]; then
|
||||
echo "::error::OSS desktop promotion must be dispatched from main, not $DISPATCH_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate and promote exact release manifest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: scripts/promote-oss-desktop-release.sh "$VERSION"
|
||||
@@ -907,6 +907,7 @@ jobs:
|
||||
[ "${#TRIPLES[@]}" -ge 3 ] || { echo "::error::too few platforms (${#TRIPLES[@]})"; exit 1; }
|
||||
bash desktop/scripts/generate-oss-latest-json.sh "$VERSION" "${TRIPLES[@]}" > latest.json
|
||||
cat latest.json
|
||||
cp latest.json staged/updater-manifest.json
|
||||
|
||||
- name: Create or verify versioned draft
|
||||
run: |
|
||||
@@ -946,7 +947,3 @@ jobs:
|
||||
- name: Publish complete versioned release
|
||||
if: env.already_published != 'true'
|
||||
run: gh release edit "desktop-v${VERSION}" --draft=false
|
||||
|
||||
- name: Upload latest.json to rolling release last
|
||||
if: ${{ !contains(needs.setup.outputs.version, '-') }}
|
||||
run: gh release upload buzz-desktop-latest latest.json --clobber
|
||||
|
||||
+23
-6
@@ -173,10 +173,10 @@ for distributable builds or builds from an immutable release tag.
|
||||
`release.yml` has no manual dispatch and cannot build from `main` or another
|
||||
caller-selected ref. If a run for an existing immutable
|
||||
`desktop-v<version>` tag fails, rerun that failed workflow from GitHub Actions
|
||||
(or use `gh run rerun <run-id> --failed --repo block/buzz`). A stable rerun also
|
||||
repairs `buzz-desktop-latest/latest.json` if the original run published the
|
||||
versioned release but failed during that final rolling-manifest upload. Do not
|
||||
recreate, move, or push the immutable tag again.
|
||||
(or use `gh run rerun <run-id> --failed --repo block/buzz`). A rerun
|
||||
repairs the versioned draft if publication did not complete. It does not
|
||||
promote that version to the auto-updater; promotion is a separate manual
|
||||
action. Do not recreate, move, or push the immutable tag again.
|
||||
|
||||
Mobile intentionally has no branch or arbitrary-ref fallback. The private
|
||||
Buildkite pipeline accepts only an exact candidate tag.
|
||||
@@ -200,8 +200,25 @@ for the rest of the private pipeline contract.
|
||||
|
||||
Desktop publishes two GitHub releases:
|
||||
|
||||
1. **`desktop-v<version>`**: the user-facing release with installers.
|
||||
2. **`buzz-desktop-latest`**: the rolling auto-updater release.
|
||||
1. **`desktop-v<version>`**: the user-facing release with installers and the
|
||||
exact `updater-manifest.json` promotion candidate. Publishing this release
|
||||
does not expose it through in-app auto-update.
|
||||
2. **`buzz-desktop-latest`**: the rolling auto-updater release. Its
|
||||
`latest.json` changes only through the manual promotion workflow.
|
||||
|
||||
### Promote an OSS desktop release to auto-update
|
||||
|
||||
After installing and testing the published `desktop-v<version>` artifacts, run
|
||||
**Promote OSS Desktop Auto-Update** from the `main` branch and enter the exact
|
||||
stable `X.Y.Z` version. The workflow validates the immutable tag and release,
|
||||
the retained manifest and every referenced updater asset, and requires the
|
||||
version to be newer than the currently promoted version before replacing
|
||||
`buzz-desktop-latest/latest.json`. Same-version retries succeed only when the
|
||||
manifest is identical; downgrades are rejected.
|
||||
|
||||
Withholding promotion leaves existing clients on the previous version. If a
|
||||
promoted release is bad, ship and promote a higher patch version; changing the
|
||||
manifest to an older version does not downgrade clients that already updated.
|
||||
|
||||
Mobile publishes only annotated `mobile-vX.Y.Z-rc.N` git tags. Store artifacts
|
||||
and rollout records retain the exact tag they used. Mobile does not publish a
|
||||
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-}"
|
||||
REPOSITORY="${GITHUB_REPOSITORY:-block/buzz}"
|
||||
TAG="desktop-v${VERSION}"
|
||||
CANDIDATE="updater-manifest.json"
|
||||
ROLLING_TAG="buzz-desktop-latest"
|
||||
EXPECTED_PLATFORMS='["darwin-aarch64","darwin-x86_64","linux-x86_64","windows-x86_64"]'
|
||||
|
||||
fail() { echo "::error::$*" >&2; exit 1; }
|
||||
[[ "$REPOSITORY" == "block/buzz" ]] || fail "promotion is restricted to block/buzz"
|
||||
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version must be stable semver X.Y.Z"
|
||||
command -v gh >/dev/null || fail "gh is required"
|
||||
command -v jq >/dev/null || fail "jq is required"
|
||||
|
||||
workdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$workdir"' EXIT
|
||||
candidate="$workdir/$CANDIDATE"
|
||||
current="$workdir/latest.json"
|
||||
|
||||
release_json="$(gh release view "$TAG" --repo "$REPOSITORY" --json isDraft,isPrerelease,targetCommitish,assets)"
|
||||
[[ "$(jq -r .isDraft <<<"$release_json")" == false ]] || fail "$TAG is still a draft"
|
||||
[[ "$(jq -r .isPrerelease <<<"$release_json")" == false ]] || fail "$TAG is a prerelease"
|
||||
|
||||
tag_sha="$(gh api "repos/$REPOSITORY/commits/$TAG" --jq .sha)"
|
||||
target="$(jq -r .targetCommitish <<<"$release_json")"
|
||||
target_sha="$(gh api "repos/$REPOSITORY/commits/$target" --jq .sha)"
|
||||
[[ -n "$tag_sha" && "$target_sha" == "$tag_sha" ]] || fail "$TAG and its release target do not resolve to the same commit"
|
||||
|
||||
release_assets="$(jq -r '.assets[].name' <<<"$release_json")"
|
||||
grep -Fxq "$CANDIDATE" <<<"$release_assets" || fail "$TAG has no $CANDIDATE asset"
|
||||
gh release download "$TAG" --repo "$REPOSITORY" --pattern "$CANDIDATE" --dir "$workdir"
|
||||
|
||||
jq -e --arg version "$VERSION" --argjson expected "$EXPECTED_PLATFORMS" '
|
||||
.version == $version and
|
||||
(.platforms | keys == $expected) and
|
||||
([.platforms[] | (.signature | type == "string" and length > 0)] | all) and
|
||||
([.platforms[] | (.url | type == "string" and startswith("https://github.com/block/buzz/releases/download/desktop-v" + $version + "/"))] | all)
|
||||
' "$candidate" >/dev/null || fail "$CANDIDATE failed version, platform, signature, or URL validation"
|
||||
|
||||
while IFS= read -r url; do
|
||||
asset="${url##*/}"
|
||||
[[ "$url" == "https://github.com/block/buzz/releases/download/$TAG/$asset" ]] || fail "$CANDIDATE contains non-canonical updater URL: $url"
|
||||
grep -Fxq "$asset" <<<"$release_assets" || fail "$CANDIDATE references missing release asset: $asset"
|
||||
done < <(jq -r '.platforms[].url' "$candidate")
|
||||
|
||||
gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"
|
||||
current_digest="$(sha256sum "$current" | awk '{print $1}')"
|
||||
current_version="$(jq -er '.version | select(type == "string")' "$current")" || fail "current latest.json has no version"
|
||||
[[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "current promoted version is not stable semver: $current_version"
|
||||
|
||||
highest="$(printf '%s\n%s\n' "$current_version" "$VERSION" | sort -V | tail -1)"
|
||||
if [[ "$VERSION" == "$current_version" ]]; then
|
||||
cmp -s "$candidate" "$current" || fail "$VERSION is already promoted with different manifest content"
|
||||
echo "Version $VERSION is already promoted with identical manifest content."
|
||||
exit 0
|
||||
fi
|
||||
[[ "$highest" == "$VERSION" ]] || fail "refusing downgrade from $current_version to $VERSION"
|
||||
|
||||
# Re-read immediately before the only write so a stale validation cannot silently
|
||||
# overwrite a promotion performed outside this workflow.
|
||||
rm -f "$current"
|
||||
gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"
|
||||
[[ "$(sha256sum "$current" | awk '{print $1}')" == "$current_digest" ]] || fail "current promotion changed during validation; retry"
|
||||
|
||||
promotion="$workdir/latest.json"
|
||||
cp "$candidate" "$promotion"
|
||||
candidate_digest="$(sha256sum "$candidate" | awk '{print $1}')"
|
||||
if ! gh release upload "$ROLLING_TAG" "$promotion" --repo "$REPOSITORY" --clobber; then
|
||||
fail "promotion upload failed; latest.json may be temporarily unavailable, retry the promotion"
|
||||
fi
|
||||
rm -f "$promotion"
|
||||
if ! gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"; then
|
||||
fail "promotion upload returned success but latest.json could not be verified; retry the promotion"
|
||||
fi
|
||||
[[ "$(sha256sum "$promotion" | awk '{print $1}')" == "$candidate_digest" ]] || fail "served latest.json does not match the promoted candidate; retry the promotion"
|
||||
{
|
||||
echo "### OSS desktop auto-update promoted"
|
||||
echo "- Version: \`$VERSION\`"
|
||||
echo "- Tag commit: \`$tag_sha\`"
|
||||
echo "- Previous version: \`$current_version\`"
|
||||
echo "- Manifest SHA-256: \`$(sha256sum "$candidate" | awk '{print $1}')\`"
|
||||
echo "- Actor: \`${GITHUB_ACTOR:-unknown}\`"
|
||||
if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_RUN_ID:-}" ]]; then
|
||||
echo "- Workflow: ${GITHUB_SERVER_URL}/${REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
fi
|
||||
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
promoter="$root/scripts/promote-oss-desktop-release.sh"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir "$tmp/bin"
|
||||
|
||||
cat > "$tmp/bin/gh" <<'MOCK'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$1 $2" == "release view" ]]; then
|
||||
printf '%s\n' "$MOCK_RELEASE_JSON"
|
||||
elif [[ "$1" == api ]]; then
|
||||
[[ "$2" == *commits/desktop-v* ]] && printf '%s\n' "${MOCK_TAG_SHA:-abc123}" || printf '%s\n' "${MOCK_TARGET_SHA:-abc123}"
|
||||
elif [[ "$1 $2" == "release download" ]]; then
|
||||
tag="$3"; shift 3; pattern= dir=
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in --pattern) pattern="$2"; shift 2;; --dir) dir="$2"; shift 2;; *) shift;; esac
|
||||
done
|
||||
if [[ "$tag" == desktop-v* ]]; then
|
||||
cp "$MOCK_CANDIDATE" "$dir/$pattern"
|
||||
else
|
||||
count=0; [[ -f "$MOCK_DOWNLOAD_COUNT" ]] && count="$(cat "$MOCK_DOWNLOAD_COUNT")"
|
||||
count=$((count + 1)); printf '%s' "$count" > "$MOCK_DOWNLOAD_COUNT"
|
||||
source="$MOCK_CURRENT"
|
||||
if [[ "$count" -eq 2 && -n "${MOCK_CURRENT_SECOND:-}" ]]; then
|
||||
source="$MOCK_CURRENT_SECOND"
|
||||
elif [[ "$count" -gt 2 && -n "${MOCK_POST_WRITE:-}" ]]; then
|
||||
source="$MOCK_POST_WRITE"
|
||||
elif [[ "$count" -gt 2 ]]; then
|
||||
source="$MOCK_CANDIDATE"
|
||||
fi
|
||||
cp "$source" "$dir/$pattern"
|
||||
fi
|
||||
elif [[ "$1 $2" == "release upload" ]]; then
|
||||
[[ "${MOCK_UPLOAD_FAIL:-false}" != true ]] || exit 1
|
||||
: > "$MOCK_UPLOAD_MARKER"
|
||||
else
|
||||
echo "unexpected gh invocation: $*" >&2; exit 70
|
||||
fi
|
||||
MOCK
|
||||
chmod +x "$tmp/bin/gh"
|
||||
|
||||
write_manifest() {
|
||||
local file="$1" version="$2" signature="${3-signed}" base_version="${4-$2}"
|
||||
jq -n --arg version "$version" --arg signature "$signature" --arg base "https://github.com/block/buzz/releases/download/desktop-v${base_version}" '{
|
||||
version: $version, notes: ("Buzz v" + $version), pub_date: "2026-08-09T00:00:00Z",
|
||||
platforms: {
|
||||
"darwin-aarch64": {signature: $signature, url: ($base + "/mac-arm.tar.gz")},
|
||||
"darwin-x86_64": {signature: $signature, url: ($base + "/mac-x64.tar.gz")},
|
||||
"linux-x86_64": {signature: $signature, url: ($base + "/linux.AppImage")},
|
||||
"windows-x86_64": {signature: $signature, url: ($base + "/windows.exe")}
|
||||
}
|
||||
}' > "$file"
|
||||
}
|
||||
|
||||
all_assets='["updater-manifest.json","mac-arm.tar.gz","mac-x64.tar.gz","linux.AppImage","windows.exe"]'
|
||||
release_json() {
|
||||
local draft="${1:-false}" prerelease="${2:-false}" asset_json="${3:-$all_assets}"
|
||||
jq -cn --argjson draft "$draft" --argjson prerelease "$prerelease" --argjson assets "$asset_json" \
|
||||
'{isDraft:$draft,isPrerelease:$prerelease,targetCommitish:"abc123",assets:[$assets[]|{name:.}]}'
|
||||
}
|
||||
|
||||
# run_case name expected-error-or-pass expected-upload candidate current release
|
||||
# [second-current] [post-write] [upload-fail] [tag-sha] [target-sha] [version]
|
||||
run_case() {
|
||||
local name="$1" expected="$2" upload="$3" candidate="$4" current="$5" release="$6"
|
||||
local second="${7:-}" post="${8:-}" upload_fail="${9:-false}" tag_sha="${10:-abc123}" target_sha="${11:-abc123}" version="${12:-1.2.3}"
|
||||
local case_dir="$tmp/$name" output status
|
||||
mkdir -p "$case_dir"; : > "$case_dir/count"; rm -f "$case_dir/uploaded"
|
||||
set +e
|
||||
output="$(PATH="$tmp/bin:$PATH" GITHUB_REPOSITORY=block/buzz \
|
||||
MOCK_RELEASE_JSON="$release" MOCK_CANDIDATE="$candidate" MOCK_CURRENT="$current" \
|
||||
MOCK_CURRENT_SECOND="$second" MOCK_POST_WRITE="$post" MOCK_UPLOAD_FAIL="$upload_fail" \
|
||||
MOCK_TAG_SHA="$tag_sha" MOCK_TARGET_SHA="$target_sha" MOCK_DOWNLOAD_COUNT="$case_dir/count" \
|
||||
MOCK_UPLOAD_MARKER="$case_dir/uploaded" GITHUB_STEP_SUMMARY="$case_dir/summary" \
|
||||
"$promoter" "$version" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ "$expected" == pass ]]; then
|
||||
[[ "$status" -eq 0 ]] || { echo "$name expected success: $output" >&2; exit 1; }
|
||||
else
|
||||
[[ "$status" -ne 0 ]] || { echo "$name expected failure" >&2; exit 1; }
|
||||
grep -Fq "$expected" <<<"$output" || { echo "$name missing error '$expected': $output" >&2; exit 1; }
|
||||
fi
|
||||
if [[ "$upload" == yes ]]; then
|
||||
[[ -f "$case_dir/uploaded" ]] || { echo "$name expected upload" >&2; exit 1; }
|
||||
else
|
||||
[[ ! -f "$case_dir/uploaded" ]] || { echo "$name unexpectedly uploaded" >&2; exit 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
write_manifest "$tmp/candidate.json" 1.2.3
|
||||
write_manifest "$tmp/current.json" 1.2.2
|
||||
write_manifest "$tmp/newer.json" 1.2.4
|
||||
write_manifest "$tmp/raced.json" 1.2.2 changed
|
||||
write_manifest "$tmp/same-different.json" 1.2.3 changed
|
||||
|
||||
run_case upgrade pass yes "$tmp/candidate.json" "$tmp/current.json" "$(release_json)"
|
||||
run_case identical-retry pass no "$tmp/candidate.json" "$tmp/candidate.json" "$(release_json)"
|
||||
run_case same-version-mismatch 'already promoted with different manifest content' no "$tmp/candidate.json" "$tmp/same-different.json" "$(release_json)"
|
||||
run_case downgrade 'refusing downgrade' no "$tmp/candidate.json" "$tmp/newer.json" "$(release_json)"
|
||||
run_case stale-manifest 'current promotion changed during validation' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json)" "$tmp/raced.json"
|
||||
run_case draft 'is still a draft' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json true false)"
|
||||
run_case prerelease 'is a prerelease' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json false true)"
|
||||
run_case target-mismatch 'do not resolve to the same commit' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json)" '' '' false abc123 different
|
||||
run_case malformed-input 'version must be stable semver' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json)" '' '' false abc123 abc123 '1.2.3";echo owned'
|
||||
|
||||
printf '{not-json' > "$tmp/malformed.json"
|
||||
run_case malformed-json 'failed version, platform, signature, or URL validation' no "$tmp/malformed.json" "$tmp/current.json" "$(release_json)"
|
||||
write_manifest "$tmp/wrong-version.json" 1.2.4
|
||||
run_case wrong-version 'failed version, platform, signature, or URL validation' no "$tmp/wrong-version.json" "$tmp/current.json" "$(release_json)"
|
||||
write_manifest "$tmp/empty-signature.json" 1.2.3 ''
|
||||
run_case empty-signature 'failed version, platform, signature, or URL validation' no "$tmp/empty-signature.json" "$tmp/current.json" "$(release_json)"
|
||||
write_manifest "$tmp/foreign-url.json" 1.2.3 signed 9.9.9
|
||||
run_case foreign-url 'failed version, platform, signature, or URL validation' no "$tmp/foreign-url.json" "$tmp/current.json" "$(release_json)"
|
||||
jq 'del(.platforms."windows-x86_64")' "$tmp/candidate.json" > "$tmp/missing-platform.json"
|
||||
run_case missing-platform 'failed version, platform, signature, or URL validation' no "$tmp/missing-platform.json" "$tmp/current.json" "$(release_json)"
|
||||
jq '.platforms["freebsd-x86_64"] = .platforms["linux-x86_64"]' "$tmp/candidate.json" > "$tmp/extra-platform.json"
|
||||
run_case extra-platform 'failed version, platform, signature, or URL validation' no "$tmp/extra-platform.json" "$tmp/current.json" "$(release_json)"
|
||||
missing_assets='["updater-manifest.json","mac-arm.tar.gz","mac-x64.tar.gz","linux.AppImage"]'
|
||||
run_case missing-asset 'references missing release asset' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json false false "$missing_assets")"
|
||||
run_case upload-failure 'promotion upload failed' no "$tmp/candidate.json" "$tmp/current.json" "$(release_json)" '' '' true
|
||||
run_case post-write-mismatch 'served latest.json does not match the promoted candidate' yes "$tmp/candidate.json" "$tmp/current.json" "$(release_json)" '' "$tmp/raced.json"
|
||||
|
||||
echo "OSS desktop promotion behavior passed"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
workflow="$root/.github/workflows/promote-oss-desktop-release.yml"
|
||||
promoter="$root/scripts/promote-oss-desktop-release.sh"
|
||||
release="$root/.github/workflows/release.yml"
|
||||
|
||||
# Pin the separation contract: tag builds retain the exact candidate but cannot
|
||||
# mutate the rolling updater release.
|
||||
grep -Fq 'cp latest.json staged/updater-manifest.json' "$release"
|
||||
[[ "$(grep -c 'gh release upload' "$release")" -eq 1 ]]
|
||||
! grep -Fq 'gh release upload buzz-desktop-latest' "$release"
|
||||
|
||||
grep -Fq 'workflow_dispatch:' "$workflow"
|
||||
grep -Fq 'group: oss-desktop-auto-update-promotion' "$workflow"
|
||||
grep -Fq 'cancel-in-progress: false' "$workflow"
|
||||
grep -Fq 'if: github.repository ==' "$workflow"
|
||||
grep -Fq 'DISPATCH_REF' "$workflow"
|
||||
grep -Fq 'contents: write' "$workflow"
|
||||
grep -Fq 'VERSION: ${{ inputs.version }}' "$workflow"
|
||||
grep -Fq 'scripts/promote-oss-desktop-release.sh "$VERSION"' "$workflow"
|
||||
if grep -F 'run:' "$workflow" | grep -Fq '${{ inputs.version }}'; then
|
||||
echo "untrusted workflow input must not be interpolated into run" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -Fq 'refusing downgrade' "$promoter"
|
||||
grep -Fq 'current_digest="$(sha256sum "$current"' "$promoter"
|
||||
grep -Fq '== "$current_digest"' "$promoter"
|
||||
grep -Fq 'updater-manifest.json' "$promoter"
|
||||
grep -Fq 'desktop-v" + $version + "/"' "$promoter"
|
||||
grep -Fq 'gh release upload "$ROLLING_TAG" "$promotion"' "$promoter"
|
||||
grep -Fq 'served latest.json does not match the promoted candidate' "$promoter"
|
||||
grep -Fq 'promotion upload failed' "$promoter"
|
||||
|
||||
echo "OSS desktop promotion contract passed"
|
||||
@@ -155,15 +155,11 @@ grep -Fq "needs.release-macos-x64.result == 'success'" "$release_workflow"
|
||||
grep -Fq "needs.release-linux.result == 'success'" "$release_workflow"
|
||||
grep -Fq "needs.release-windows.result == 'success'" "$release_workflow"
|
||||
grep -Fq "refs/tags/desktop-v{0}" "$release_workflow"
|
||||
grep -Fq "if: \${{ !contains(needs.setup.outputs.version, '-') }}" "$release_workflow"
|
||||
if grep -Fq "env.already_published != 'true' && !contains(needs.setup.outputs.version, '-')" "$release_workflow"; then
|
||||
echo "rolling updater retry is incorrectly gated by versioned publication state" >&2; exit 1
|
||||
fi
|
||||
grep -Fq 'group: desktop-release-${{ github.ref }}' "$release_workflow"
|
||||
grep -Fq 'cancel-in-progress: false' "$release_workflow"
|
||||
grep -Fq 'release artifact basename collision' "$release_workflow"
|
||||
[[ "$(grep -c 'gh release upload' "$release_workflow")" -eq 2 ]] || {
|
||||
echo "only the final writer may upload versioned and rolling release assets" >&2; exit 1;
|
||||
[[ "$(grep -c 'gh release upload' "$release_workflow")" -eq 1 ]] || {
|
||||
echo "desktop release must only upload versioned release assets" >&2; exit 1;
|
||||
}
|
||||
grep -Fq 'if: env.already_published' "$release_workflow"
|
||||
grep -Fq 'if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --silent 2>/dev/null; then' "$auto_tag"
|
||||
|
||||
Reference in New Issue
Block a user