markdown · 28981 bytes Raw Blame History

Changelog

All notable changes to shithub are documented here. This project follows Keep a Changelog conventions and Semantic Versioning.

Pre-1.0 versioning: minor versions may break the API. The stability contract begins at v1.0.0; until then, expect changes between minor releases.

Unreleased

Fixed

  • PRO08 Pro tier GA audit remediation. Thirteen audit findings across the Stripe webhook layer, subscription state machine, and Pro v1 feature gates: webhook receipts now record (subject_kind, subject_id) for operator queries; the cross-kind price guard refuses subscription events with empty Items.Data; concurrent webhook replays serialize via a session-scoped advisory lock; the snapshot CTE preserves grace-lock columns under past_due transitions instead of wiping them; subscription-overwrite guard refuses to repoint a principal's bound subscription id at a different one; reverse-ordered (stale) Stripe events are dropped via a new last_event_at column; customer.subscription.deleted for unknown subjects is now a 200 no-op so Stripe stops retrying. Charge refunds flip invoices to status='refunded' with UI surfacing on both user and org billing pages (new billing_invoice_status='refunded' enum value + refunded_at column). Advanced branch protection gate rewired to fire on the PRO01-ratified inputs (prevent_force_push, prevent_deletion, require_signed_commits) rather than only on required status checks — require_signed_commits is now exposed in the rule form (visible toggle; underlying enforcement ships with commit signing). Multi-reviewer denies carry a distinct upgrade copy (required-reviewers-multi-upgrade(-pro)) and user-tier denies point at /settings/billing instead of the org settings page. profilePinsRemaining now respects the entitled cap for Pro users. Migrations 0077 (last_event_at) and 0078 (refunded enum + refunded_at column) ship with the fix. Audit closure in docs/internal/billing.md; runbook updates in docs/internal/runbooks/stripe-billing.md.

Added

  • Personal Pro tier feature gates (PRO07). Pro v1 lights up four user-tier paygates ratified in PRO01: required reviewers on private personal repos, multi-reviewer thresholds, advanced branch protection (prevent force-push / deletion / require signed commits), and profile pins above the Free cap of 6 (Pro raises this to 100). Each gate is enforced via a per-feature operator flag in billing.enforce.* so launches are reversible without a code rollback. PRO07 ships dark (all flags default false) and operators flip each feature on after the 7-day report-only telemetry soak. Pro user accounts retain existing required-reviewer rules and pin configuration through cancellation; the gate refuses to create new gated state on Free but never deletes prior data. Added the FeatureProfilePinsBeyondFree (user-only) and FeatureCodeOwnersReview (placeholder; no-op enforce until the CODEOWNERS parser ships) entitlement constants, plus the LimitProfilePinsFreeCap / LimitProfilePinsProCap limit constants. Migration 0076_profile_pins_pro_cap raises the profile_pins.position check constraint from 1..6 to 1..100.
  • REST API contract (S50 §0). GET /api/v1/meta returns the server's version stamp and a list of feature capability strings for client-side feature detection. Every /api/v1/* response now carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and (when PAT-authenticated) X-OAuth-Scopes. The 403 scope-reject response also carries X-Accepted-OAuth-Scopes. Operators tune the API rate-limit budgets via ratelimit.api.authed_per_hour / ratelimit.api.anon_per_hour (defaults: 5000 / 60).
  • Pagination helper internal/web/handlers/api/apipage — emits canonical RFC 8288 Link headers (first/prev/next/last) with absolute URLs rooted at the configured public base URL.
  • REST: user emails (S50 §1). GET /api/v1/user/emails lists the authenticated user's emails. Optional ?verified=true|false filter. Scope: user:read.
  • REST: user SSH keys (S50 §1). GET/POST /api/v1/user/keys and GET/DELETE /api/v1/user/keys/{id} expose CRUD for git authentication keys. Signing keys are tracked separately by a new kind column on user_ssh_keys and remain on the HTML surface for now. Scopes: user:read for GETs, user:write for mutations.
  • Capabilities: user-emails, ssh-keys added to /api/v1/meta response.
  • REST: repos core (S50 §2). GET /api/v1/user/repos, GET /api/v1/users/{username}/repos, GET /api/v1/orgs/{org}/repos, GET /api/v1/repos/{owner}/{repo}, POST /api/v1/user/repos, POST /api/v1/orgs/{org}/repos, PATCH /api/v1/repos/{owner}/{repo} (description, has_issues, has_pulls, archived, visibility), and DELETE /api/v1/repos/{owner}/{repo} (soft-delete). Visibility-aware listing: a user's /users/{u}/repos shows private rows only to that user; an org's /orgs/{o}/repos shows private rows only to members. Single-repo GETs 404 for callers who can't see the row (no existence leak).
  • Capability: repos added to /api/v1/meta.
  • REST: issues + comments + lock (S50 §3). GET /api/v1/repos/{o}/{r}/issues (with ?state= filter and Link:-header pagination), GET /api/v1/repos/{o}/{r}/issues/{number}, POST /api/v1/repos/{o}/{r}/issues, PATCH /api/v1/repos/{o}/{r}/issues/{number} (title/body author-gated, state/state_reason policy-gated), GET / POST /api/v1/repos/{o}/{r}/issues/{number}/comments, PATCH / DELETE /api/v1/repos/{o}/{r}/issues/comments/{cid}, PUT / DELETE /api/v1/repos/{o}/{r}/issues/{number}/lock.
  • REST: repo labels (S50 §3). GET / POST /api/v1/repos/{o}/{r}/labels and GET / PATCH / DELETE /api/v1/repos/{o}/{r}/labels/{name}.
  • Capabilities: issues, labels added to /api/v1/meta.
  • REST: milestones + assignees (S50 §3 follow-up). Full CRUD for /api/v1/repos/{o}/{r}/milestones (with ?state= filter on list and live open_issues/closed_issues counters on every response), plus GET /api/v1/repos/{o}/{r}/assignees (repo owner + collaborators eligible for issue assignment). Scope: repo:read on GETs, repo:write on mutations. Mutations gate on ActionIssueLabel.
  • Issue PATCH extensions. PATCH /api/v1/repos/{o}/{r}/issues/{n} now accepts labels, assignees, and milestone fields with GitHub-style full-replace semantics. Each gates on its own policy action (ActionIssueLabel / ActionIssueAssign) so a caller missing one capability gets a clean 403 rather than a partial update. Unknown label names or assignee usernames → 422; cross-repo milestone ids → 422.
  • Capabilities: milestones, assignees added to /api/v1/meta.
  • Reach: internal/web/handlers/api.resolveAPIRepo now resolves both user-owner and org-owner repos — check-runs and every later batch implicitly gain org-repo support.

Added (internal)

  • REST: pull requests core (S50 §4). GET /api/v1/repos/{o}/{r}/pulls with ?state= and ?draft= filters, GET /api/v1/repos/{o}/{r}/pulls/{number}, POST /api/v1/repos/{o}/{r}/pulls, PATCH /api/v1/repos/{o}/{r}/pulls/{number} (title/body author-gated, state via ActionPullClose, draft→ready author-only), GET /api/v1/repos/{o}/{r}/pulls/{number}/commits, GET /api/v1/repos/{o}/{r}/pulls/{number}/files, PUT /api/v1/repos/{o}/{r}/pulls/{number}/merge (honoring the repo's default merge method and the optional sha head guard). Reviews + comments + reviewers + update-branch + auto-merge land in a follow-up.
  • Capability: pulls added to /api/v1/meta.
  • REST: PR reviews + inline comments + requested reviewers (S50 §4b). GET / POST /api/v1/repos/{o}/{r}/pulls/{number}/reviews (events APPROVE / REQUEST_CHANGES / COMMENT, with pending-draft attachment on submit), GET / POST /api/v1/repos/{o}/{r}/pulls/{number}/comments (inline review comments with file_path / side / position anchoring, pending drafts, in_reply_to_id threading), and GET / POST / DELETE /api/v1/repos/{o}/{r}/pulls/{number}/requested_reviewers (by user_id or username).
  • Capability: pr-reviews added to /api/v1/meta.
  • REST: search (S50 §5). GET /api/v1/search/repositories, GET /api/v1/search/issues?type=issue|pr, and GET /api/v1/search/code over the existing FTS corpus. Canonical gh-shaped envelope { total_count, incomplete_results, items } with Link: pagination. Anonymous callers allowed (visibility filter inside the search package narrows to public). ?q= honors the existing operator vocabulary (repo:, is:, state:, author:, phrase). The search/commits and search/users endpoints, plus the sort=/order= knobs, are deferred to follow-ups.
  • Capability: search added to /api/v1/meta.
  • REST: orgs (S50 §7). GET /api/v1/user/orgs (self), GET /api/v1/users/{username}/orgs (public; shithub has no hidden membership distinction in v1), GET /api/v1/orgs/{org} (single fetch; 404 for soft-deleted), GET /api/v1/orgs/{org}/members. Scope: user:read.
  • Capability: orgs added to /api/v1/meta.
  • REST: repo webhooks (S50 §8). Full CRUD over a repo's webhook subscriptions: GET/POST /api/v1/repos/{o}/{r}/hooks, GET/PATCH/DELETE /api/v1/repos/{o}/{r}/hooks/{id}. Deliveries read-side: GET /api/v1/repos/{o}/{r}/hooks/{id}/deliveries (paginated; Link: headers) and GET /api/v1/repos/{o}/{r}/hooks/{id}/deliveries/{did} (full transcript). POST .../deliveries/{did}/redeliver re-enqueues. Scope: repo:write; role floor: settings:general. Webhook secrets are write-only — set on create, rotated via PATCH's secret field, never echoed back. Create-time SSRF gate rejects loopback / private / disallowed-port targets so misconfigurations surface synchronously instead of as silent delivery failures.
  • Capability: webhooks added to /api/v1/meta.
  • REST: branches + tags (S50 §9). Read-only ref enumeration: GET /api/v1/repos/{o}/{r}/branches (paginated; each entry carries protected reflecting the longest-prefix match against the configured branch-protection rules, plus is_default), GET /api/v1/repos/{o}/{r}/branches/{name} (slashes in branch names accepted verbatim or URL-encoded), and GET /api/v1/repos/{o}/{r}/tags (paginated). Scope: repo:read. Empty / uninitialised repos return [] rather than 404.
  • Capabilities: branches, tags added to /api/v1/meta.
  • REST: repo collaborators (S50 §10). GET /api/v1/repos/{o}/{r}/collaborators (list), GET .../collaborators/{username} (204 membership probe), GET .../collaborators/{username}/permission (permission level — "none" when not a collaborator), PUT .../collaborators/{username} (add / upgrade, body {"role": "..."} accepting both shithub names and gh-style aliases pull/push), DELETE .../collaborators/{username} (remove). Scope: repo:read on GETs, repo:write on mutations; mutations layer ActionRepoAdmin on top. Refuses (422) to enrol the repo owner.
  • Capability: collaborators added to /api/v1/meta.
  • REST: commits (S50 §11). Read-only git history surface: GET /api/v1/repos/{o}/{r}/commits (paginated; honours ?sha=, ?path=, ?author=, ?since=, ?until=) and GET /api/v1/repos/{o}/{r}/commits/{sha} (full commit detail with committer/parents/tree + per-file status and additions/deletions, plus a rollup stats object). Backed by internal/repos/git.Log / git.GetCommit — the response stays in lock-step with the bare repository. Scope: repo:read. Empty repos return []; the single-commit GET accepts any unambiguous SHA prefix.
  • Capability: commits added to /api/v1/meta.
  • GPG keys + commit signature verification (S51). Full vertical: upload OpenPGP public keys at Settings → SSH and GPG keys; shithub parses the armored block (RSA≥2048, ECC, multi-subkey, encryption-only all accepted) and stores primary + subkey fingerprint index. A background worker eagerly backfills verification rows for the uploader's existing commits across every repo, and the shithubd gpg-backfill-all admin subcommand performs the once-off bulk walk on deploy. Signed commits + annotated tags render a green Verified pill on the commit list, single-commit, and tag list pages; the popover surfaces the signer, key id, and verified-at timestamp. The reason enum mirrors GitHub's documented set (valid, unsigned, unknown_key, bad_email, unverified_email, expired_key, not_signing_key, malformed_signature, invalid). REST surface: GET/POST/DELETE /api/v1/user/gpg_keys[/{id}] with the gh-exact JSON shape (split can_encrypt_comms / can_encrypt_storage, both public_key and raw_key, subkeys-as-nested-objects, emails[].verified cross-checked against the user's verified-email set). Scopes: user:read for GETs, user:write for mutations. Existing /api/v1/repos/{o}/{r}/commits[/{sha}] responses now carry a verification object with the same shape gh emits. Migrations: 0068_user_gpg_keys.sql, 0069_user_gpg_subkeys.sql, 0070_commit_verification_cache.sql.
  • Capability: gpg-keys added to /api/v1/meta.
  • REST: rulesets (S50 §9). Three read-only endpoints synthesizing GitHub's modern rulesets shape from shithub's existing branch_protection_rules rows: GET /api/v1/repos/{o}/{r}/rulesets (list), GET /api/v1/repos/{o}/{r}/rulesets/{id} (single), GET /api/v1/repos/{o}/{r}/rules/branches/{branch} (rules applying to a branch — every matching pattern, not just the longest-match the pre-receive enforcer uses). One protection row maps to one ruleset; each configured field projects as a typed rule (pull_request, non_fast_forward, deletion, required_signatures, required_status_checks). Scope: repo:read. Cross-repo lookups 404. Mutating rulesets via REST is a future surface — for now use the web UI at Settings → Branches.
  • Capability: rulesets added to /api/v1/meta.
  • REST: repo contents (S50 §12). GET /api/v1/repos/{o}/{r}/contents/{path}[?ref=] returns either a directory listing (dirs first, then files alphabetically) or a single file with base64-encoded content, encoding, size, and a binary flag (UTF-8 validity check). Files over 1 MiB come back as truncated: true with empty content — clients fall through to the raw download path. Scope: repo:read. Empty /contents path returns the repo root.
  • Capability: contents added to /api/v1/meta.
  • REST: forks (S50 §13). GET /api/v1/repos/{o}/{r}/forks (paginated; per-row visibility filter so private forks of public repos only surface to authorized viewers) and POST /api/v1/repos/{o}/{r}/forks (fork into the authenticated user's namespace; optional name/visibility body). Reuses the existing internal/repos/fork orchestrator and enqueues the on-disk clone via the repo:fork_clone worker, so the response returns immediately with init_status: "init_pending". Org-target forks land in a follow-up.
  • Capability: forks added to /api/v1/meta.
  • REST: notifications (S50 §14). User-scoped inbox surface: GET /api/v1/notifications (defaults to unread only; ?all=true includes read; paginated with Link: headers), GET /api/v1/notifications/threads/{id} (single fetch with existence-leak-safe cross-user 404), PATCH /api/v1/notifications/threads/{id} (mark read/unread — empty body / unread:false → read; unread:true flips back), and PUT /api/v1/notifications (mark all read). Scopes: user:read on GETs, user:write on mutations. All routes are implicitly scoped to the authenticated user.
  • Capability: notifications added to /api/v1/meta.
  • REST: watching / subscriptions (S50 §15). Per-repo watch-level management mirroring GitHub's /repos/{o}/{r}/subscription shape: GET .../subscribers (paginated watcher list excluding ignore and suspended users), GET .../subscription (viewer's level + explicit flag distinguishing the implicit participating default from an explicit choice), PUT .../subscription (set all / participating / ignore), and DELETE .../subscription (revert to implicit). Reuses internal/social.SetWatch / UnsetWatch and socialdb.ListWatchersForRepo. Scope: repo:read on GETs, user:write on mutations.
  • Capability: watching added to /api/v1/meta.
  • REST: events / activity (S50 §16). Read-only activity feed over domain_events: GET /api/v1/repos/{o}/{r}/events (paginated; returns every event for the repo, gated by ActionRepoRead) and GET /api/v1/users/{username}/events (paginated; only public=true rows — matches gh, which never surfaces private-repo activity on a user feed). Scope: repo:read / user:read. Reuses the existing socialdb.ListEventsForRepo and socialdb.ListPublicEventsForActor queries.
  • Capability: events added to /api/v1/meta.
  • REST: followers / following (S50 §17). GET /api/v1/users/{username}/followers and GET /api/v1/users/{username}/following (both paginated; Link: headers), plus the authenticated-user-scoped GET /api/v1/user/following/{target} (204/404 membership probe matching gh), PUT /api/v1/user/following/{target} (follow), and DELETE (unfollow). Self-follow returns 422. Reuses internal/social.FollowUser / UnfollowUser so the follow rate-limit and followed_user domain event stay in one place. Org-follow variants remain on the HTML surface.
  • Capability: followers added to /api/v1/meta.
  • REST: actions workflow runs (S50 §18). Read-only access to the Actions run history: GET /api/v1/repos/{o}/{r}/actions/runs (paginated; filterable by workflow_file, head_ref, actor, event, status, conclusion), GET /api/v1/repos/{o}/{r}/actions/runs/{run_id} (single run; existence-leak-safe cross-repo 404), and GET /api/v1/repos/{o}/{r}/actions/runs/{run_id}/jobs (job-index-ordered jobs list with needs_jobs graph). Reuses the existing actionsdb.ListWorkflowRunsForRepo / GetWorkflowRunByID / ListJobsForRun queries. Scope: repo:read. Lifecycle controls (cancel / rerun / approve) remain on the actions-lifecycle routes; this surface is read-only.
  • Capability: actions-runs added to /api/v1/meta.
  • REST: stargazers + starred lists (S50 §19). GET /api/v1/repos/{owner}/{repo}/stargazers paginates the users who starred a repo (scope: repo:read); private-repo lists are gated by ActionRepoRead. GET /api/v1/users/{username}/starred paginates the repos a user has starred (scope: user:read); cross-user views post-filter private repos the caller can't see. Both endpoints emit standard Link: pagination headers and recency-sort by starred_at DESC. The S26 caller-self star routes (/api/v1/user/starred and /api/v1/user/starred/{o}/{r}) are unchanged.
  • Capability: stargazers added to /api/v1/meta.
  • REST: issue events / timeline (S50 §20). GET /api/v1/repos/{owner}/{repo}/issues/{number}/events returns the issue's recorded timeline (every closed / reopened / labeled / unlabeled / milestoned / demilestoned / locked / unlocked / referenced / merged / push event), with actor_username LEFT-joined and the raw event meta payload preserved verbatim. Paginated with the standard Link: headers, sorted oldest-first. Scope: repo:read.
  • Capability: issue-events added to /api/v1/meta.
  • Device-code login (S50 §1, RFC 8628). POST /login/device/code issues a fresh authorization grant for a non-browser client (CLI / TV / IoT). POST /login/oauth/access_token polls for the user's approval and, on success, mints a PAT bound to the requested scopes. The browser-facing verification page is served at GET /login/device (CSRF-protected). The matching CLI endpoints are CSRF-exempt. client_id is enforced against an allowlist (default: shithub-cli); requested scopes go through the standard pat.ValidScope filter so unknown scopes fail cleanly with invalid_scope. The minted PAT is disclosed exactly once — subsequent exchanges of the same device_code return invalid_grant even after successful approval. RFC 8628 §3.5 error semantics (authorization_pending, slow_down, access_denied, expired_token) are honored.
  • Capability: device-code added to /api/v1/meta.
  • REST: actions workflows + workflow_dispatch (S50 §13 part 1). GET /api/v1/repos/{o}/{r}/actions/workflows lists the workflows discovered in .shithub/workflows/ at the repo's default-branch HEAD (or ?ref=); GET .../workflows/{id_or_file} fetches a single workflow by basename, full path, or a deterministic 64-bit hash of the path. POST .../workflows/{file}/dispatches triggers workflow_dispatch with optional ref and a typed inputs map (choice / boolean / string with required/default enforcement, same semantics as the HTML "Run workflow" button). Workflow_dispatch input validation now lives in a shared internal/actions/dispatch package consumed by both the REST and HTML surfaces. Enable/disable knobs are deferred to a follow-up (needs a new workflow_disabled table); every listed workflow is reported as state: "active" for now.
  • Capability: actions-workflows added to /api/v1/meta.
  • REST: actions lifecycle + artifacts + job logs (S50 §13 part 2). New migration 0064_workflow_disabled adds a per-workflow disable knob; trigger.Enqueue now consults it and short-circuits matching events for disabled workflows. The list endpoint surfaces this as "state": "disabled". REST additions: PUT /api/v1/repos/{o}/{r}/actions/workflows/{file}/enable and .../disable, DELETE /api/v1/repos/{o}/{r}/actions/runs/{run_id} (cascades through jobs/steps/log-chunks/artifacts; object-store cleanup is async best-effort), GET /api/v1/repos/{o}/{r}/actions/runs/{run_id}/artifacts + GET .../actions/artifacts/{aid} + GET .../actions/artifacts/{aid}/zip (streams from object store) + DELETE .../actions/artifacts/{aid}, GET /api/v1/repos/{o}/{r}/actions/jobs/{job_id}/logs (assembled transcript with ##[group]/##[endgroup] step markers).
  • Capabilities: actions-artifacts, actions-job-logs added to /api/v1/meta.
  • REST: actions secrets + variables (S50 §13 part 3). New internal/auth/sealbox package owns the server's X25519 keypair and exposes NaCl SealedBox decode. Clients fetch the public key via GET /api/v1/repos/{o}/{r}/actions/secrets/public-key (and the org analog), encrypt the secret value with crypto_box_seal, and PUT the base64 ciphertext + key_id. Server validates key_id, decrypts in memory, then re-encrypts with the shared storage AEAD for at-rest persistence — plaintext never reaches postgres. Secrets CRUD: GET .../secrets, GET .../secrets/{name}, PUT .../secrets/{name}, DELETE .../secrets/{name}. Variables CRUD (plaintext, ${{ vars.NAME }} namespace): GET/POST .../variables, GET/PATCH/DELETE .../variables/{name}. Both surfaces have repo- and org-scoped variants. Operator setup: SHITHUB_ACTIONS__SECRETS__BOX_PRIVATE_KEY_B64 carries the X25519 private key (32 bytes base64); unset means auto-generated-per-process with a loud warning at startup, which is dev-only behavior.
  • Capabilities: actions-secrets, actions-variables added to /api/v1/meta.
  • REST: actions caches (S50 §13 part 4 — §13 closure). New migration 0065_workflow_caches adds the workflow_caches table keyed on (repo_id, cache_key, cache_version, git_ref) with size + last_accessed_at + object_key columns and a unique constraint. REST surface: GET /api/v1/repos/{o}/{r}/actions/caches (paginated; optional ?key= and ?ref= filters; standard Link: headers; sorted recency-DESC by last_accessed_at), DELETE .../actions/caches/{cache_id} (single delete with cross-repo 404 guard + best-effort async S3 cleanup), DELETE .../actions/caches?key=...[&ref=...] (bulk delete by key; idempotent — 204 even with zero matches). The runner-side upload protocol that POPULATES this table is a future sprint; this REST surface lands first so operators have an audit + purge seat for when caches arrive.
  • Capability: actions-caches added to /api/v1/meta.
  • REST: repos follow-ups (S50 §2 closure). GET /api/v1/repos/{o}/{r}/readme[?ref=] returns the repo's README as a base64-encoded blob with download_url for the raw bytes (capped at 1 MiB to match the HTML render cap; prefers .md/.markdown over plain text when multiple READMEs exist). PUT /api/v1/repos/{o}/{r}/topics and DELETE .../topics replace and clear the topic set atomically (server-side normalization: lowercase, dedup; constraints: max 20, 1–50 chars of [a-z0-9-]). POST .../merge-upstream fast-forwards a fork's default branch to its upstream — refuses non-forks with 422 and divergent forks with 409 (users must reconcile locally). Scopes: repo:read on README, repo:write on topics and merge-upstream.
  • Capabilities: readme, topics, merge-upstream added to /api/v1/meta.

Added (internal)

  • issues.Edit orchestrator wraps UpdateIssueTitleBody with markdown re-render + cross-reference re-indexing. Used by the new PATCH-issue endpoint; available for the HTML edit flow when it lands.

Changed

  • JSON error envelope on /api/v1/*. 401 and 403 responses now emit {"error": "..."} with Content-Type: application/json (previously text/plain). Existing 4xx/5xx responses from the handler bodies are unchanged.

0.1.0 — TBD (operator fills in cutover date)

The first public release of shithub. Pre-1.0: there is no backward-compatibility promise yet. Migrations are forward-only; schema may change between minor versions.

Initial public surface

  • Identity — signup, email verification, password reset, TOTP 2FA + recovery codes, SSH keys, scoped PATs, sessions with per-account epoch invalidation.
  • Repositories — create, fork, archive, transfer, soft-delete with grace, rename with redirects, visibility toggles, branch protection, default-branch swap, topics, README/license/ .gitignore templates.
  • Git — bare repos on disk; HTTPS smart-HTTP push/pull; pre/post-receive hook integration.
  • Code browsing — tree, blob (chroma syntax highlighting), raw, blame, commit history, individual commit views, branch/tag listings, compare views, file finder.
  • Issues + PRs — full CRUD; reviews; required-reviewer enforcement; status-check gates; three merge methods.
  • Social — stars, watches, forks, /explore, stargazer/ watcher lists.
  • Search — code, repo, user, issue.
  • Notifications — in-app inbox, email fan-out, one-click unsubscribe.
  • Orgs + teams — roles, invitations, one-level nesting, max-of-sources policy.
  • Webhooks — HMAC-signed delivery, exponential backoff, auto-disable, SSRF defense, redelivery UI.
  • Observability — structured logs, Prometheus metrics, optional OTel tracing, Sentry-protocol error reporting.
  • Operations — Ansible playbook, systemd units, Caddy edge, WireGuard mesh for monitoring, Postgres WAL archive + daily logical backups to Spaces, cross-region DR, restore drill.
  • Public landing page on / for anonymous viewers; signed-in viewers get a quick-link dashboard.
  • Lightweight status page at docs.<host>/status.html.
  • Cutover artifacts under deploy/cutover/.
  • Public docs site built with mdBook.
  • Operator runbooks for incidents, backups, restore, upgrade, rollback, rotate-secrets, rotate-keys, regenerate-akc, drain-workers, read-only-mode, day-one.
  • a11y tooling (pa11y + axe) and k6 load-test scenarios.
  • THIRD_PARTY_NOTICES.md with a CI-verified generator.

Known gaps at v0.1.0

  • SSH git transport (HTTPS only)
  • Actions / CI runner
  • Packages, Releases, Pages, Projects, Gists
  • GraphQL API (only a small REST surface today)
  • Activity feed UI

These are all on the post-MVP roadmap.

View source
1 # Changelog
2
3 All notable changes to shithub are documented here. This project
4 follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5 conventions and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
7 Pre-1.0 versioning: minor versions may break the API. The
8 stability contract begins at v1.0.0; until then, expect changes
9 between minor releases.
10
11 ## [Unreleased]
12
13 ### Fixed
14
15 - **PRO08 Pro tier GA audit remediation.** Thirteen audit findings
16 across the Stripe webhook layer, subscription state machine, and
17 Pro v1 feature gates: webhook receipts now record (subject_kind,
18 subject_id) for operator queries; the cross-kind price guard
19 refuses subscription events with empty `Items.Data`; concurrent
20 webhook replays serialize via a session-scoped advisory lock; the
21 snapshot CTE preserves grace-lock columns under `past_due`
22 transitions instead of wiping them; subscription-overwrite guard
23 refuses to repoint a principal's bound subscription id at a
24 different one; reverse-ordered (stale) Stripe events are dropped
25 via a new `last_event_at` column; `customer.subscription.deleted`
26 for unknown subjects is now a 200 no-op so Stripe stops retrying.
27 Charge refunds flip invoices to `status='refunded'` with UI
28 surfacing on both user and org billing pages (new
29 `billing_invoice_status='refunded'` enum value + `refunded_at`
30 column). Advanced branch protection gate rewired to fire on the
31 PRO01-ratified inputs (`prevent_force_push`, `prevent_deletion`,
32 `require_signed_commits`) rather than only on required status
33 checks — `require_signed_commits` is now exposed in the rule
34 form (visible toggle; underlying enforcement ships with commit
35 signing). Multi-reviewer denies carry a distinct upgrade copy
36 (`required-reviewers-multi-upgrade(-pro)`) and user-tier denies
37 point at `/settings/billing` instead of the org settings page.
38 `profilePinsRemaining` now respects the entitled cap for Pro users.
39 Migrations 0077 (`last_event_at`) and 0078 (`refunded` enum +
40 `refunded_at` column) ship with the fix. Audit closure in
41 `docs/internal/billing.md`; runbook updates in
42 `docs/internal/runbooks/stripe-billing.md`.
43
44 ### Added
45
46 - **Personal Pro tier feature gates (PRO07).** Pro v1 lights up four
47 user-tier paygates ratified in PRO01: required reviewers on private
48 personal repos, multi-reviewer thresholds, advanced branch
49 protection (prevent force-push / deletion / require signed commits),
50 and profile pins above the Free cap of 6 (Pro raises this to 100).
51 Each gate is enforced via a per-feature operator flag in
52 `billing.enforce.*` so launches are reversible without a code
53 rollback. PRO07 ships dark (all flags default false) and operators
54 flip each feature on after the 7-day report-only telemetry soak.
55 Pro user accounts retain existing required-reviewer rules and pin
56 configuration through cancellation; the gate refuses to create new
57 gated state on Free but never deletes prior data. Added the
58 `FeatureProfilePinsBeyondFree` (user-only) and
59 `FeatureCodeOwnersReview` (placeholder; no-op enforce until the
60 CODEOWNERS parser ships) entitlement constants, plus the
61 `LimitProfilePinsFreeCap` / `LimitProfilePinsProCap` limit
62 constants. Migration `0076_profile_pins_pro_cap` raises the
63 `profile_pins.position` check constraint from `1..6` to `1..100`.
64 - **REST API contract (S50 §0).** `GET /api/v1/meta` returns the
65 server's version stamp and a list of feature capability strings
66 for client-side feature detection. Every `/api/v1/*` response
67 now carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
68 `X-RateLimit-Reset`, and (when PAT-authenticated) `X-OAuth-Scopes`.
69 The 403 scope-reject response also carries
70 `X-Accepted-OAuth-Scopes`. Operators tune the API rate-limit
71 budgets via `ratelimit.api.authed_per_hour` /
72 `ratelimit.api.anon_per_hour` (defaults: 5000 / 60).
73 - **Pagination helper** `internal/web/handlers/api/apipage`
74 emits canonical RFC 8288 Link headers (`first`/`prev`/`next`/`last`)
75 with absolute URLs rooted at the configured public base URL.
76 - **REST: user emails (S50 §1).** `GET /api/v1/user/emails` lists
77 the authenticated user's emails. Optional `?verified=true|false`
78 filter. Scope: `user:read`.
79 - **REST: user SSH keys (S50 §1).** `GET/POST /api/v1/user/keys`
80 and `GET/DELETE /api/v1/user/keys/{id}` expose CRUD for git
81 authentication keys. Signing keys are tracked separately by a
82 new `kind` column on `user_ssh_keys` and remain on the HTML
83 surface for now. Scopes: `user:read` for GETs, `user:write` for
84 mutations.
85 - **Capabilities:** `user-emails`, `ssh-keys` added to
86 `/api/v1/meta` response.
87 - **REST: repos core (S50 §2).**
88 `GET /api/v1/user/repos`, `GET /api/v1/users/{username}/repos`,
89 `GET /api/v1/orgs/{org}/repos`,
90 `GET /api/v1/repos/{owner}/{repo}`,
91 `POST /api/v1/user/repos`,
92 `POST /api/v1/orgs/{org}/repos`,
93 `PATCH /api/v1/repos/{owner}/{repo}` (description, has_issues,
94 has_pulls, archived, visibility), and
95 `DELETE /api/v1/repos/{owner}/{repo}` (soft-delete).
96 Visibility-aware listing: a user's `/users/{u}/repos` shows
97 private rows only to that user; an org's `/orgs/{o}/repos`
98 shows private rows only to members. Single-repo GETs `404`
99 for callers who can't see the row (no existence leak).
100 - **Capability:** `repos` added to `/api/v1/meta`.
101 - **REST: issues + comments + lock (S50 §3).**
102 `GET /api/v1/repos/{o}/{r}/issues` (with `?state=` filter and
103 `Link:`-header pagination),
104 `GET /api/v1/repos/{o}/{r}/issues/{number}`,
105 `POST /api/v1/repos/{o}/{r}/issues`,
106 `PATCH /api/v1/repos/{o}/{r}/issues/{number}` (title/body
107 author-gated, state/state_reason policy-gated),
108 `GET / POST /api/v1/repos/{o}/{r}/issues/{number}/comments`,
109 `PATCH / DELETE /api/v1/repos/{o}/{r}/issues/comments/{cid}`,
110 `PUT / DELETE /api/v1/repos/{o}/{r}/issues/{number}/lock`.
111 - **REST: repo labels (S50 §3).**
112 `GET / POST /api/v1/repos/{o}/{r}/labels` and
113 `GET / PATCH / DELETE /api/v1/repos/{o}/{r}/labels/{name}`.
114 - **Capabilities:** `issues`, `labels` added to `/api/v1/meta`.
115 - **REST: milestones + assignees (S50 §3 follow-up).** Full CRUD
116 for `/api/v1/repos/{o}/{r}/milestones` (with `?state=` filter
117 on list and live `open_issues`/`closed_issues` counters on
118 every response), plus `GET /api/v1/repos/{o}/{r}/assignees`
119 (repo owner + collaborators eligible for issue assignment).
120 Scope: `repo:read` on GETs, `repo:write` on mutations.
121 Mutations gate on `ActionIssueLabel`.
122 - **Issue PATCH extensions.** `PATCH /api/v1/repos/{o}/{r}/issues/{n}`
123 now accepts `labels`, `assignees`, and `milestone` fields with
124 GitHub-style full-replace semantics. Each gates on its own
125 policy action (`ActionIssueLabel` / `ActionIssueAssign`) so a
126 caller missing one capability gets a clean 403 rather than a
127 partial update. Unknown label names or assignee usernames →
128 422; cross-repo milestone ids → 422.
129 - **Capabilities:** `milestones`, `assignees` added to
130 `/api/v1/meta`.
131 - **Reach:** `internal/web/handlers/api.resolveAPIRepo` now
132 resolves both user-owner and org-owner repos — check-runs and
133 every later batch implicitly gain org-repo support.
134
135 ### Added (internal)
136
137 - **REST: pull requests core (S50 §4).**
138 `GET /api/v1/repos/{o}/{r}/pulls` with `?state=` and `?draft=`
139 filters,
140 `GET /api/v1/repos/{o}/{r}/pulls/{number}`,
141 `POST /api/v1/repos/{o}/{r}/pulls`,
142 `PATCH /api/v1/repos/{o}/{r}/pulls/{number}` (title/body
143 author-gated, state via `ActionPullClose`, draft→ready
144 author-only),
145 `GET /api/v1/repos/{o}/{r}/pulls/{number}/commits`,
146 `GET /api/v1/repos/{o}/{r}/pulls/{number}/files`,
147 `PUT /api/v1/repos/{o}/{r}/pulls/{number}/merge` (honoring
148 the repo's default merge method and the optional `sha`
149 head guard). Reviews + comments + reviewers + update-branch +
150 auto-merge land in a follow-up.
151 - **Capability:** `pulls` added to `/api/v1/meta`.
152 - **REST: PR reviews + inline comments + requested reviewers (S50 §4b).**
153 `GET / POST /api/v1/repos/{o}/{r}/pulls/{number}/reviews`
154 (events `APPROVE` / `REQUEST_CHANGES` / `COMMENT`, with
155 pending-draft attachment on submit),
156 `GET / POST /api/v1/repos/{o}/{r}/pulls/{number}/comments`
157 (inline review comments with file_path / side / position
158 anchoring, `pending` drafts, `in_reply_to_id` threading), and
159 `GET / POST / DELETE /api/v1/repos/{o}/{r}/pulls/{number}/requested_reviewers`
160 (by `user_id` or `username`).
161 - **Capability:** `pr-reviews` added to `/api/v1/meta`.
162 - **REST: search (S50 §5).** `GET /api/v1/search/repositories`,
163 `GET /api/v1/search/issues?type=issue|pr`, and
164 `GET /api/v1/search/code` over the existing FTS corpus.
165 Canonical gh-shaped envelope `{ total_count,
166 incomplete_results, items }` with `Link:` pagination.
167 Anonymous callers allowed (visibility filter inside the search
168 package narrows to public). `?q=` honors the existing operator
169 vocabulary (`repo:`, `is:`, `state:`, `author:`, phrase). The
170 `search/commits` and `search/users` endpoints, plus the
171 `sort=`/`order=` knobs, are deferred to follow-ups.
172 - **Capability:** `search` added to `/api/v1/meta`.
173 - **REST: orgs (S50 §7).** `GET /api/v1/user/orgs` (self),
174 `GET /api/v1/users/{username}/orgs` (public; shithub has no
175 hidden membership distinction in v1),
176 `GET /api/v1/orgs/{org}` (single fetch; 404 for soft-deleted),
177 `GET /api/v1/orgs/{org}/members`. Scope: `user:read`.
178 - **Capability:** `orgs` added to `/api/v1/meta`.
179 - **REST: repo webhooks (S50 §8).** Full CRUD over a repo's
180 webhook subscriptions: `GET/POST /api/v1/repos/{o}/{r}/hooks`,
181 `GET/PATCH/DELETE /api/v1/repos/{o}/{r}/hooks/{id}`. Deliveries
182 read-side: `GET /api/v1/repos/{o}/{r}/hooks/{id}/deliveries`
183 (paginated; `Link:` headers) and
184 `GET /api/v1/repos/{o}/{r}/hooks/{id}/deliveries/{did}` (full
185 transcript). `POST .../deliveries/{did}/redeliver` re-enqueues.
186 Scope: `repo:write`; role floor: settings:general. Webhook
187 secrets are write-only — set on create, rotated via PATCH's
188 `secret` field, never echoed back. Create-time SSRF gate
189 rejects loopback / private / disallowed-port targets so
190 misconfigurations surface synchronously instead of as silent
191 delivery failures.
192 - **Capability:** `webhooks` added to `/api/v1/meta`.
193 - **REST: branches + tags (S50 §9).** Read-only ref enumeration:
194 `GET /api/v1/repos/{o}/{r}/branches` (paginated; each entry
195 carries `protected` reflecting the longest-prefix match against
196 the configured branch-protection rules, plus `is_default`),
197 `GET /api/v1/repos/{o}/{r}/branches/{name}` (slashes in branch
198 names accepted verbatim or URL-encoded), and
199 `GET /api/v1/repos/{o}/{r}/tags` (paginated). Scope: `repo:read`.
200 Empty / uninitialised repos return `[]` rather than `404`.
201 - **Capabilities:** `branches`, `tags` added to `/api/v1/meta`.
202 - **REST: repo collaborators (S50 §10).**
203 `GET /api/v1/repos/{o}/{r}/collaborators` (list),
204 `GET .../collaborators/{username}` (204 membership probe),
205 `GET .../collaborators/{username}/permission` (permission
206 level — `"none"` when not a collaborator),
207 `PUT .../collaborators/{username}` (add / upgrade, body
208 `{"role": "..."}` accepting both shithub names and gh-style
209 aliases `pull`/`push`),
210 `DELETE .../collaborators/{username}` (remove). Scope:
211 `repo:read` on GETs, `repo:write` on mutations; mutations
212 layer `ActionRepoAdmin` on top. Refuses (422) to enrol the
213 repo owner.
214 - **Capability:** `collaborators` added to `/api/v1/meta`.
215 - **REST: commits (S50 §11).** Read-only git history surface:
216 `GET /api/v1/repos/{o}/{r}/commits` (paginated; honours
217 `?sha=`, `?path=`, `?author=`, `?since=`, `?until=`) and
218 `GET /api/v1/repos/{o}/{r}/commits/{sha}` (full commit detail
219 with committer/parents/tree + per-file `status` and
220 `additions`/`deletions`, plus a rollup `stats` object). Backed
221 by `internal/repos/git.Log` / `git.GetCommit` — the response
222 stays in lock-step with the bare repository. Scope:
223 `repo:read`. Empty repos return `[]`; the single-commit GET
224 accepts any unambiguous SHA prefix.
225 - **Capability:** `commits` added to `/api/v1/meta`.
226 - **GPG keys + commit signature verification (S51).** Full
227 vertical: upload OpenPGP public keys at
228 `Settings → SSH and GPG keys`; shithub parses the armored
229 block (RSA≥2048, ECC, multi-subkey, encryption-only all
230 accepted) and stores primary + subkey fingerprint index. A
231 background worker eagerly backfills verification rows for the
232 uploader's existing commits across every repo, and the
233 `shithubd gpg-backfill-all` admin subcommand performs the
234 once-off bulk walk on deploy. Signed commits + annotated tags
235 render a green **Verified** pill on the commit list,
236 single-commit, and tag list pages; the popover surfaces the
237 signer, key id, and verified-at timestamp. The `reason` enum
238 mirrors GitHub's documented set (`valid`, `unsigned`,
239 `unknown_key`, `bad_email`, `unverified_email`, `expired_key`,
240 `not_signing_key`, `malformed_signature`, `invalid`). REST
241 surface: `GET/POST/DELETE /api/v1/user/gpg_keys[/{id}]` with
242 the gh-exact JSON shape (split `can_encrypt_comms` /
243 `can_encrypt_storage`, both `public_key` and `raw_key`,
244 subkeys-as-nested-objects, `emails[].verified` cross-checked
245 against the user's verified-email set). Scopes: `user:read`
246 for GETs, `user:write` for mutations. Existing
247 `/api/v1/repos/{o}/{r}/commits[/{sha}]` responses now carry a
248 `verification` object with the same shape gh emits.
249 Migrations: `0068_user_gpg_keys.sql`,
250 `0069_user_gpg_subkeys.sql`,
251 `0070_commit_verification_cache.sql`.
252 - **Capability:** `gpg-keys` added to `/api/v1/meta`.
253 - **REST: rulesets (S50 §9).** Three read-only endpoints
254 synthesizing GitHub's modern rulesets shape from shithub's
255 existing `branch_protection_rules` rows:
256 `GET /api/v1/repos/{o}/{r}/rulesets` (list),
257 `GET /api/v1/repos/{o}/{r}/rulesets/{id}` (single),
258 `GET /api/v1/repos/{o}/{r}/rules/branches/{branch}` (rules
259 applying to a branch — every matching pattern, not just the
260 longest-match the pre-receive enforcer uses). One protection
261 row maps to one ruleset; each configured field projects as a
262 typed rule (`pull_request`, `non_fast_forward`, `deletion`,
263 `required_signatures`, `required_status_checks`). Scope:
264 `repo:read`. Cross-repo lookups 404. Mutating rulesets via
265 REST is a future surface — for now use the web UI at
266 `Settings → Branches`.
267 - **Capability:** `rulesets` added to `/api/v1/meta`.
268 - **REST: repo contents (S50 §12).**
269 `GET /api/v1/repos/{o}/{r}/contents/{path}[?ref=]` returns
270 either a directory listing (dirs first, then files
271 alphabetically) or a single file with base64-encoded
272 `content`, `encoding`, `size`, and a `binary` flag (UTF-8
273 validity check). Files over 1 MiB come back as
274 `truncated: true` with empty content — clients fall through
275 to the raw download path. Scope: `repo:read`. Empty `/contents`
276 path returns the repo root.
277 - **Capability:** `contents` added to `/api/v1/meta`.
278 - **REST: forks (S50 §13).**
279 `GET /api/v1/repos/{o}/{r}/forks` (paginated; per-row
280 visibility filter so private forks of public repos only
281 surface to authorized viewers) and
282 `POST /api/v1/repos/{o}/{r}/forks` (fork into the
283 authenticated user's namespace; optional `name`/`visibility`
284 body). Reuses the existing `internal/repos/fork` orchestrator
285 and enqueues the on-disk clone via the
286 `repo:fork_clone` worker, so the response returns
287 immediately with `init_status: "init_pending"`. Org-target
288 forks land in a follow-up.
289 - **Capability:** `forks` added to `/api/v1/meta`.
290 - **REST: notifications (S50 §14).** User-scoped inbox surface:
291 `GET /api/v1/notifications` (defaults to unread only;
292 `?all=true` includes read; paginated with `Link:` headers),
293 `GET /api/v1/notifications/threads/{id}` (single fetch with
294 existence-leak-safe cross-user 404),
295 `PATCH /api/v1/notifications/threads/{id}` (mark read/unread
296 — empty body / `unread:false` → read; `unread:true` flips
297 back), and `PUT /api/v1/notifications` (mark all read).
298 Scopes: `user:read` on GETs, `user:write` on mutations. All
299 routes are implicitly scoped to the authenticated user.
300 - **Capability:** `notifications` added to `/api/v1/meta`.
301 - **REST: watching / subscriptions (S50 §15).** Per-repo
302 watch-level management mirroring GitHub's
303 `/repos/{o}/{r}/subscription` shape:
304 `GET .../subscribers` (paginated watcher list excluding
305 `ignore` and suspended users), `GET .../subscription`
306 (viewer's level + `explicit` flag distinguishing the
307 implicit `participating` default from an explicit choice),
308 `PUT .../subscription` (set `all` / `participating` /
309 `ignore`), and `DELETE .../subscription` (revert to implicit).
310 Reuses `internal/social.SetWatch` / `UnsetWatch` and
311 `socialdb.ListWatchersForRepo`. Scope: `repo:read` on GETs,
312 `user:write` on mutations.
313 - **Capability:** `watching` added to `/api/v1/meta`.
314 - **REST: events / activity (S50 §16).** Read-only activity feed
315 over `domain_events`: `GET /api/v1/repos/{o}/{r}/events`
316 (paginated; returns every event for the repo, gated by
317 `ActionRepoRead`) and `GET /api/v1/users/{username}/events`
318 (paginated; only `public=true` rows — matches gh, which never
319 surfaces private-repo activity on a user feed). Scope:
320 `repo:read` / `user:read`. Reuses the existing
321 `socialdb.ListEventsForRepo` and
322 `socialdb.ListPublicEventsForActor` queries.
323 - **Capability:** `events` added to `/api/v1/meta`.
324 - **REST: followers / following (S50 §17).**
325 `GET /api/v1/users/{username}/followers` and
326 `GET /api/v1/users/{username}/following` (both paginated;
327 `Link:` headers), plus the authenticated-user-scoped
328 `GET /api/v1/user/following/{target}` (204/404 membership
329 probe matching gh), `PUT /api/v1/user/following/{target}`
330 (follow), and `DELETE` (unfollow). Self-follow returns 422.
331 Reuses `internal/social.FollowUser` / `UnfollowUser` so the
332 follow rate-limit and `followed_user` domain event stay in
333 one place. Org-follow variants remain on the HTML surface.
334 - **Capability:** `followers` added to `/api/v1/meta`.
335 - **REST: actions workflow runs (S50 §18).** Read-only access to
336 the Actions run history:
337 `GET /api/v1/repos/{o}/{r}/actions/runs` (paginated; filterable
338 by `workflow_file`, `head_ref`, `actor`, `event`, `status`,
339 `conclusion`),
340 `GET /api/v1/repos/{o}/{r}/actions/runs/{run_id}` (single
341 run; existence-leak-safe cross-repo 404), and
342 `GET /api/v1/repos/{o}/{r}/actions/runs/{run_id}/jobs`
343 (job-index-ordered jobs list with `needs_jobs` graph). Reuses
344 the existing `actionsdb.ListWorkflowRunsForRepo` /
345 `GetWorkflowRunByID` / `ListJobsForRun` queries. Scope:
346 `repo:read`. Lifecycle controls (cancel / rerun / approve)
347 remain on the actions-lifecycle routes; this surface is
348 read-only.
349 - **Capability:** `actions-runs` added to `/api/v1/meta`.
350 - **REST: stargazers + starred lists (S50 §19).**
351 `GET /api/v1/repos/{owner}/{repo}/stargazers` paginates the users
352 who starred a repo (scope: `repo:read`); private-repo lists are
353 gated by `ActionRepoRead`. `GET /api/v1/users/{username}/starred`
354 paginates the repos a user has starred (scope: `user:read`);
355 cross-user views post-filter private repos the caller can't see.
356 Both endpoints emit standard `Link:` pagination headers and
357 recency-sort by `starred_at DESC`. The S26 caller-self star
358 routes (`/api/v1/user/starred` and `/api/v1/user/starred/{o}/{r}`)
359 are unchanged.
360 - **Capability:** `stargazers` added to `/api/v1/meta`.
361 - **REST: issue events / timeline (S50 §20).**
362 `GET /api/v1/repos/{owner}/{repo}/issues/{number}/events` returns
363 the issue's recorded timeline (every `closed` / `reopened` /
364 `labeled` / `unlabeled` / `milestoned` / `demilestoned` /
365 `locked` / `unlocked` / `referenced` / merged / push event), with
366 `actor_username` LEFT-joined and the raw event `meta` payload
367 preserved verbatim. Paginated with the standard `Link:` headers,
368 sorted oldest-first. Scope: `repo:read`.
369 - **Capability:** `issue-events` added to `/api/v1/meta`.
370 - **Device-code login (S50 §1, RFC 8628).**
371 `POST /login/device/code` issues a fresh authorization grant for
372 a non-browser client (CLI / TV / IoT). `POST /login/oauth/access_token`
373 polls for the user's approval and, on success, mints a PAT bound
374 to the requested scopes. The browser-facing verification page is
375 served at `GET /login/device` (CSRF-protected). The matching CLI
376 endpoints are CSRF-exempt. `client_id` is enforced against an
377 allowlist (default: `shithub-cli`); requested scopes go through
378 the standard `pat.ValidScope` filter so unknown scopes fail
379 cleanly with `invalid_scope`. The minted PAT is disclosed exactly
380 once — subsequent exchanges of the same `device_code` return
381 `invalid_grant` even after successful approval. RFC 8628 §3.5
382 error semantics (`authorization_pending`, `slow_down`,
383 `access_denied`, `expired_token`) are honored.
384 - **Capability:** `device-code` added to `/api/v1/meta`.
385 - **REST: actions workflows + workflow_dispatch (S50 §13 part 1).**
386 `GET /api/v1/repos/{o}/{r}/actions/workflows` lists the workflows
387 discovered in `.shithub/workflows/` at the repo's default-branch
388 HEAD (or `?ref=`); `GET .../workflows/{id_or_file}` fetches a
389 single workflow by basename, full path, or a deterministic
390 64-bit hash of the path. `POST .../workflows/{file}/dispatches`
391 triggers `workflow_dispatch` with optional `ref` and a typed
392 `inputs` map (choice / boolean / string with required/default
393 enforcement, same semantics as the HTML "Run workflow" button).
394 Workflow_dispatch input validation now lives in a shared
395 `internal/actions/dispatch` package consumed by both the REST
396 and HTML surfaces. Enable/disable knobs are deferred to a
397 follow-up (needs a new `workflow_disabled` table); every listed
398 workflow is reported as `state: "active"` for now.
399 - **Capability:** `actions-workflows` added to `/api/v1/meta`.
400 - **REST: actions lifecycle + artifacts + job logs (S50 §13 part 2).**
401 New migration `0064_workflow_disabled` adds a per-workflow
402 disable knob; `trigger.Enqueue` now consults it and short-circuits
403 matching events for disabled workflows. The list endpoint surfaces
404 this as `"state": "disabled"`. REST additions:
405 `PUT /api/v1/repos/{o}/{r}/actions/workflows/{file}/enable` and
406 `.../disable`,
407 `DELETE /api/v1/repos/{o}/{r}/actions/runs/{run_id}` (cascades
408 through jobs/steps/log-chunks/artifacts; object-store cleanup is
409 async best-effort),
410 `GET /api/v1/repos/{o}/{r}/actions/runs/{run_id}/artifacts` +
411 `GET .../actions/artifacts/{aid}` +
412 `GET .../actions/artifacts/{aid}/zip` (streams from object store) +
413 `DELETE .../actions/artifacts/{aid}`,
414 `GET /api/v1/repos/{o}/{r}/actions/jobs/{job_id}/logs` (assembled
415 transcript with `##[group]`/`##[endgroup]` step markers).
416 - **Capabilities:** `actions-artifacts`, `actions-job-logs` added to
417 `/api/v1/meta`.
418 - **REST: actions secrets + variables (S50 §13 part 3).**
419 New `internal/auth/sealbox` package owns the server's X25519
420 keypair and exposes NaCl `SealedBox` decode. Clients fetch the
421 public key via
422 `GET /api/v1/repos/{o}/{r}/actions/secrets/public-key` (and the
423 org analog), encrypt the secret value with `crypto_box_seal`, and
424 PUT the base64 ciphertext + key_id. Server validates key_id,
425 decrypts in memory, then re-encrypts with the shared storage
426 AEAD for at-rest persistence — plaintext never reaches postgres.
427 Secrets CRUD: `GET .../secrets`, `GET .../secrets/{name}`,
428 `PUT .../secrets/{name}`, `DELETE .../secrets/{name}`. Variables
429 CRUD (plaintext, `${{ vars.NAME }}` namespace): `GET/POST
430 .../variables`, `GET/PATCH/DELETE .../variables/{name}`. Both
431 surfaces have repo- and org-scoped variants. Operator setup:
432 `SHITHUB_ACTIONS__SECRETS__BOX_PRIVATE_KEY_B64` carries the
433 X25519 private key (32 bytes base64); unset means
434 auto-generated-per-process with a loud warning at startup, which
435 is dev-only behavior.
436 - **Capabilities:** `actions-secrets`, `actions-variables` added to
437 `/api/v1/meta`.
438 - **REST: actions caches (S50 §13 part 4 — §13 closure).** New
439 migration `0065_workflow_caches` adds the `workflow_caches` table
440 keyed on `(repo_id, cache_key, cache_version, git_ref)` with
441 size + last_accessed_at + object_key columns and a unique
442 constraint. REST surface:
443 `GET /api/v1/repos/{o}/{r}/actions/caches` (paginated; optional
444 `?key=` and `?ref=` filters; standard `Link:` headers; sorted
445 recency-DESC by `last_accessed_at`),
446 `DELETE .../actions/caches/{cache_id}` (single delete with
447 cross-repo 404 guard + best-effort async S3 cleanup),
448 `DELETE .../actions/caches?key=...[&ref=...]` (bulk delete by
449 key; idempotent — 204 even with zero matches). The runner-side
450 upload protocol that POPULATES this table is a future sprint;
451 this REST surface lands first so operators have an audit + purge
452 seat for when caches arrive.
453 - **Capability:** `actions-caches` added to `/api/v1/meta`.
454 - **REST: repos follow-ups (S50 §2 closure).**
455 `GET /api/v1/repos/{o}/{r}/readme[?ref=]` returns the repo's
456 README as a base64-encoded blob with `download_url` for the raw
457 bytes (capped at 1 MiB to match the HTML render cap; prefers
458 `.md`/`.markdown` over plain text when multiple READMEs exist).
459 `PUT /api/v1/repos/{o}/{r}/topics` and
460 `DELETE .../topics` replace and clear the topic set atomically
461 (server-side normalization: lowercase, dedup; constraints: max
462 20, 1–50 chars of `[a-z0-9-]`). `POST .../merge-upstream`
463 fast-forwards a fork's default branch to its upstream — refuses
464 non-forks with 422 and divergent forks with 409 (users must
465 reconcile locally). Scopes: `repo:read` on README,
466 `repo:write` on topics and merge-upstream.
467 - **Capabilities:** `readme`, `topics`, `merge-upstream` added to
468 `/api/v1/meta`.
469
470 ### Added (internal)
471
472 - `issues.Edit` orchestrator wraps `UpdateIssueTitleBody` with
473 markdown re-render + cross-reference re-indexing. Used by the
474 new PATCH-issue endpoint; available for the HTML edit flow when
475 it lands.
476
477 ### Changed
478
479 - **JSON error envelope on `/api/v1/*`.** `401` and `403`
480 responses now emit `{"error": "..."}` with
481 `Content-Type: application/json` (previously `text/plain`).
482 Existing `4xx`/`5xx` responses from the handler bodies are
483 unchanged.
484
485 ## [0.1.0] — TBD (operator fills in cutover date)
486
487 The first public release of shithub. Pre-1.0: there is no
488 backward-compatibility promise yet. Migrations are forward-only;
489 schema may change between minor versions.
490
491 ### Initial public surface
492
493 - **Identity** — signup, email verification, password reset, TOTP
494 2FA + recovery codes, SSH keys, scoped PATs, sessions with
495 per-account epoch invalidation.
496 - **Repositories** — create, fork, archive, transfer, soft-delete
497 with grace, rename with redirects, visibility toggles, branch
498 protection, default-branch swap, topics, README/license/
499 .gitignore templates.
500 - **Git** — bare repos on disk; HTTPS smart-HTTP push/pull;
501 pre/post-receive hook integration.
502 - **Code browsing** — tree, blob (chroma syntax highlighting),
503 raw, blame, commit history, individual commit views, branch/tag
504 listings, compare views, file finder.
505 - **Issues + PRs** — full CRUD; reviews; required-reviewer
506 enforcement; status-check gates; three merge methods.
507 - **Social** — stars, watches, forks, `/explore`, stargazer/
508 watcher lists.
509 - **Search** — code, repo, user, issue.
510 - **Notifications** — in-app inbox, email fan-out, one-click
511 unsubscribe.
512 - **Orgs + teams** — roles, invitations, one-level nesting,
513 max-of-sources policy.
514 - **Webhooks** — HMAC-signed delivery, exponential backoff,
515 auto-disable, SSRF defense, redelivery UI.
516 - **Observability** — structured logs, Prometheus metrics,
517 optional OTel tracing, Sentry-protocol error reporting.
518 - **Operations** — Ansible playbook, systemd units, Caddy edge,
519 WireGuard mesh for monitoring, Postgres WAL archive + daily
520 logical backups to Spaces, cross-region DR, restore drill.
521 - **Public landing page** on `/` for anonymous viewers; signed-in
522 viewers get a quick-link dashboard.
523 - **Lightweight status page** at `docs.<host>/status.html`.
524 - **Cutover artifacts** under `deploy/cutover/`.
525 - **Public docs site** built with mdBook.
526 - **Operator runbooks** for incidents, backups, restore, upgrade,
527 rollback, rotate-secrets, rotate-keys, regenerate-akc,
528 drain-workers, read-only-mode, day-one.
529 - **a11y tooling** (pa11y + axe) and **k6 load-test scenarios**.
530 - **THIRD_PARTY_NOTICES.md** with a CI-verified generator.
531
532 ### Known gaps at v0.1.0
533
534 - SSH git transport (HTTPS only)
535 - Actions / CI runner
536 - Packages, Releases, Pages, Projects, Gists
537 - GraphQL API (only a small REST surface today)
538 - Activity feed UI
539
540 These are all on the post-MVP roadmap.
541
542 [Unreleased]: https://shithub.sh/shithub/shithub/compare/v0.1.0...trunk
543 [0.1.0]: https://shithub.sh/shithub/shithub/releases/tag/v0.1.0