Files
qqq-cli/scripts/publish-packaging.sh
James MaesandClaude Opus 5.5 bf9c093bdd
node / node (push) Successful in 1m5s
build / node (push) Successful in 1m5s
ci(release): open tap and bucket pull requests instead of pushing to main
For a final version the publish job now pushes branch release/qqq-<version>
to QRun-IO/homebrew-tap and QRun-IO/scoop-bucket with PACKAGES_TOKEN and
opens a pull request "qqq <version>" in each; a human merges. A prerelease,
or a refused push or pull request, attaches qqq.rb / qqq.json to the release
as before, and never fails the release. The logic moves to
scripts/publish-packaging.sh, with a DRY_RUN=1 path that clones and commits
locally but writes nothing remote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:23:00 -06:00

86 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright 2026 QRun.IO, Inc.
# Licensed under the Apache License, Version 2.0 (see LICENSE).
#
# Offer the filled Homebrew formula and Scoop manifest (scripts/update-packaging.mjs) to the tap and the
# bucket as pull requests; a human merges them. Called by the release workflow's publish job.
#
# final version push branch release/qqq-<version> to QRun-IO/homebrew-tap and QRun-IO/scoop-bucket with
# PACKAGES_TOKEN and open a pull request "qqq <version>" in each
# otherwise (prerelease, or the push or the pull request is refused) attach qqq.rb / qqq.json to the
# release instead, for a hand-made pull request (docs/release-runbook.md step 4)
#
# Never fails the release over the tap or the bucket. DRY_RUN=1 clones and commits locally but pushes,
# opens and attaches nothing; it needs only VERSION (and GITHUB_SERVER_URL, default https://git.kof22.com):
# DRY_RUN=1 VERSION=1.2.3 scripts/publish-packaging.sh
#
# Environment: VERSION, GITHUB_SERVER_URL, GITHUB_REPOSITORY, RELEASE_ID, TOKEN (job token, release assets),
# PACKAGES_TOKEN (tap and bucket), DRY_RUN.
set -euo pipefail
server=${GITHUB_SERVER_URL:-https://git.kof22.com}
api="$server/api/v1"
dry=${DRY_RUN:-0}
: "${VERSION:?VERSION is required}"
[[ $dry == 1 ]] || : "${TOKEN:?TOKEN is required}" "${RELEASE_ID:?RELEASE_ID is required}" "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
branch="release/qqq-$VERSION"
title="qqq $VERSION"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
json() { node -e 'const o = {}; for (let i = 1; i < process.argv.length; i += 2) o[process.argv[i]] = process.argv[i + 1];
process.stdout.write(JSON.stringify(o))' -- "$@"; }
field() { node -e 'try { const v = JSON.parse(require("fs").readFileSync(0, "utf8")); const r = [].concat(v)
.find((p) => !process.argv[2] || (p.head && p.head.ref === process.argv[2])); process.stdout.write(String(r ? r[process.argv[1]] ?? "" : "")) }
catch { }' -- "$@"; }
attach() { # attach <file>: the fallback, a hand-made pull request from the release asset
local src=$1 name
name=$(basename "$src")
if [[ $dry == 1 ]]; then echo "DRY RUN: would attach $name to the release"; return 0; fi
curl -fsS -o /dev/null -X POST -H "Authorization: token $TOKEN" -F "attachment=@$src" \
"$api/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$name" ||
{ echo "::warning::could not attach $name to the release"; return 0; }
echo "Attached $name to the release for a hand-made pull request"
}
propose() { # propose <repo> <src> <dest>: branch + pull request; non-zero means fall back
local repo=$1 src=$2 dest=$3 dir="$work/${1##*/}" header=() base pr body
[[ $dry == 1 || -n ${PACKAGES_TOKEN:-} ]] || { echo "PACKAGES_TOKEN is empty"; return 1; }
[[ -n ${PACKAGES_TOKEN:-} ]] && header=(-c "http.extraHeader=Authorization: Basic $(printf 'ci:%s' "$PACKAGES_TOKEN" | base64 | tr -d '\n')")
git "${header[@]}" clone -q --depth 1 "$server/$repo.git" "$dir" || return 1
base=$(git -C "$dir" rev-parse --abbrev-ref HEAD)
git -C "$dir" checkout -q -b "$branch"
mkdir -p "$(dirname "$dir/$dest")"
cp "$src" "$dir/$dest"
git -C "$dir" add "$dest"
if git -C "$dir" diff --cached --quiet; then echo "$repo $base already has qqq $VERSION"; return 0; fi
git -C "$dir" -c user.name=CI -c user.email=ci@kof22.com commit -q -m "$title"
git -C "$dir" --no-pager show --stat --format='%s' HEAD
if [[ $dry == 1 ]]; then
echo "DRY RUN: would push $branch to $repo and open pull request \"$title\" into $base"; return 0
fi
# CI owns release/qqq-<version>: a re-run replaces it.
git -C "$dir" "${header[@]}" push -q --force origin "HEAD:refs/heads/$branch" || { echo "push to $repo refused"; return 1; }
body="Formula/manifest for qqq $VERSION, filled by the qqq-cli release workflow from the release's SHA256SUMS: $server/$GITHUB_REPOSITORY/releases/tag/v$VERSION. Check the sums, then merge."
pr=$(curl -fsS -X POST -H "Authorization: token $PACKAGES_TOKEN" -H 'Content-Type: application/json' \
-d "$(json head "$branch" base "$base" title "$title" body "$body")" "$api/repos/$repo/pulls" 2>/dev/null | field html_url) || pr=''
if [[ -z $pr ]]; then # a re-run: the pull request may already be open
pr=$(curl -fsS -H "Authorization: token $PACKAGES_TOKEN" "$api/repos/$repo/pulls?state=open&limit=50" 2>/dev/null |
field html_url "$branch") || pr=''
fi
[[ -n $pr ]] || { echo "pushed $branch to $repo but could not open the pull request"; return 1; }
echo "Opened $pr"
}
node "$(dirname "$0")/update-packaging.mjs" "${SUMS_FILE:-/tmp/assets/SHA256SUMS}"
for spec in "QRun-IO/homebrew-tap docs/packaging/homebrew-tap/Formula/qqq.rb Formula/qqq.rb" \
"QRun-IO/scoop-bucket docs/packaging/qqq.json bucket/qqq.json"; do
read -r repo src dest <<< "$spec"
# Final versions only: the tap and the bucket follow the stable channel, like scripts/install.sh.
if [[ $VERSION == *-* ]]; then
echo "$repo not updated: $VERSION is a prerelease"; attach "$src"; continue
fi
propose "$repo" "$src" "$dest" || { echo "$repo not updated: falling back to the release asset"; attach "$src"; }
done