Shared stateless builds and deployments
Documentation version: 858adedd187db2e94614b1dfd389e4bf98e6e202:ac5d527caaa1f97eee9d71262e7c843ae7b24000e01ba5b44931108b2b489c0d
Status: step 2B complete for the stateless operator profile on 27 October 2026, with implementation, tests and cleanup recorded in the acceptance evidence. This is the operator/synthetic Linux profile. It does not advertise customer hosting or replace the current production dashboard release procedure.
Implemented boundary
One Go coordinator uses existing PostgreSQL operations, lease epochs, transactional
outbox and native pg-boss delivery. Native application and project queues remain separate within that engine; the committed receipt retains its queue owner. Framework and Git adapters do not choose scope
or own traffic. Registered management credentials require resource.manage for
mutations and resource.read for state/log reads. The server assigns product and
tenant from the credential, not request-body permissions.
A source deployment atomically records the release and deployment intent. It freezes exact repository ID, branch/SHA, required check publisher/result, binding revision, runtime profile digest and public build inputs. Duplicate requests keep one operation. An environment revision reserves its single deployment owner; competing requests fail before execution. Retried external effects use that same identity and a persisted environment fence.
GitHub is implemented with real Actions verification and immutable SHA archive acquisition. The HMAC push parser is implemented and tested. This rehearsal uses the authenticated management trigger after checks; no public webhook or automatic production push policy is activated. A future GitHub/Forgejo receiver must resolve an explicitly registered repository/environment policy and call this same source operation. It cannot supply trusted permissions or mutate a runtime directly.
modules/runtime runs production builds through native BuildKit and Docker. A
worker/cache namespace separates project, environment, trust and public inputs.
Build inputs include the lockfile, exact profile/toolchain and architecture.
Builds receive no provider/Git/metadata credential, Docker socket or host path.
The build runs as the image's non-root user. Runtime containers have a read-only
root, no capabilities, no-new-privileges, CPU/RAM/PID budgets, bounded local logs
and a private loopback port. The execution socket is private and never mounted
into builds/apps. Shared-kernel containers and the current network are not an
accepted hostile-customer sandbox. Step 4 must validate its stronger offered
execution/network profile before customer code is admitted.
Supported output: Next.js 16.4.3 standalone, React 19.3.1 and React/Vite 8.3.5 static output at repository root with an npm lockfile. Vite uses an owned static production server. Unsupported/ambiguous profiles fail explicitly. Private npm registries, monorepo dependency graphs, Server Actions/session coordination, customer secrets, stateful data and Dockerfile alternatives are later profiles.
Optional portable source intent is deliberately small:
{"version": 1, "framework": "auto"}The filename is astrogate.json; framework is auto, next or vite. Unknown
versions/fields or a mismatch with the pinned target fail validation. Omission
means automatic detection. This file cannot select tenant, credentials, domains,
Production or installation settings. Hostname, readiness path, authorized source,
compatibility and budgets are registered separately by the operator through
eu-infra --mode register-deployment --binding-file <reviewed-file> with explicit
product/tenant/project/environment. The HTTP caller cannot change this policy.
Management client
The unpublished local @astrogate/sdk/management export is server-only. Wire types
are generated from api/openapi.json; do not handwrite an alternative contract.
The database guide owns the implemented native db
browser/server/React subset. The hosting guide extends
these management methods with domain/workspace lifecycle and explicit startup.
Complete auth, storage, jobs and schedules namespaces retain their later
gates; the historical step 2B evidence does not establish them.
import {createManagement} from '@astrogate/sdk/management';
const infra = createManagement({
endpoint: installation.managementUrl,
tenant: registeredTenantId,
token: scopedManagementToken,
deadlineMs: transportBudgetMs,
maxAttempts: transportAttempts,
});
const before = await infra.environments.getDeploymentState(projectId, environmentId);
const accepted = await infra.deployments.fromSource(projectId, environmentId, {
revision: checkedCommitSha,
bindingRevision: before.bindingRevision,
expectedRevision: before.revision,
}, {idempotencyKey: logicalDeploymentKey, signal});
const operation = await infra.operations.get(accepted.operationId, signal);
const output = await infra.operations.logs(accepted.operationId, 'build', signal);Client cancellation stops waiting; it does not claim to cancel durable work. Transport retries keep the logical idempotency key and share a caller-selected deadline. Redirects cannot forward management credentials. Revision conflicts require a new state read and operator/client decision, not blind automatic retry.
For rollback use infra.deployments.create(projectId, environmentId, {releaseId: previousCompatibleRelease, expectedRevision}, {idempotencyKey}).
Readiness precedes the atomic route switch. Native request counters prevent old
container removal while requests drain. A drain timeout retains an actionable
incomplete operation rather than claiming all traffic finished. Immutable old
assets remain available for the retained release window, using Next deployment
identity where asset paths overlap. This is code rollback for compatible stateless
bindings, not database/data rollback. No host/reboot redundancy is claimed.
Storage, budgets and retention
PostgreSQL holds status, immutable references, source proof and recovery progress. Content-addressed protected filesystem storage holds actual image bytes. Another protected directory holds raw build/execution output. None of these paths is a customer-upload or infrastructure-global Loki authority. A second installation must transfer the corresponding protected artifacts/runtime inputs, not just SQL.
The explicit rehearsal profile is experimental capacity configuration for the approved 8 GiB/80 GB host, not a hidden global product maximum. Every registration freezes it; changing an execution profile while its build is pending fails rather than silently using new limits/toolchains. Retention-only policy changes can retain compatible code.
- The operator requested active plus five previous successful releases, seven days raw output and ninety days compact completed failure context. Active and pending/recovery references take precedence over count. Failed candidates are not counted as successful rollback releases.
- RAM admission accounts for native running container reservations plus a 1 GiB host reserve. A build reserves 4 GiB; each runtime slot 512 MiB. Two slots per environment allow blue/green overlap. Reservations are checked against actual physical capacity. These are rehearsal allocations, not sizing promises.
- The profile allocates 8 GiB artifacts per scope, 2 GiB per isolated cache worker, 512 MiB source and 16 MiB per raw output file. These consume explicit portions of the temporary 80 GB disk; the host always reserves 8 GiB free. Build/source, artifact, cache and logs are separately budgeted. Native BuildKit GC/prune and Docker log rotation own their physical cache/log policies. Capacity exhaustion is actionable and never deletes a running or required recovery artifact.
- The 15-minute build, 30-second readiness/drain and 20-minute operation wait are explicit observation/timeout budgets for the synthetic fixture. The API/queue lease is 10 seconds in the crash test; native retries wait at least that lease. They are configurable and must be remeasured for another workload/profile.
Retirement is committed in PostgreSQL before deletion. Repeated collection uses explicit retired digest tombstones and a durable collection acknowledgement. A stale global allowlist cannot delete a new publication. Native publication/collection locks and live manifests protect newer or shared content, even after an earlier metadata retirement decision. Hash/fsync/no-overwrite publication protects incomplete transfers. Full-quota replay may adopt an already stored object without duplicate storage. Raw-log expiry does not remove operation identity, source/artifact references or required recovery state. Compact error context expires after ninety days; minimal operation/idempotency and retirement identities persist to prevent replay. A failed or unavailable collector preserves state and is retried by pg-boss.
Recovery and operations
Build agent, coordinator, ingress and raw logs have distinct lifecycle ownership.
Use protected owned directories and private Unix sockets, a pinned native Docker
installation and systemd KillMode=control-group for the agent. The ingress is
modules/runtime/cmd/gateway; it reads durable routes/assets and continues while
the coordinator or agent is stopped. The agent reconciles stopped scoped workers,
stale source/upload staging and its own socket before accepting work. Do not run
two agents against one state root.
After an interrupted coordinator, pg-boss retries the same job and the database lease/fence rejects stale attempts. Checkpoints adopt the actual published build, prepared candidate or activated route. The gateway's independent request counts remain authoritative for draining. Loss of a response is not proof an effect did not occur.
Transient source/execution failure remains eligible for native retries. After the
native retry budget is exhausted, the job is failed and the operation is waiting
with dependency_unavailable. Unsafe candidate cleanup uses cleanup_required.
Neither is acknowledged as success. Inspect actual runtime/route state first,
restore the dependency or complete safe cleanup, then use the trusted worker
operator role and the observed epoch:
eu-infra --mode resume-operation --product <product> --tenant <tenant> \
--operation-id <waiting-operation> --expected-epoch <observed-epoch>Then retry that operation's existing pg-boss job using its supported retry
method and the job ID in infra_queue_receipts.deliveries. Do not insert another
receipt/job or edit native queue tables. A stale epoch/review is rejected.
Terminal compile/readiness/capacity/input errors remain explicit failures after
safe reconciliation; fix the cause before requesting a new logical deployment.
A rebuild requires source/toolchain access; starting or rolling back a retained compatible artifact does not require GitHub. Raw log unavailability alone does not make a retained artifact unusable. Cross-host recovery, distributed replicas, TLS/domain management and hostile-customer isolation still require their later profile-specific evidence.
Rehearsal tools and evidence
controllers/deployment owns the reproducible synthetic fixtures and runner. It uses real GitHub checks, PostgreSQL, pg-boss, the generated management client and native Linux build/runtime processes. Do not substitute mocks for transaction, lease, cache or isolation guarantees. The acceptance record links measurements, tested limits and exact resource cleanup. Step 3 remains separately gated.