Skip to content

Commit ac185b3

Browse files
committed
refactor(v1.5.0): releases move to CI: pushing a tag now builds and publishes, and push.sh only ships commits and tag unless --full is given — Actions is free on public repositories, so the published binaries stop depending on whichever toolchain this workstation happens to have
1 parent 49056eb commit ac185b3

6 files changed

Lines changed: 195 additions & 65 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# The same three checks the commit gate runs locally (scripts/commit.sh), so a green commit
2+
# cannot turn red on push for a reason the author could have seen beforehand.
3+
name: CI
4+
5+
on:
6+
push:
7+
branches: ["**"]
8+
pull_request:
9+
10+
jobs:
11+
check:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-go@v5
16+
with:
17+
go-version-file: go.mod
18+
19+
# Reported as a failure with the file list, rather than rewriting the tree: CI that edits
20+
# the code hides the problem instead of showing it.
21+
- name: gofmt
22+
run: |
23+
unformatted="$(gofmt -l .)"
24+
if [ -n "$unformatted" ]; then
25+
echo "Files not gofmt'd:"
26+
echo "$unformatted"
27+
exit 1
28+
fi
29+
30+
- name: go vet
31+
run: go vet ./...
32+
33+
- name: go build
34+
run: go build ./...
35+
36+
- name: go test
37+
run: go test ./... -count=1

.github/workflows/release.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Builds the binaries and publishes them as Release assets when a vX.Y.Z tag is pushed.
2+
#
3+
# This is the NORMAL path: `scripts/push.sh` pushes the commits and the tag, and the tag is what
4+
# starts this. Building here rather than on a workstation means the published artefact depends
5+
# only on the tag and on this file — not on which Go version someone happened to have installed.
6+
#
7+
# It is free: Actions on standard runners costs nothing for public repositories, so there is no
8+
# quota argument for building elsewhere.
9+
#
10+
# `scripts/push.sh --full` does the same job locally and uploads the result, for when this
11+
# workflow cannot run.
12+
name: release
13+
14+
on:
15+
push:
16+
tags: ["v*"]
17+
workflow_dispatch: {}
18+
19+
permissions:
20+
contents: write # create the Release and attach the assets
21+
22+
jobs:
23+
build-and-release:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0 # the tag's annotation becomes the release notes
29+
30+
- uses: actions/setup-go@v5
31+
with:
32+
go-version-file: go.mod
33+
34+
# The suite runs before anything is published. A tag that does not pass must not become a
35+
# release somebody downloads.
36+
- name: Test
37+
run: go test ./... -count=1
38+
39+
- name: Build
40+
run: |
41+
set -euo pipefail
42+
tag="${GITHUB_REF_NAME}"
43+
mkdir -p dist
44+
# CGO off gives a static binary that runs on any distribution — no glibc version to
45+
# match. -trimpath keeps local paths out of it, and -s -w drops the debug tables.
46+
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
47+
os="${target%/*}"; arch="${target#*/}"
48+
out="dist/api-mcp_${tag}_${os}_${arch}"
49+
[ "$os" = "windows" ] && out="${out}.exe"
50+
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -trimpath \
51+
-ldflags "-s -w -X main.version=${tag#v}" -o "$out" .
52+
done
53+
# Checksums travel with the binaries so anyone can verify what they downloaded. One
54+
# file, in the format `sha256sum -c` reads directly.
55+
( cd dist && sha256sum api-mcp_* > SHA256SUMS )
56+
ls -l dist
57+
58+
- name: Publish
59+
env:
60+
GH_TOKEN: ${{ github.token }}
61+
run: |
62+
set -euo pipefail
63+
tag="${GITHUB_REF_NAME}"
64+
notes="$(git tag -l "$tag" --format='%(contents)')"
65+
# `--latest` is explicit on both paths. Left to GitHub it is decided by DATE, which is
66+
# wrong whenever two releases go out close together — the label can land on the LOWER
67+
# version.
68+
if gh release view "$tag" >/dev/null 2>&1; then
69+
# `--clobber`: without it the Release keeps the previous run's binaries, silently.
70+
gh release upload "$tag" dist/* --clobber
71+
gh release edit "$tag" --notes "$notes" --latest
72+
else
73+
gh release create "$tag" dist/* --title "$tag" --notes "$notes" --latest
74+
fi

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
> Generated from `git` by `scripts/gen-changelog.sh` — do not edit by hand.
44
5+
- **v1.5.0** (2026-08-23) · `refactor` — releases move to CI: pushing a tag now builds and publishes, and push.sh only ships commits and tag unless --full is given — Actions is free on public repositories, so the published binaries stop depending on whichever toolchain this workstation happens to have
56
- **v1.4.0** (2026-08-22) · `feat` — sign gains method, base64 and iso8601 timestamps: a scheme that signs the verb, encodes base64 and stamps an ISO instant had no way to be expressed — and every one of those mismatches fails as an authentication error that never names the format
67
- **v1.3.1** (2026-08-19) · `fix` — property names the MCP client refuses are aliased: it validates arguments against ^[a-zA-Z0-9_.-]{1,64}$ and rejects the whole CALL when one fails — PHP-style filters[offer_id] broke 7 of 9 tools on a real API
78
- **v1.3.0** (2026-08-19) · `feat` — per-request signature auth: APIs that sign every call over its own content — sha256 or hmac-sha256, with the payload and destination as templates

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,12 @@ A large spec becomes dozens of tools, and each one takes up the model's context:
177177

178178
```sh
179179
./commit.sh feat "what changed" # gate → version bump → CHANGELOG → tag
180-
./push.sh # build the binaries, push, tag and publish the Release
180+
./push.sh # push the commits and the tag; CI builds and publishes
181+
./push.sh --full # ...or build the binaries here and upload them
181182
```
182183

183-
Both are shortcuts to `scripts/`.
184+
Both are shortcuts to `scripts/`. Pushing a `vX.Y.Z` tag starts the release workflow, which
185+
cross-compiles and publishes; `--full` does the same locally, for when the workflow cannot run.
184186

185187
## Documentation
186188

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.4.0
1+
1.5.0

scripts/push.sh

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
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
1621
set -euo pipefail
1722

1823
ROOT="$(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

2437
TAG_VERSION="$(cat "$ROOT/VERSION" 2>/dev/null)" && [[ -n "$TAG_VERSION" ]] \
2538
|| die "VERSION missing in $ROOT"
@@ -34,76 +47,79 @@ TAG="v${TAG_VERSION}"
3447
git -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 -----------------------------------------------------------------
95104
log "git push"
96105
git -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.
103112
log "git push of tag ${TAG}"
104113
git -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

Comments
 (0)