diff --git a/.github/workflows/bump-winget.yml b/.github/workflows/bump-winget.yml index c50965e4e5..ef19cfd79a 100644 --- a/.github/workflows/bump-winget.yml +++ b/.github/workflows/bump-winget.yml @@ -1,12 +1,31 @@ -name: Bump Winget Manifest +name: Sync Winget Manifest Reference -# Fires after "Create Release Assets" finishes successfully for a tag push. +# Fourth sibling of the three Homebrew sync workflows, same shape: +# bump-homebrew-formula.yml -> Formula `amy` +# bump-homebrew-geode-formula.yml -> Formula `geode` +# bump-homebrew.yml -> Cask `amethyst-nostr` +# this workflow -> Winget `VitorPamplona.Amethyst` # -# NOT `release: types: [released]`. That event never fires here: the release is -# created by create-release.yml using GITHUB_TOKEN, and GitHub does not raise -# workflow-triggering events for GITHUB_TOKEN actions. This workflow sat -# silently dead through every release up to v1.13.1 for exactly that reason. -# See the longer note in bump-homebrew.yml. +# What it does: after a stable release, download the published Windows MSI, +# compute its sha256, read its ProductCode, and open a PR syncing +# desktopApp/packaging/winget/*.yaml to that release. +# +# What it does NOT do: open a PR against microsoft/winget-pkgs. That step is +# deliberately MANUAL and runs on a maintainer's machine — +# `scripts/bump-winget.sh`. Reason: submitting requires push access to a fork of +# winget-pkgs. The previous design stored a classic `public_repo` PAT as +# WINGET_TOKEN and handed it to a third-party action; that scope grants write to +# every public repo the account can reach, and as an Actions secret it was +# usable by anyone with push access to this repo. The local script uses the +# maintainer's existing `gh` auth instead, so no PAT is created at all. +# See BUILDING.md § Winget. +# +# Consequence: this workflow needs NO external token and no third-party action. +# +# Trigger: after "Create Release Assets" succeeds for a tag push. NOT +# `release: types: [released]` — that event never fires, because the release is +# created by create-release.yml under GITHUB_TOKEN and GitHub suppresses +# workflow-triggering events for it. See the note in bump-homebrew-formula.yml. on: workflow_run: workflows: ["Create Release Assets"] @@ -14,15 +33,15 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag to submit (for manual recovery)' + description: 'Release tag to sync (for manual recovery)' required: true type: string permissions: - contents: read - # The "Report failure" step below opens a [release-ops] issue via - # github.rest.issues.create; that needs issues:write. Without it the failure - # reporter itself 403s and no alert is ever filed. + contents: write + pull-requests: write + # The "Report failure" step opens a [release-ops] issue via + # github.rest.issues.create, which needs issues:write. issues: write concurrency: @@ -30,16 +49,18 @@ concurrency: cancel-in-progress: false jobs: - bump: - # See bump-homebrew.yml for why these three conditions: successful, tag-push - # (not a dry-run dispatch), v-prefixed. Exact format enforced downstream. + sync-manifest: + # See bump-homebrew-formula.yml for why these three conditions: successful, + # tag-push (not a dry-run dispatch), v-prefixed. Format enforced downstream. if: >- github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && startsWith(github.event.workflow_run.head_branch, 'v')) - runs-on: windows-latest - timeout-minutes: 30 + # Linux, not Windows: msitools reads the MSI Property table just as well, and + # this leg is billed 1x instead of 2x. + runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout code uses: actions/checkout@v7 @@ -58,33 +79,114 @@ jobs: is_prerelease: ${{ steps.rel.outputs.is_prerelease }} is_draft: ${{ steps.rel.outputs.is_draft }} - # winget-releaser UPDATES an existing package; `VitorPamplona.Amethyst` - # has never been submitted to microsoft/winget-pkgs. Until the one-time - # new-package PR lands (BUILDING.md § Bootstrap), no-op instead of failing - # every release with a spurious [release-ops] issue. - - name: Check the package exists in winget-pkgs - id: pkg - shell: bash + - name: Install msitools + run: sudo apt-get update -qq && sudo apt-get install -y -qq msitools + + - name: Download MSI, compute sha256 + ProductCode + id: asset run: | set -euo pipefail - URL=https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/v/VitorPamplona/Amethyst - if curl -fsSL -o /dev/null -H "Accept: application/vnd.github+json" "$URL"; then - echo "bootstrapped=true" >> "$GITHUB_OUTPUT" - echo "VitorPamplona.Amethyst found in winget-pkgs; proceeding with submission" - else - echo "bootstrapped=false" >> "$GITHUB_OUTPUT" - echo "::warning::Package 'VitorPamplona.Amethyst' is not in microsoft/winget-pkgs yet, so there is nothing to update for ${{ steps.rel.outputs.tag }}. Amethyst is NOT shipping via Winget. Submit the one-time new-package PR (BUILDING.md § Bootstrap) to activate this channel." + TAG="${{ steps.rel.outputs.tag }}" + VER="${{ steps.rel.outputs.ver }}" + URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/amethyst-desktop-${VER}-windows-x64.msi" + echo "Fetching $URL" + # workflow_run fires only after every upload leg has finished, so the + # asset should already be there. Retry anyway for release-CDN + # propagation (mirrors the repo's push/pull retry ethos). + ok=0 + for i in 1 2 3 4 5; do + if curl -fsSL -o amethyst.msi "$URL"; then ok=1; break; fi + wait=$(( 2 ** i )) + echo "attempt $i failed; retrying in ${wait}s" + sleep "$wait" + done + [[ "$ok" == 1 ]] || { echo "::error::could not download $URL"; exit 1; } + test -s amethyst.msi + + SHA=$(sha256sum amethyst.msi | awk '{print $1}' | tr '[:lower:]' '[:upper:]') + + # ProductCode is the ARP key winget uses to detect an existing install. + # jpackage regenerates it per build, so read it rather than pin it. + PRODUCT_CODE=$(msiinfo export amethyst.msi Property \ + | awk -F'\t' '$1 == "ProductCode" { print $2 }' | tr -d '\r') + if [[ ! "$PRODUCT_CODE" =~ ^\{[0-9A-Fa-f-]{36}\}$ ]]; then + echo "::error::could not read a valid ProductCode from the MSI (got: '${PRODUCT_CODE}')" + exit 1 fi - - name: Submit manifest to winget-pkgs - if: steps.pkg.outputs.bootstrapped == 'true' - uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 + echo "url=$URL" >> "$GITHUB_OUTPUT" + echo "sha256=$SHA" >> "$GITHUB_OUTPUT" + echo "product_code=$PRODUCT_CODE" >> "$GITHUB_OUTPUT" + echo "sha256=$SHA" + echo "ProductCode=$PRODUCT_CODE" + + - name: Update reference manifests + run: | + set -euo pipefail + DIR=desktopApp/packaging/winget + TAG="${{ steps.rel.outputs.tag }}" + VER="${{ steps.rel.outputs.ver }}" + SHA="${{ steps.asset.outputs.sha256 }}" + URL="${{ steps.asset.outputs.url }}" + PC="${{ steps.asset.outputs.product_code }}" + + # Anchored substitutions so the header comments are never touched. + sed -i -E "s|^(PackageVersion: ).*|\1${VER}|" \ + "$DIR/VitorPamplona.Amethyst.yaml" \ + "$DIR/VitorPamplona.Amethyst.installer.yaml" \ + "$DIR/VitorPamplona.Amethyst.locale.en-US.yaml" + sed -i -E "s|^( InstallerUrl: ).*|\1${URL}|" "$DIR/VitorPamplona.Amethyst.installer.yaml" + # Quoted: an all-digit 64-char digest would otherwise parse as a YAML + # integer and fail the schema's `string` type. + sed -i -E "s|^( InstallerSha256: ).*|\1'${SHA}'|" "$DIR/VitorPamplona.Amethyst.installer.yaml" + sed -i -E "s|^( ProductCode: ).*|\1'${PC}'|" "$DIR/VitorPamplona.Amethyst.installer.yaml" + sed -i -E "s|^(ReleaseNotesUrl: ).*|\1https://github.com/${{ github.repository }}/releases/tag/${TAG}|" \ + "$DIR/VitorPamplona.Amethyst.locale.en-US.yaml" + + echo "----- synced -----" + grep -hE "^(PackageVersion| InstallerUrl| InstallerSha256| ProductCode|ReleaseNotesUrl): " "$DIR"/*.yaml + + - name: Sanity-check the manifests still parse + run: | + set -euo pipefail + python3 - <<'PY' + import glob, sys, yaml + for f in sorted(glob.glob('desktopApp/packaging/winget/*.yaml')): + d = yaml.safe_load(open(f)) + assert d['PackageIdentifier'] == 'VitorPamplona.Amethyst', f + assert d['PackageVersion'], f + print('OK', f, d['ManifestType']) + PY + + - name: Open or update the manifest-sync PR + # peter-evans/create-pull-request is MIT-licensed CI-only tooling (not + # linked into any shipped artifact). It no-ops when there is no diff. + uses: peter-evans/create-pull-request@v8 with: - identifier: VitorPamplona.Amethyst - version: ${{ steps.rel.outputs.tag }} - # Asset naming contract: scripts/asset-name.sh - installers-regex: '^amethyst-desktop-.*-windows-x64\.msi$' - token: ${{ secrets.WINGET_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} + base: main + branch: chore/bump-winget-manifest-${{ steps.rel.outputs.tag }} + add-paths: desktopApp/packaging/winget + commit-message: 'chore: sync winget manifests to ${{ steps.rel.outputs.tag }}' + title: 'chore: sync winget manifests to ${{ steps.rel.outputs.tag }}' + body: | + Auto-synced `desktopApp/packaging/winget/` to the + `${{ steps.rel.outputs.tag }}` release: + + - `PackageVersion` -> `${{ steps.rel.outputs.ver }}` + - `InstallerSha256` -> `${{ steps.asset.outputs.sha256 }}` + - `ProductCode` -> `${{ steps.asset.outputs.product_code }}` + + **Merge this, then push it upstream from a maintainer machine:** + + ```bash + scripts/bump-winget.sh ${{ steps.rel.outputs.tag }} + ``` + + That step is manual on purpose — it needs push access to a fork of + `microsoft/winget-pkgs`, which is deliberately NOT stored as a CI + secret. The script uses your existing `gh` auth. See + BUILDING.md § Winget. - name: Report failure if: failure() @@ -96,17 +198,17 @@ jobs: await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, - title: `[release-ops] bump-winget failed for ${tag}`, + title: `[release-ops] sync-winget-manifest failed for ${tag}`, body: [ - `Winget manifest submission failed for release \`${tag}\`.`, + `Winget manifest sync failed for release \`${tag}\`.`, ``, `- Run: ${runUrl}`, `- Channel: Winget (\`VitorPamplona.Amethyst\`)`, ``, `Recovery options:`, - `1. Re-run the workflow once underlying issue is fixed`, - `2. Manually submit via \`wingetcreate update VitorPamplona.Amethyst -v ${tag.replace(/^v/, '')}\``, - `3. File PR directly against microsoft/winget-pkgs` + `1. Re-run the workflow once the underlying issue is fixed`, + `2. Check the release actually published \`amethyst-desktop-${tag.replace(/^v/, '')}-windows-x64.msi\``, + `3. Manually update \`desktopApp/packaging/winget/*.yaml\` (version, sha256, ProductCode)` ].join('\n'), labels: ['release-ops', 'bug'] }); diff --git a/BUILDING.md b/BUILDING.md index f817b78a0e..5a04173603 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -404,7 +404,7 @@ provided automatically; everything else you set yourself.) | `MAC_NOTARY_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same | | `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | Same | | ~~`HOMEBREW_TOKEN`~~ | *Not used.* The cask bump runs on a maintainer's machine — see § Homebrew cask | — | -| `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) | +| ~~`WINGET_TOKEN`~~ | *Not used.* The winget bump runs on a maintainer's machine — see § Winget | — | | `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) | Note the **three distinct signing identities** people often conflate: @@ -521,19 +521,15 @@ reads an optional per-release changelog from > below are the work that activates them; until then treat the desktop app as > GitHub-Releases-only on macOS and Windows. -### Secrets to provision in GitHub repo settings +### Package-manager credentials (and why there are none) The full secret inventory is in [§ Secrets the CI needs](#secrets-the-ci-needs). -The two that need the most setup care are the package-manager PATs, because of -their token type and scope: +Neither package-manager channel adds anything to it: -| Secret | Purpose | Scope | -|---|---|---| -| `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) | - -**There is deliberately no `HOMEBREW_TOKEN`.** The cask bump is the one release -step that runs on a maintainer's machine rather than in CI. The reasoning is -worth keeping, because the same trade-off applies to `WINGET_TOKEN`: +**There are deliberately no package-manager PATs in CI.** Both the Homebrew +cask and the Winget manifest bumps run on a maintainer's machine. The reasoning +is worth keeping, because it is the reason this repo has no third secret to +rotate: `brew bump-cask-pr` forks `Homebrew/homebrew-cask` **into the token owner's account** (`POST /repos/Homebrew/homebrew-cask/forks`), pushes a branch to that @@ -575,8 +571,29 @@ Create one at Prefer a dedicated bot account whose only asset is a fork of `homebrew-cask`, so a leak reaches nothing else. -Rotate `WINGET_TOKEN` on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md` -or equivalent issue tracker. +### Winget + +Same split, and it needs **no token at all**. `scripts/bump-winget.sh` drives +`gh`, which a maintainer is already authenticated with, and it does not need +`wingetcreate` (Windows-only) because winget manifests are plain YAML — so it +runs fine from macOS or Linux: + +```bash +scripts/bump-winget.sh v1.13.2 +``` + +CI (`bump-winget.yml`, `GITHUB_TOKEN` only) does the bookkeeping: downloads the +MSI, computes the sha256, reads the `ProductCode` out of the MSI Property table +with `msitools`, and opens an in-repo PR syncing +`desktopApp/packaging/winget/*.yaml`. The script re-verifies the sha256 against +the live asset, then forks `microsoft/winget-pkgs`, commits the three manifests +to `manifests/v/VitorPamplona/Amethyst//`, and opens the PR. + +The previous design stored a classic `public_repo` PAT as `WINGET_TOKEN` and +passed it to the third-party `vedantmgoyal9/winget-releaser` action — a token +with write access to every public repo the account owns, handed to code we do +not control, in a place any push-access collaborator could read it from. None of +that is needed. ### Homebrew cask (one-time initial PR) diff --git a/RELEASE_OPS.md b/RELEASE_OPS.md index 5eca4e3694..5b4b6eef39 100644 --- a/RELEASE_OPS.md +++ b/RELEASE_OPS.md @@ -210,24 +210,27 @@ Until someone does that bootstrap, a green release run means the bump workflows *skipped cleanly* — not that Homebrew/Winget shipped. Check the run's warnings if you want to confirm which case you're in. -**The cask bump is half-manual by design.** CI (`bump-homebrew.yml`) does the -bookkeeping with `GITHUB_TOKEN` only — verifies the DMG is notarized + stapled, -computes the sha256, and opens an in-repo PR syncing the reference cask. Pushing -that upstream needs a classic `repo`-scoped PAT, which is deliberately *not* a -CI secret (it would grant write to this repo to anyone with push access), so a -maintainer runs it: +**Both bumps are half-manual by design.** CI does the bookkeeping with +`GITHUB_TOKEN` only — verifying the artifact, computing hashes, and opening an +in-repo PR syncing the reference packaging files. Pushing upstream needs +credentials that would be dangerous as CI secrets (a `repo`-scoped PAT is +readable by anyone with push access here), so a maintainer runs the last step: ```bash +# after merging the sync PRs export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope scripts/bump-homebrew-cask.sh v1.13.2 + +scripts/bump-winget.sh v1.13.2 # no token — uses your `gh` auth ``` -The script re-verifies sha256 + notarization against the live asset and refuses -to submit otherwise. See BUILDING.md § Homebrew cask. +Both scripts re-verify the published artifact's sha256 before submitting, and +the Homebrew one additionally refuses if the DMG is not notarized + stapled. +See BUILDING.md § Package-manager credentials. -The three in-repo sync workflows (`amy` formula, `geode` formula, `amethyst-nostr` -cask) all open PRs against *this* repo on every release. Merge them to keep the -reference packaging files current. +All four in-repo sync workflows (`amy` formula, `geode` formula, +`amethyst-nostr` cask, winget manifests) open PRs against *this* repo on every +release. Merge them to keep the reference packaging files current. --- @@ -269,7 +272,7 @@ ownership: | `SIGNING_KEY`, `KEY_ALIAS`, `KEY_STORE_PASSWORD`, `KEY_PASSWORD` | The **Android upload keystore** — losing/leaking it is the worst case; Play app signing identity | Keep the keystore backed up offline; never rotate casually (Play upload key reset is a support process) | | `SONATYPE_USERNAME`, `SONATYPE_PASSWORD` | Maven Central namespace `com.vitorpamplona` | On compromise | | `SIGNING_PRIVATE_KEY`, `SIGNING_PASSWORD` | The **GPG key** signing Maven artifacts | Per GPG key expiry | -| `WINGET_TOKEN` | Winget bump PRs | **90-day cadence** (see BUILDING.md § Bootstrap). No `HOMEBREW_TOKEN` exists — the cask bump runs locally via `scripts/bump-homebrew-cask.sh`, so the `repo`-scoped PAT it needs never becomes a CI secret | +| *(none for Homebrew/Winget)* | — | Both bumps run on a maintainer's machine — `scripts/bump-homebrew-cask.sh` and `scripts/bump-winget.sh` — so neither channel's PAT ever becomes a CI secret. See BUILDING.md § Package-manager credentials | | `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Translation sync | On compromise | Owner assignments and rotation reminders live with the team (issue tracker). @@ -288,13 +291,13 @@ Owner assignments and rotation reminders live with the team (issue tracker). - [ ] Play Console: rollout started, no policy rejection. - [ ] Zapstore: release event visible. - [ ] F-Droid: new version detected (may lag days). -- [ ] `amy` / `geode` formula + `amethyst-nostr` cask sync PRs opened against - this repo — merge them. +- [ ] Four sync PRs opened against this repo (`amy` formula, `geode` formula, + `amethyst-nostr` cask, winget manifests) — merge them. - [ ] Cask pushed upstream: `scripts/bump-homebrew-cask.sh vX.Y.Z` (manual, needs - `HOMEBREW_GITHUB_API_TOKEN` in your shell). Skips with a clear error until - the one-time new-cask PR has been bootstrapped (§ 3). -- [ ] Winget: **expected to skip** until bootstrapped (§ 3). If it ever starts - opening real PRs, that means someone landed the bootstrap. + `HOMEBREW_GITHUB_API_TOKEN` in your shell). +- [ ] Winget pushed upstream: `scripts/bump-winget.sh vX.Y.Z` (manual, no token — + uses your `gh` auth). + Both scripts error clearly until the one-time bootstrap PRs land (§ 3). - [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID` (only bumped on minor releases — patches keep pointing at the x.y.0 note). - [ ] Push still works on a `play` build (only if the push contract changed — diff --git a/desktopApp/packaging/winget/VitorPamplona.Amethyst.installer.yaml b/desktopApp/packaging/winget/VitorPamplona.Amethyst.installer.yaml new file mode 100644 index 0000000000..b3c349a0bb --- /dev/null +++ b/desktopApp/packaging/winget/VitorPamplona.Amethyst.installer.yaml @@ -0,0 +1,32 @@ +# Reference Winget installer manifest for the Amethyst desktop app. +# +# InstallerUrl / InstallerSha256 / ProductCode are rewritten on each stable +# release by .github/workflows/bump-winget.yml, which downloads the published MSI, +# hashes it, and reads the ProductCode out of the MSI's Property table. +# +# InstallerType is `wix` because jpackage builds the MSI with the WiX Toolset. +# ProductCode matters: it is the ARP key winget uses to detect an existing +# install, so `winget upgrade` misbehaves without it. jpackage regenerates it per +# build, which is why CI re-reads it every release rather than pinning it here. +# (UpgradeCode is the stable one — see `upgradeUuid` in desktopApp/build.gradle.kts, +# which must never change.) +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json + +PackageIdentifier: VitorPamplona.Amethyst +PackageVersion: 1.13.1 +Platform: +- Windows.Desktop +MinimumOSVersion: 10.0.0.0 +InstallerType: wix +Scope: machine +UpgradeBehavior: install +Installers: +- Architecture: x64 + InstallerUrl: https://github.com/vitorpamplona/amethyst/releases/download/v1.13.1/amethyst-desktop-1.13.1-windows-x64.msi + # Quoted deliberately: an all-digit 64-char value is a YAML *integer*, which + # fails the schema's `string` type. Quoting makes it a string whatever the + # digest happens to be. + InstallerSha256: '0000000000000000000000000000000000000000000000000000000000000000' + ProductCode: '{00000000-0000-0000-0000-000000000000}' +ManifestType: installer +ManifestVersion: 1.12.0 diff --git a/desktopApp/packaging/winget/VitorPamplona.Amethyst.locale.en-US.yaml b/desktopApp/packaging/winget/VitorPamplona.Amethyst.locale.en-US.yaml new file mode 100644 index 0000000000..42a1bff5ff --- /dev/null +++ b/desktopApp/packaging/winget/VitorPamplona.Amethyst.locale.en-US.yaml @@ -0,0 +1,36 @@ +# Reference Winget locale manifest for the Amethyst desktop app. +# +# Only PackageVersion and ReleaseNotesUrl are touched by +# .github/workflows/bump-winget.yml; everything else is hand-maintained. +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json + +PackageIdentifier: VitorPamplona.Amethyst +PackageVersion: 1.13.1 +PackageLocale: en-US +Publisher: Vitor Pamplona +PublisherUrl: https://github.com/vitorpamplona +PublisherSupportUrl: https://github.com/vitorpamplona/amethyst/issues +Author: Vitor Pamplona +PackageName: Amethyst +PackageUrl: https://github.com/vitorpamplona/amethyst +License: MIT +LicenseUrl: https://github.com/vitorpamplona/amethyst/blob/main/LICENSE +Copyright: Copyright (c) Vitor Pamplona +CopyrightUrl: https://github.com/vitorpamplona/amethyst/blob/main/LICENSE +ShortDescription: The all-in-one Nostr client +Description: |- + A privacy-focused Nostr client. Built-in Tor support, the most configurable relay + system, encrypted messaging, zaps, marketplaces, live streams and complete data + sovereignty. +Moniker: amethyst +Tags: +- decentralized +- nostr +- social +- social-network +ReleaseNotesUrl: https://github.com/vitorpamplona/amethyst/releases/tag/v1.13.1 +Documentations: +- DocumentLabel: Building + DocumentUrl: https://github.com/vitorpamplona/amethyst/blob/main/BUILDING.md +ManifestType: defaultLocale +ManifestVersion: 1.12.0 diff --git a/desktopApp/packaging/winget/VitorPamplona.Amethyst.yaml b/desktopApp/packaging/winget/VitorPamplona.Amethyst.yaml new file mode 100644 index 0000000000..2e98e7de36 --- /dev/null +++ b/desktopApp/packaging/winget/VitorPamplona.Amethyst.yaml @@ -0,0 +1,15 @@ +# Reference Winget version manifest for the Amethyst desktop app. +# +# Not consumed by any build here. `scripts/bump-winget.sh` copies this directory +# into a fork of microsoft/winget-pkgs at +# manifests/v/VitorPamplona/Amethyst// and opens the PR. +# +# PackageVersion is kept in sync automatically on each stable release by +# .github/workflows/bump-winget.yml. +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json + +PackageIdentifier: VitorPamplona.Amethyst +PackageVersion: 1.13.1 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.12.0 diff --git a/scripts/bump-winget.sh b/scripts/bump-winget.sh new file mode 100755 index 0000000000..2bed5a3a12 --- /dev/null +++ b/scripts/bump-winget.sh @@ -0,0 +1,159 @@ +#!/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. +# +# Unlike the Homebrew script this 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"