Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Merging 11 PRs#1326

Merged
jonnii merged 12 commits into
main from
stack-merge-stack-1782266218
Jun 24, 2026
Merged

Merging 11 PRs #1326
jonnii merged 12 commits into
main from
stack-merge-stack-1782266218

Conversation

@jonnii

@jonnii jonnii commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

This PR merges the following changes:

  1. refactor(web): remove sample alice/bob stacks from repo provider #1313 refactor(web): remove sample alice/bob stacks from repo provider
  2. feat(server): drive deploy posture from STACKIT_ENV, not $PORT #1314 feat(server): drive deploy posture from STACKIT_ENV, not $PORT
  3. refactor(api): extract Syncer.SyncRepo for per-repo sync triggers #1315 refactor(api): extract Syncer.SyncRepo for per-repo sync triggers
  4. feat(api): add GitHub webhook signature + push parsing helper #1316 feat(api): add GitHub webhook signature + push parsing helper
  5. feat(api): add GitHub webhook receiver for evented refresh #1317 feat(api): add GitHub webhook receiver for evented refresh
  6. feat(api): coalesce webhook-driven syncs per repo #1318 feat(api): coalesce webhook-driven syncs per repo
  7. feat(api): add manual sync endpoint for on-demand refresh #1319 feat(api): add manual sync endpoint for on-demand refresh
  8. feat(server): default sync interval to 5m and document evented refresh #1320 feat(server): default sync interval to 5m and document evented refresh
  9. feat(server): index repos by owner/repo for GitHub-style routes #1322 feat(server): index repos by owner/repo for GitHub-style routes
  10. feat(server): serve repos at /repos/{owner}/{repo} path routes #1323 feat(server): serve repos at /repos/{owner}/{repo} path routes
  11. feat(web): GitHub-style path URLs for repos, branches, stacks, PRs #1324 feat(web): GitHub-style path URLs for repos, branches, stacks, PRs

Stack

main
 └─ jonnii/20260623021900/remove-sample-alice/bob-stacks-from-repo-provider (#1313)
 └─ jonnii/20260623024214/drive-deploy-posture-from-STACKIT_ENV-not-PORT (#1314)
 └─ jonnii/20260623024716/extract-Syncer.SyncRepo-for-per-repo-sync-triggers (#1315)
 └─ jonnii/20260623024903/add-GitHub-webhook-signature-push-parsing-helper (#1316)
 └─ jonnii/20260623025415/add-GitHub-webhook-receiver-for-evented-refresh (#1317)
 └─ jonnii/20260623025733/coalesce-webhook-driven-syncs-per-repo (#1318)
 └─ jonnii/20260623030002/add-manual-sync-endpoint-for-on-demand-refresh (#1319)
 └─ jonnii/20260623030405/default-sync-interval-to-5m-and-document-evented (#1320)
 └─ jonnii/20260623111304/index-repos-by-owner/repo-for-GitHub-style-routes (#1322)
 └─ jonnii/20260623120406/serve-repos-at-/repos/-owner-/-repo-path-routes (#1323)
 └─ jonnii/20260623122911/GitHub-style-path-URLs-for-repos-branches (#1324)

Stackit-Stack-Size: 11
Stackit-PRs: 1313,1314,1315,1316,1317,1318,1319,1320,1322,1323,1324

jonnii added 12 commits June 22, 2026 22:19
The server inferred "public mode" (bind 0.0.0.0, JSON logs, Secure
cookies, required auth) from $PORT or STACKIT_PUBLIC being set. That
overloaded $PORT and collided with dev shells (cmux, Heroku toolbelt)
that export $PORT generically, silently flipping a local server into a
public, auth-gated one — the "Couldn't reach the API" failure locally.
Introduce STACKIT_ENV (local default | production) as the single source
of truth for posture. $PORT is demoted to a listen-port hint honored
only in production, so a stray $PORT can't move the local listener.
The auth requirement now keys off actual exposure — a non-loopback bind
must have auth configured or -read-only, and -auth-disabled is refused
there — rather than the env name. This makes the safety guarantee
independent of how the server got exposed and fixes a latent gap where a
read-only public server with no OAuth env couldn't boot.
BREAKING: hosted deploys must set STACKIT_ENV=production (Railway/Fly/
etc.); STACKIT_PUBLIC is removed. Docs and .env.template updated.
Pull the per-repo unit of work out of the interval loop so a future webhook
receiver can trigger a refresh for a single repo by its GitHub coordinates,
not just the ticker iterating over managed entries.
- Registry.FindManaged(owner, name) resolves a managed mirror case-insensitively;
 it deliberately never returns the unmanaged -cwd working repo, which must not
 be mirror-fetched (mirror-fetch detaches HEAD).
- Syncer.syncEntry now returns an error instead of logging-and-swallowing;
 syncOnce logs failures, and the new SyncRepo(owner, name) wrapper resolves the
 entry and delegates, returning ErrRepoNotManaged for un-onboarded repos.
Pure refactor: the interval loop behaves identically. Sets up the webhook-driven
refresh stack.
Pure, transport-free building blocks for the webhook receiver, kept out of the
HTTP handler so they unit-test without a server:
- Verify(secret, body, sigHeader) checks the X-Hub-Signature-256 HMAC-SHA256
 with a constant-time compare, and fails closed on an empty secret/header so a
 misconfigured receiver never accepts unsigned payloads.
- ParsePush(body) extracts owner/name from a push payload, preferring
 owner.login + repository.name and falling back to splitting full_name.
No routing yet; the receiver wires these in the next change.
POST /api/v1/webhooks/github turns a verified push into an immediate refresh of
the matching managed checkout, so server state tracks GitHub without waiting for
the next interval tick. The interval loop stays as a backstop.
- The route bypasses the session/CSRF gate (GitHub carries neither) but stays
 rate-limited, mirroring how /config is mounted on the public mux. It is
 authenticated solely by the X-Hub-Signature-256 HMAC and fails closed (404)
 when STACKIT_GITHUB_WEBHOOK_SECRET is unset, so it is never an open trigger —
 the correct posture for local/dev servers GitHub cannot reach.
- The handler acks immediately (202) and runs the mirror-fetch in the
 background, since a fetch can outlast GitHub's delivery timeout. Coalescing
 bursts is a follow-up.
- The shared *reposync.Syncer is now built once in NewServer (guarding the
 typed-nil provider trap) and drives both the interval loop and the webhook,
 so on-demand refresh works even with the loop disabled. handlers depends on it
 only through a narrow RepoSyncer interface.
The webhook receiver previously spawned a goroutine and a git fetch per
delivery. A burst of pushes for one repo (or a rapid-fire series) would fan out
into concurrent fetches of the same checkout.
reposync.Coalescer fronts the syncer: at most one sync runs per repo at a time,
and any triggers that arrive while one is in flight collapse into a single
follow-up run. This bounds live goroutines to the number of distinct repos
seeing traffic and keeps git subprocesses in check.
The receiver now hands off through a narrow RepoSyncTrigger interface
(Trigger(owner, name)) and acks immediately, so it never blocks on a fetch and
owns no concurrency itself. The shared coalescer is built once in NewServer.
POST /api/v1/repos/{repoID}/sync forces an immediate refresh of one repo. It is
the manual complement to the webhook and interval refreshes, and the primary way
to pull remote changes on a local server that GitHub cannot reach with a webhook
(webhooks are server-mode only; locally the fsnotify watcher covers local edits
and this covers on-demand remote pulls).
The refresh path is safety-aware:
- Managed mirror: synchronous mirror-fetch + rebuild (detached HEAD, safe).
- Local -cwd working repo: re-read on-disk refs only, never mirror-fetch — that
 would detach the operator working tree HEAD (safety-invariants.md).
The route is session-gated like submit and refused on a public read-only server,
so it is never an anonymous fetch trigger; on a local auth-disabled server it
stays reachable.
The sync loop was opt-in: STACKIT_SYNC_INTERVAL unset meant 0, which disabled it
entirely, so a server with no explicit interval never pulled remote changes. With
webhooks now able to drive immediate refreshes, the interval becomes the
backstop — so it should exist by default.
- envSyncInterval defaults to 5m when STACKIT_SYNC_INTERVAL is unset; "0" still
 explicitly disables, and an unparseable value falls back to the default rather
 than silently disabling the loop.
- docs/deploy.md: explain the three refresh triggers (interval, webhook, manual),
 document webhook setup (payload URL, secret, push event), the manual-sync
 endpoint, and how local servers stay current without webhooks (fsnotify
 watcher + manual sync). Calls out that GitHub sends no push event for
 refs/stackit/metadata/*, so the interval loop must stay on as a backstop.
Add an owner/repo secondary index to the registry (GetByOwnerRepo,
case-insensitive, with duplicate detection) and surface the GitHub
coordinates on RepoSummary so clients can build /{owner}/{repo} URLs.
provision.BuildEntry derives owner/name from the git remote when not
passed explicitly, so the local single-repo "default" checkout is
reachable by owner/repo like onboarded mirrors. Additive groundwork for
moving the API and web URLs from query params to path params.
Replace the {repoID} and unscoped API routes with GitHub-style
/api/v1/repos/{owner}/{repo}/... routes, resolved case-insensitively
through the registry's owner/repo index. Branch-diff moves from a
?branch= query to a /branch-diff/{name...} path segment: branch names
contain slashes and Go's mux requires a trailing wildcard to be the
final segment, so .../branches/{name}/diff is not expressible.
Rewrite the OpenAPI spec to the scoped shape (it had drifted to the old
unscoped paths) and document the submit, sync, and events routes. Drops
the legacy default-fallback resolution. The web client moves to these
routes in the next change.
Move the web app off ?repo=/?branch= query params to path-based URLs that
mirror GitHub:
 /{owner}/{repo} repo home
 /{owner}/{repo}/tree/{branch} a branch
 /{owner}/{repo}/stack/{rootBranch} a whole stack
 /{owner}/{repo}/pull/{number} a PR (resolved client-side to its branch)
The single static page becomes an optional catch-all ([[...slug]]) that
renders one SPA shell; the Go server already serves index.html for deep
links on refresh. New lib/repo-route.ts is the single parser/builder for
these paths, encoding branch/stack names per-segment so slashes stay
separators. The API client and SSE hook now take a RepoRef ({owner,
repo}) and call the /repos/{owner}/{repo}/... routes; RepoProvider takes
owner/repo props and exposes repoRef via useRepo().
Stackit-Stack-Size: 11
Stackit-PRs: 1313,1314,1315,1316,1317,1318,1319,1320,1322,1323,1324
@jonnii jonnii merged commit f0d337d into main Jun 24, 2026
6 checks passed
@jonnii jonnii deleted the stack-merge-stack-1782266218 branch June 24, 2026 02:00
This was referenced Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /