refactor(ci): move the Winget bump out of CI onto a maintainer machine
Mirrors the Homebrew cask change. The old design stored a classic `public_repo` PAT as WINGET_TOKEN and handed it to the third-party vedantmgoyal9/winget-releaser action: a token with write access to every public repo the owning account can reach, given to code we do not control, in a place any push-access collaborator could read it from (a pushed branch containing a workflow runs with repo secrets). - CI (bump-winget.yml, now "Sync Winget Manifest Reference") uses GITHUB_TOKEN only: downloads the MSI, computes the sha256, reads the ProductCode from the MSI Property table via msitools, and opens an in-repo PR syncing desktopApp/packaging/winget/. Runs on ubuntu (1x billing) rather than Windows since msitools reads the Property table fine. - scripts/bump-winget.sh does the upstream PR. It needs NO new token: it drives `gh`, which a maintainer already has authenticated, and it does not need wingetcreate (Windows-only) because the manifests are plain YAML — so it runs from macOS or Linux. Add the three reference manifests under desktopApp/packaging/winget/, matching the schema 1.12.0 shape used upstream. All three validate against Microsoft's published JSON schemas. Validation caught one real bug worth noting: an all-digit 64-char InstallerSha256 parses as a YAML *integer* and fails the schema's `string` type, so it is written quoted. ProductCode is re-read every release because jpackage regenerates it per build; it is the ARP key `winget upgrade` matches on. Drops the last package-manager PAT from the secret inventory.
This commit is contained in:
@@ -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
|
# What it does: after a stable release, download the published Windows MSI,
|
||||||
# created by create-release.yml using GITHUB_TOKEN, and GitHub does not raise
|
# compute its sha256, read its ProductCode, and open a PR syncing
|
||||||
# workflow-triggering events for GITHUB_TOKEN actions. This workflow sat
|
# desktopApp/packaging/winget/*.yaml to that release.
|
||||||
# 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 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:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Create Release Assets"]
|
workflows: ["Create Release Assets"]
|
||||||
@@ -14,15 +33,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release tag to submit (for manual recovery)'
|
description: 'Release tag to sync (for manual recovery)'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
# The "Report failure" step below opens a [release-ops] issue via
|
pull-requests: write
|
||||||
# github.rest.issues.create; that needs issues:write. Without it the failure
|
# The "Report failure" step opens a [release-ops] issue via
|
||||||
# reporter itself 403s and no alert is ever filed.
|
# github.rest.issues.create, which needs issues:write.
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -30,16 +49,18 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump:
|
sync-manifest:
|
||||||
# See bump-homebrew.yml for why these three conditions: successful, tag-push
|
# See bump-homebrew-formula.yml for why these three conditions: successful,
|
||||||
# (not a dry-run dispatch), v-prefixed. Exact format enforced downstream.
|
# tag-push (not a dry-run dispatch), v-prefixed. Format enforced downstream.
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event.workflow_run.conclusion == 'success' &&
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.event == 'push' &&
|
github.event.workflow_run.event == 'push' &&
|
||||||
startsWith(github.event.workflow_run.head_branch, 'v'))
|
startsWith(github.event.workflow_run.head_branch, 'v'))
|
||||||
runs-on: windows-latest
|
# Linux, not Windows: msitools reads the MSI Property table just as well, and
|
||||||
timeout-minutes: 30
|
# this leg is billed 1x instead of 2x.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
@@ -58,33 +79,114 @@ jobs:
|
|||||||
is_prerelease: ${{ steps.rel.outputs.is_prerelease }}
|
is_prerelease: ${{ steps.rel.outputs.is_prerelease }}
|
||||||
is_draft: ${{ steps.rel.outputs.is_draft }}
|
is_draft: ${{ steps.rel.outputs.is_draft }}
|
||||||
|
|
||||||
# winget-releaser UPDATES an existing package; `VitorPamplona.Amethyst`
|
- name: Install msitools
|
||||||
# has never been submitted to microsoft/winget-pkgs. Until the one-time
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq msitools
|
||||||
# new-package PR lands (BUILDING.md § Bootstrap), no-op instead of failing
|
|
||||||
# every release with a spurious [release-ops] issue.
|
- name: Download MSI, compute sha256 + ProductCode
|
||||||
- name: Check the package exists in winget-pkgs
|
id: asset
|
||||||
id: pkg
|
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
URL=https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/v/VitorPamplona/Amethyst
|
TAG="${{ steps.rel.outputs.tag }}"
|
||||||
if curl -fsSL -o /dev/null -H "Accept: application/vnd.github+json" "$URL"; then
|
VER="${{ steps.rel.outputs.ver }}"
|
||||||
echo "bootstrapped=true" >> "$GITHUB_OUTPUT"
|
URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/amethyst-desktop-${VER}-windows-x64.msi"
|
||||||
echo "VitorPamplona.Amethyst found in winget-pkgs; proceeding with submission"
|
echo "Fetching $URL"
|
||||||
else
|
# workflow_run fires only after every upload leg has finished, so the
|
||||||
echo "bootstrapped=false" >> "$GITHUB_OUTPUT"
|
# asset should already be there. Retry anyway for release-CDN
|
||||||
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."
|
# 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
|
fi
|
||||||
|
|
||||||
- name: Submit manifest to winget-pkgs
|
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
||||||
if: steps.pkg.outputs.bootstrapped == 'true'
|
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
|
||||||
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
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:
|
with:
|
||||||
identifier: VitorPamplona.Amethyst
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
version: ${{ steps.rel.outputs.tag }}
|
base: main
|
||||||
# Asset naming contract: scripts/asset-name.sh
|
branch: chore/bump-winget-manifest-${{ steps.rel.outputs.tag }}
|
||||||
installers-regex: '^amethyst-desktop-.*-windows-x64\.msi$'
|
add-paths: desktopApp/packaging/winget
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
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
|
- name: Report failure
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -96,17 +198,17 @@ jobs:
|
|||||||
await github.rest.issues.create({
|
await github.rest.issues.create({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
title: `[release-ops] bump-winget failed for ${tag}`,
|
title: `[release-ops] sync-winget-manifest failed for ${tag}`,
|
||||||
body: [
|
body: [
|
||||||
`Winget manifest submission failed for release \`${tag}\`.`,
|
`Winget manifest sync failed for release \`${tag}\`.`,
|
||||||
``,
|
``,
|
||||||
`- Run: ${runUrl}`,
|
`- Run: ${runUrl}`,
|
||||||
`- Channel: Winget (\`VitorPamplona.Amethyst\`)`,
|
`- Channel: Winget (\`VitorPamplona.Amethyst\`)`,
|
||||||
``,
|
``,
|
||||||
`Recovery options:`,
|
`Recovery options:`,
|
||||||
`1. Re-run the workflow once underlying issue is fixed`,
|
`1. Re-run the workflow once the underlying issue is fixed`,
|
||||||
`2. Manually submit via \`wingetcreate update VitorPamplona.Amethyst -v ${tag.replace(/^v/, '')}\``,
|
`2. Check the release actually published \`amethyst-desktop-${tag.replace(/^v/, '')}-windows-x64.msi\``,
|
||||||
`3. File PR directly against microsoft/winget-pkgs`
|
`3. Manually update \`desktopApp/packaging/winget/*.yaml\` (version, sha256, ProductCode)`
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
labels: ['release-ops', 'bug']
|
labels: ['release-ops', 'bug']
|
||||||
});
|
});
|
||||||
|
|||||||
+30
-13
@@ -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_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same |
|
||||||
| `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | 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 | — |
|
| ~~`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) |
|
| `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:
|
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
|
> below are the work that activates them; until then treat the desktop app as
|
||||||
> GitHub-Releases-only on macOS and Windows.
|
> 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 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
|
Neither package-manager channel adds anything to it:
|
||||||
their token type and scope:
|
|
||||||
|
|
||||||
| Secret | Purpose | Scope |
|
**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
|
||||||
| `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) |
|
is worth keeping, because it is the reason this repo has no third secret to
|
||||||
|
rotate:
|
||||||
**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`:
|
|
||||||
|
|
||||||
`brew bump-cask-pr` forks `Homebrew/homebrew-cask` **into the token owner's
|
`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
|
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
|
Prefer a dedicated bot account whose only asset is a fork of `homebrew-cask`, so
|
||||||
a leak reaches nothing else.
|
a leak reaches nothing else.
|
||||||
|
|
||||||
Rotate `WINGET_TOKEN` on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md`
|
### Winget
|
||||||
or equivalent issue tracker.
|
|
||||||
|
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/<version>/`, 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)
|
### Homebrew cask (one-time initial PR)
|
||||||
|
|
||||||
|
|||||||
+21
-18
@@ -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
|
*skipped cleanly* — not that Homebrew/Winget shipped. Check the run's warnings
|
||||||
if you want to confirm which case you're in.
|
if you want to confirm which case you're in.
|
||||||
|
|
||||||
**The cask bump is half-manual by design.** CI (`bump-homebrew.yml`) does the
|
**Both bumps are half-manual by design.** CI does the bookkeeping with
|
||||||
bookkeeping with `GITHUB_TOKEN` only — verifies the DMG is notarized + stapled,
|
`GITHUB_TOKEN` only — verifying the artifact, computing hashes, and opening an
|
||||||
computes the sha256, and opens an in-repo PR syncing the reference cask. Pushing
|
in-repo PR syncing the reference packaging files. Pushing upstream needs
|
||||||
that upstream needs a classic `repo`-scoped PAT, which is deliberately *not* a
|
credentials that would be dangerous as CI secrets (a `repo`-scoped PAT is
|
||||||
CI secret (it would grant write to this repo to anyone with push access), so a
|
readable by anyone with push access here), so a maintainer runs the last step:
|
||||||
maintainer runs it:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# after merging the sync PRs
|
||||||
export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope
|
export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope
|
||||||
scripts/bump-homebrew-cask.sh v1.13.2
|
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
|
Both scripts re-verify the published artifact's sha256 before submitting, and
|
||||||
to submit otherwise. See BUILDING.md § Homebrew cask.
|
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`
|
All four in-repo sync workflows (`amy` formula, `geode` formula,
|
||||||
cask) all open PRs against *this* repo on every release. Merge them to keep the
|
`amethyst-nostr` cask, winget manifests) open PRs against *this* repo on every
|
||||||
reference packaging files current.
|
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) |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Translation sync | On compromise |
|
||||||
|
|
||||||
Owner assignments and rotation reminders live with the team (issue tracker).
|
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.
|
- [ ] Play Console: rollout started, no policy rejection.
|
||||||
- [ ] Zapstore: release event visible.
|
- [ ] Zapstore: release event visible.
|
||||||
- [ ] F-Droid: new version detected (may lag days).
|
- [ ] F-Droid: new version detected (may lag days).
|
||||||
- [ ] `amy` / `geode` formula + `amethyst-nostr` cask sync PRs opened against
|
- [ ] Four sync PRs opened against this repo (`amy` formula, `geode` formula,
|
||||||
this repo — merge them.
|
`amethyst-nostr` cask, winget manifests) — merge them.
|
||||||
- [ ] Cask pushed upstream: `scripts/bump-homebrew-cask.sh vX.Y.Z` (manual, needs
|
- [ ] 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
|
`HOMEBREW_GITHUB_API_TOKEN` in your shell).
|
||||||
the one-time new-cask PR has been bootstrapped (§ 3).
|
- [ ] Winget pushed upstream: `scripts/bump-winget.sh vX.Y.Z` (manual, no token —
|
||||||
- [ ] Winget: **expected to skip** until bootstrapped (§ 3). If it ever starts
|
uses your `gh` auth).
|
||||||
opening real PRs, that means someone landed the bootstrap.
|
Both scripts error clearly until the one-time bootstrap PRs land (§ 3).
|
||||||
- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID`
|
- [ ] 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).
|
(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 —
|
- [ ] Push still works on a `play` build (only if the push contract changed —
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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/<version>/ 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
|
||||||
Executable
+159
@@ -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"
|
||||||
Reference in New Issue
Block a user