projects

package
v1.801.242 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package projects is the Hanzo Cloud projects control plane: the ONE org-scoped store of buildable/deployable sites, shared by every surface that shows a user's projects.

Why it exists: hanzo.app (the builder) and console.hanzo.ai (the Projects module) must show the SAME projects for the same org. They do, because both call this one /v1/projects surface through the gateway, which mints the org (X-Org-Id) from the validated IAM JWT (HIP-0111). There is no second copy of project state anywhere — this SQLite-backed store is the source of truth; the builder keeps only per-project working state (chat, draft files) in Hanzo Base.

Surface (all org-scoped; see CONTRACT.md — the published shape console consumes):

POST   /v1/projects                      create
GET    /v1/projects                      list (org)
GET    /v1/projects/:slug                get
PATCH  /v1/projects/:slug                update
DELETE /v1/projects/:slug                delete (+ purge S3 site)
POST   /v1/projects/:slug/deploy         deploy (tar body | git json)
POST   /v1/projects/:slug/purge          purge the edge cache-tag (no redeploy)
GET    /v1/projects/:slug/deployments    deploy history
GET    /v1/projects/:slug/deployments/:id one deployment
POST   /v1/projects/:slug/deployments/:id/complete  CI completion hook

Sites surface (the surface-agnostic deploy_site capability, shared with agents):

POST   /v1/sites                         generate a responsive site from a brief + deploy
POST   /v1/sites/deploy                  deploy a raw file manifest (the deploy_site tool)
GET    /v1/sites                         list the org's live sites

Releases (the server-side promote — see release.go; mirrored under /v1/platform/sites/:slug/…):

POST   /v1/sites/:slug/publish                      promote a build output + go live
POST   /v1/sites/:slug/releases                     promote only (no flip)
GET    /v1/sites/:slug/releases                     rollback menu, newest first
POST   /v1/sites/:slug/releases/:release/activate   flip the pointer (go live / roll back)

Deploy pipeline: a deploy uploads the built static site to OUR S3 (CLOUD_PROJECTS_BUCKET on s3.hanzo.ai) under "<org>/<slug>/", marks the bucket public-read, and records a live URL. The hanzoai/static container (the static-app image) serves the same bucket behind the gateway for a pretty host; GitHub export is an optional second step that never blocks going live.

release.go — how content GETS to a site's serving prefix.

The agentic builder produces build output INSIDE our object store (its code execution writes there). Pushing those bytes back out through an HTTP upload API and in again would be pure waste, so publishing is a SERVER-SIDE PROMOTE: no bytes traverse the API, and no client ever holds an S3 credential.

The model is values plus a pointer (HIP-0014, "Static Sites: Releases and Pointers"): a build output is promoted into a Release, and the site's pointer is flipped to it.

  • A Release is a VALUE: an immutable prefix whose id is a digest of the object manifest it was built from. Identical bytes ⇒ identical id, so re-publishing the same build is idempotent by construction rather than by a remembered request key. Different bytes can never reuse an id.
  • The pointer is projects.current_release. Serving reads THROUGH it (siteResolver.Resolve), so activation is one atomic UPDATE and rollback is the same flip aimed at an older release — free, because releases are immutable and retained.

TENANT ISOLATION — the make-or-break property, and the reason the source is NOT an S3 URL. A caller names a path RELATIVE to their own org's storage space; the org segment is prepended server-side from the VALIDATED principal (org(c), the one org rule this package already uses for every site prefix), and the bucket is server-owned and never appears in the request at all. So the worst a hostile source string can address is something the caller's own org already owns — "copy an arbitrary prefix" is not a reachable state, and the server-side copy is therefore not an exfiltration primitive. Traversal is killed by safeRel, the SAME rooted-clean rule the artifact walker uses.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires the projects surface onto app per HIP-0106. Complex flavour: it keeps a package global (mounted) for Shutdown and registers cross-package resolvers, so it constructs the Service value directly rather than through cloud.Mount.

func SetDeployObserver added in v1.801.30

func SetDeployObserver(o DeployObserver)

SetDeployObserver registers the package-level deploy observer (nil clears it). It is safe for concurrent use; the last writer wins. Wiring is intentionally package-level (not per-service) because there is one mounted projects surface per binary and the sessions lane wires the observer at startup.

func Shutdown

func Shutdown() error

Shutdown closes the projects store. Idempotent. Mirrors the provisioning Shutdown contract so the serve layer releases subsystem resources uniformly.

Types

type DeployObserver added in v1.801.30

type DeployObserver interface {
	OnDeploy(ctx context.Context, org, slug, url, deploymentID string)
}

DeployObserver receives one notification whenever a site goes live. It is the seam the (separately-landed) agent-sessions lane hooks to record a "site deployed + URL" session event, WITHOUT projects taking any hard dependency on that lane: projects only ever calls this interface. The default is nil — a no-op — so a deployment that never registers an observer is unaffected.

type Deployment

type Deployment struct {
	ID        string
	ProjectID string
	Org       string
	Version   int
	Status    string
	Source    string
	Commit    string
	LiveURL   string
	Bucket    string
	Prefix    string
	Files     int
	Bytes     int64
	Message   string
	CreatedAt int64
	UpdatedAt int64
}

Deployment is one deploy attempt for a project, versioned monotonically per project. A deploy moves through queued→building→uploading→live (or →error); the upload path (tar body) lands directly in "live", the git/CI path starts "queued" and is flipped by the CI completion call.

type Project

type Project struct {
	ID            string
	Org           string
	Slug          string
	Name          string
	Description   string
	RepoURL       string
	RepoBranch    string
	RepoProvider  string
	Framework     string
	Status        string
	LiveURL       string
	Bucket        string
	CurrentDeploy string
	// CurrentRelease is the site's serving POINTER — the id of the immutable
	// Release whose prefix the site edge reads. Empty means "serve the legacy
	// mutable <org>/<slug>/ prefix" (every site published before releases, and
	// every site whose last go-live was a full-artifact deploy). Flipping this one
	// field is the whole of activation and rollback.
	CurrentRelease string
	// CacheControl is the per-project override for the HTML/document Cache-Control
	// header applied to deployed objects (and honored by the site server). Empty =
	// the honest default (public, max-age=60, s-maxage=86400). Content-hashed
	// assets are always immutable regardless of this override.
	CacheControl string
	// LastPurgeAt is the unix time of the last successful (or attempted) Cloudflare
	// edge purge for this site. Surfaced on the API so a console can show cache freshness.
	LastPurgeAt int64
	CreatedAt   int64
	UpdatedAt   int64
	// Analytics is the per-project web-analytics flag, wired ON by default: a
	// freshly created project collects analytics unless the caller opts out
	// (analytics:false at create). It is the source of truth the app's
	// static-builder reads as deployment.analytics, so the beacon is injected with
	// no opt-in. Mutable via update (read-modify-write); immutable columns are
	// org/slug/id/created_at.
	Analytics bool
	// SpaceId is the project's Base data space — the "<org>/<slug>" namespace under
	// which its deployed site's form/forum/data submissions live in Hanzo Base
	// (/v1/base). Set once at create (the app's namespace/repoId convention);
	// immutable thereafter. A Base space is provisioned best-effort at create.
	SpaceId string
}

Project is the org-scoped, canonical record of a buildable/deployable site. It is the SAME record whether read from hanzo.app (the builder) or console.hanzo.ai (the Projects module): org isolation is the org column, enforced at the query layer, and the gateway-minted X-Org-Id selects the org. Repo fields are flat columns here; the HTTP surface nests them under "repo" (see projects.go). It never stores a secret.

Distinct from tracker.Project (not a duplicate): this is the Slug-keyed deployable site; a tracker.Project is a KEY-prefixed issue team that lives INSIDE one of these (the IAM/deploy project is the tracker's tenant boundary).

type Release added in v1.801.186

type Release struct {
	ID     string
	Org    string
	Slug   string
	Prefix string
	// Source is the org-relative build-output prefix this release was promoted
	// from, kept for provenance. It is never re-read to serve.
	Source    string
	Objects   int
	Bytes     int64
	CreatedAt int64
}

Release is an IMMUTABLE, content-addressed snapshot of a site's bytes at one S3 prefix. It is a VALUE, not an event: its ID is a digest of the object manifest it was built from, so publishing identical content twice yields the SAME release (idempotence for free) and different content can never reuse an ID. Nothing mutates a release after PutRelease — a rollback is a pointer flip to an older one, never a rewrite. (Distinct from Deployment, which is the EVENT log of deploy attempts: attempts fail and are still recorded; releases only exist once their bytes are fully copied.)

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the projects metadata database. ONE SQLite file ({DataDir}/projects.db) holds every org's records; org-scoping is the org column. MaxOpenConns(1) serializes writes against the file lock without busy retries.

func (*Store) ActivateRelease added in v1.801.186

func (s *Store) ActivateRelease(ctx context.Context, org, slug, id string, now int64) error

ActivateRelease flips a site's serving pointer to a release. This is the whole of activation, and it is ATOMIC in the strongest available sense: ONE statement whose WHERE clause both scopes the project to the tenant AND requires a matching release row to exist in the same tenant. There is no read-then-write window in which the pointer could name a release that was never fully copied, and two concurrent activations serialize into one winner (never a blend) — SQLite runs them one at a time on the single write connection.

A release row exists only after every object was copied (PutRelease), so "pointer set" implies "content complete" by construction. n==0 means the project or the release does not exist FOR THIS TENANT; the caller renders the same 404 for both, so a foreign id yields no signal. Re-activating the already-active release matches a row and succeeds — activation is idempotent.

func (*Store) BindHost

func (s *Store) BindHost(ctx context.Context, host, org, slug string, now int64) error

BindHost claims the global public host for (org, slug), first-come. It is idempotent for the SAME owner (a re-deploy just refreshes updated_at). If host is already bound to a DIFFERENT project it returns errHostTaken WITHOUT overwriting — a losing bind must never hijack another org's live subdomain.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) CreateProject

func (s *Store) CreateProject(ctx context.Context, p Project) error

CreateProject inserts one project. A UNIQUE(org,slug) violation surfaces as errConflict.

func (*Store) DeleteProject

func (s *Store) DeleteProject(ctx context.Context, org, slug string) (Project, bool, error)

DeleteProject removes a project and all its deployment rows. Reports whether a project row was deleted.

func (*Store) DeleteReleases added in v1.801.186

func (s *Store) DeleteReleases(ctx context.Context, org, slug string) error

DeleteReleases drops every release row for a site. Called on project delete, alongside the purge of the release object space.

func (*Store) GetDeployment

func (s *Store) GetDeployment(ctx context.Context, org, projectID, id string) (Deployment, error)

GetDeployment returns one deployment scoped to (org, project, id).

func (*Store) GetProject

func (s *Store) GetProject(ctx context.Context, org, slug string) (Project, error)

GetProject returns the project for (org,slug) or errNotFound.

func (*Store) GetRelease added in v1.801.186

func (s *Store) GetRelease(ctx context.Context, org, slug, id string) (Release, error)

GetRelease returns one release scoped to (org, slug, id), or errNotFound. The org is part of the key, not a filter applied afterwards, so a foreign release id is indistinguishable from a nonexistent one — no existence oracle.

func (*Store) InsertDeployment

func (s *Store) InsertDeployment(ctx context.Context, d Deployment) error

InsertDeployment writes one deployment row.

func (*Store) ListDeployments

func (s *Store) ListDeployments(ctx context.Context, org, projectID string) ([]Deployment, error)

ListDeployments returns deployments for a project, newest version first.

func (*Store) ListHostsForProject added in v1.786.165

func (s *Store) ListHostsForProject(ctx context.Context, org, slug string) ([]string, error)

ListHostsForProject returns every public host bound to (org, slug), oldest first. It powers GET .../domains so a console/user can see which hostnames the site serves — its `<slug>.hanzo.app` subdomain plus any bound custom domains.

func (*Store) ListProjects

func (s *Store) ListProjects(ctx context.Context, org string) ([]Project, error)

ListProjects returns every project for org, most-recently-updated first.

func (*Store) ListReleases added in v1.801.186

func (s *Store) ListReleases(ctx context.Context, org, slug string, limit int) ([]Release, error)

ListReleases returns a site's releases, newest first — the rollback menu.

func (*Store) MarkLive added in v1.801.186

func (s *Store) MarkLive(ctx context.Context, org, slug, liveURL, bucket string, lastPurgeAt, now int64) error

MarkLive refreshes ONLY the denormalized go-live display fields of a site. It deliberately does NOT touch current_release: ActivateRelease is the SOLE writer of the serving pointer on the activate path, so a slow activation can never lose a race to a newer one and leave the pointer disagreeing with the last atomic flip. (The two full-artifact go-live paths clear the pointer through UpdateProject — that is their intent, not a side effect.)

func (*Store) NextVersion

func (s *Store) NextVersion(ctx context.Context, projectID string) (int, error)

NextVersion returns the next monotonic deploy version for a project (1-based).

func (*Store) ProjectOwnership

func (s *Store) ProjectOwnership(ctx context.Context, org, idOrSlug string) (mine, other bool, err error)

ProjectOwnership reports whether a project addressed by id-or-slug is owned by org (mine) and/or by some OTHER org (other) — the cross-org impersonation signal the identity trust boundary (cloud.SanitizeIdentity) uses to refuse a forged X-Project-Id. Matched by BOTH slug and id so the check holds whichever addressing the caller used, in one indexed round trip. Org isolation is the org column, exactly as everywhere else in this store.

func (*Store) PutRelease added in v1.801.186

func (s *Store) PutRelease(ctx context.Context, r Release) error

PutRelease records a fully-copied release. It is INSERT-only and idempotent on the content address: re-publishing identical bytes hits the (org,slug,id) PK and is a no-op rather than a conflict, because the row already describes exactly those bytes at exactly that prefix. Call it ONLY after every object has landed — the existence of the row is the promise that the prefix is complete, and ActivateRelease will not flip to a release that has no row.

func (*Store) ResolveHost

func (s *Store) ResolveHost(ctx context.Context, host string) (Project, error)

ResolveHost returns the project a public host is bound to, joining the global site_hosts binding to the org-scoped project. This is the authoritative slug→project resolution the site server uses; the org and bucket come ONLY from here, never from the request. Missing binding OR missing project ⇒ errNotFound.

func (*Store) ResolveOrgLiveSlug added in v1.801.186

func (s *Store) ResolveOrgLiveSlug(ctx context.Context, org, slug string) (Project, error)

ResolveOrgLiveSlug resolves a bare slug PINNED to a specific org — the LIVE project that org owns with that slug. Unlike ResolveUniqueLiveSlug (unique across ALL orgs), this can never return another org's project, so a first-party site host (cd.hanzo.ai → org "hanzo", slug "cd") is served ONLY by OUR project, never shadowed by a customer who named their project "cd". (org,slug) is unique in the store, so LIMIT 1.

func (*Store) ResolveUniqueLiveSlug added in v1.801.113

func (s *Store) ResolveUniqueLiveSlug(ctx context.Context, slug string) (Project, error)

ResolveUniqueLiveSlug resolves a BARE subdomain slug (`<slug>.hanzo.app`) to the single LIVE project owning that slug across all orgs. Slugs are only org-unique, so the bare host is servable ONLY when unambiguous: zero or 2+ live owners ⇒ errNotFound (each project still serves at its org-scoped host and its S3 URL). LIMIT 2 — a second row is the whole ambiguity signal; we never enumerate. This is what keeps pre-binding publishes servable with no backfill migration.

func (*Store) UnbindHost

func (s *Store) UnbindHost(ctx context.Context, host, org, slug string) error

UnbindHost releases a host binding, but only the row owned by (org, slug) — so deleting project A can never drop project B's host even if they somehow shared a row (they cannot, but the scoping makes the guarantee explicit).

func (*Store) UpdateDeployment

func (s *Store) UpdateDeployment(ctx context.Context, d Deployment) error

UpdateDeployment overwrites the mutable fields of a deployment (status flow).

func (*Store) UpdateProject

func (s *Store) UpdateProject(ctx context.Context, p Project) error

UpdateProject overwrites the mutable fields of an existing project. The caller reads-modifies-writes the whole Project; org+slug+id+created_at are immutable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL