Files
cloud/tracker_seam.go
3baf677016 feat(sync/tracker/git): GitHub App → native tracker mirror + index-on-import + .hanzo/workflows orchestrator (#357)
The native side of the org-wide GitHub App sync. The push→reconcile path already
existed (githubWebhook → cloud.Sync); this adds the two missing halves the owner
asked for and layers /v1/code over every mirrored repo.

TRACKER MIRROR (issues → native tracker, the one way, no second store):
- tracker_seam.go: cloud.UpsertIssue inversion seam (twin of cloud.Sync) — feeders
  never import clients/tracker.
- clients/tracker/github_sink.go: registers the sink; idempotent-by-ExtRef upsert
  into one "GH" team (Source=git, Repo discriminator), open/closed→todo/done.
- clients/tracker/store.go: GetIssueByExtRef + (org,project,ext_ref) index.
- clients/integrations/github_webhook.go: `issues`/`issue_comment` → mirror the
  parent issue (same signed-installation→org resolution as push).
- clients/integrations/github_issues.go: mirror mapping + POST
  /v1/integrations/github/issues/backfill (bounded, idempotent, returns counts).

INDEX-ON-IMPORT (/v1/code covers EVERY mirrored repo, not just pushed-since-import):
- clients/git/index_on_import.go + ImportRepo/mirror: emit the SAME
  cloud.LifecyclePushLanded the push/inbound paths emit, so index_on_push indexes
  the default-branch tip on import too. Origin = source host → mirror_out suppresses
  the echo. Detached + best-effort (never blocks/fails the writer).

.hanzo/workflows NATIVE CI/CD (design + dormant MVP):
- clients/git/build_on_push.go: a lifecycle reactor that reads .hanzo/workflows/*.yml
  (or root hanzo.yml, same images: schema) at the pushed tip and enqueues each image
  to platform /v1/arcd/enqueue (BuildKit→operator), NO GitHub Actions. Ships DORMANT
  (no-op unless CLOUD_NATIVE_CICD_ENABLED + the enqueue token) so linking is inert.

Tests: tracker sink (idempotent upsert + status map + isolation), integrations
compile+run, git import emits push.landed, orchestrator parse/body/tag/owner map.
All green (CGO_ENABLED=0, pure-Go as prod ships).

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-26 12:30:54 -07:00

80 lines
3.6 KiB
Go

package cloud
import (
"context"
"errors"
)
// tracker_seam.go is the inversion layer between the native TRACKER (clients/tracker)
// and the surfaces that FEED it work items without importing it — today the GitHub
// App webhook + backfill (clients/integrations), tomorrow any provider that mirrors
// external issues into the one Hanzo work-item store. It is the SAME idiom as
// sync_seam.go's SyncFunc and git_import.go's GitImporter: the tracker registers its
// sink here at Mount; feeders call UpsertIssue with NO import of the tracker package,
// so nothing imports tracker except apps (which mounts it). One seam, one direction,
// no cycles.
// IssueUpsert is a provider-agnostic external work item mirrored into the native
// tracker, keyed idempotently by ExtRef so a webhook redelivery or a backfill re-run
// UPDATES the same row instead of duplicating it. Flat + string-typed so it crosses
// the feeder→tracker seam without importing the tracker's domain types.
//
// - Org the tenant (resolved from the signed installation, never a header).
// - Project the IAM project scope; "" ⇒ the org's default project store.
// - ProjectKey the tracker team the item files under (e.g. "GH"); ensured on first use.
// - ProjectName the display name for that team when it is first created (e.g. "GitHub").
// - Repo the git repo the item belongs to — the per-repo filter discriminator.
// - ExtRef the external anchor + idempotency key (e.g. "github:owner/repo#123").
// - Kind/Source what it IS / which surface opened it ("issue"|"pr" / "git").
// - State the upstream open/closed state; the tracker maps it to a board column.
// - Labels upstream label names (the tracker joins them for storage).
type IssueUpsert struct {
Org string
Project string
ProjectKey string
ProjectName string
Repo string
ExtRef string
Kind string
Source string
Title string
Description string
State string // "open" | "closed"
Assignee string
Labels []string
}
// IssueUpsertResult reports what the upsert did — Created (a new row) vs updated,
// plus the tracker identity, so a feeder can log/count precisely (the backfill count).
type IssueUpsertResult struct {
Created bool
Number int
Identifier string // KEY-<number>
}
// IssueSink is the tracker's upsert entry the sink registers at Mount; feeders reach
// it via UpsertIssue. A function, not a tracker noun — the one implementation
// (clients/tracker) registers it, and the feeders never see the store.
type IssueSink func(ctx context.Context, in IssueUpsert) (IssueUpsertResult, error)
// issueSink is the registered sink (nil until the tracker mounts). Read on the
// webhook path, written once at Mount (before serving), so a plain var suffices —
// the same discipline as syncFn / gitImporter.
var issueSink IssueSink
// RegisterIssueSink installs the tracker upsert sink. nil-safe.
func RegisterIssueSink(fn IssueSink) { issueSink = fn }
// ErrIssueSinkUnavailable is returned when the tracker is not mounted — fail-closed,
// never a silent success, so a feeder logs precisely rather than dropping the item.
var ErrIssueSinkUnavailable = errors.New("cloud: tracker issue sink not registered")
// UpsertIssue mirrors one external work item into the native tracker via the
// registered sink. Fails closed when the tracker is unmounted.
func UpsertIssue(ctx context.Context, in IssueUpsert) (IssueUpsertResult, error) {
if issueSink == nil {
return IssueUpsertResult{}, ErrIssueSinkUnavailable
}
return issueSink(ctx, in)
}