Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
.githooks/generated-artifact-merge merge=generated-artifact
.githooks/pre-commit merge=generated-artifact
.githooks/pre-push merge=generated-artifact
.github/workflows/deploy.yml merge=generated-artifact
.github/workflows/fleet-converge.yml merge=generated-artifact
.github/workflows/witnesses.yml merge=generated-artifact
.gitignore merge=generated-artifact
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: deploy
on:
workflow_dispatch:
push:
branches: [main]
concurrency:
group: gunbc-live-deploy-srv1
jobs:
deploy:
runs-on: [self-hosted, linux, arm64, srv1]
timeout-minutes: 90
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Build the deploy binary
run: |
cargo build --release -p v1-compiler --bin gunbc
- name: Materialize the pinned provider runtime
run: |
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
"$ROOT/target/release/gunbc" run --source-root dag --source-root src/v2 --entry dag/gunbc/host_codex_runtime_provision.dag --function materialize_codex_runtime_on_local_host
- name: Deploy the dashboard to srv1
run: |
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
"$ROOT/target/release/gunbc" run --source-root dag --source-root src/v2 --entry dag/gunbc/live_deploy/apply.dag --function live_deploy_apply_srv1_wet
- name: Advance the admitted fleet revision
run: |
# 🟡 dissolve-on: gunbc_ci_advance_fleet_desired_script — concat-built foreign-executor (GitHub Actions run:) CAS ref advance (git ls-remote + push --force-with-lease onto gunbc.fleet_main_revision fleet_desired_ref_name); DISSOLVES WHEN the SCM ref-update realization lands as the typed git.Core CAS service op (membership-diff spine classify_change SCM-ref arm) invoked via host_effect_apply
set -eu
DESIRED_REF=refs/fleet/desired
CURRENT=$(git ls-remote origin "$DESIRED_REF" | cut -f1)
if [ -z "$CURRENT" ]; then
git push origin "$GITHUB_SHA:$DESIRED_REF"
echo "[fleet-desired] created $DESIRED_REF -> $GITHUB_SHA"
else
git push --force-with-lease="$DESIRED_REF:$CURRENT" origin "$GITHUB_SHA:$DESIRED_REF"
echo "[fleet-desired] advanced $DESIRED_REF: $CURRENT -> $GITHUB_SHA (CAS expected-prior held)"
fi
6 changes: 5 additions & 1 deletion dag/gunbc/generated_artifact.dag
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import gunbc.generated_artifact_merge_driver { generated_artifact_merge_driver_s
type GeneratedArtifact
= WitnessFloorYamlArtifact
| FleetConvergeYamlArtifact
| LiveDeployYamlArtifact
| GitignoreArtifact
| GitattributesArtifact
| RoadmapArtifact
Expand All @@ -28,7 +29,7 @@ type GeneratedArtifact
data generated_artifact_registry: List<GeneratedArtifact> = concat(
concat(
concat(
[WitnessFloorYamlArtifact, FleetConvergeYamlArtifact, GitignoreArtifact, GitattributesArtifact, RoadmapArtifact, DesignArtifact, GithooksPrePushArtifact, GithooksPreCommitArtifact, GeneratedArtifactMergeDriverArtifact, RunnerDeployArtifact, Stage0CrateLayoutGeneratedDagArtifact, Stage0CrateLayoutGeneratedRsArtifact, Stage0CratePartitionGeneratedDagArtifact, V1InterpreterDispatchGeneratedRsArtifact],
[WitnessFloorYamlArtifact, FleetConvergeYamlArtifact, LiveDeployYamlArtifact, GitignoreArtifact, GitattributesArtifact, RoadmapArtifact, DesignArtifact, GithooksPrePushArtifact, GithooksPreCommitArtifact, GeneratedArtifactMergeDriverArtifact, RunnerDeployArtifact, Stage0CrateLayoutGeneratedDagArtifact, Stage0CrateLayoutGeneratedRsArtifact, Stage0CratePartitionGeneratedDagArtifact, V1InterpreterDispatchGeneratedRsArtifact],
map(fleet_autoinstall_specs, s => AutoinstallUserDataArtifact { spec: s })
),
map(fleet_install_server_specs, s => ProxyDhcpDnsmasqArtifact { spec: s })
Expand All @@ -50,6 +51,7 @@ fn artifact_path(a: GeneratedArtifact) -> String {
match a {
WitnessFloorYamlArtifact => ".github/workflows/witnesses.yml"
FleetConvergeYamlArtifact => ".github/workflows/fleet-converge.yml"
LiveDeployYamlArtifact => ".github/workflows/deploy.yml"
GitignoreArtifact => ".gitignore"
GitattributesArtifact => ".gitattributes"
RoadmapArtifact => "ROADMAP.md"
Expand Down Expand Up @@ -77,6 +79,7 @@ fn artifact_commit_policy(a: GeneratedArtifact) -> CommitPolicy {
GeneratedArtifactMergeDriverArtifact => CommitRequired { consumer: GitProtocol }
WitnessFloorYamlArtifact => CommitRequired { consumer: GithubActionsWorkflow }
FleetConvergeYamlArtifact => CommitRequired { consumer: GithubActionsWorkflow }
LiveDeployYamlArtifact => CommitRequired { consumer: GithubActionsWorkflow }
RoadmapArtifact => CommitRequired { consumer: ProjectDocumentation }
DesignArtifact => CommitRequired { consumer: ProjectDocumentation }
PlanArtifact { plan: _ } => CommitRequired { consumer: ProjectDocumentation }
Expand Down Expand Up @@ -111,6 +114,7 @@ fn artifact_eq(a: GeneratedArtifact, b: GeneratedArtifact) -> Bool {
match a {
WitnessFloorYamlArtifact => match b { WitnessFloorYamlArtifact => true _ => false }
FleetConvergeYamlArtifact => match b { FleetConvergeYamlArtifact => true _ => false }
LiveDeployYamlArtifact => match b { LiveDeployYamlArtifact => true _ => false }
GitignoreArtifact => match b { GitignoreArtifact => true _ => false }
GitattributesArtifact => match b { GitattributesArtifact => true _ => false }
RoadmapArtifact => match b { RoadmapArtifact => true _ => false }
Expand Down
5 changes: 4 additions & 1 deletion dag/gunbc/generated_artifact_emit.dag
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module gunbc.generated_artifact_emit

import gunbc.generated_artifact {
GeneratedArtifact,
WitnessFloorYamlArtifact, FleetConvergeYamlArtifact, GitignoreArtifact, GitattributesArtifact, RoadmapArtifact, DesignArtifact,
WitnessFloorYamlArtifact, FleetConvergeYamlArtifact, LiveDeployYamlArtifact, GitignoreArtifact, GitattributesArtifact, RoadmapArtifact, DesignArtifact,
RunnerDeployArtifact, GithooksPrePushArtifact, GithooksPreCommitArtifact, GeneratedArtifactMergeDriverArtifact,
AutoinstallUserDataArtifact, ProxyDhcpDnsmasqArtifact, PlanArtifact,
Stage0CrateLayoutGeneratedDagArtifact, Stage0CrateLayoutGeneratedRsArtifact,
Expand All @@ -12,6 +12,7 @@ import gunbc.generated_artifact {
artifact_path
}
import gunbc.fleet_converge_workflow { expected_fleet_converge_yml }
import gunbc.live_deploy_workflow { expected_deploy_yml }
import gunbc.witness_floor_workflow { expected_witness_floor_yml }
import gunbc.ci_yaml_validate { ci_yml_parses }
import gunbc.gitignore_emit { expected_gitignore }
Expand Down Expand Up @@ -51,6 +52,7 @@ fn artifact_generate(a: GeneratedArtifact) -> ArtifactGenerationOutcome {
match a {
WitnessFloorYamlArtifact => artifact_generated(content: expected_witness_floor_yml())
FleetConvergeYamlArtifact => artifact_generated(content: expected_fleet_converge_yml())
LiveDeployYamlArtifact => artifact_generated(content: expected_deploy_yml())
GitignoreArtifact => artifact_generated(content: expected_gitignore())
GitattributesArtifact => match expected_gitattributes() {
GitattributesGenerated { content } => artifact_generated(content: content)
Expand Down Expand Up @@ -82,6 +84,7 @@ fn artifact_extra_valid(a: GeneratedArtifact, generated: ArtifactGenerationOutco
match a {
WitnessFloorYamlArtifact => ci_yml_parses(path: artifact_path(a: a))
FleetConvergeYamlArtifact => ci_yml_parses(path: artifact_path(a: a))
LiveDeployYamlArtifact => ci_yml_parses(path: artifact_path(a: a))
GitignoreArtifact => true
GitattributesArtifact => true
RoadmapArtifact => true
Expand Down
240 changes: 240 additions & 0 deletions dag/gunbc/live_deploy_workflow.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
module gunbc.live_deploy_workflow

import extdeps.languages.yaml.types { yaml_string, kv }
import extdeps.languages.yaml.emit { serialize_yaml }
import extdeps.languages.yaml.gha_workflow { project_workflow_to_yaml }
import extdeps.github.actions {
Workflow, Job, Step, RunStep, UsesStep,
WorkflowTrigger, Push, WorkflowDispatch,
WorkflowPermissions, PermRead, PermWrite,
ConcurrencySpec, ConcurrencyMappingQueueNotMax,
checkout_action
}
import gunbc.ci_deploy_target_host { gunbc_ci_deploy_srv1_runner_spec }
import gunbc.repo_identity { gunbc_default_branch_name }
import gunbc.merge_admission_produce { ci_repo_root_shell }
import gunbc.ci_spec {
live_deploy_apply_entry,
live_deploy_apply_srv1_wet_function,
gunbc_ci_advance_fleet_desired_script,
}
import v2.std.optional { Present }
import std.types { String }

// WHY THIS WORKFLOW EXISTS AGAIN, AND WHY IT IS NOT A RESTORE.
//
// The 2026-08-15 floor cut deleted ci.yml, and with it the only thing that ever actuated a
// deployment or advanced the fleet's admitted revision. Both stayed modeled and neither ran, which
// is the specification-without-execution state DESIGN section 5 names -- gunbc.ci_spec
// gunbc_ci_deploy_srv1_stage described a deploy that nothing performed, and
// gunbc_ci_advance_fleet_desired_script described a ref advance that nothing performed.
//
// MEASURED COST, 2026-08-19/20: srv1 served a revision from weeks earlier because no merge ever
// reached it; refs/fleet/desired stayed pinned at a sha from the deleted green slot, so every belt
// tick refused wholesale on a revision-drift check; and the pinned Codex provider bundle was absent
// from /opt/gunbc/provider-runtime/codex, so every spawn refused at its first gate. Three separate
// symptoms, one cause: the host had no scheduled convergence at all. Each was repaired by hand on
// 2026-08-19/20, which is exactly the manual application DESIGN section 6 counts as debt.
//
// This is re-derived rather than restored, per the floor cut's own re-add rule. The deleted job was
// one stage inside a large composed ci.yml that also carried gates, batching and a scheduler; what
// is authored here is only the actuation that had no other home, as one job of three steps against
// the host it targets. The gates are NOT reinstated with it and remain their own re-adds.
//
// ORDER IS LOAD-BEARING, and it is the order the outage taught. Provider runtime first, because a
// deployment whose provider binary is missing serves a dashboard that cannot dispatch -- the state
// srv1 sat in for weeks. Deploy second. The fleet-desired advance LAST, because that ref is the
// belt's admission gate: advancing it before the deploy converges would tell every host that a
// revision is desired while the host serving it is still mid-apply, which is the fabricated
// plausible answer DESIGN section 5 forbids. A failed deploy therefore leaves the ref where it was,
// and the belt keeps refusing against the last revision that actually landed.
data live_deploy_workflow_note: String = "This module emits the deploy workflow. See the annotation above it for why it exists and why its step order is load-bearing."

data live_deploy_workflow_job_id: String = "deploy"

data live_deploy_workflow_concurrency_group: String = "gunbc-live-deploy-srv1"

fn live_deploy_checkout_step() -> Step {
UsesStep {
name: Present { value: "Checkout" },
id: none,
uses: checkout_action,
with: Present {
value: [kv(key: "fetch-depth", value: yaml_string(s: "0"))]
},
env: none,
if_condition: none,
continue_on_error: none,
timeout_minutes: none
}
}

fn live_deploy_build_script() -> String {
"cargo build --release -p v1-compiler --bin gunbc"
}

fn live_deploy_build_step() -> Step {
RunStep {
name: Present { value: "Build the deploy binary" },
id: none,
run: concat(live_deploy_build_script(), "\n"),
shell: none,
env: none,
working_directory: none,
if_condition: none,
continue_on_error: none,
timeout_minutes: none
}
}

fn live_deploy_gunbc_run_script(entry: String, function: String) -> String {
concat(
concat("ROOT=", concat(ci_repo_root_shell(), "\n")),
concat(
"\"$ROOT/target/release/gunbc\" run --source-root dag --source-root src/v2 --entry ",
concat(entry, concat(" --function ", concat(function, "\n"))),
),
)
}

// The provider runtime is materialized on every deploy rather than once at host standup, and the
// reason is that a host standup is not an event this repository observes. The bundle went missing
// from srv1 with nothing to notice, and the modeled actuator that replaces it is content-addressed
// and idempotent -- it re-links the same release when the pinned version is already present, so
// running it every deploy costs a probe and buys the guarantee that the deployed dashboard can
// actually dispatch.
fn live_deploy_provider_runtime_step() -> Step {
RunStep {
name: Present { value: "Materialize the pinned provider runtime" },
id: none,
run: live_deploy_gunbc_run_script(
entry: "dag/gunbc/host_codex_runtime_provision.dag",
function: "materialize_codex_runtime_on_local_host",
),
shell: none,
env: none,
working_directory: none,
if_condition: none,
continue_on_error: none,
timeout_minutes: none
}
}

fn live_deploy_apply_step() -> Step {
RunStep {
name: Present { value: "Deploy the dashboard to srv1" },
id: none,
run: live_deploy_gunbc_run_script(
entry: live_deploy_apply_entry,
function: live_deploy_apply_srv1_wet_function,
),
shell: none,
env: none,
working_directory: none,
if_condition: none,
continue_on_error: none,
timeout_minutes: none
}
}

// THE ADVANCE REUSES THE ADMITTED AUTHORITY RATHER THAN RESTATING IT, and the first cut of this
// module got that wrong in two ways at once (review 53944 caught the second; the first is worse and
// went unnamed). It authored its own copy of a script gunbc.ci_spec already owns -- a byte-for-byte
// re-derivation. That is the DESIGN section 3 fork: two functions answering one question,
// guaranteed to drift, and drift here means two different opinions about how the fleet's admitted
// revision advances.
//
// The review's mechanical finding was that the script builds shell by concatenation, which
// section 3 names as the medium-as-string blocker. That is true of the text -- and it is ALREADY
// ADMITTED DEBT rather than new debt: gunbc_ci_advance_fleet_desired_shell_emit_scaffold declares
// it a Scaffold dissolving to SingleAuthority, and its dissolution trigger names this exact
// concat-built foreign-executor CAS advance. So the correct repair is not to mark the copy as a
// scaffold -- section 6 is explicit that an author cannot admit their own -- and not to hand-roll a
// second bash rendering beside the one that trigger already tracks. It is to consume the admitted
// authority and let its existing trigger carry the conversion for both consumers at once.
//
// A consequence worth stating because it is visible in the emitted file: the shared script prefixes
// itself with its own dissolution description, so deploy.yml now carries the debt marker inline.
// That is the correct direction -- the generated artifact declares the scaffold it came from,
// rather than laundering it into anonymous yaml.
fn live_deploy_advance_fleet_desired_step() -> Step {
RunStep {
name: Present { value: "Advance the admitted fleet revision" },
id: none,
run: gunbc_ci_advance_fleet_desired_script(),
shell: none,
env: none,
working_directory: none,
if_condition: none,
continue_on_error: none,
timeout_minutes: none
}
}

// contents: WRITE, and the write is the point rather than an over-grant. The last step pushes
// refs/fleet/desired, so a read-only token would 403 on exactly the step whose absence caused the
// belt to refuse every tick for weeks -- and it would 403 AFTER the deploy had already converged,
// leaving the host serving a revision the fleet does not admit. The narrower grants stay off: no
// pull-requests, no issues, no actions, no id_token.
//
// This annotation sits here rather than beside the permissions field because DESIGN section 4c
// models annotations at MODULE-ITEM grain only; the first cut put it inside the Job body and the
// floor refused the module outright (run 32324870744). A comment is not free-floating prose that
// can live wherever it reads best -- it attaches to a declaration, and inside a body there is no
// declaration for it to attach to.
fn live_deploy_job() -> Job {
Job {
id: live_deploy_workflow_job_id,
name: none,
runner: gunbc_ci_deploy_srv1_runner_spec(),
steps: [
live_deploy_checkout_step(),
live_deploy_build_step(),
live_deploy_provider_runtime_step(),
live_deploy_apply_step(),
live_deploy_advance_fleet_desired_step()
],
needs: [],
env: none,
outputs: none,
if_condition: none,
timeout_minutes: Present { value: 90 },
continue_on_error: none,
concurrency: none,
permissions: Present {
value: WorkflowPermissions {
contents: Present { value: PermWrite },
pull_requests: none,
issues: none,
actions: none,
id_token: none,
}
}
}
}

// Push-on-main only, plus manual dispatch. There is deliberately no pull_request trigger: a deploy
// is a host mutation, and a proposed change is not an admitted one. Concurrency is a queue with NO
// cancel-in-progress, because cancelling a half-applied deploy leaves the host between two
// revisions -- the queue makes a second merge wait for the first to converge instead.
data live_deploy_workflow: Workflow = Workflow {
name: "deploy",
on: [
WorkflowDispatch { inputs: [] },
Push { branches: [gunbc_default_branch_name], paths: [] }
],
concurrency: Present {
value: ConcurrencyMappingQueueNotMax {
group: live_deploy_workflow_concurrency_group,
cancel_in_progress: none,
explicit_single: none
}
},
env: none,
permissions: none,
jobs: [live_deploy_job()],
}

fn expected_deploy_yml() -> String {
serialize_yaml(v: project_workflow_to_yaml(workflow: live_deploy_workflow))
}
Loading