11#! /usr/bin/env bash
2- # push.sh — ships the work: commits, this version's tag and the GitHub Release .
2+ # push.sh — ships the work: the commits and this version's tag.
33#
4- # Usage: ./push.sh (root shortcut → this file, same as ./commit.sh)
4+ # Usage:
5+ # ./push.sh push the commits and the tag; CI builds and publishes the Release
6+ # ./push.sh --full ALSO build the binaries here and upload them (-f works too)
57#
6- # What CLOSES a version is ./commit.sh (bump, changelog, tag). This script only publishes it.
7- # The expensive checks run BEFORE anything is pushed, so the script fails without leaving half
8- # the work out there.
8+ # What CLOSES a version is ./commit.sh (bump, changelog, tag). This script only ships it.
99#
10- # Order: build the binaries → git push → git push of this version's tag → publish/update the
11- # Release with the binaries attached. Building comes first on purpose: a compile error must not
12- # be discovered after the commits are already public.
10+ # By default nothing is built here. Pushing the tag starts .github/workflows/release.yml, which
11+ # cross-compiles and publishes — and on a public repository that costs nothing, so there is no
12+ # reason to spend a workstation's time on it or to make the artefact depend on whichever Go
13+ # version that machine has.
1314#
14- # Requires: `gh` authenticated with write access to the repo
15+ # `--full` is the escape hatch: it builds locally and uploads, for when the workflow cannot run
16+ # (GitHub Actions down, or a release that has to go out from here). The tag still triggers the
17+ # workflow, which will overwrite the assets with its own build — same inputs, same output.
18+ #
19+ # Requires: `gh` authenticated with write access to the repo (only with --full)
1520# gh auth login
1621set -euo pipefail
1722
1823ROOT=" $( cd " $( dirname " $( readlink -f " ${BASH_SOURCE[0]} " ) " ) /.." && pwd) "
1924
20- log () { printf ' \n\033[1m==> %s\033[0m\n' " $* " ; }
21- ok () { printf ' \033[0;32m✓\033[0m %s\n' " $* " ; }
22- die () { printf ' \n\033[0;31m!! %s\033[0m\n' " $* " >&2 ; exit 1; }
25+ log () { printf ' \n\033[1m==> %s\033[0m\n' " $* " ; }
26+ ok () { printf ' \033[0;32m✓\033[0m %s\n' " $* " ; }
27+ die () { printf ' \n\033[0;31m!! %s\033[0m\n' " $* " >&2 ; exit 1; }
28+
29+ FULL=0
30+ for arg in " $@ " ; do
31+ case " $arg " in
32+ -f|--full) FULL=1 ;;
33+ * ) die " unknown argument: $arg (usage: $0 [--full])" ;;
34+ esac
35+ done
2336
2437TAG_VERSION=" $( cat " $ROOT /VERSION" 2> /dev/null) " && [[ -n " $TAG_VERSION " ]] \
2538 || die " VERSION missing in $ROOT "
@@ -34,76 +47,79 @@ TAG="v${TAG_VERSION}"
3447git -C " $ROOT " rev-parse " $TAG " > /dev/null 2>&1 \
3548 || die " tag ${TAG} does not exist — ./commit.sh creates it alongside the release commit"
3649
37- command -v gh > /dev/null 2>&1 || die " gh (GitHub CLI) is not installed — https://cli.github.com"
38- command -v go > /dev/null 2>&1 || die " go is not installed — the binaries are built here"
50+ if [[ $FULL -eq 1 ]]; then
51+ command -v gh > /dev/null 2>&1 || die " gh (GitHub CLI) is not installed — https://cli.github.com"
52+ command -v go > /dev/null 2>&1 || die " go is not installed — --full builds the binaries here"
3953
40- # A REAL credential check: it asks for WRITE permission, not just whether the repo can be read.
41- #
42- # Reading is not the question — the repository is public, so any account can do it, including
43- # one that cannot publish a thing. Checking only for read is how the commits and the tag went
44- # up and the Release did not: `gh` was authenticated as another account, and the failure only
45- # surfaced at the last step, with everything already public.
46- #
47- # The error `gh` reports in that case blames the "workflow" scope, which sends you off
48- # refreshing a token that was never the problem. Hence the explicit message here.
49- log " checking GitHub credentials"
50- GH_USER=" $( gh api user --jq .login 2> /dev/null) " \
51- || die " not authenticated with gh.
54+ # A REAL credential check: it asks for WRITE permission, not just whether the repo can be read.
55+ #
56+ # Reading is not the question — the repository is public, so any account can do it, including
57+ # one that cannot publish a thing. Checking only for read is how commits and tag go up and the
58+ # Release does not: `gh` authenticated as another account, and the failure surfacing at the
59+ # last step with everything already public.
60+ #
61+ # The error `gh` reports in that case blames the "workflow" scope, which sends you off
62+ # refreshing a token that was never the problem. Hence the explicit message here.
63+ log " checking GitHub credentials"
64+ GH_USER=" $( gh api user --jq .login 2> /dev/null) " \
65+ || die " not authenticated with gh.
5266 Authenticate with:
5367 gh auth login"
54- # owner/repo out of the remote URL, whatever its shape: https://github.com/owner/repo.git,
55- # git@github.com:owner/repo.git, or an SSH host alias (git@github-work:owner/repo).
56- REMOTE_URL=" $( git -C " $ROOT " remote get-url origin) "
57- REPO=" $( printf ' %s\n' " ${REMOTE_URL% .git} " | awk -F' [:/]' ' {print $(NF-1)"/"$NF}' ) "
58- gh api " repos/${REPO} " --jq ' .permissions.push' 2> /dev/null | grep -q true \
59- || die " the account '${GH_USER} ' has no write access to ${REPO} .
68+ # owner/repo out of the remote URL, whatever its shape: https://github.com/owner/repo.git,
69+ # git@github.com:owner/repo.git, or an SSH host alias (git@github-work:owner/repo).
70+ REMOTE_URL=" $( git -C " $ROOT " remote get-url origin) "
71+ REPO=" $( printf ' %s\n' " ${REMOTE_URL% .git} " | awk -F' [:/]' ' {print $(NF-1)"/"$NF}' ) "
72+ gh api " repos/${REPO} " --jq ' .permissions.push' 2> /dev/null | grep -q true \
73+ || die " the account '${GH_USER} ' has no write access to ${REPO} .
6074 You are probably authenticated as the wrong account. Check with:
6175 gh auth status
6276 and switch with:
6377 gh auth switch --user <account>"
64- ok " credentials ok (${GH_USER} can write to ${REPO} )"
78+ ok " credentials ok (${GH_USER} can write to ${REPO} )"
79+ fi
6580
66- # --- 1. the binaries --------------------------------------------------------
67- # Cross-compiled here, not by CI: this repository has no workflow, and a release whose binaries
68- # depend on a machine nobody controls is a release that stops working without warning.
69- #
70- # CGO_ENABLED=0 gives a static binary that runs on any distribution — no glibc version to match.
71- # `-trimpath` keeps local paths out of the binary, and `-s -w` drops the debug tables (~30%).
72- # The version is embedded so `api-mcp` in the wild can say which build it is.
73- BUILD_DIR=" $( mktemp -d) "
74- # The binaries are disposable: they are the Release's artifact, not the repository's. They go
75- # even if publishing fails, so they never become stray files the next `git status` reports.
76- trap ' rm -rf "$BUILD_DIR"' EXIT
77-
78- log " building binaries for ${TAG} "
79- for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
80- os=" ${target%/* } " ; arch=" ${target#*/ } "
81- out=" ${BUILD_DIR} /api-mcp_${TAG} _${os} _${arch} "
82- [[ " $os " == " windows" ]] && out=" ${out} .exe"
83- CGO_ENABLED=0 GOOS=" $os " GOARCH=" $arch " go build -trimpath \
84- -ldflags " -s -w -X main.version=${TAG_VERSION} " -o " $out " " $ROOT " \
85- || die " build failed for ${target} — nothing was pushed"
86- ok " $( basename " $out " ) ($( du -h " $out " | cut -f1) )"
87- done
81+ # --- 1. the binaries, only with --full --------------------------------------
82+ if [[ $FULL -eq 1 ]]; then
83+ BUILD_DIR=" $( mktemp -d) "
84+ # The binaries are disposable: they are the Release's artifact, not the repository's. They go
85+ # even if publishing fails, so they never become stray files the next `git status` reports.
86+ trap ' rm -rf "$BUILD_DIR"' EXIT
87+
88+ log " building binaries for ${TAG} "
89+ for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
90+ os=" ${target%/* } " ; arch=" ${target#*/ } "
91+ out=" ${BUILD_DIR} /api-mcp_${TAG} _${os} _${arch} "
92+ [[ " $os " == " windows" ]] && out=" ${out} .exe"
93+ CGO_ENABLED=0 GOOS=" $os " GOARCH=" $arch " go build -trimpath \
94+ -ldflags " -s -w -X main.version=${TAG_VERSION} " -o " $out " " $ROOT " \
95+ || die " build failed for ${target} — nothing was pushed"
96+ ok " $( basename " $out " ) ($( du -h " $out " | cut -f1) )"
97+ done
8898
89- # Checksums travel with the binaries so anyone can verify what they downloaded is what was
90- # published. One file, the format `sha256sum -c` reads directly.
91- ( cd " $BUILD_DIR " && sha256sum api-mcp_* > SHA256SUMS )
92- ok " SHA256SUMS"
99+ ( cd " $BUILD_DIR " && sha256sum api-mcp_* > SHA256SUMS )
100+ ok " SHA256SUMS"
101+ fi
93102
94103# --- 2. git -----------------------------------------------------------------
95104log " git push"
96105git -C " $ROOT " push
97106
98107# ONLY this version's tag, never `--tags`.
99108#
100- # `--tags` pushes every pending tag at once, and each one triggers a CI job . With two going up
101- # together the jobs run in parallel and the "Latest" label lands on whichever finishes last —
102- # which may well be the LOWER version. One tag per release, in order.
109+ # `--tags` pushes every pending tag at once, and each one starts a workflow run . With two going
110+ # up together the runs happen in parallel and the "Latest" label lands on whichever finishes
111+ # last — which may well be the LOWER version. One tag per release, in order.
103112log " git push of tag ${TAG} "
104113git -C " $ROOT " push origin " $TAG "
105114
106115# --- 3. the Release ---------------------------------------------------------
116+ if [[ $FULL -eq 0 ]]; then
117+ log " done"
118+ ok " $( git -C " $ROOT " rev-parse --short HEAD) · ${TAG} "
119+ ok " CI is building and publishing the Release — watch it with: gh run watch"
120+ exit 0
121+ fi
122+
107123# Idempotent: it exists → update the notes; it does not → create it.
108124#
109125# `--latest` is EXPLICIT on both paths. Without it GitHub decides by DATE, and that decision is
0 commit comments