From d5e473959cd90bcf9a29551f33a39a1b16f2dddc Mon Sep 17 00:00:00 2001 From: Benny Date: Sun, 20 Sep 2026 21:11:41 -0400 Subject: [PATCH] chore: set up branching, CI, and PR workflow Prepares the repo for parallel agent work. No application code. - CLAUDE.md: conventions, branch naming, and the six non-negotiable invariants from the design (immutable raw bytes, no stored odometers, SI integers, dual-layer user isolation, secret containment, single ingestion path). Also records a model-allocation policy: the orchestrator runs Opus 5, workers default to Sonnet, and Opus is reserved for review plus the areas where a mistake is silent and expensive (ingest, wear SQL, auth/RLS, the Bryton protocol client). And the Gitea Actions gotchas, so nobody rediscovers them: GITEA_TOKEN cannot push to the container registry, jobs.*.environment is ignored, and cron needs a workflow_dispatch pair. - CONTRIBUTING.md: day-to-day flow, worktrees for parallel branches, review expectations. - .gitea/workflows/ci.yml: repo hygiene (branch naming, secret scan, no ride data in git), plus API/web/migration jobs that guard on whether the code exists yet, so CI is meaningful now and grows into the real thing rather than being rewritten. - .gitea/PULL_REQUEST_TEMPLATE.md: forces an honest "how this was verified" and an invariant checklist. - scripts/pr.sh, scripts/review.sh: open and inspect PRs via the Gitea API. - Directory scaffold with placeholder READMEs. Agents open PRs; humans merge them. Co-Authored-By: Claude Opus 5 --- .gitea/PULL_REQUEST_TEMPLATE.md | 27 ++++ .gitea/workflows/ci.yml | 204 ++++++++++++++++++++++++++ CLAUDE.md | 176 ++++++++++++++++++++++ CONTRIBUTING.md | 76 ++++++++++ apps/api/README.md | 1 + apps/api/tests/fixtures/fit/README.md | 1 + apps/web/README.md | 1 + deploy/README.md | 1 + packages/openapi/README.md | 1 + scripts/pr.sh | 58 ++++++++ scripts/review.sh | 28 ++++ 11 files changed, 574 insertions(+) create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitea/workflows/ci.yml create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 apps/api/README.md create mode 100644 apps/api/tests/fixtures/fit/README.md create mode 100644 apps/web/README.md create mode 100644 deploy/README.md create mode 100644 packages/openapi/README.md create mode 100755 scripts/pr.sh create mode 100755 scripts/review.sh diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..664cfef --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## What and why + + + +## How this was verified + + + +- [ ] CI is green +- [ ] Tests added or updated for the behaviour that changed +- [ ] Verified manually (describe how): + +## Invariants + + + +- [ ] Raw ingested bytes remain immutable; derived tables stay rebuildable +- [ ] No stored odometer added; wear still derived from installs +- [ ] Physical quantities stored as SI integers +- [ ] New user-owned tables have `user_id` + RLS policy + repository scope +- [ ] No secret can reach a response model, log line, or error message +- [ ] Migration survives `upgrade -> downgrade -1 -> upgrade` + +## Risks and follow-ups + + diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..2595d36 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,204 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ gitea.ref }} + cancel-in-progress: true + +jobs: + meta: + name: Repo hygiene + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Branch name follows convention + if: gitea.event_name == 'pull_request' + run: | + BRANCH="${{ gitea.head_ref }}" + echo "Branch: $BRANCH" + if echo "$BRANCH" | grep -Eq '^(feat|fix|refactor|test|docs|chore|ci)/[a-z0-9._-]+$'; then + echo "OK" + else + echo "::error::Branch '$BRANCH' does not match /." + echo "Valid types: feat fix refactor test docs chore ci" + exit 1 + fi + + - name: No secrets committed + run: | + # Deliberately narrow: high-signal patterns only, so this never cries wolf. + PATTERN='BEGIN (RSA|OPENSSH|EC|PGP) PRIVATE KEY|xoxb-[0-9A-Za-z-]{10,}|AKIA[0-9A-Z]{16}' + if git grep -nIE "$PATTERN" -- . ':!.gitea/workflows/*' ; then + echo "::error::Possible secret committed (see matches above)." + exit 1 + fi + echo "No secret patterns found." + + - name: No ride data committed + run: | + if git ls-files | grep -E '\.(fit|gpx|tcx)$' | grep -v '^apps/api/tests/fixtures/fit/'; then + echo "::error::Ride files belong in the blob store, not git." + echo "Only apps/api/tests/fixtures/fit/ may contain .fit files (golden fixtures)." + exit 1 + fi + echo "Clean." + + api: + name: API (lint, types, tests) + runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis:16-3.4 + env: + POSTGRES_USER: velodrome + POSTGRES_PASSWORD: velodrome + POSTGRES_DB: velodrome_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql+asyncpg://velodrome:velodrome@postgres:5432/velodrome_test + steps: + - uses: actions/checkout@v4 + + - name: Is there API code yet? + id: guard + run: | + if [ -n "$(find apps/api -name '*.py' -not -path '*/.*' 2>/dev/null | head -1)" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No Python sources under apps/api yet — skipping." + fi + + - uses: actions/setup-python@v5 + if: steps.guard.outputs.present == 'true' + with: + python-version: '3.12' + + - name: Cache uv + if: steps.guard.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-${{ hashFiles('apps/api/pyproject.toml') }} + restore-keys: uv-${{ runner.os }}- + + - name: Install + if: steps.guard.outputs.present == 'true' + working-directory: apps/api + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + uv sync --all-extras + + - name: Lint (ruff) + if: steps.guard.outputs.present == 'true' + working-directory: apps/api + run: | + export PATH="$HOME/.local/bin:$PATH" + uv run ruff check . + uv run ruff format --check . + + - name: Types (mypy --strict) + if: steps.guard.outputs.present == 'true' + working-directory: apps/api + run: | + export PATH="$HOME/.local/bin:$PATH" + uv run mypy --strict velodrome + + - name: Tests + if: steps.guard.outputs.present == 'true' + working-directory: apps/api + run: | + export PATH="$HOME/.local/bin:$PATH" + uv run pytest -q + + web: + name: Web (lint, typecheck, build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Is there web code yet? + id: guard + run: | + if [ -f apps/web/package.json ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No apps/web/package.json yet — skipping." + fi + + - uses: actions/setup-node@v4 + if: steps.guard.outputs.present == 'true' + with: + node-version: '22' + + - name: Install, lint, build + if: steps.guard.outputs.present == 'true' + working-directory: apps/web + run: | + corepack enable + pnpm install --frozen-lockfile + pnpm run lint + pnpm run check + pnpm run build + + migrations: + name: Migrations reversible + runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis:16-3.4 + env: + POSTGRES_USER: velodrome + POSTGRES_PASSWORD: velodrome + POSTGRES_DB: velodrome_mig + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql+asyncpg://velodrome:velodrome@postgres:5432/velodrome_mig + steps: + - uses: actions/checkout@v4 + + - name: Are there migrations yet? + id: guard + run: | + if [ -d apps/api/alembic/versions ] && [ -n "$(ls -A apps/api/alembic/versions/*.py 2>/dev/null)" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No migrations yet — skipping." + fi + + - uses: actions/setup-python@v5 + if: steps.guard.outputs.present == 'true' + with: + python-version: '3.12' + + - name: upgrade -> downgrade -> upgrade, and check for model drift + if: steps.guard.outputs.present == 'true' + working-directory: apps/api + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + uv sync --all-extras + uv run alembic upgrade head + uv run alembic downgrade -1 + uv run alembic upgrade head + # Fails if the models have drifted from the migrations. + uv run alembic check diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6ca09ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,176 @@ +# CLAUDE.md + +Conventions for this repo. Agents and humans both follow these. Read before making changes. + +## What this is + +A self-hosted cycling app: ride sync from a Bryton Rider 650, mileage tracking, spare-parts +inventory, maintenance records with mileage-milestone reminders. See `docs/PLAN.md` for the full +design, `docs/DECISIONS.md` for settled decisions, `docs/RESEARCH.md` for source material. + +**Read `docs/DECISIONS.md` before proposing an architectural change.** If a decision there is wrong, +say so and argue it — but don't silently contradict it. + +## Layout + +``` +apps/api/ Python 3.12 / FastAPI / SQLAlchemy async / Alembic +apps/web/ SvelteKit static SPA (installable PWA) +packages/openapi/ openapi.json — COMMITTED contract artefact, CI enforces it matches the code +deploy/ docker-compose, Caddyfile, systemd units, backup scripts +docs/ plan, decisions, research +scripts/ repo tooling (PR helpers, etc.) +.gitea/workflows/ CI +``` + +## Non-negotiable invariants + +These are load-bearing. Breaking one is a correctness bug, not a style choice. + +1. **Raw bytes are the only truth.** Every ingested file is written to the content-addressed blob + store *before* parsing, and is never mutated or deleted. Every table is a rebuildable projection: + deleting everything derived from a `raw_file_id` and re-parsing must be semantically a no-op. +2. **No odometer columns.** Component wear is always derived by replaying activities against + time-ranged `component_installs`. Never add a stored running total to a component. +3. **All physical quantities are SI integers** in storage — metres, seconds, mm/s, centimetres, + grams, minor currency units. Imperial is display-only. Never store a float mile. +4. **Every user-owned table has `user_id`, an RLS policy, and a repository-layer scope.** Both + layers, always. Never rely on the query alone. +5. **Secrets never leave the server.** The Bryton credential is password-equivalent. It must not + appear in any API response model, any log line, or any error message. +6. **One ingestion path.** All sources funnel through `ingest_bytes()`. Never add a second parse + path for a new source. + +## Style + +- **Python:** `ruff` (lint + format), `mypy --strict`. Type everything. Async throughout; no sync DB + calls in request handlers. +- **SQL:** migrations via Alembic only, never manual DDL. Every migration must survive + `upgrade -> downgrade -1 -> upgrade`. +- **Tests:** pytest against a real Postgres service container, never mocks for DB behaviour. + Parser changes need a golden fixture in `apps/api/tests/fixtures/fit/`. +- **Commits:** imperative mood, explain *why* in the body. Conventional-commit prefixes + (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, `ci:`). +- Match surrounding code. Don't introduce a new pattern when one exists. + +## Branching + +**Never commit directly to `main`.** `main` is protected and only moves via merged PRs. + +``` +feat/- new capability feat/ingest-fit-parser +fix/- bug fix fix/wear-wet-multiplier +refactor/- no behaviour change +test/- tests only +docs/ documentation +chore/ tooling, deps, CI +``` + +Scope is usually the area: `ingest`, `wear`, `auth`, `notify`, `web`, `deploy`, `ci`. + +One branch = one reviewable change. If a branch grows past roughly 400 changed lines, it should +probably have been two. + +## PR workflow + +1. Branch from an up-to-date `main`. +2. Commit in logical steps. Push the branch. +3. Open a PR with `scripts/pr.sh` (see below) or the web UI. Fill in the template honestly — + especially "How this was verified." +4. CI must be green. A red PR is not ready for review, and shouldn't be requested. +5. Review happens on the PR. Address feedback with new commits, don't force-push over review history + unless asked. +6. Squash-merge into `main`. Delete the branch. + +**Agents:** you open PRs, you do not merge them. Merging is a human decision. + +## Model allocation + +The orchestrator runs **Opus 5**. Worker agents do not default to it. + +The rule is **match the model to the cost of being wrong, not the size of the task.** A big, +well-specified job with obvious failure modes is cheap work. A small change to dedupe logic that +silently corrupts data for six months is expensive work. + +### Opus 5 — orchestration, and anything subtly wrong-able + +- Task decomposition, planning, and **all code review** (see below — this is where it pays for itself) +- Anything touching the six invariants above +- `ingest/` — FIT parsing, the three dedupe layers, activity-vs-course discrimination. Errors here + are silent and corrupt the archive. +- `wear/` — the wear SQL. Wrong numbers that still look plausible are the worst failure mode in the + product, because nobody notices. +- `auth/`, RLS policies — security, and a mistake exposes another user's data. +- `sources/bryton/` — a reverse-engineered protocol with no spec to check against. +- Schema migrations that alter or drop existing columns. +- Debugging anything that two Sonnet attempts have already failed to fix. + +### Sonnet — the bulk of implementation + +Default for normal feature work. `docs/PLAN.md` is detailed enough that most implementation is +careful transcription plus ordinary judgement, and CI catches the rest: + +- CRUD endpoints, Pydantic models, repository methods +- SvelteKit components, routes, styling, the service worker +- Tests against an already-decided behaviour +- Additive migrations, docker-compose and Caddy config, CI workflows +- Documentation + +### Haiku — mechanical work + +- Dependency bumps, formatting, renames, changelog entries +- Log triage, "find every call site of X" +- Anything where a script would also work + +### The economics + +**Sonnet implements, Opus reviews** is the default pairing, and it is much cheaper than Opus +implementing while catching most of the same problems. Review reads a focused diff; implementation +reads the whole repo and writes for hours. If budget is tight, cut Opus from implementation before +you cut it from review. + +Escalate a task to a stronger model when it has actually failed, not preemptively. Two failed Sonnet +attempts is a real signal; "this feels hard" is not. + +Never run more agents in parallel than there are genuinely independent branches of work. Parallel +agents that touch the same files cost more than one agent working in sequence, because the merge +conflicts and re-review are paid twice. + +## Working as a parallel agent + +Each agent works in its own git worktree so parallel branches don't collide: + +```bash +git worktree add ../bike-app- -b feat/- +# work there, push, open PR +git worktree remove ../bike-app- +``` + +Rules: +- **Stay in your lane.** Touch only files your task needs. If you need a change in someone else's + area, note it in the PR rather than making it. +- **Never rebase or force-push another agent's branch.** +- **Rebase on `main` before opening the PR**, so the reviewer sees a clean diff. +- **Don't invent scope.** If the task is ambiguous, ask rather than guess — a wrong guess costs a + full review cycle. +- **Report honestly.** If tests fail, say so with the output. If you skipped something, say that. + "Done" means done and verified. + +## What needs a human decision + +Don't do these autonomously: +- Merging a PR +- Anything touching `docs/DECISIONS.md` (propose it in a PR, argue the case) +- Schema changes that drop or rewrite existing columns +- Anything that sends data off the server, or adds a third-party runtime dependency on one +- Rotating or changing secrets +- Force-pushing anything, ever, to `main` + +## Environment + +- Gitea at `http://192.168.0.3:3000`, repo `BBergle/bike-app`, act_runner on the same host. +- SSH remote `git@192.168.0.3:BBergle/bike-app.git`, key pinned in `~/.ssh/config`. +- **`secrets.GITEA_TOKEN` cannot push to the Gitea container registry** — use the `REGISTRY_TOKEN` + PAT secret (`package:write`). This is a documented Gitea limitation, not a misconfiguration. +- `jobs.*.environment` is silently ignored by Gitea Actions. Don't build a gate on it. +- Always pair `schedule:` with `workflow_dispatch:` — Gitea's cron has shipped flaky. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4f99a58 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing + +Short version: branch, commit, push, open a PR, get it reviewed, squash-merge. +`main` is never committed to directly. + +Conventions live in [`CLAUDE.md`](CLAUDE.md) — branch naming, style, the non-negotiable +invariants, and the rules for agents working in parallel. Read that first. + +## One-time setup + +SSH to Gitea should already be pinned in `~/.ssh/config`: + +``` +Host 192.168.0.3 + HostName 192.168.0.3 + User git + IdentityFile ~/.ssh/gitea + IdentitiesOnly yes +``` + +`IdentitiesOnly yes` matters: without it, SSH may offer an older deploy key first and Gitea will +authorize against *that* key's narrower permissions. + +For opening PRs from the command line, create a Gitea token at +http://192.168.0.3:3000/user/settings/applications with scopes `read:user`, `write:repository`, +`write:issue`, then: + +```bash +export GITEA_TOKEN= # add to your shell profile +export GITEA_URL=http://192.168.0.3:3000 +``` + +## Day-to-day + +```bash +git switch main && git pull +git switch -c feat/ingest-fit-parser + +# work, commit in logical steps +git push -u origin feat/ingest-fit-parser + +scripts/pr.sh "feat(ingest): parse Bryton FIT activities" +``` + +`scripts/pr.sh` opens the PR against `main` and prints its URL. Pass `--draft` if it isn't ready. + +## Parallel work + +Use worktrees so branches don't fight over one checkout: + +```bash +git worktree add ../bike-app-ingest -b feat/ingest-fit-parser +cd ../bike-app-ingest +# ... +git worktree remove ../bike-app-ingest +``` + +## Review + +- CI must be green before review is requested. A red PR isn't ready. +- Reviewers look for: correctness, the invariants in `CLAUDE.md`, whether the tests actually prove + what the PR claims, and whether it reuses what already exists. +- Address feedback with new commits. Don't force-push over review history unless asked. +- **Agents open PRs; humans merge them.** + +## Commits + +Conventional prefixes, imperative mood, and a body that explains *why*: + +``` +feat(ingest): discriminate activities from Bryton course files + +Bryton writes routes as .fit too, so a naive importer turns saved routes +into phantom rides. Check file_id.type first, falling back to message-shape +inspection since Bryton's encoder omits it on some firmware. +``` diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..fc5bed6 --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1 @@ +Python 3.12 / FastAPI / SQLAlchemy async / Alembic. Not yet scaffolded — Phase 0. diff --git a/apps/api/tests/fixtures/fit/README.md b/apps/api/tests/fixtures/fit/README.md new file mode 100644 index 0000000..8db00c3 --- /dev/null +++ b/apps/api/tests/fixtures/fit/README.md @@ -0,0 +1 @@ +Golden FIT fixtures for parser tests. Real Rider 650 files go here. diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..665047e --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1 @@ +SvelteKit static SPA, installable PWA. Not yet scaffolded — Phase 0. diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..138b89b --- /dev/null +++ b/deploy/README.md @@ -0,0 +1 @@ +docker-compose, Caddyfile, systemd backup units. Not yet written — Phase 0. diff --git a/packages/openapi/README.md b/packages/openapi/README.md new file mode 100644 index 0000000..8e39ea5 --- /dev/null +++ b/packages/openapi/README.md @@ -0,0 +1 @@ +openapi.json lives here, committed. CI enforces it matches the code. diff --git a/scripts/pr.sh b/scripts/pr.sh new file mode 100755 index 0000000..427cee1 --- /dev/null +++ b/scripts/pr.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Open a pull request against main from the current branch. +# +# scripts/pr.sh "feat(ingest): parse Bryton FIT activities" [--draft] +# +# Requires GITEA_TOKEN (scopes: write:repository, write:issue) and optionally GITEA_URL. +set -euo pipefail + +GITEA_URL="${GITEA_URL:-http://192.168.0.3:3000}" +OWNER="${GITEA_OWNER:-BBergle}" +REPO="${GITEA_REPO:-bike-app}" +BASE="${PR_BASE:-main}" + +die() { echo "error: $*" >&2; exit 1; } + +[ -n "${GITEA_TOKEN:-}" ] || die "GITEA_TOKEN is not set. See CONTRIBUTING.md." +command -v jq >/dev/null || die "jq is required (brew install jq)." + +TITLE="${1:-}" +[ -n "$TITLE" ] || die "usage: scripts/pr.sh \"\" [--draft]" +DRAFT=false +[ "${2:-}" = "--draft" ] && DRAFT=true + +BRANCH="$(git rev-parse --abbrev-ref HEAD)" +[ "$BRANCH" != "$BASE" ] || die "refusing to open a PR from $BASE onto itself." + +git diff --quiet && git diff --cached --quiet || die "working tree is dirty; commit or stash first." + +if ! git rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then + echo "Branch not on the remote yet; pushing." + git push -u origin "$BRANCH" +fi + +# Prefer the PR template; otherwise summarise the commits on this branch. +if [ -f .gitea/PULL_REQUEST_TEMPLATE.md ]; then + BODY="$(cat .gitea/PULL_REQUEST_TEMPLATE.md)" +else + BODY="$(git log --format='- %s' "origin/$BASE..$BRANCH")" +fi + +RESPONSE="$(jq -n \ + --arg title "$TITLE" --arg body "$BODY" \ + --arg head "$BRANCH" --arg base "$BASE" \ + --argjson draft "$DRAFT" \ + '{title:$title, body:$body, head:$head, base:$base}' \ + | curl -sS -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d @- \ + "$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls")" + +if URL="$(echo "$RESPONSE" | jq -er '.html_url' 2>/dev/null)"; then + echo "PR opened: $URL" +else + echo "Gitea rejected the request:" >&2 + echo "$RESPONSE" | jq . >&2 2>/dev/null || echo "$RESPONSE" >&2 + exit 1 +fi diff --git a/scripts/review.sh b/scripts/review.sh new file mode 100755 index 0000000..d00bb94 --- /dev/null +++ b/scripts/review.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Fetch a PR's diff and metadata for review. +# +# scripts/review.sh 7 # print the diff +# scripts/review.sh 7 --meta # title, author, branch, CI state +# +# Requires GITEA_TOKEN. +set -euo pipefail + +GITEA_URL="${GITEA_URL:-http://192.168.0.3:3000}" +OWNER="${GITEA_OWNER:-BBergle}" +REPO="${GITEA_REPO:-bike-app}" + +[ -n "${GITEA_TOKEN:-}" ] || { echo "error: GITEA_TOKEN not set" >&2; exit 1; } +PR="${1:-}" +[ -n "$PR" ] || { echo "usage: scripts/review.sh <pr-number> [--meta]" >&2; exit 1; } + +api() { curl -sS -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/repos/$OWNER/$REPO/$1"; } + +if [ "${2:-}" = "--meta" ]; then + api "pulls/$PR" | jq '{number, title, user: .user.login, head: .head.ref, base: .base.ref, + mergeable, draft, additions, deletions, changed_files}' + echo "--- commit status ---" + SHA="$(api "pulls/$PR" | jq -r '.head.sha')" + api "commits/$SHA/status" | jq '{state, statuses: [.statuses[]? | {context, state}]}' +else + api "pulls/$PR.diff" +fi