diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 9b9f022..1bbcf4a 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -3,9 +3,17 @@ name: CI (lib) # First-ever CI for go/lib (ADR-KV-014). lib is publicly mirrored on # code.tnxs.net, so every push and PR gets a build/vet/test job plus a # gitleaks secret-scan job. lib is a Go library — no Dockerfile, no deploy. +# +# push trigger is scoped to main + release tags (not every branch): with +# `branches: ['**']` *and* `pull_request` both set, every PR ran CI twice — +# once for the pull_request event, once for the push to the PR's source +# branch. main+tags replaced branch-per-version (ADR-KV-014, DT-KV-004), so +# there's no longer a fleet of long-lived version branches that need their +# own push-triggered runs independent of a PR. on: push: - branches: ['**'] + branches: [main] + tags: ['v*'] pull_request: defaults: diff --git a/Makefile b/Makefile index bff01d8..5d645d3 100644 --- a/Makefile +++ b/Makefile @@ -1,157 +1,35 @@ -.PHONY: swagger +# Pinned go-swagger version for `make install-swagger` / `make regen`. +# Bump deliberately, verify `swagger version` matches, and record the change +# here and in README.md's Release Procedure. Chosen 2026-07-12: latest +# stable go-swagger release at the time (ADR-KV-014). +SWAGGER_VERSION := v0.35.0 +export SWAGGER_VERSION -swagger: - cp ../auth/swagger/auth-vernonkeenan.yaml ./swagger - cp ../auth/swagger/auth-vernonkeenan.yaml ./swagger/external - cp ../crm/swagger/crm-vernonkeenan.yaml ./swagger - cp ../crm/swagger/crm-vernonkeenan.yaml ./swagger/external - cp ../stash/swagger/stash-vernonkeenan.yaml ./swagger - cp ../stash/swagger/stash-vernonkeenan.yaml ./swagger/external - cp ../sf-gate/swagger/sf-gate-vernonkeenan.yaml ./swagger - cp ../sf-gate/swagger/sf-gate-vernonkeenan.yaml ./swagger/external - cp ../research/swagger/research-vernonkeenan.yaml ./swagger - cp ../research/swagger/research-vernonkeenan.yaml ./swagger/external - cp ../members/swagger/members-vernonkeenan.yaml ./swagger - cp ../members/swagger/members-vernonkeenan.yaml ./swagger/external - cp ../plex/swagger/plex-vernonkeenan.yaml ./swagger - cp ../plex/swagger/plex-vernonkeenan.yaml ./swagger/external - # - rm -rf api - # - # generate auth client - # - mkdir -p api/auth - swagger generate client \ - --log-output=./swagger/logs/generate-auth-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=auth \ - --spec=./swagger/auth-vernonkeenan.yaml \ - --target=./api/auth \ - --client-package=auth-client \ - --model-package=auth-models \ - --principal=app.User - # - # update external auth client - # - sed -i 's|"http"|"https"|g' ./swagger/external/auth-vernonkeenan.yaml - sed -i 's|auth.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/auth-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/auth/v1"|g' ./swagger/external/auth-vernonkeenan.yaml - # - # generate crm client - # - mkdir api/crm - swagger generate client \ - --log-output=./swagger/logs/generate-crm-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=crm \ - --spec=./swagger/crm-vernonkeenan.yaml \ - --target=./api/crm \ - --client-package=crm-client \ - --model-package=crm-models \ - --principal=app.User - # - # update external crm client - # - sed -i 's|"http"|"https"|g' ./swagger/external/crm-vernonkeenan.yaml - sed -i 's|crm.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/crm-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/crm/v1"|g' ./swagger/external/crm-vernonkeenan.yaml - # - # generate stash client - # - mkdir api/stash - swagger generate client \ - --log-output=./swagger/logs/generate-stash-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=stash \ - --spec=./swagger/stash-vernonkeenan.yaml \ - --target=./api/stash \ - --client-package=stash-client \ - --model-package=stash-models \ - --principal=app.User - # - # update external stash client - # - sed -i 's|"http"|"https"|g' ./swagger/external/stash-vernonkeenan.yaml - sed -i 's|stash.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/stash-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/stash/v1"|g' ./swagger/external/stash-vernonkeenan.yaml - # - # generate sfgate client - # - mkdir api/sfgate - swagger generate client \ - --log-output=./swagger/logs/generate-sfgate-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=sfgate \ - --spec=./swagger/sf-gate-vernonkeenan.yaml \ - --target=./api/sfgate \ - --client-package=sfgate-client \ - --model-package=sfgate-models \ - --principal=app.User - # - # update external crm client - # - sed -i 's|"http"|"https"|g' ./swagger/external/sf-gate-vernonkeenan.yaml - sed -i 's|sf-gate.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/sf-gate-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/sf-gate/v1"|g' ./swagger/external/sf-gate-vernonkeenan.yaml - # - # generate research client - # - mkdir api/research - swagger generate client \ - --log-output=./swagger/logs/generate-research-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=research \ - --spec=./swagger/research-vernonkeenan.yaml \ - --target=./api/research \ - --client-package=research-client \ - --model-package=research-models \ - --principal=app.User - # - # update external research client - # - sed -i 's|"http"|"https"|g' ./swagger/external/research-vernonkeenan.yaml - sed -i 's|research.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/research-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/research/v1"|g' ./swagger/external/research-vernonkeenan.yaml - # - # generate members client - # - mkdir api/members - swagger generate client \ - --log-output=./swagger/logs/generate-members-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=members \ - --spec=./swagger/members-vernonkeenan.yaml \ - --target=./api/members \ - --client-package=members-client \ - --model-package=members-models \ - --principal=app.User - # - # update external members client - # - sed -i 's|"http"|"https"|g' ./swagger/external/members-vernonkeenan.yaml - sed -i 's|members.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/members-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/members/v1"|g' ./swagger/external/members-vernonkeenan.yaml - # - # generate plex client - # - mkdir api/plex - swagger generate client \ - --log-output=./swagger/logs/generate-plex-client.log \ - --copyright-file=./build/COPYRIGHT \ - --name=plex \ - --spec=./swagger/plex-vernonkeenan.yaml \ - --target=./api/plex \ - --client-package=plex-client \ - --model-package=plex-models \ - --principal=app.User - # - # update external plex client - # - sed -i 's|"http"|"https"|g' ./swagger/external/plex-vernonkeenan.yaml - sed -i 's|plex.vernonkeenan.com:8080|gw.tnxs.net|g' ./swagger/external/plex-vernonkeenan.yaml - sed -i 's|"/v1"|"/vk/plex/v1"|g' ./swagger/external/plex-vernonkeenan.yaml - # - # copy external swagger files - # - scp ./swagger/external/*.yaml root@whm.noc.tnxs.net:/home/taxassets/www/swagger/external +.PHONY: install-swagger regen release release-dry-run swagger + +# Install the pinned go-swagger CLI and verify its version. +install-swagger: + ./scripts/release.sh install-swagger + +# Regenerate api/ clients from the sibling service specs (../{auth,crm,stash, +# sf-gate,research,members,plex}/swagger/*.yaml). Requires the sibling repos +# to be cloned adjacently (same prerequisite the old README ritual had). +# Review and commit the result as its own PR before `make release`. +regen: install-swagger + ./scripts/release.sh regen + +# Cut a release: tag HEAD of main, push the tag, publish the GitHub release, +# and trigger the Gitea mirror-sync. Requires a clean main checkout that +# matches origin/main. Usage: make release VERSION=v0.7.5 +release: + ./scripts/release.sh release "$(VERSION)" + +# Same as `release` but stops before tag/push/release/mirror-sync — prints +# what would happen. Usage: make release-dry-run VERSION=v0.7.5 +release-dry-run: + ./scripts/release.sh release "$(VERSION)" --dry-run + +# Deprecated alias for `regen`, kept because old muscle memory (and the +# README ritual) called this `make swagger`. +swagger: regen diff --git a/README.md b/README.md index 78b0758..963bd27 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,69 @@ vernonkeenan Shared Libraries (go) ## Dependent Systems -The Gitea server must be up and running with admin access. +The Gitea server must be up and running with admin access. It hosts world-readable +pull-mirrors of this repo at `vernonkeenan/lib` and `work/lib`. -## Update Procedure +## Release Procedure -Prerequisite: All the other repos are cloned locally in adjacent directories. +As of 2026-07-12 (ADR-KV-014, DT-KV-004) this repo is `main` + `v*` tags — branch-per-version is retired, and +so is the old three-web-UI-plus-CLI manual ritual. Releasing is one command: `make release VERSION=vX.Y.Z`. -* Increment branch number in local repo +### Prerequisites -* Generate all OpenAPI libraries and then cleanup modules +* All the sibling service repos (`auth`, `crm`, `stash`, `sf-gate`, `research`, `members`, `plex`) are cloned + locally in adjacent directories — `regen` reads each one's `swagger/-vernonkeenan.yaml`. +* [`gh`](https://cli.github.com/) installed and authenticated (`gh auth login`) with push access to + `vernonkeenan/lib` — the release step calls `gh release create`. +* `GITEA_TOKEN` exported, set to a Gitea API token (Settings -> Applications on ) with + rights to trigger a mirror-sync on `vernonkeenan/lib` and `work/lib`. If unset, `make release` still tags, + pushes, and publishes the GitHub release — it just skips the Gitea mirror-sync step with a loud warning, so + the mirrors will lag until someone clicks Synchronize Now manually or reruns with the token set. (There is + no existing Gitea/tnxs token variable defined anywhere in this workstation's env files today — `GITEA_TOKEN` + is a new name, not a rename of something already in use.) + +### 1. Regenerate the API clients (its own PR) ```bash -make swagger -go get -u -f ./... -go mod tidy +make regen ``` -* Commit and push new branch to GitHub -* On GitHub goto set the new branch to default. -* On GitHub goto and publish a new release for the new branch. Make the new tag the branch name. -* On Gitea goto and click Synchronize Now. -* Update the ``go.mod`` line with ``code.tnxs.net/vernonkeenan/lib`` in each project to match the latest version. +This installs the pinned go-swagger CLI (`SWAGGER_VERSION` in the `Makefile`, currently `v0.35.0` — bump +deliberately and update both places when you do), copies each sibling repo's `swagger/-vernonkeenan.yaml` +into `./swagger` and `./swagger/external`, regenerates every client under `api/`, and rewrites the external +specs' scheme/host/path exactly as the old `make swagger` ritual did. It fails fast, before touching anything, +if any sibling spec is missing. + +Review `git status`/`git diff`, commit, open a PR, and merge it to `main` like any other change — client regen +is not bundled into the release step itself. `make regen` runs a non-fatal `go build ./...` at the end as an +early warning; the real gate is CI on that PR. + +**Known gap:** if any sibling spec has an operation with no top-level `tags:`, go-swagger routes it into a +fallback `operations` package whose import path in `_client/_client.go` doesn't respect +`--client-package` — reproduced across go-swagger v0.33.2 through v0.35.0 while building this tooling. As of +this writing that's `sf-gate-vernonkeenan.yaml`'s `PUT /researchprojectservices` (no `tags:`). Fix it at the +source (add a `tags:` entry in the `sf-gate` repo's spec) rather than hand-patching generated code, since the +next regen would just reintroduce it. + +### 2. Cut the release (from a clean `main` checkout) + +```bash +make release VERSION=v0.7.5 +``` + +`scripts/release.sh` (invoked by the `release` target): + +1. Validates `VERSION` is `vMAJOR.MINOR.PATCH` and strictly greater than the latest existing tag. +2. Requires you're on `main`, the tree is clean, and local `main` matches `origin/main` (no local-only commits). +3. Runs `go build ./...` and `go vet ./...` as a last sanity check. +4. Creates an annotated tag at HEAD, pushes it, and runs `gh release create` with generated notes. +5. POSTs to the Gitea mirror-sync API for `vernonkeenan/lib` and `work/lib` (skipped with a warning if + `GITEA_TOKEN` is unset). + +Use `make release-dry-run VERSION=v0.7.5` to run steps 1-3 and print what steps 4-5 would do, without tagging, +pushing, publishing, or syncing anything. + +### 3. Downstream consumers + +Update the `go.mod` line for `code.tnxs.net/vernonkeenan/lib` in each dependent project to the new tag, same +as before — that part of the old ritual is unchanged. diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..1e5cb45 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash +# +# scripts/release.sh — scripted release for vernonkeenan/lib (ADR-KV-014). +# +# Replaces the manual "Update Procedure" in README.md (three web UIs + a +# go-swagger CLI installed nowhere) with a single, repeatable script driven +# through `make`. See README.md's "Release Procedure" section for the +# operator-facing walkthrough. +# +# Subcommands: +# install-swagger Install the pinned go-swagger CLI and verify its version. +# regen Regenerate api/ clients from the sibling service specs. +# release [--dry-run] +# Validate, tag, push, publish the GitHub release, and +# trigger the Gitea mirror sync. With --dry-run, every +# step up to (but not including) push/tag/release/mirror +# is performed and the remaining steps are only printed. +# +# Env: +# SWAGGER_VERSION Pinned go-swagger version (default set by Makefile; can override). +# GITHUB_REPO owner/repo on GitHub (default vernonkeenan/lib). +# GITEA_BASE_URL Gitea API base (default https://code.tnxs.net/api/v1). +# GITEA_TOKEN Gitea API token with repo mirror-sync rights. If unset, the +# mirror-sync step is skipped with a loud warning — it is +# never required to fail the release. There is no +# Gitea/tnxs API token variable defined in this workstation's +# env files today (checked kv.env, taxnexus.env, telnexus.env, +# rosetta.env, wdwai.env); GITEA_TOKEN is a new variable name +# introduced by this script, not a rename of an existing one. +# GITEA_MIRRORS Space-separated "owner/repo" mirror list (default +# "vernonkeenan/lib work/lib"). +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +SWAGGER_VERSION="${SWAGGER_VERSION:-v0.35.0}" +GITHUB_REPO="${GITHUB_REPO:-vernonkeenan/lib}" +GITEA_BASE_URL="${GITEA_BASE_URL:-https://code.tnxs.net/api/v1}" +GITEA_MIRRORS="${GITEA_MIRRORS:-vernonkeenan/lib work/lib}" + +# Sibling service repos this ritual pulls specs from, and the go-swagger +# generate flags the README's `make swagger` target used to run inline. +# Format: "dir:spec_basename:client_name:target_subdir:client_pkg:model_pkg" +SERVICES=( + "auth:auth-vernonkeenan:auth:api/auth:auth-client:auth-models" + "crm:crm-vernonkeenan:crm:api/crm:crm-client:crm-models" + "stash:stash-vernonkeenan:stash:api/stash:stash-client:stash-models" + "sf-gate:sf-gate-vernonkeenan:sfgate:api/sfgate:sfgate-client:sfgate-models" + "research:research-vernonkeenan:research:api/research:research-client:research-models" + "members:members-vernonkeenan:members:api/members:members-client:members-models" + "plex:plex-vernonkeenan:plex:api/plex:plex-client:plex-models" +) + +# Host/path substitutions the ritual applied to swagger/external/*.yaml, +# keyed by service dir (same mapping the Makefile hardcoded per client). +external_host_for() { + case "$1" in + auth) echo "auth.vernonkeenan.com:8080" ;; + crm) echo "crm.vernonkeenan.com:8080" ;; + stash) echo "stash.vernonkeenan.com:8080" ;; + sf-gate) echo "sf-gate.vernonkeenan.com:8080" ;; + research) echo "research.vernonkeenan.com:8080" ;; + members) echo "members.vernonkeenan.com:8080" ;; + plex) echo "plex.vernonkeenan.com:8080" ;; + esac +} + +external_path_for() { + case "$1" in + auth) echo "/vk/auth/v1" ;; + crm) echo "/vk/crm/v1" ;; + stash) echo "/vk/stash/v1" ;; + sf-gate) echo "/vk/sf-gate/v1" ;; + research) echo "/vk/research/v1" ;; + members) echo "/vk/members/v1" ;; + plex) echo "/vk/plex/v1" ;; + esac +} + +log() { printf '[release] %s\n' "$*" >&2; } +warn() { printf '\033[33m[release] WARNING: %s\033[0m\n' "$*" >&2; } +die() { printf '\033[31m[release] ERROR: %s\033[0m\n' "$*" >&2; exit 1; } + +usage() { + cat >&2 <<'EOF' +Usage: + scripts/release.sh install-swagger + scripts/release.sh regen + scripts/release.sh release [--dry-run] + +See the top of this file for env vars. +EOF +} + +# --------------------------------------------------------------------------- +# install-swagger +# --------------------------------------------------------------------------- + +swagger_bin() { + local gobin + gobin="$(go env GOBIN)" + if [[ -z "$gobin" ]]; then + gobin="$(go env GOPATH)/bin" + fi + echo "${gobin}/swagger" +} + +cmd_install_swagger() { + log "Installing go-swagger ${SWAGGER_VERSION} (pinned; see README.md and Makefile SWAGGER_VERSION)" + GOFLAGS=-mod=mod go install "github.com/go-swagger/go-swagger/cmd/swagger@${SWAGGER_VERSION}" + + local bin + bin="$(swagger_bin)" + [[ -x "$bin" ]] || die "go install reported success but ${bin} is not executable" + + local got + got="$("$bin" version 2>&1 | head -1)" + log "swagger version: ${got}" + case "$got" in + *"${SWAGGER_VERSION#v}"*) ;; + *) die "installed swagger version does not match pinned ${SWAGGER_VERSION} (got: ${got})" ;; + esac + log "go-swagger ${SWAGGER_VERSION} verified at ${bin}" +} + +# --------------------------------------------------------------------------- +# regen +# --------------------------------------------------------------------------- + +check_sibling_specs() { + local missing=() + local svc dir spec_base rest + for svc in "${SERVICES[@]}"; do + IFS=':' read -r dir spec_base rest <<<"$svc" + local spec_path="${REPO_ROOT}/../${dir}/swagger/${spec_base}.yaml" + if [[ ! -f "$spec_path" ]]; then + missing+=("$spec_path") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + die "missing sibling spec(s), cannot regen (need all sibling repos cloned adjacently per README prerequisite): +$(printf ' - %s\n' "${missing[@]}")" + fi + log "all ${#SERVICES[@]} sibling specs present" +} + +cmd_regen() { + check_sibling_specs + + local bin + bin="$(swagger_bin)" + [[ -x "$bin" ]] || die "swagger CLI not found at ${bin} — run 'make install-swagger' first" + + cd "$REPO_ROOT" + mkdir -p swagger/logs + + log "copying sibling specs into ./swagger and ./swagger/external" + local svc dir spec_base name target pkg model + for svc in "${SERVICES[@]}"; do + IFS=':' read -r dir spec_base name target pkg model <<<"$svc" + cp "../${dir}/swagger/${spec_base}.yaml" "./swagger/" + cp "../${dir}/swagger/${spec_base}.yaml" "./swagger/external/" + done + + log "removing generated api/ tree" + rm -rf api + + for svc in "${SERVICES[@]}"; do + IFS=':' read -r dir spec_base name target pkg model <<<"$svc" + log "generating ${name} client -> ${target}" + mkdir -p "$target" + "$bin" generate client \ + --log-output="./swagger/logs/generate-${name}-client.log" \ + --copyright-file=./build/COPYRIGHT \ + --name="${name}" \ + --spec="./swagger/${spec_base}.yaml" \ + --target="${target}" \ + --client-package="${pkg}" \ + --model-package="${model}" \ + --principal=app.User + + local host path ext="./swagger/external/${spec_base}.yaml" + host="$(external_host_for "$dir")" + path="$(external_path_for "$dir")" + sed -i 's|"http"|"https"|g' "$ext" + sed -i "s|${host}|gw.tnxs.net|g" "$ext" + sed -i "s|\"/v1\"|\"${path}\"|g" "$ext" + done + + log "regen complete for: $(printf '%s ' "${SERVICES[@]%%:*}")" + + # Best-effort publish of the external swagger docs, exactly as the manual + # ritual's last step did. Never fails the regen — CI/dev boxes typically + # have no SSH access to whm.noc.tnxs.net. + if command -v scp >/dev/null 2>&1; then + log "attempting best-effort scp of swagger/external/*.yaml to whm.noc.tnxs.net (skip on failure)" + if ! scp -o ConnectTimeout=5 -o BatchMode=yes \ + ./swagger/external/*.yaml root@whm.noc.tnxs.net:/home/taxassets/www/swagger/external 2>/dev/null; then + warn "scp to whm.noc.tnxs.net failed or is unreachable from this host — skipped (not fatal)." + warn "If external swagger docs need publishing, run the scp manually from a host with access." + fi + fi + + log "sanity build (non-fatal; real gate is CI on the regen PR)" + if ! go build ./... 2>&1; then + warn "generated code does not build. Known cause: any sibling spec operation missing a top-level" + warn "'tags:' entry lands in a fallback 'operations' package whose import path in" + warn "_client/_client.go does not respect --client-package across at least go-swagger" + warn "v0.33.2-v0.35.0 (reproduced on all four during this tooling's own verification). As of this" + warn "writing that's sf-gate-vernonkeenan.yaml's 'PUT /researchprojectservices' (no tags:). Fix at" + warn "the source (add a tags: entry in the sf-gate repo's spec) rather than hand-patching generated" + warn "code, since the next regen would just reintroduce it." + fi + + log "review 'git status' / 'git diff' and commit the regen output as its own PR before running 'make release'." +} + +# --------------------------------------------------------------------------- +# release +# --------------------------------------------------------------------------- + +validate_semver() { + local v="$1" + [[ "$v" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || \ + die "VERSION '${v}' is not a valid semver tag (expected vMAJOR.MINOR.PATCH, e.g. v0.7.5)" +} + +latest_tag() { + git -C "$REPO_ROOT" tag --list 'v*' | sort -V | tail -1 +} + +check_monotonic() { + local new="$1" latest="$2" + [[ -n "$latest" ]] || { log "no existing v* tags found; ${new} will be the first"; return; } + local top + top="$(printf '%s\n%s\n' "$latest" "$new" | sort -V | tail -1)" + if [[ "$new" == "$latest" ]]; then + die "VERSION ${new} already exists as a tag" + fi + if [[ "$top" != "$new" ]]; then + die "VERSION ${new} is not greater than latest tag ${latest} (releases must be monotonically increasing)" + fi + log "monotonicity OK: ${latest} -> ${new}" +} + +cmd_release() { + local version="${1:-}" + local dry_run="false" + shift || true + while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) dry_run="true" ;; + *) die "unknown release argument: $1" ;; + esac + shift + done + + [[ -n "$version" ]] || die "usage: scripts/release.sh release [--dry-run] (or: make release VERSION=v0.7.5)" + validate_semver "$version" + + cd "$REPO_ROOT" + + log "fetching tags from origin" + git fetch --tags --quiet origin + + local latest + latest="$(latest_tag)" + check_monotonic "$version" "$latest" + + local branch + branch="$(git rev-parse --abbrev-ref HEAD)" + [[ "$branch" == "main" ]] || die "must be on 'main' to release (currently on '${branch}'); main+tags replaced branch-per-version (ADR-KV-014)" + + if [[ -n "$(git status --porcelain)" ]]; then + die "working tree is not clean; commit or stash changes before releasing. (Regen output belongs in its own PR, reviewed and merged to main *before* running 'make release'.)" + fi + + git fetch origin main --quiet + local head_sha origin_sha + head_sha="$(git rev-parse HEAD)" + origin_sha="$(git rev-parse origin/main)" + if [[ "$head_sha" != "$origin_sha" ]]; then + die "local main (${head_sha:0:12}) does not match origin/main (${origin_sha:0:12}); pull/push so the tag lands on a commit that's actually on GitHub main" + fi + + log "sanity build/vet before tagging ${version} at ${head_sha:0:12}" + go build ./... + go vet ./... + + log "About to release ${version}:" + log " - annotated tag ${version} @ ${head_sha:0:12} on ${GITHUB_REPO}" + log " - push tag to origin" + log " - gh release create ${version} --repo ${GITHUB_REPO} --generate-notes" + log " - Gitea mirror-sync: ${GITEA_MIRRORS}" + + if [[ "$dry_run" == "true" ]]; then + log "DRY RUN: stopping before tag/push/release/mirror-sync. Nothing was written." + return 0 + fi + + log "creating annotated tag ${version}" + git tag -a "$version" -m "Release ${version}" + + log "pushing tag ${version} to origin" + git push origin "$version" + + log "publishing GitHub release ${version}" + gh release create "$version" \ + --repo "$GITHUB_REPO" \ + --title "$version" \ + --generate-notes + + gitea_mirror_sync +} + +gitea_mirror_sync() { + if [[ -z "${GITEA_TOKEN:-}" ]]; then + warn "GITEA_TOKEN is not set — skipping Gitea mirror-sync for: ${GITEA_MIRRORS}" + warn "Run manually later: curl -sS -X POST -H \"Authorization: token \$GITEA_TOKEN\" ${GITEA_BASE_URL}/repos///mirror-sync" + return 0 + fi + + local mirror owner repo status + for mirror in $GITEA_MIRRORS; do + owner="${mirror%%/*}" + repo="${mirror##*/}" + log "Gitea mirror-sync: ${mirror}" + status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_BASE_URL}/repos/${owner}/${repo}/mirror-sync")" + if [[ "$status" != "200" ]]; then + warn "mirror-sync for ${mirror} returned HTTP ${status} (expected 200) — check manually at https://code.tnxs.net/${mirror}/settings" + else + log "mirror-sync triggered for ${mirror} (HTTP 200; sync happens async on the Gitea side)" + fi + done +} + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + +main() { + local cmd="${1:-}" + shift || true + case "$cmd" in + install-swagger) cmd_install_swagger "$@" ;; + regen) cmd_regen "$@" ;; + release) cmd_release "$@" ;; + *) usage; exit 1 ;; + esac +} + +main "$@"