Skip to content

Installer Release Flow

Use one public install URL in docs, demos, and release notes:

Terminal window
installer_file="$(mktemp)"
trap 'rm -f "$installer_file"' EXIT
curl --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:

Terminal window
cd docs
npm run sync-installer

That command copies:

../install.sh -> docs/public/installer.sh
../install.sh -> docs/public/install.sh

Keep /install.sh as a compatibility alias, but write new user-facing examples with /installer.sh.

The packaged binary entrypoint is:

installer/cmd/ostwin-installer/main.go

From a local checkout, compile it directly with:

Terminal window
cd installer
go build -o /tmp/ostwin-installer ./cmd/ostwin-installer

For 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.

The active installer workflow is .github/workflows/installer-release.yml.

Pull requests and main pushes validate the package:

Terminal window
cd installer
go mod verify
go test ./...
go build -o /tmp/ostwin-installer ./cmd/ostwin-installer
cd ..
bash -n install.sh
bash -n build.sh
goreleaser check
goreleaser release --snapshot --clean --skip=publish

Tag pushes publish the release:

Terminal window
git tag vX.Y.Z
git push origin vX.Y.Z

The tag path runs:

Terminal window
goreleaser release --clean

That publishes:

install.sh
checksums.txt
ostwin-installer_darwin_amd64.tar.gz
ostwin-installer_darwin_arm64.tar.gz
ostwin-installer_linux_amd64.tar.gz
ostwin-installer_linux_arm64.tar.gz
ostwin-installer_windows_amd64.tar.gz
ostwin-installer_windows_arm64.tar.gz

The 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.

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 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=300

For OSTwin, the consistency rule is:

  1. The repository root install.sh owns the public bootstrapper source.
  2. The docs build copies that file into Cloudflare Pages static assets.
  3. Cloudflare serves https://twin.igot.ai/installer.sh directly.
  4. 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.

The active Cloudflare Pages deployment workflow is:

.github/workflows/deploy-docs.yml

It 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_TOKEN
CLOUDFLARE_ACCOUNT_ID

The deploy command should remain:

Terminal window
cd docs
npm install --package-lock=false
npm run build
npx wrangler pages deploy dist --project-name=ostwin-docs

The 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.

Before tagging:

Terminal window
cd installer
go test ./...
go build -o /tmp/ostwin-installer ./cmd/ostwin-installer
cd ..
bash -n install.sh
bash -n .agents/install.sh
cd docs
npm install --package-lock=false
npm run build

After tagging:

  1. Confirm .github/workflows/installer-release.yml completed.
  2. Confirm the release includes install.sh, checksums.txt, and all six platform archives.
  3. Confirm the public URL resolves:
Terminal window
curl -I https://twin.igot.ai/installer.sh
installer_file="$(mktemp)"
trap 'rm -f "$installer_file"' EXIT
curl --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 --yes

If 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.