Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
165 commits
Select commit Hold shift + click to select a range
a641aae
test: cover hosted user onboarding
lawrencecchen Jul 31, 2026
e702574
feat: onboard hosted Subrouter users with Stack Auth
lawrencecchen Jul 31, 2026
b0c50d0
test: cover onboarding review regressions
lawrencecchen Jul 31, 2026
be727eb
fix: harden hosted onboarding state transitions
lawrencecchen Jul 31, 2026
e9d12a3
test: isolate hosted health fixture
lawrencecchen Jul 31, 2026
6fe3c5b
test: cover unavailable hosted login configuration
lawrencecchen Jul 31, 2026
4f19a15
fix: explain unavailable hosted login
lawrencecchen Jul 31, 2026
2ffa6e3
test: cover resumable Codex provider auth
lawrencecchen Jul 31, 2026
266f41f
fix: keep Codex remote auth across resumes
lawrencecchen Jul 31, 2026
8b87e0b
test: prohibit SSH credential uploads
lawrencecchen Jul 31, 2026
674d6b9
test: detect unusable GCP account pools
lawrencecchen Jul 31, 2026
b571a76
test: close account import security gaps
lawrencecchen Jul 31, 2026
40140d8
test: make coalescing overlap deterministic
lawrencecchen Jul 31, 2026
de8d1c6
test: allow loaded hosts to run GCS fakes
lawrencecchen Jul 31, 2026
1767efd
test: keep user servers free of SSH services
lawrencecchen Jul 31, 2026
9442fd1
fix: upload server accounts over protected HTTP
lawrencecchen Jul 31, 2026
aa38ab3
test: cover concurrent account import safety
lawrencecchen Jul 31, 2026
1643d2b
fix: serialize protected account imports
lawrencecchen Jul 31, 2026
d8cc1c5
test: serialize server control token updates
lawrencecchen Jul 31, 2026
1104d01
fix: serialize server control token updates
lawrencecchen Jul 31, 2026
dcd1539
test: prevent Claude credential rollback
lawrencecchen Jul 31, 2026
35e6364
fix: serialize Claude credential rotation
lawrencecchen Jul 31, 2026
0cce2fa
test: close account import state races
lawrencecchen Jul 31, 2026
b215520
fix: make account state mutations atomic
lawrencecchen Jul 31, 2026
aecabe6
test: pin account lifecycle ownership
lawrencecchen Jul 31, 2026
941d89b
fix: own account state across worker lifecycles
lawrencecchen Jul 31, 2026
d6a4c17
test: close final account ownership gaps
lawrencecchen Jul 31, 2026
81b268c
fix: make account snapshots transactionally coherent
lawrencecchen Jul 31, 2026
2c6cd8e
test: enforce scoped proxy authority
lawrencecchen Jul 31, 2026
c53efbb
fix: separate import and proxy authority
lawrencecchen Jul 31, 2026
20a467b
test: bound websocket proxy resources
lawrencecchen Jul 31, 2026
ceb9508
fix: bound websocket proxy resources
lawrencecchen Jul 31, 2026
bb35253
test: bound supervised worker retirement
lawrencecchen Jul 31, 2026
cbc8313
fix: bound supervised worker retirement
lawrencecchen Jul 31, 2026
fd8d749
test: define hardened docker deployments
lawrencecchen Jul 31, 2026
e0c5c65
feat: add hardened docker deployments
lawrencecchen Jul 31, 2026
925823e
test: reserve container memory headroom
lawrencecchen Jul 31, 2026
d137181
fix: reserve container memory headroom
lawrencecchen Jul 31, 2026
f31ed1d
test: require live Codex GCP deploy gate
lawrencecchen Aug 1, 2026
b98a697
feat: deploy GCP through live Codex gate
lawrencecchen Aug 1, 2026
f4be212
test: pin GCP live upgrade evidence
lawrencecchen Aug 1, 2026
7b0c456
test: reproduce Docker team config mismatch
lawrencecchen Aug 1, 2026
bacf8d2
fix: make Docker team config portable
lawrencecchen Aug 1, 2026
ddf8dd0
feat: verify GCP deploys with live transports
lawrencecchen Aug 1, 2026
2768b55
Merge hosted Stack onboarding into GCP deploy
lawrencecchen Aug 1, 2026
f9b8a7a
test: cover GCP leases and local egress mode
lawrencecchen Jul 31, 2026
1ed2047
feat: preserve hosted and local-egress modes
lawrencecchen Aug 1, 2026
103cce5
test: require public GCP routing and read-only listing
lawrencecchen Aug 1, 2026
08cc598
test: require HTTPS upgrade observation
lawrencecchen Aug 1, 2026
a4dc5ee
fix: route GCP dogfood through public endpoints
lawrencecchen Aug 1, 2026
79893e1
test: allow end-user login against staging
lawrencecchen Aug 1, 2026
34106c9
feat: support public staging login
lawrencecchen Aug 1, 2026
d0bc537
Merge remote-tracking branch 'origin/main' into fix/server-login-http…
lawrencecchen Aug 1, 2026
d0fe9dc
test: exercise Docker profiles under memory cap
lawrencecchen Aug 1, 2026
0cac3b8
test: fix Docker API key fixture
lawrencecchen Aug 1, 2026
f1dd3e4
test: wait for supervisor generation changes
lawrencecchen Aug 1, 2026
2e98c6c
fix: await supervisor generation activation
lawrencecchen Aug 1, 2026
ae37e5d
test: keep tenant account reads scoped
lawrencecchen Aug 1, 2026
6d299a3
fix: authorize tenant account reads
lawrencecchen Aug 1, 2026
0fc3950
test: preserve observer upstream routing
lawrencecchen Aug 1, 2026
4306194
fix: preserve observer upstream routing
lawrencecchen Aug 1, 2026
6ee5863
test: cover deployment hardening failures
lawrencecchen Aug 3, 2026
c7de656
test: preserve runtime continuity across upgrades
lawrencecchen Aug 3, 2026
b29140e
test: expose credential integrity gaps
lawrencecchen Aug 3, 2026
78d338a
fix: preserve credential integrity under contention
lawrencecchen Aug 3, 2026
c1addc2
test: cover Claude routing and removal rollback
lawrencecchen Aug 3, 2026
5340c19
test: expand runtime continuity regressions
lawrencecchen Aug 3, 2026
44aa718
fix: route and remove Claude credentials consistently
lawrencecchen Aug 3, 2026
9d58a02
fix: preserve runtime continuity across deployments
lawrencecchen Aug 3, 2026
afc0d57
test: bound websocket copy buffers
lawrencecchen Aug 3, 2026
b9366c8
fix: budget websocket copy buffers before reads
lawrencecchen Aug 3, 2026
0477ee0
test: expose supervisor retirement status
lawrencecchen Aug 3, 2026
5d71da6
fix: report supervisor retirement lifecycle
lawrencecchen Aug 3, 2026
3654764
fix: harden hosted release deployments
lawrencecchen Aug 3, 2026
e87705d
test: canonicalize hosted account rotations
lawrencecchen Aug 3, 2026
9fe6fb9
fix: canonicalize hosted account rotations
lawrencecchen Aug 3, 2026
ea87665
test: require structured deployment evidence
lawrencecchen Aug 3, 2026
b17cb16
feat: split deployment evidence phases
lawrencecchen Aug 3, 2026
8a58b54
feat: gate production transitions on golden evidence
lawrencecchen Aug 3, 2026
ff62c0d
fix: keep activation rollbackable through commit
lawrencecchen Aug 3, 2026
16a8c55
test: close hosted onboarding review gaps
lawrencecchen Aug 3, 2026
1bd35c0
test: validate hosted repair uploads
lawrencecchen Aug 3, 2026
9f0b6db
test: hide hosted lease persistence failures
lawrencecchen Aug 3, 2026
7a7b0c9
fix: close hosted onboarding review gaps
lawrencecchen Aug 3, 2026
237ecec
feat: normalize staging predecessor with evidence
lawrencecchen Aug 3, 2026
ec71d1c
test: cover fresh VM topology and normalization reruns
lawrencecchen Aug 3, 2026
eeba202
feat: provision verified front topology on fresh VMs
lawrencecchen Aug 3, 2026
ab2cf74
test: add local production continuity golden gate
lawrencecchen Aug 3, 2026
6a5d6dc
test: pin golden continuity acceptance gates
lawrencecchen Aug 3, 2026
276dc2f
feat: enforce production golden continuity gate
lawrencecchen Aug 3, 2026
85f8462
test: bind deployment installer to release evidence
lawrencecchen Aug 3, 2026
b61f561
fix: deploy the attested front installer
lawrencecchen Aug 3, 2026
7db5000
test: bound failed topology activation cleanup
lawrencecchen Aug 3, 2026
e1233a4
fix: make topology activation cleanup one-shot
lawrencecchen Aug 3, 2026
7b243a1
test: require legacy lease POST evidence
lawrencecchen Aug 3, 2026
6b82f96
fix: bind local leases to legacy POST
lawrencecchen Aug 3, 2026
99df22c
test: validate golden fixtures with production schema
lawrencecchen Aug 3, 2026
a1573ca
fix: make deterministic golden evidence canonical
lawrencecchen Aug 3, 2026
be34403
test: require active fresh VM acceptance evidence
lawrencecchen Aug 3, 2026
b73ef5e
feat: require active fresh VM acceptance proof
lawrencecchen Aug 3, 2026
f1eca6a
test: bind deploy targets to URL and VM identity
lawrencecchen Aug 3, 2026
8cf9aca
test: reject golden continuity evidence gaps
lawrencecchen Aug 3, 2026
78bae59
fix: bind deployment evidence to live targets
lawrencecchen Aug 3, 2026
f4fd79b
test: reject dropped golden health probes
lawrencecchen Aug 3, 2026
3b0770d
test: cover golden runtime evidence shapes
lawrencecchen Aug 3, 2026
07afec1
test: cover macOS deployment temp collisions
lawrencecchen Aug 3, 2026
87cbe48
test: cover portable VM creation temp files
lawrencecchen Aug 3, 2026
8dad35a
fix: make deployment temp files portable
lawrencecchen Aug 3, 2026
fac1291
test: cover continuous golden runtime checks
lawrencecchen Aug 3, 2026
8e27567
test: reject chained HTTP socket overlap
lawrencecchen Aug 3, 2026
f1f00ac
test: bound deployment script subprocesses
lawrencecchen Aug 3, 2026
e01b7dc
test: reap deployment process groups
lawrencecchen Aug 3, 2026
9f68c1c
fix: enforce golden runtime continuity
lawrencecchen Aug 3, 2026
85fcc6f
test: isolate deployment scripts with system bash
lawrencecchen Aug 3, 2026
2ab1453
test: cover golden continuity input helper
lawrencecchen Aug 3, 2026
630d0a1
test: cover deployment contract CLI
lawrencecchen Aug 3, 2026
5d142a2
feat: add deployment contract CLI
lawrencecchen Aug 3, 2026
14e20b5
fix: centralize deployment script contracts
lawrencecchen Aug 3, 2026
bd153d5
test: gate golden streams on lifecycle phases
lawrencecchen Aug 3, 2026
b25cdf2
fix: make golden sampling lifecycle deterministic
lawrencecchen Aug 3, 2026
eae6f45
test: require process readiness before sampling
lawrencecchen Aug 3, 2026
f9c1c0d
fix: synchronize golden process registration
lawrencecchen Aug 3, 2026
3f85e12
test: require request drain before fake streams
lawrencecchen Aug 3, 2026
84193d7
fix: drain fake request bodies before streaming
lawrencecchen Aug 3, 2026
66bdbc3
test: require sender completion before fake streams
lawrencecchen Aug 3, 2026
2548ec9
fix: rendezvous on outbound request completion
lawrencecchen Aug 3, 2026
42cab9f
test: avoid durable sync in request rendezvous
lawrencecchen Aug 3, 2026
7900e4a
test: allow resume identifiers to repeat across observers
lawrencecchen Aug 3, 2026
81255ca
fix: scope resume freshness to observers
lawrencecchen Aug 3, 2026
d98874d
test: require observer writer lifecycle drain
lawrencecchen Aug 3, 2026
65f278b
fix: drain observer evidence lifecycle
lawrencecchen Aug 3, 2026
f792572
test: scope fake stream release to session
lawrencecchen Aug 3, 2026
7249ef0
fix: scope fake stream release ownership
lawrencecchen Aug 3, 2026
0e75b6b
test: exclude orderly probe cancellation
lawrencecchen Aug 3, 2026
bfd4783
fix: omit canceled probe samples
lawrencecchen Aug 3, 2026
5796d98
test: serialize probes per target
lawrencecchen Aug 3, 2026
27903df
fix: use persistent health probe workers
lawrencecchen Aug 3, 2026
4651d07
test: require resumable immutable releases
lawrencecchen Aug 3, 2026
2b406b5
fix: make immutable releases resumable
lawrencecchen Aug 3, 2026
4518013
test: reject ambiguous release lookup failures
lawrencecchen Aug 3, 2026
c7d88f5
fix: fail closed on release lookup errors
lawrencecchen Aug 3, 2026
2841568
test: require secure published Docker defaults
lawrencecchen Aug 3, 2026
691cabc
fix: secure published Docker defaults
lawrencecchen Aug 3, 2026
d95ebed
test: exercise initialized Docker Compose secrets
lawrencecchen Aug 3, 2026
b720daf
fix: preserve private Compose secrets
lawrencecchen Aug 3, 2026
b8e06a2
test: keep Docker state out of build cache
lawrencecchen Aug 3, 2026
0bf0386
fix: exclude Docker state from build context
lawrencecchen Aug 3, 2026
88f6b70
test: keep health probes on cadence
lawrencecchen Aug 3, 2026
19871cf
fix: launch health probes on cadence
lawrencecchen Aug 3, 2026
271e035
test: await probe shutdown on errors
lawrencecchen Aug 3, 2026
9764618
fix: await all health probes on shutdown
lawrencecchen Aug 3, 2026
8b036ed
test: require local daemon startup cleanup
lawrencecchen Aug 3, 2026
7396db5
fix: reap failed local daemon startups
lawrencecchen Aug 3, 2026
5035151
test: make daemon cleanup stress reliable
lawrencecchen Aug 3, 2026
9a104c2
test: reject golden readiness busy loops
lawrencecchen Aug 3, 2026
cbd7a77
fix: latch golden session readiness
lawrencecchen Aug 3, 2026
c472c68
test: require distinct socket continuity snapshots
lawrencecchen Aug 3, 2026
29cfe68
fix: prove candidate socket across retirement
lawrencecchen Aug 3, 2026
42ef883
test: preserve socket identity coverage
lawrencecchen Aug 3, 2026
786853a
test: require durable candidate socket proof
lawrencecchen Aug 3, 2026
4a2eb4a
fix: validate durable candidate socket proof
lawrencecchen Aug 3, 2026
b0baaee
test: keep retirement ordering on one clock
lawrencecchen Aug 3, 2026
bd59ad7
fix: compare local retirement timestamps
lawrencecchen Aug 3, 2026
738cfc2
test: require kernel-assigned daemon ports
lawrencecchen Aug 3, 2026
44fe537
fix: let kernel assign golden daemon port
lawrencecchen Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
.DS_Store
**/*.log
**/*.tmp
deploy/docker/secrets
deploy/docker/state
dist
tmp
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ jobs:
echo "building ${target}"
GOOS="${target%/*}" GOARCH="${target#*/}" go build -o /dev/null ./...
done

docker-smoke:
name: Docker local and team profiles
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Exercise both profiles under the production memory limit
run: deploy/docker/smoke-test.sh
189 changes: 189 additions & 0 deletions .github/workflows/gcp-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: GCP Deploy Preflight

on:
workflow_dispatch:
inputs:
deploy_environment:
description: "GCP environment to deploy"
required: true
default: "staging"
type: choice
options:
- staging
- production
operation:
description: "Topology the local Mac golden will mutate after this preflight"
required: true
default: "slot"
type: choice
options:
- slot
- migrate-front
release_tag:
description: "Existing immutable GitHub release tag (for example v0.1.52)"
required: true
type: string

permissions:
contents: read
id-token: write
attestations: read

concurrency:
# Production and staging share one URL map, so serialize their one-time
# migrations. Routine slot deploys remain isolated by environment.
group: gcp-deploy-${{ inputs.operation == 'migrate-front' && 'shared-url-map' || inputs.deploy_environment }}
cancel-in-progress: false

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
SUBROUTER_GCP_PROJECT: ${{ vars.SUBROUTER_GCP_PROJECT }}
SUBROUTER_GCP_ZONE: ${{ vars.SUBROUTER_GCP_ZONE }}
SUBROUTER_GCP_INSTANCE: ${{ inputs.deploy_environment == 'production' && 'subrouter-team' || 'subrouter-staging' }}
SUBROUTER_PUBLIC_BASE_URL: ${{ inputs.deploy_environment == 'production' && 'https://sr.cmux.com' || 'https://staging.sr.cmux.com' }}
SUBROUTER_RELEASE_TAG: ${{ inputs.release_tag }}
SUBROUTER_RELEASE_ARCH: amd64
SUBROUTER_RELEASE_OUTPUT: ${{ github.workspace }}/dist/subrouter-linux-amd64
SUBROUTER_DEPLOY_BINARY: ${{ github.workspace }}/dist/subrouter-linux-amd64
SUBROUTER_RELEASE_SHA256_FILE: ${{ github.workspace }}/dist/subrouter-linux-amd64.sha256

jobs:
deploy:
name: Preflight ${{ inputs.release_tag }} for ${{ inputs.deploy_environment }} ${{ inputs.operation }}
runs-on: ubuntu-latest
timeout-minutes: 360
environment: subrouter-${{ inputs.deploy_environment }}
steps:
- name: Check out trusted deployment tooling
uses: actions/checkout@v6
with:
ref: main
path: deploy-tooling

- name: Check out matching release source for observer tooling
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
path: release-source
persist-credentials: false
fetch-depth: 0

- uses: actions/setup-go@v6
with:
go-version-file: release-source/go.mod

- name: Verify trusted release before Google authentication
working-directory: deploy-tooling
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
umask 077
mkdir -p "${GITHUB_WORKSPACE}/dist" "${GITHUB_WORKSPACE}/artifacts/build-provenance"
deploy/gcp/fetch-release.sh | tee "${GITHUB_WORKSPACE}/artifacts/build-provenance/release-fetch.txt"
version="${SUBROUTER_RELEASE_TAG#v}"
binary_asset="subrouter_${version}_linux_amd64"
release_asset_dir="${GITHUB_WORKSPACE}/dist/release-assets"
mkdir -p "${release_asset_dir}"
required_assets=(SHA256SUMS SOURCE_PROVENANCE.json deployment-contract.py install.sh install-front-slots.sh "${binary_asset}")
download_args=()
for asset in "${required_assets[@]}"; do
download_args+=(--pattern "${asset}")
done
gh release download "${SUBROUTER_RELEASE_TAG}" --repo manaflow-ai/subrouter \
--dir "${release_asset_dir}" "${download_args[@]}"
release_state="$(gh release view "${SUBROUTER_RELEASE_TAG}" --repo manaflow-ai/subrouter \
--json isDraft,isImmutable,tagName,targetCommitish,assets)"
jq -e --arg tag "${SUBROUTER_RELEASE_TAG}" \
'.tagName == $tag and (.isDraft | not) and .isImmutable' <<<"${release_state}" >/dev/null
printf '%s\n' "${release_state}" >"${GITHUB_WORKSPACE}/artifacts/build-provenance/release-state.json"
metadata="$(go version -m "${SUBROUTER_DEPLOY_BINARY}")"
printf '%s\n' "${metadata}" | tee "${GITHUB_WORKSPACE}/artifacts/build-provenance/candidate.txt"
release_commit="$(git -C "${GITHUB_WORKSPACE}/release-source" rev-parse HEAD)"
printf '%s\n' "${release_commit}" | tee "${GITHUB_WORKSPACE}/artifacts/build-provenance/release-source-commit.txt"
git -C "${GITHUB_WORKSPACE}/release-source" fetch --no-tags origin main
git -C "${GITHUB_WORKSPACE}/release-source" merge-base --is-ancestor \
"${release_commit}" origin/main || {
echo "release tag commit is not on protected main" >&2
exit 1
}
grep -Fq "vcs.revision=${release_commit}" <<<"${metadata}"
grep -Fq "vcs.modified=false" <<<"${metadata}"
declare -A digests=()
for asset in "${required_assets[@]}"; do
path="${release_asset_dir}/${asset}"
[[ -f "${path}" && ! -L "${path}" ]]
digests["${asset}"]="$(sha256sum "${path}" | awk '{print $1}')"
jq -e --arg asset "${asset}" --arg digest "sha256:${digests[${asset}]}" \
'[.assets[]? | select(.name == $asset and .digest == $digest)] | length == 1' \
<<<"${release_state}" >/dev/null
gh release verify-asset "${SUBROUTER_RELEASE_TAG}" "${path}" \
--repo manaflow-ai/subrouter --format json >/dev/null
verification="$(gh attestation verify "${path}" \
--repo manaflow-ai/subrouter \
--signer-workflow manaflow-ai/subrouter/.github/workflows/release.yml \
--source-ref "refs/tags/${SUBROUTER_RELEASE_TAG}" \
--source-digest "${release_commit}" \
--deny-self-hosted-runners --format json)"
jq -e 'length > 0' <<<"${verification}" >/dev/null
done
[[ "$(sha256sum "${SUBROUTER_DEPLOY_BINARY}" | awk '{print $1}')" == "${digests[${binary_asset}]}" ]]
for asset in SOURCE_PROVENANCE.json deployment-contract.py install.sh install-front-slots.sh "${binary_asset}"; do
[[ "$(python3 deploy/gcp/deployment-contract.py manifest-sha \
"${release_asset_dir}/SHA256SUMS" "${asset}")" == "${digests[${asset}]}" ]]
done
jq -e --arg tag "${SUBROUTER_RELEASE_TAG}" --arg revision "${release_commit}" \
'(. | keys | sort) == (["source_revision","tag","tag_on_main"] | sort) and
.tag == $tag and .source_revision == $revision and .tag_on_main == true' \
"${release_asset_dir}/SOURCE_PROVENANCE.json" >/dev/null
release_verification="${GITHUB_WORKSPACE}/artifacts/build-provenance/release-verification.json"
jq -n --arg schema 'subrouter.release-verification/v1' \
--arg release_tag "${SUBROUTER_RELEASE_TAG}" --arg source_revision "${release_commit}" \
--arg sums "${digests[SHA256SUMS]}" \
--arg provenance "${digests[SOURCE_PROVENANCE.json]}" \
--arg deployment_contract "${digests[deployment-contract.py]}" \
--arg installer "${digests[install.sh]}" \
--arg front_installer "${digests[install-front-slots.sh]}" \
--arg binary_name "${binary_asset}" --arg binary "${digests[${binary_asset}]}" \
'{schema:$schema,release_tag:$release_tag,source_revision:$source_revision,
tag_on_main:true,release_published:true,release_immutable:true,
asset_digest_verified:true,strict_build_attestation_verified:true,
provenance_verified:true,embedded_revision_verified:true,
assets:{"SHA256SUMS":$sums,"SOURCE_PROVENANCE.json":$provenance,
"deployment-contract.py":$deployment_contract,"install.sh":$installer,
"install-front-slots.sh":$front_installer,($binary_name):$binary}}' \
>"${release_verification}"
chmod 0600 "${release_verification}"
{
echo "SUBROUTER_DEPLOY_REVISION=${release_commit}"
echo "SUBROUTER_RELEASE_TAG_ON_MAIN=true"
echo "SUBROUTER_RELEASE_ATTESTATION_VERIFIED=true"
echo "SUBROUTER_RELEASE_IMMUTABLE=true"
echo "SUBROUTER_RELEASE_ASSET_DIR=${release_asset_dir}"
echo "SUBROUTER_RELEASE_VERIFICATION_JSON=${release_verification}"
echo "SUBROUTER_INSTALL_FRONT_SLOTS=${release_asset_dir}/install-front-slots.sh"
echo "SUBROUTER_DEPLOYMENT_CONTRACT=${release_asset_dir}/deployment-contract.py"
} >>"${GITHUB_ENV}"

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ vars.SUBROUTER_GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.SUBROUTER_GCP_DEPLOY_SERVICE_ACCOUNT }}

- uses: google-github-actions/setup-gcloud@v3

- name: Validate live topology without mutation
working-directory: deploy-tooling
env:
SUBROUTER_PREFLIGHT_TOPOLOGY: ${{ inputs.operation }}
SUBROUTER_DEPLOY_ARTIFACT_DIR: ${{ github.workspace }}/artifacts/gcp-preflight
run: deploy/gcp/preflight-deployment.sh --evidence-json "${GITHUB_WORKSPACE}/artifacts/gcp-preflight/result.json"

- name: Upload deployment evidence
if: always()
uses: actions/upload-artifact@v5
with:
name: gcp-${{ inputs.deploy_environment }}-${{ inputs.operation }}-${{ inputs.release_tag }}-${{ github.run_attempt }}
path: artifacts
if-no-files-found: warn
83 changes: 64 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Release

# A pushed tag builds + tests, then publishes a GitHub release with the binary.
# The VM auto-updater pulls that release; that is the only deploy path we care
# about. npm/PyPI are NOT published on a tag; they are manual opt-in only via
# the workflow_dispatch toggles below.
# A pushed tag builds and tests, then publishes the immutable binaries consumed
# by protected GCP deploys. The legacy pre-front updater reads the same release.
# npm/PyPI are not published on a tag; they are manual opt-in only through the
# workflow_dispatch toggles below.

on:
push:
Expand All @@ -28,15 +28,27 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
group: release-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build and verify artifacts
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
source_commit: ${{ steps.version.outputs.source_commit }}
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Check out repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
Expand All @@ -49,20 +61,58 @@ jobs:
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
[[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || {
echo "release tag must be versioned, for example v0.1.52" >&2
exit 1
}
version="${GITHUB_REF_NAME#v}"
else
version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
fi
echo "version=${version}" >> "${GITHUB_OUTPUT}"
source_commit="$(git rev-parse "${GITHUB_REF}^{commit}")"
echo "source_commit=${source_commit}" >> "${GITHUB_OUTPUT}"

- name: Require release commit on protected main
env:
SOURCE_COMMIT: ${{ steps.version.outputs.source_commit }}
run: |
set -euo pipefail
git fetch --no-tags origin main
git merge-base --is-ancestor "${SOURCE_COMMIT}" origin/main || {
echo "refusing to release a tag whose commit is not on origin/main" >&2
exit 1
}

- name: Test Go packages
run: CGO_ENABLED=0 go test ./...

- name: Build Go release binaries
run: scripts/build-release.sh "${{ steps.version.outputs.version }}"

- name: Copy shell installer into release assets
run: cp install.sh dist/release/install.sh
- name: Copy deployment helpers into release assets
env:
SOURCE_COMMIT: ${{ steps.version.outputs.source_commit }}
run: |
set -euo pipefail
cp install.sh dist/release/install.sh
cp deploy/gcp/install-front-slots.sh dist/release/install-front-slots.sh
cp deploy/gcp/deployment-contract.py dist/release/deployment-contract.py
jq -n --arg tag "${GITHUB_REF_NAME}" --arg source_revision "${SOURCE_COMMIT}" \
'{tag:$tag,source_revision:$source_revision,tag_on_main:true}' \
>dist/release/SOURCE_PROVENANCE.json
(
cd dist/release
find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' \
| LC_ALL=C sort \
| xargs sha256sum >SHA256SUMS
sha256sum --check SHA256SUMS
)

- name: Attest complete release asset set
uses: actions/attest-build-provenance@v3
with:
subject-path: dist/release/*

- name: Upload Go release artifacts
uses: actions/upload-artifact@v5
Expand All @@ -75,36 +125,32 @@ jobs:
name: Publish GitHub release
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
permissions:
contents: write
attestations: read
steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Download Go release artifacts
uses: actions/download-artifact@v5
with:
name: release-assets
path: dist/release

- name: Create or update GitHub release
- name: Stage, verify, and publish immutable GitHub release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG_NAME: v${{ needs.build.outputs.version }}
run: |
set -euo pipefail
if gh release view "${TAG_NAME}" >/dev/null 2>&1; then
gh release upload "${TAG_NAME}" dist/release/* --clobber
else
gh release create "${TAG_NAME}" dist/release/* \
--title "Subrouter ${TAG_NAME}" \
--notes "Subrouter ${TAG_NAME}"
fi
SOURCE_COMMIT: ${{ needs.build.outputs.source_commit }}
run: bash scripts/publish-github-release.sh dist/release

# Manual opt-in only. These never run on a tag push; trigger this workflow via
# the Actions "Run workflow" button with the toggle enabled to publish.
npm-publish:
name: Publish npm package (manual)
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_npm }}
environment: npm
Expand All @@ -126,7 +172,6 @@ jobs:

pypi-publish:
name: Publish PyPI package (manual)
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_pypi }}
environment: pypi
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
deploy/docker/secrets/
deploy/docker/state/
/subrouter
bin/
!npm/bin/
Expand Down
Loading