Files
amethyst/scripts/bump-winget.sh
Vitor PamplonaandClaude Opus 5.5 cbb5dbe1d4 docs(release): treat Homebrew as autobumped; cask script verifies instead of submitting
The amethyst-nostr cask and the amy formula are on Homebrew's autobump
list, so BrewTestBot opens their version PRs and `brew bump-cask-pr`
refuses to open a competing one (it errored on v1.17.0).

scripts/bump-homebrew-cask.sh keeps re-verifying the live DMG's sha256
and stapled notarization (BrewTestBot checks neither against ours), then
finds BrewTestBot's PR and fails if its sha256 differs. No token is
needed. It falls back to `brew bump-cask-pr` only if the cask leaves
autobump.

RELEASE_OPS.md, BUILDING.md and the sync workflows now describe that
flow, drop the per-release HOMEBREW_GITHUB_API_TOKEN step, and record
that winget is live (initial PR merged 2026-09-23).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:10:16 -04:00

160 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Push the VitorPamplona.Amethyst manifests upstream to microsoft/winget-pkgs.
#
# Like scripts/bump-homebrew-cask.sh, this is deliberately not automated: it
# needs push access to a fork of winget-pkgs, and the previous design kept that
# as a classic `public_repo` PAT in the WINGET_TOKEN Actions secret, handed to a
# third-party action. That scope grants write to every public repo the account
# can reach, and any Actions secret is usable by anyone with push access to this
# repo. See BUILDING.md § Winget.
#
# It needs NO new token: it drives `gh`, which you
# are already authenticated with. It also does not need `wingetcreate` (which is
# Windows-only) — winget manifests are plain YAML, so this works from macOS or
# Linux.
#
# Everything error-prone (version, sha256, ProductCode) is already done in CI by
# .github/workflows/bump-winget.yml, which opens a PR syncing
# desktopApp/packaging/winget/. Merge that PR first; this script reads the merged
# values so the two can never disagree.
#
# Usage:
# scripts/bump-winget.sh # uses the manifests as-is
# scripts/bump-winget.sh v1.13.2 # asserts the manifests match this tag
# DRY_RUN=1 scripts/bump-winget.sh # print what would happen, do nothing
set -euo pipefail
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
SRC="$REPO_ROOT/desktopApp/packaging/winget"
PKG_ID="VitorPamplona.Amethyst"
UPSTREAM="microsoft/winget-pkgs"
EXPECTED_TAG="${1:-}"
DRY_RUN="${DRY_RUN:-}"
die() { echo "error: $*" >&2; exit 1; }
command -v gh >/dev/null || die "gh not found — https://cli.github.com"
gh auth status >/dev/null 2>&1 || die "gh is not authenticated; run: gh auth login"
[[ -d "$SRC" ]] || die "manifests not found at $SRC"
VERSION=$(awk '/^PackageVersion: /{print $2; exit}' "$SRC/$PKG_ID.yaml")
[[ -n "$VERSION" ]] || die "could not parse PackageVersion out of $SRC/$PKG_ID.yaml"
if [[ -n "$EXPECTED_TAG" && "v$VERSION" != "$EXPECTED_TAG" ]]; then
die "manifests are at v$VERSION but you asked for $EXPECTED_TAG.
Merge the 'chore: sync winget manifests to $EXPECTED_TAG' PR first, then pull."
fi
# InstallerSha256 and ProductCode are single-quoted in the YAML (see the
# comments there); strip the quotes when reading them back.
SHA=$(awk '/^ InstallerSha256: /{print $2; exit}' "$SRC/$PKG_ID.installer.yaml" | tr -d "'")
URL=$(awk '/^ InstallerUrl: /{print $2; exit}' "$SRC/$PKG_ID.installer.yaml")
PC=$(awk '/^ ProductCode: /{print $2; exit}' "$SRC/$PKG_ID.installer.yaml" | tr -d "'")
[[ "$SHA" =~ ^0+$ ]] && die "InstallerSha256 is still the placeholder.
Run the sync workflow first: gh workflow run bump-winget.yml -f tag=v$VERSION"
echo "package : $PKG_ID"
echo "version : $VERSION"
echo "sha256 : $SHA"
echo "product : $PC"
echo "url : $URL"
echo
# Re-verify against the live asset. CI already did, but the whole point of a
# manual gate is that a human confirms what is about to be published.
echo "==> verifying the release MSI"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
curl -fsSL -o "$TMP/amethyst.msi" "$URL" || die "could not download $URL"
if command -v sha256sum >/dev/null; then
ACTUAL=$(sha256sum "$TMP/amethyst.msi" | awk '{print $1}')
else
ACTUAL=$(shasum -a 256 "$TMP/amethyst.msi" | awk '{print $1}')
fi
ACTUAL=$(echo "$ACTUAL" | tr '[:lower:]' '[:upper:]')
[[ "$ACTUAL" == "$SHA" ]] || die "sha256 mismatch!
manifest says : $SHA
actual : $ACTUAL"
echo " sha256 matches"
echo
DEST="manifests/v/VitorPamplona/Amethyst/$VERSION"
BRANCH="$PKG_ID-$VERSION"
if [[ -n "$DRY_RUN" ]]; then
echo "DRY_RUN set — would now:"
echo " fork $UPSTREAM (if needed), branch $BRANCH"
echo " add $DEST/{$PKG_ID.yaml,$PKG_ID.installer.yaml,$PKG_ID.locale.en-US.yaml}"
echo " open a PR against $UPSTREAM"
exit 0
fi
# A new package needs a human-reviewed first submission; after that this script
# handles every version bump.
if ! gh api "repos/$UPSTREAM/contents/manifests/v/VitorPamplona/Amethyst" >/dev/null 2>&1; then
echo "note: $PKG_ID is not in $UPSTREAM yet — this will be the NEW-PACKAGE submission."
echo " Expect a slower, human-reviewed first pass."
echo
fi
WORK="$TMP/winget-pkgs"
echo "==> preparing the fork"
gh repo fork "$UPSTREAM" --clone=false --remote=false >/dev/null 2>&1 || true
FORK="$(gh api user --jq .login)/winget-pkgs"
# Shallow clone of the fork only — winget-pkgs full history is enormous.
gh repo clone "$FORK" "$WORK" -- --depth=1 >/dev/null 2>&1 \
|| die "could not clone $FORK"
cd "$WORK"
git remote add upstream "https://github.com/$UPSTREAM.git" 2>/dev/null || true
git fetch --depth=1 upstream master >/dev/null 2>&1 || git fetch --depth=1 upstream main >/dev/null 2>&1
DEFAULT_BRANCH=$(gh api "repos/$UPSTREAM" --jq .default_branch)
git checkout -B "$BRANCH" "upstream/$DEFAULT_BRANCH" >/dev/null 2>&1
mkdir -p "$DEST"
cp "$SRC/$PKG_ID.yaml" "$SRC/$PKG_ID.installer.yaml" "$SRC/$PKG_ID.locale.en-US.yaml" "$DEST/"
# The reference copies carry an Amethyst-repo header comment; upstream manifests
# should not. Strip leading comment lines, keep the yaml-language-server line.
for f in "$DEST"/*.yaml; do
python3 - "$f" <<'PY'
import sys
p = sys.argv[1]
lines = open(p).read().splitlines(keepends=True)
out, started = [], False
for ln in lines:
if not started and ln.startswith('#') and 'yaml-language-server' not in ln:
continue
started = True
out.append(ln)
open(p, 'w').writelines(out)
PY
done
git add "$DEST"
git -c user.name="$(gh api user --jq '.name // .login')" \
-c user.email="$(gh api user --jq .id)+$(gh api user --jq .login)@users.noreply.github.com" \
commit -q -m "$PKG_ID version $VERSION"
git push -q -u origin "$BRANCH" --force
echo "==> opening the PR"
gh pr create --repo "$UPSTREAM" \
--base "$DEFAULT_BRANCH" \
--head "$(gh api user --jq .login):$BRANCH" \
--title "$PKG_ID version $VERSION" \
--body "Automated manifest sync for [Amethyst](https://github.com/vitorpamplona/amethyst) $VERSION.
- Installer: \`amethyst-desktop-$VERSION-windows-x64.msi\` from the official GitHub release
- SHA256 verified against the published asset
- ProductCode read from the MSI Property table
Manifests are generated from \`desktopApp/packaging/winget/\` in the upstream repo."
echo
echo "Done. Watch the PR at https://github.com/$UPSTREAM/pulls"