diff --git a/AGENTS.md b/AGENTS.md index ad3728b..f907235 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,7 +104,7 @@ gridline/ ├── .github/workflows/ # release.yml (monorepo paths) + auto-assign.yml ├── docs/ # Project docs ├── screenshots/ # README marketing images -├── scripts/ # Release tooling (render-cask.sh) +├── scripts/ # Release tooling (render-cask.sh, release-mac.sh) + Gitea mirror (mirror-gitea.sh, hooks/, install-git-hooks.sh) └── AGENTS.md # This file ``` @@ -177,9 +177,16 @@ cargo test # Rust tests (from desktop/sr Direct pushes to `prod` will be bypassed only in emergencies; prefer the PR path so changes land through the PR gate. +**The Gitea fallback keeps itself current — run `scripts/install-git-hooks.sh` once per clone.** It sets `core.hooksPath=scripts/hooks`, so: + +- `pre-push` mirrors `prod` + all tags to `origin` (Gitea) whenever you push to the `github` remote, +- `post-merge` mirrors again after a `git pull`/merge while on `prod` (a PR squash-merged on GitHub only reaches the mirror when the local `prod` pulls it). + +`scripts/mirror-gitea.sh` does the same by hand (`--branches` also pushes feature branches); it refuses to push to a GitHub remote and exits non-zero if the mirror could not be updated. The hooks downgrade that to a warning — **a Gitea outage never blocks a push to GitHub**. + ### Releases -**Releases are GitHub-only.** Tag `prod` and push the tag to the `github` remote — `git tag vN.M.N && git push github vN.M.N`. GitHub Actions (`release.yml`) builds installers for macOS (Apple Silicon + Intel), Windows, and Linux and opens a **draft** release (review + publish on GitHub). Never push release tags to `origin`/Gitea; it exists only as a fallback copy of the code. +**Releases are GitHub-only.** Tag `prod` and push the tag to the `github` remote — `git tag vN.M.N && git push github vN.M.N`. GitHub Actions (`release.yml`) builds installers for macOS (Apple Silicon + Intel), Windows, and Linux and opens a **draft** release (review + publish on GitHub). The Gitea tag arrives automatically via the `pre-push` hook (or `scripts/mirror-gitea.sh`) — never `git push origin vN.M.N` expecting a build, and never publish a release on Gitea. **Before tagging**, keep everything in sync: - Version number across `desktop/package.json`, `desktop/src-tauri/Cargo.toml`, `desktop/src-tauri/tauri.conf.json`, and `www/src/pages/index.astro` (the landing page version string) diff --git a/scripts/hooks/post-merge b/scripts/hooks/post-merge new file mode 100755 index 0000000..ab8d65e --- /dev/null +++ b/scripts/hooks/post-merge @@ -0,0 +1,25 @@ +#!/bin/sh +# +# post-merge hook — after `git pull`/merge, refresh the Gitea fallback. +# +# Installed via `scripts/install-git-hooks.sh` (git core.hooksPath = scripts/hooks). +# GitHub merges (e.g. a squash-merged PR) happen server-side, so a local +# `git pull` on prod is the moment to bring the Gitea mirror along. +# +# args: $1 = 1 when the merge was a squash, otherwise 0 + +if [ "${GRIDLINE_MIRROR_RUNNING:-}" = "1" ]; then + exit 0 +fi + +branch="$(git symbolic-ref --short -q HEAD)" || exit 0 +case "$branch" in + prod|main) ;; + *) exit 0 ;; +esac + +root="$(git rev-parse --show-toplevel)" || exit 0 +GRIDLINE_MIRROR_RUNNING=1 sh "$root/scripts/mirror-gitea.sh" || + echo "post-merge: warning: Gitea mirror was not updated" >&2 + +exit 0 diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push new file mode 100755 index 0000000..3dff4d0 --- /dev/null +++ b/scripts/hooks/pre-push @@ -0,0 +1,46 @@ +#!/bin/sh +# +# pre-push hook — mirrors the trunk branch and tags to the Gitea fallback. +# +# Installed via `scripts/install-git-hooks.sh` (git core.hooksPath = scripts/hooks). +# Git has no client-side post-push hook, so the mirror runs here: the refs are +# already committed locally, so whether Gitea or GitHub receives them first makes +# no difference. A Gitea outage prints a warning and never blocks the push. +# +# stdin receives one line per ref being pushed: +# +# +# args: $1 = remote name, $2 = remote URL + +remote_url="${2:-}" + +# Only react to pushes of the canonical GitHub remote. +case "$remote_url" in + *github.com*) ;; + *) exit 0 ;; +esac + +# Don't recurse when the mirror script itself is pushing to Gitea. +if [ "${GRIDLINE_MIRROR_RUNNING:-}" = "1" ]; then + exit 0 +fi + +mirror_needed=0 +while read -r _local_ref local_sha remote_ref _remote_sha; do + # Ignore deletions (all-zero object id). + case "$local_sha" in + 0000000000000000000000000000000000000000) continue ;; + esac + case "$remote_ref" in + refs/heads/prod|refs/heads/main|refs/tags/*) mirror_needed=1 ;; + esac +done + +[ "$mirror_needed" = "1" ] || exit 0 + +root="$(git rev-parse --show-toplevel)" || exit 0 +GRIDLINE_MIRROR_RUNNING=1 sh "$root/scripts/mirror-gitea.sh" || + echo "pre-push: warning: Gitea mirror was not updated (continuing with the push)" >&2 + +# Never block the push to GitHub because of a mirror problem. +exit 0 diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..a60f79e --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# +# Install the repo's git hooks (once per clone). +# +# Points `core.hooksPath` at `scripts/hooks/` so the hook files are versioned +# with the repo instead of copied into `.git/hooks` (a stale copy can't drift +# out of date this way). Currently: +# +# pre-push — mirror prod + tags to the Gitea fallback (git.ranio.xyz) +# post-merge — same, after a `git pull`/merge while on prod +# +# The mirror itself lives in `scripts/mirror-gitea.sh`; run it by hand any time +# with `scripts/mirror-gitea.sh`. Gitea is a just-in-case copy: GitHub is the +# source of truth, and a Gitea outage never blocks a push. + +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "install-git-hooks: not inside a git repository" >&2 + exit 1 +} +cd "$repo_root" + +if [ ! -d scripts/hooks ]; then + echo "install-git-hooks: scripts/hooks/ is missing — run this from the repo root" >&2 + exit 1 +fi + +chmod +x scripts/hooks/* scripts/mirror-gitea.sh 2>/dev/null || true +git config core.hooksPath scripts/hooks + +gitea_remote="${GRIDLINE_GITEA_REMOTE:-origin}" +echo "install-git-hooks: core.hooksPath -> scripts/hooks" +for hook in scripts/hooks/*; do + echo "install-git-hooks: $(basename "$hook")" +done +echo "install-git-hooks: prod + tags mirror to the '$gitea_remote' remote on push/pull" diff --git a/scripts/mirror-gitea.sh b/scripts/mirror-gitea.sh new file mode 100755 index 0000000..f6a5605 --- /dev/null +++ b/scripts/mirror-gitea.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# +# Keep the Gitea "just in case" mirror (git.ranio.xyz) in step with GitHub. +# +# GitHub (`AdrianBonpin/gridline`, remote `github`) is the source of truth; the +# Gitea remote (`origin`) exists only as a fallback copy of the code, so this +# mirrors the trunk branch and every tag — not feature branches. +# +# Run it manually after a merge that happened on GitHub, or let the repo's git +# hooks call it (`scripts/install-git-hooks.sh` wires up `core.hooksPath`): +# * pre-push — before pushing prod/tags to GitHub +# * post-merge — after `git pull`/merge while on prod +# +# Exit status is non-zero when the mirror could not be updated, so callers can +# decide whether that is fatal. The hooks treat it as a warning: a Gitea outage +# must never block a push to GitHub. +# +# Usage: +# scripts/mirror-gitea.sh # mirror prod + all tags +# GRIDLINE_GITEA_REMOTE=mirror scripts/… # use another remote name +# scripts/mirror-gitea.sh --branches # also mirror every local branch + +set -uo pipefail + +GITEA_REMOTE="${GRIDLINE_GITEA_REMOTE:-origin}" +MIRROR_BRANCHES=0 +TRUNK_BRANCH="${GRIDLINE_TRUNK_BRANCH:-prod}" + +for arg in "$@"; do + case "$arg" in + --branches) MIRROR_BRANCHES=1 ;; + -h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "mirror-gitea: unknown argument '$arg' (try --help)" >&2; exit 2 ;; + esac +done + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "mirror-gitea: not inside a git repository" >&2 + exit 1 +} +cd "$repo_root" || exit 1 + +url="$(git remote get-url "$GITEA_REMOTE" 2>/dev/null || true)" +if [ -z "$url" ]; then + echo "mirror-gitea: remote '$GITEA_REMOTE' not found — nothing to mirror" >&2 + exit 1 +fi + +# Safety: never push to GitHub through this script (it is the source, and a +# mis-pointed remote could otherwise rewrite the wrong repository). +case "$url" in + *github.com*) + echo "mirror-gitea: '$GITEA_REMOTE' points at GitHub ($url) — refusing to mirror" >&2 + exit 1 + ;; +esac + +status=0 +note() { printf 'mirror-gitea: %s\n' "$1" >&2; } + +# Trunk branch. Only mirror it when it exists locally; a fresh clone with just +# the feature branch checked out still mirrors the tags below. +if git show-ref --verify --quiet "refs/heads/$TRUNK_BRANCH"; then + if ! git push "$GITEA_REMOTE" "refs/heads/$TRUNK_BRANCH:refs/heads/$TRUNK_BRANCH"; then + status=1 + note "could not update $TRUNK_BRANCH" + fi +fi + +# Every local tag, so release tags appear on the mirror too (`--tags` is +# idempotent — already-present tags are skipped). +if [ -n "$(git tag --list)" ]; then + if ! git push "$GITEA_REMOTE" --tags; then + status=1 + note "could not update tags" + fi +fi + +# Optional: every local branch (off by default — feature branches are ephemeral). +if [ "$MIRROR_BRANCHES" = "1" ]; then + if ! git push "$GITEA_REMOTE" --all; then + status=1 + note "could not update branches" + fi +fi + +if [ "$status" = "0" ]; then + note "mirror up to date ($url)" +fi +exit "$status"