-
Notifications
You must be signed in to change notification settings - Fork 1
Merging 11 PRs#1326
Merged
Merged
Conversation
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
This was referenced Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR merges the following changes:
Stack
Stackit-Stack-Size: 11
Stackit-PRs: 1313,1314,1315,1316,1317,1318,1319,1320,1322,1323,1324