Installer Release Flow
Canonical install URL
Section titled “Canonical install URL”Use one public install URL in docs, demos, and release notes:
installer_file="$(mktemp)"trap 'rm -f "$installer_file"' EXITcurl --proto '=https' --tlsv1.2 -fsSL \ https://twin.igot.ai/installer.sh -o "$installer_file"chmod 0700 "$installer_file"# Inspect and verify the artifact before execution.export OSTWIN_INSTALLER_VERSION=v1.0.0 # Replace with the approved tag.bash "$installer_file"The canonical URL is mutable. Controlled deployments must instead pin a tagged release and verify its checksum and approved provenance against an independently obtained manifest before executing it.
The docs build copies the repository root bootstrapper into the Cloudflare Pages static assets:
cd docsnpm run sync-installerThat command copies:
../install.sh -> docs/public/installer.sh../install.sh -> docs/public/install.shKeep /install.sh as a compatibility alias, but write new user-facing examples with /installer.sh.
What gets compiled
Section titled “What gets compiled”The packaged binary entrypoint is:
installer/cmd/ostwin-installer/main.goFrom a local checkout, compile it directly with:
cd installergo build -o /tmp/ostwin-installer ./cmd/ostwin-installerFor release builds, .goreleaser.yml is the source of truth:
builds: - id: ostwin-installer dir: installer main: ./cmd/ostwin-installer binary: ostwin-installer env: - CGO_ENABLED=0 ldflags: - "-s -w" - "-X main.version={{ .Version }}" - "-X main.commit={{ .Commit }}" - "-X main.date={{ .Date }}" - "-X main.sourceRef={{ .Tag }}" - "-X main.sourceSHA256={{ .Env.SOURCE_SHA256 }}"GoReleaser builds darwin, linux, and windows archives for amd64 and arm64. The release workflow hashes the tagged source archive first; sourceRef and sourceSHA256 embed that immutable selection in every binary. Development builds have neither value and must use --source-dir or provide an explicit immutable ref and independently trusted digest.
GitHub release workflow
Section titled “GitHub release workflow”The active installer workflow is .github/workflows/installer-release.yml.
Pull requests and main pushes validate the package:
cd installergo mod verifygo test ./...go build -o /tmp/ostwin-installer ./cmd/ostwin-installercd ..bash -n install.shbash -n build.shgoreleaser checkgoreleaser release --snapshot --clean --skip=publishTag pushes publish the release:
git tag vX.Y.Zgit push origin vX.Y.ZThe tag path runs:
goreleaser release --cleanThat publishes:
install.shchecksums.txtostwin-installer_darwin_amd64.tar.gzostwin-installer_darwin_arm64.tar.gzostwin-installer_linux_amd64.tar.gzostwin-installer_linux_arm64.tar.gzostwin-installer_windows_amd64.tar.gzostwin-installer_windows_arm64.tar.gzThe release install.sh is the bootstrap script. It downloads the matching packaged ostwin-installer archive from GitHub Releases and requires a valid checksums.txt entry. The verified binary then downloads only its matching tagged source archive, checks the embedded SHA-256 digest, enforces compressed, extracted, member, and entry-count ceilings, and only then delegates to .agents/install.sh or .agents/install.ps1.
Production container dependency boundary
Section titled “Production container dependency boundary”The release container invokes .agents/install.sh --production-locked. This
mode requires dependencies to be provisioned before the installer starts and
uses uv sync --frozen --no-dev --check only as a no-write conformance check.
It refuses missing or drifted dependencies and never falls back to broad-range
uv pip install, alternate package indexes, or registry resolution.
Bun, OpenCode, and the CodeGraph platform bundle are installed from exact
architecture-specific archives whose SHA-256 or SHA-512 digests are committed
in the container provisioner. The release path does not execute npm as root.
Frontend and bot dependency graphs come only from committed Bun lockfiles with
lifecycle scripts disabled, and build-only node_modules, .next, and caches
are removed before the runtime layer is finalized.
Application code under /app, the managed virtual environment, and installed
.agents/dashboard trees remain root-owned and non-writable. Runtime state is
limited to /workspace and explicit state, log, memory, vault, and plan
directories. Container startup accepts declarative environment values but does
not source .env.sh shell hooks.
The base-image digest is pinned, but Debian apt package retrieval still uses
the distribution’s live signed repositories rather than a dated snapshot.
Release evidence must therefore record the final image digest and vulnerability
scan; bit-for-bit rebuilds additionally require an approved Debian snapshot
mirror and retention policy.
Cloudflare Pages consistency
Section titled “Cloudflare Pages consistency”Cloudflare Pages serves files from the docs build output. docs/scripts/sync-installer.mjs copies root install.sh into docs/public/installer.sh and docs/public/install.sh before astro build, so both files are emitted into docs/dist/.
Cloudflare Pages also reads docs/public/_headers during the docs build. The installer endpoints set explicit shell-script content type and a short cache:
/installer.sh Content-Type: text/x-shellscript; charset=utf-8 Cache-Control: public, max-age=300
/install.sh Content-Type: text/x-shellscript; charset=utf-8 Cache-Control: public, max-age=300For OSTwin, the consistency rule is:
- The repository root
install.showns the public bootstrapper source. - The docs build copies that file into Cloudflare Pages static assets.
- Cloudflare serves
https://twin.igot.ai/installer.shdirectly. - The installer itself downloads versioned release assets from GitHub, not from Cloudflare.
This keeps Cloudflare deployment independent from installer release publishing. A docs deploy can update the public bootstrapper copy or prose, but it does not need to rebuild installer binaries.
Docs deployment
Section titled “Docs deployment”The active Cloudflare Pages deployment workflow is:
.github/workflows/deploy-docs.ymlIt runs on main pushes that touch install.sh, docs/**, or the deploy workflow itself, and it can also be started with workflow_dispatch.
The workflow requires these repository secrets:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDThe deploy command should remain:
cd docsnpm install --package-lock=falsenpm run buildnpx wrangler pages deploy dist --project-name=ostwin-docsThe docs project already has wrangler as a dev dependency, and docs/wrangler.toml sets the Pages output directory to dist. The workflow uses npm install --package-lock=false because this repo does not currently include docs/package-lock.json; switch to npm ci only after adding and maintaining that lockfile.
Release checklist
Section titled “Release checklist”Before tagging:
cd installergo test ./...go build -o /tmp/ostwin-installer ./cmd/ostwin-installercd ..bash -n install.shbash -n .agents/install.shcd docsnpm install --package-lock=falsenpm run buildAfter tagging:
- Confirm
.github/workflows/installer-release.ymlcompleted. - Confirm the release includes
install.sh,checksums.txt, and all six platform archives. - Confirm the public URL resolves:
curl -I https://twin.igot.ai/installer.shinstaller_file="$(mktemp)"trap 'rm -f "$installer_file"' EXITcurl --proto '=https' --tlsv1.2 -fsSL \ https://twin.igot.ai/installer.sh -o "$installer_file"export OSTWIN_INSTALLER_VERSION="$GITHUB_REF_NAME"bash "$installer_file" --dry-run --yesIf the public URL returns 404, deploy the docs site or verify that the Cloudflare Pages custom domain is attached to the project that contains docs/public/installer.sh in its build output.
If the GitHub release URL returns 404, the selected release does not have
installer assets. Tag a new release after
.github/workflows/installer-release.yml is green. The bootstrapper deliberately
refuses to fall back to unsigned source content.
