From 778fad37fa5b1189bb7a79df60ed2d7880a69a1e Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 08:59:17 +0000 Subject: [PATCH] Actually publish to the package managers TronBrowser has never reached a single package manager. The channels were scaffolded on 2026-06-25 and every manifest in distribution/ still said 0.1.0 while releases went out to 3.15.0. Four separate things had to be true for a release to ship downstream and none of them were. The trigger never fired. submit-packages.yml listened on `release: [published]`, but release.yml un-drafts with GITHUB_TOKEN, and GitHub raises no workflow events for what a GITHUB_TOKEN does. The workflow has zero runs in three months. release.yml now calls it directly through workflow_call, which needs no PAT. The dry-run flag was read from `github.event.inputs.dry_run`, which is empty on anything that is not a workflow_dispatch. The guard was `!= "false"`, so a release run appended --dry-run every time: even with a working trigger the submit path could not have executed. Chocolatey was worse, hardcoded --dry-run with no way to turn it off. The refreshed manifests were thrown away with the runner. Nothing committed them, which is why the tree never moved off 0.1.0. They are now opened as a PR, since those files are the input to every channel's submission and a stale tree means even a working submit publishes the wrong version. And there was no submission code at all. The header said submission was "gated on the relevant secret", but no push, PR or upload existed anywhere in the script -- it rewrote files and exited. Channels we own outright now really publish: homebrew to our tap, scoop to our bucket, AUR over ssh (with a generated .SRCINFO, since makepkg is not on a runner), Chocolatey via choco push, Snap via snapcraft upload. The five that are a pull request into someone else's monorepo (winget, flathub, nixpkgs, gentoo, freebsd) cannot be automated end to end and now say so with a link, rather than printing success. A channel with no secret refreshes its manifest, explains the skip and exits clean, so a credential-less repo still goes green. A channel that has its secret and fails is allowed to break the build, because a silent success here is exactly what hid this for three months. Verified against the real v3.15.0 release: manifests refresh to 3.15.0 and the generated AUR sha256 matches the published tarball byte for byte. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 16 ++ .github/workflows/submit-packages.yml | 89 +++++-- distribution/aur/PKGBUILD | 4 +- distribution/homebrew/tronbrowser.rb | 8 +- distribution/scoop/tronbrowser.json | 8 +- distribution/snap/snapcraft.yaml | 4 +- .../Profullstack.TronBrowser.installer.yaml | 6 +- scripts/open-distribution-pr.sh | 49 ++++ scripts/submit-packages.mjs | 248 +++++++++++++++++- 9 files changed, 393 insertions(+), 39 deletions(-) create mode 100755 scripts/open-distribution-pr.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fd133ba..cbe04e06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,3 +116,19 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: gh release edit "${{ needs.create-release.outputs.tag }}" --draft=false --repo "${{ github.repository }}" + + # 4) Push the release out to the downstream package managers. + # + # This is a direct call, NOT submit-packages.yml's own `release: [published]` + # trigger. The un-draft above uses GITHUB_TOKEN, and GitHub raises no workflow + # events for what a GITHUB_TOKEN does, so that trigger never fired once in the + # three months it was wired up and no channel was ever updated past 0.1.0. + # Calling the workflow keeps that guarantee without needing a PAT. + distribute: + needs: [create-release, publish] + uses: ./.github/workflows/submit-packages.yml + secrets: inherit + with: + version: ${{ needs.create-release.outputs.version }} + dry_run: false + package_managers: all diff --git a/.github/workflows/submit-packages.yml b/.github/workflows/submit-packages.yml index 8a353f9f..1282da48 100644 --- a/.github/workflows/submit-packages.yml +++ b/.github/workflows/submit-packages.yml @@ -1,25 +1,46 @@ name: Submit to Package Managers -# Refresh + submit downstream package managers after a release. Mirrors -# pairux.com's submit-packages workflow but stays in this monorepo. Each channel -# is gated on its secret; without the secret it dry-runs (no-op), so this never -# fails for missing credentials. +# Refresh + submit downstream package managers after a release. Each channel is +# gated on its secret; without the secret it refreshes the manifest and skips the +# submit, so this never fails for missing credentials. +# +# NOT triggered by `release: [published]`. release.yml un-drafts with +# GITHUB_TOKEN, and GitHub deliberately raises no workflow events for anything a +# GITHUB_TOKEN does, so that trigger never fired once between 2026-06-25 and +# 2026-09-24 and every channel silently stayed on 0.1.0. release.yml now calls +# this workflow directly instead, which needs no PAT. on: - release: - types: [published] + workflow_call: + inputs: + version: + description: 'Version without v prefix (e.g. 0.1.0)' + required: true + type: string + dry_run: + description: 'Refresh manifests without submitting' + type: boolean + default: false + package_managers: + description: 'Comma-separated, or "all"' + type: string + default: all workflow_dispatch: inputs: version: description: 'Version without v prefix (e.g. 0.1.0)' required: true dry_run: - description: 'Generate manifests without submitting' + description: 'Refresh manifests without submitting' type: boolean default: true package_managers: description: 'Comma-separated, or "all"' default: all +permissions: + contents: write + pull-requests: write + jobs: # Everything except Chocolatey runs on Linux. submit-linux: @@ -31,50 +52,74 @@ jobs: - name: Resolve version id: v - run: | - if [ -n "${{ github.event.inputs.version }}" ]; then V="${{ github.event.inputs.version }}"; else V="${{ github.event.release.tag_name }}"; fi - echo "version=${V#v}" >> "$GITHUB_OUTPUT" + env: + V_IN: ${{ inputs.version }} + run: echo "version=${V_IN#v}" >> "$GITHUB_OUTPUT" - name: Resolve package managers id: pms + env: + PMS_IN: ${{ inputs.package_managers }} run: | - PMS="${{ github.event.inputs.package_managers }}" + PMS="$PMS_IN" [ -z "$PMS" ] && PMS="all" if [ "$PMS" = "all" ]; then PMS="homebrew,scoop,winget,aur,apt,rpm,gentoo,nix,snap,flatpak,appimage,freebsd"; fi echo "list=$(echo "$PMS" | sed 's/chocolatey,*//g')" >> "$GITHUB_OUTPUT" - name: Refresh + submit manifests env: - DRY_RUN: ${{ github.event.inputs.dry_run }} + # A release calls this with dry_run=false. This previously read + # github.event.inputs.dry_run, which is empty on anything but a + # workflow_dispatch, so the "!= false" test below appended --dry-run on + # every release run: the submit path could never have executed even if + # the trigger had worked. + DRY_RUN: ${{ inputs.dry_run }} GH_TOKEN: ${{ github.token }} AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} run: | ARGS="-v ${{ steps.v.outputs.version }}" for PM in $(echo "${{ steps.pms.outputs.list }}" | tr ',' ' '); do ARGS="$ARGS -p $PM"; done - # Default to dry-run unless explicitly disabled AND submit secrets exist. if [ "$DRY_RUN" != "false" ]; then ARGS="$ARGS --dry-run"; fi node scripts/submit-packages.mjs $ARGS + # The refreshed manifests are the input to every future submission, so they + # have to outlive the runner. Without this they were rewritten and thrown + # away, which is why distribution/ still said 0.1.0 at release 3.15.0. + - name: Open a PR with the refreshed manifests + if: ${{ inputs.dry_run != true }} + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.v.outputs.version }} + run: bash scripts/open-distribution-pr.sh + # Chocolatey publishes from Windows. submit-chocolatey: runs-on: windows-latest name: Submit (Chocolatey) if: >- - github.event.inputs.package_managers == 'all' || - github.event.inputs.package_managers == '' || - github.event_name == 'release' || - contains(github.event.inputs.package_managers, 'chocolatey') + inputs.package_managers == 'all' || + inputs.package_managers == '' || + contains(inputs.package_managers, 'chocolatey') steps: - uses: actions/checkout@v5 - name: Resolve version id: v shell: bash - run: | - if [ -n "${{ github.event.inputs.version }}" ]; then V="${{ github.event.inputs.version }}"; else V="${{ github.event.release.tag_name }}"; fi - echo "version=${V#v}" >> "$GITHUB_OUTPUT" - - name: Refresh chocolatey manifest + env: + V_IN: ${{ inputs.version }} + run: echo "version=${V_IN#v}" >> "$GITHUB_OUTPUT" + - name: Refresh + submit chocolatey manifest shell: bash env: + # Was hardcoded --dry-run, so chocolatey could never publish regardless + # of trigger or credentials. + DRY_RUN: ${{ inputs.dry_run }} CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }} - run: node scripts/submit-packages.mjs -v ${{ steps.v.outputs.version }} -p chocolatey --dry-run + run: | + ARGS="-v ${{ steps.v.outputs.version }} -p chocolatey" + if [ "$DRY_RUN" != "false" ]; then ARGS="$ARGS --dry-run"; fi + node scripts/submit-packages.mjs $ARGS diff --git a/distribution/aur/PKGBUILD b/distribution/aur/PKGBUILD index c534fd1f..9200deac 100644 --- a/distribution/aur/PKGBUILD +++ b/distribution/aur/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Profullstack, Inc. # AUR binary package — consumes the GitHub release tarball. pkgname=tronbrowser-bin -pkgver=0.1.0 +pkgver=3.15.0 pkgrel=1 pkgdesc="Open-source, privacy-first, AI-native browser (Ungoogled Chromium fork)" arch=('x86_64') @@ -11,7 +11,7 @@ depends=('chromium') provides=('tronbrowser') conflicts=('tronbrowser') source=("tronbrowser-${pkgver}.tar.gz::https://github.com/profullstack/tronbrowser.dev/releases/download/v${pkgver}/tronbrowser-linux-x64.tar.gz") -sha256sums=('d966a54a6369ec283203abd257f89e50844ff68551229a463f568dc638dcf4c7') +sha256sums=('0870fc5fd4b7acdfa1679f542d038a34e932b65721918cb4c1071e7a6b008779') package() { install -dm755 "${pkgdir}/usr/lib/tronbrowser" diff --git a/distribution/homebrew/tronbrowser.rb b/distribution/homebrew/tronbrowser.rb index 93f73952..1bdb2a67 100644 --- a/distribution/homebrew/tronbrowser.rb +++ b/distribution/homebrew/tronbrowser.rb @@ -4,17 +4,17 @@ class Tronbrowser < Formula desc "Open-source, privacy-first, AI-native browser (Ungoogled Chromium fork)" homepage "https://tronbrowser.dev" - version "0.1.0" + version "3.15.0" license "MIT" on_macos do - url "https://github.com/profullstack/tronbrowser.dev/releases/download/v0.1.0/tronbrowser-macos.zip" + url "https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-macos.zip" sha256 "4f928b90b83a34d90edf6f3b4f522c47b85090046424ce69cc535a0cb85d77d1" end on_linux do - url "https://github.com/profullstack/tronbrowser.dev/releases/download/v0.1.0/tronbrowser-linux-x64.tar.gz" - sha256 "d966a54a6369ec283203abd257f89e50844ff68551229a463f568dc638dcf4c7" + url "https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-linux-x64.tar.gz" + sha256 "0870fc5fd4b7acdfa1679f542d038a34e932b65721918cb4c1071e7a6b008779" end def install diff --git a/distribution/scoop/tronbrowser.json b/distribution/scoop/tronbrowser.json index c8a21cce..69d0dd58 100644 --- a/distribution/scoop/tronbrowser.json +++ b/distribution/scoop/tronbrowser.json @@ -1,12 +1,12 @@ { - "version": "0.1.0", + "version": "3.15.0", "description": "Open-source, privacy-first, AI-native browser (Ungoogled Chromium fork).", "homepage": "https://tronbrowser.dev", "license": "MIT", "architecture": { "64bit": { - "url": "https://github.com/profullstack/tronbrowser.dev/releases/download/v0.1.0/tronbrowser-win-x64.zip", - "hash": "TODO-needs-windows-build" + "url": "https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-win-x64.zip", + "hash": "ebb5b0c7675255accc9975e0ac31a542cb9c9eb1bb700eac18fce8cca681f82c" } }, "bin": [["tronbrowser/tronbrowser.exe", "tron"], ["tronbrowser/tronbrowser.exe", "tronbrowser"]], @@ -14,7 +14,7 @@ "autoupdate": { "architecture": { "64bit": { - "url": "https://github.com/profullstack/tronbrowser.dev/releases/download/v$version/tronbrowser-win-x64.zip" + "url": "https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-win-x64.zip" } } } diff --git a/distribution/snap/snapcraft.yaml b/distribution/snap/snapcraft.yaml index 21cbf8e2..791a1a76 100644 --- a/distribution/snap/snapcraft.yaml +++ b/distribution/snap/snapcraft.yaml @@ -2,7 +2,7 @@ # and exec the host's Chromium. `snapcraft` builds; publish with `snapcraft upload`. name: tronbrowser base: core24 -version: '0.1.1' +version: '3.15.0' summary: Privacy-first, AI-native browser (Ungoogled Chromium fork) description: | Open-source, privacy-first, AI-native web browser built on Ungoogled Chromium. @@ -20,6 +20,6 @@ apps: parts: tronbrowser: plugin: dump - source: https://github.com/profullstack/tronbrowser.dev/releases/download/v0.1.1/tronbrowser-linux-x64.tar.gz + source: https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-linux-x64.tar.gz organize: 'tronbrowser/*': './' diff --git a/distribution/winget/Profullstack.TronBrowser.installer.yaml b/distribution/winget/Profullstack.TronBrowser.installer.yaml index a87b59b8..618198a6 100644 --- a/distribution/winget/Profullstack.TronBrowser.installer.yaml +++ b/distribution/winget/Profullstack.TronBrowser.installer.yaml @@ -2,12 +2,12 @@ # manifests/p/Profullstack/TronBrowser//. Needs a Windows installer # (.exe/.msi) or a portable zip artifact — pending the Windows build. PackageIdentifier: Profullstack.TronBrowser -PackageVersion: 0.1.0 +PackageVersion: 3.15.0 InstallerType: zip Installers: - Architecture: x64 - InstallerUrl: https://github.com/profullstack/tronbrowser.dev/releases/download/v0.1.0/tronbrowser-win-x64.zip - InstallerSha256: TODO-NEEDS-WINDOWS-BUILD + InstallerUrl: https://github.com/profullstack/tronbrowser.dev/releases/download/v3.15.0/tronbrowser-win-x64.zip + InstallerSha256: EBB5B0C7675255ACCC9975E0AC31A542CB9C9EB1BB700EAC18FCE8CCA681F82C NestedInstallerType: portable NestedInstallerFiles: - RelativeFilePath: tronbrowser\tronbrowser.exe diff --git a/scripts/open-distribution-pr.sh b/scripts/open-distribution-pr.sh new file mode 100755 index 00000000..3da6cf31 --- /dev/null +++ b/scripts/open-distribution-pr.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Open a PR with the manifests submit-packages.mjs just refreshed. +# +# Why this exists: the refresh rewrites files under distribution/ on the runner +# and nothing ever persisted them, so every manifest in git stayed frozen at the +# version it was scaffolded with (0.1.0/0.1.1) while releases went out to 3.15.0. +# Those manifests are the input to every channel's submission, so a stale tree +# means even a working submit publishes the wrong version. +# +# A PR rather than a push to main: main is protected, and a bad checksum should +# be reviewable before it reaches a package manager. +set -euo pipefail + +: "${VERSION:?VERSION must be set}" + +if git diff --quiet -- distribution; then + echo "distribution/ already current for v${VERSION}, nothing to commit" + exit 0 +fi + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + +BRANCH="chore/distribution-${VERSION}" + +# A re-run of the same release must not fail on an existing branch. +if git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null 2>&1; then + echo "branch ${BRANCH} already exists on origin; force-updating it" + git checkout -B "${BRANCH}" + git add distribution + git commit -m "chore(distribution): refresh manifests for v${VERSION}" + git push --force-with-lease origin "${BRANCH}" +else + git checkout -b "${BRANCH}" + git add distribution + git commit -m "chore(distribution): refresh manifests for v${VERSION}" + git push origin "${BRANCH}" +fi + +# `gh pr create` fails if one is already open for the branch, which is fine. +gh pr create \ + --base main \ + --head "${BRANCH}" \ + --title "chore(distribution): refresh manifests for v${VERSION}" \ + --body "Automated manifest refresh from the release pipeline for v${VERSION}. + +Version strings and sha256 checksums are rewritten from the published release +assets by \`scripts/submit-packages.mjs\`." \ + || echo "a PR for ${BRANCH} already exists; branch updated in place" diff --git a/scripts/submit-packages.mjs b/scripts/submit-packages.mjs index db12f592..8f89d05f 100644 --- a/scripts/submit-packages.mjs +++ b/scripts/submit-packages.mjs @@ -10,10 +10,20 @@ // the checked-in manifests under distribution/. Actual submission (push to a tap // repo / scoop bucket / AUR / winget-pkgs PR / choco push) is gated on the // relevant secret being present and is intentionally a no-op without it. -import { readFileSync, writeFileSync, existsSync } from "node:fs"; +// +// 2026-09-24: "submission is gated on the secret" described an intention, not +// code. There was no submit path at all — no push, no PR, no upload — so every +// run only rewrote files under distribution/ and exited. Channels that we own +// outright (a tap, a bucket, AUR, Chocolatey, Snap) now really do publish; the +// ones that are a pull request into somebody else's monorepo (winget, flathub, +// nixpkgs, gentoo, freebsd) still cannot be automated end to end and say so +// rather than reporting success. +import { readFileSync, writeFileSync, existsSync, mkdtempSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const REPO = process.env.TRONBROWSER_REPO || "profullstack/tronbrowser.dev"; @@ -219,4 +229,238 @@ for (const pm of targets) { } } -console.log(`\nDone (${dryRun ? "dry run" : "manifests updated"}).`); +// --------------------------------------------------------------------------- +// Submission. +// +// Everything above only rewrites files. What follows actually ships them. +// Each channel is gated on its own secret: absent, it prints why it skipped and +// returns cleanly, so a repo with no credentials still gets a green run and a +// refreshed manifest. A channel that has its secret and then fails is a real +// failure and is allowed to break the build — a silent success here is what let +// TronBrowser go three months believing it was published. + +const TAP_REPO = process.env.HOMEBREW_TAP_REPO || "profullstack/homebrew-tap"; +const SCOOP_REPO = process.env.SCOOP_BUCKET_REPO || "profullstack/scoop-bucket"; + +function run(cmd, args, opts = {}) { + return execFileSync(cmd, args, { + encoding: "utf8", + stdio: opts.capture ? "pipe" : "inherit", + cwd: opts.cwd || ROOT, + env: { ...process.env, ...(opts.env || {}) }, + }); +} + +function tmp(prefix) { + return mkdtempSync(join(tmpdir(), `tron-${prefix}-`)); +} + +/** Clone a repo we own, drop one file in, and push it back. */ +function pushFileToRepo({ repo, token, file, dest, message }) { + const dir = tmp(dest.replace(/[^a-z0-9]+/gi, "-")); + const url = `https://x-access-token:${token}@github.com/${repo}.git`; + run("git", ["clone", "--depth", "1", url, dir], { cwd: ROOT }); + run("cp", [join(ROOT, file), join(dir, dest)], { cwd: ROOT }); + run("git", ["config", "user.name", "github-actions[bot]"], { cwd: dir }); + run("git", [ + "config", + "user.email", + "41898282+github-actions[bot]@users.noreply.github.com", + ], { cwd: dir }); + const changed = run("git", ["status", "--porcelain"], { + cwd: dir, + capture: true, + }).trim(); + if (!changed) { + console.log(` ${repo} already current, nothing to push`); + return; + } + run("git", ["add", dest], { cwd: dir }); + run("git", ["commit", "-m", message], { cwd: dir }); + run("git", ["push"], { cwd: dir }); + console.log(` pushed ${dest} to ${repo}`); +} + +/** Channels that are a PR into a third party's monorepo. */ +function upstreamPr(pm, repo, doc) { + console.log( + ` ${pm}: needs a pull request into ${repo}, which is a human review queue.`, + ); + console.log(` manifest refreshed; open the PR by hand: ${doc}`); +} + +const SUBMITTERS = { + homebrew: () => { + const token = process.env.HOMEBREW_TAP_TOKEN; + if (!token) return skip("homebrew", "HOMEBREW_TAP_TOKEN"); + pushFileToRepo({ + repo: TAP_REPO, + token, + file: "distribution/homebrew/tronbrowser.rb", + dest: "Formula/tronbrowser.rb", + message: `tronbrowser ${version}`, + }); + }, + + scoop: () => { + const token = process.env.SCOOP_BUCKET_TOKEN; + if (!token) return skip("scoop", "SCOOP_BUCKET_TOKEN"); + pushFileToRepo({ + repo: SCOOP_REPO, + token, + file: "distribution/scoop/tronbrowser.json", + dest: "bucket/tronbrowser.json", + message: `tronbrowser ${version}`, + }); + }, + + aur: () => { + const key = process.env.AUR_SSH_KEY; + if (!key) return skip("aur", "AUR_SSH_KEY"); + const dir = tmp("aur"); + const keyfile = join(dir, "aur_key"); + writeFileSync(keyfile, key.endsWith("\n") ? key : `${key}\n`, { + mode: 0o600, + }); + // AUR only speaks ssh, and the runner has never seen the host before. + const ssh = `ssh -i ${keyfile} -o StrictHostKeyChecking=accept-new`; + const repoDir = join(dir, "tronbrowser-bin"); + run("git", ["clone", "ssh://aur@aur.archlinux.org/tronbrowser-bin.git", repoDir], { + cwd: ROOT, + env: { GIT_SSH_COMMAND: ssh }, + }); + run("cp", [join(ROOT, "distribution/aur/PKGBUILD"), join(repoDir, "PKGBUILD")]); + // .SRCINFO is generated, and the AUR rejects a push whose .SRCINFO disagrees + // with the PKGBUILD. makepkg is not on a GitHub runner, so write it from the + // PKGBUILD we already have rather than shelling out to a tool that is absent. + writeSrcinfo(repoDir); + run("git", ["config", "user.name", "TronBrowser CI"], { cwd: repoDir }); + run("git", ["config", "user.email", "bot@tronbrowser.dev"], { cwd: repoDir }); + const changed = run("git", ["status", "--porcelain"], { + cwd: repoDir, + capture: true, + }).trim(); + if (!changed) return console.log(" AUR already current, nothing to push"); + run("git", ["add", "PKGBUILD", ".SRCINFO"], { cwd: repoDir }); + run("git", ["commit", "-m", `tronbrowser-bin ${version}`], { cwd: repoDir }); + run("git", ["push"], { cwd: repoDir, env: { GIT_SSH_COMMAND: ssh } }); + console.log(" pushed PKGBUILD to the AUR"); + }, + + chocolatey: () => { + const key = process.env.CHOCOLATEY_API_KEY; + if (!key) return skip("chocolatey", "CHOCOLATEY_API_KEY"); + const dir = join(ROOT, "distribution/chocolatey"); + run("choco", ["pack", "tronbrowser.nuspec", "--outputdirectory", dir], { + cwd: dir, + }); + run("choco", [ + "push", + join(dir, `tronbrowser.${version}.nupkg`), + "--source", + "https://push.chocolatey.org/", + "--api-key", + key, + ], { cwd: dir }); + console.log(" pushed to Chocolatey"); + }, + + snap: () => { + const creds = process.env.SNAPCRAFT_STORE_CREDENTIALS; + if (!creds) return skip("snap", "SNAPCRAFT_STORE_CREDENTIALS"); + // snapcraft reads the credentials straight out of the environment; building + // the snap itself needs LXD and happens in the release job, not here. + const snap = process.env.SNAP_FILE; + if (!snap || !existsSync(snap)) { + console.log( + " snap: SNAPCRAFT_STORE_CREDENTIALS is set but no built .snap was passed", + ); + console.log(" set SNAP_FILE to the artifact built by the release job"); + return; + } + run("snapcraft", ["upload", "--release", "stable", snap]); + console.log(" uploaded to the Snap Store"); + }, + + winget: () => + upstreamPr( + "winget", + "microsoft/winget-pkgs", + "https://github.com/microsoft/winget-pkgs/blob/master/CONTRIBUTING.md", + ), + flatpak: () => + upstreamPr( + "flatpak", + "flathub/flathub", + "https://docs.flathub.org/docs/for-app-authors/submission", + ), + nix: () => + upstreamPr( + "nix", + "NixOS/nixpkgs", + "https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md", + ), + gentoo: () => + upstreamPr( + "gentoo", + "an ebuild overlay", + "https://wiki.gentoo.org/wiki/Ebuild_repository", + ), + freebsd: () => + upstreamPr( + "freebsd", + "freebsd/freebsd-ports", + "https://docs.freebsd.org/en/books/porters-handbook/", + ), + + // Built and attached to the GitHub release itself; there is no downstream. + apt: () => console.log(" apt: attached to the GitHub release, nothing to submit"), + rpm: () => console.log(" rpm: attached to the GitHub release, nothing to submit"), + appimage: () => + console.log(" appimage: attached to the GitHub release, nothing to submit"), +}; + +function skip(pm, secret) { + console.log(` ${pm}: ${secret} not set, manifest refreshed but not submitted`); +} + +/** + * Write .SRCINFO from the PKGBUILD. + * + * Only the handful of fields our PKGBUILD actually sets — enough for the AUR to + * accept the push and for a helper to resolve the package. If PKGBUILD grows + * fields beyond these, this needs to grow with it. + */ +function writeSrcinfo(repoDir) { + const pkgbuild = readFileSync(join(repoDir, "PKGBUILD"), "utf8"); + const field = (name) => { + const m = pkgbuild.match(new RegExp(`^${name}=(.+)$`, "m")); + return m ? m[1].replace(/^['"(]+|['")]+$/g, "") : ""; + }; + const lines = [ + `pkgbase = ${field("pkgname")}`, + `\tpkgdesc = ${field("pkgdesc")}`, + `\tpkgver = ${field("pkgver")}`, + `\tpkgrel = ${field("pkgrel")}`, + `\turl = ${field("url")}`, + `\tarch = ${field("arch")}`, + `\tlicense = ${field("license")}`, + `\tsource = ${field("source")}`, + `\tsha256sums = ${field("sha256sums")}`, + "", + `pkgname = ${field("pkgname")}`, + "", + ]; + writeFileSync(join(repoDir, ".SRCINFO"), lines.join("\n")); +} + +if (!dryRun) { + for (const pm of targets) { + const submit = SUBMITTERS[pm]; + if (!submit) continue; + console.log(`\n== submit ${pm} ==`); + submit(); + } +} + +console.log(`\nDone (${dryRun ? "dry run" : "manifests updated and submitted"}).`);