Harden Forge rollout settlement

f9fbf12b44f3 · AtlantisPleb · · parent d497583fb30e

Harden Forge rollout settlement

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/forge/deploy_receipt.ex
  • modified lib/openagents/forge/sync.ex
  • modified test/openagents/forge/sync_test.exs
  • modified test/openagents/forge/target_lifecycle_test.exs

Diff

4 files changed, +66 -4

lib/openagents/forge/deploy_receipt.ex modified +1 -1

@@ -80,7 +80,7 @@ defmodule OpenAgents.Forge.DeployReceipt do

80 80
    |> validate_format(:sha, ~r/^[0-9a-f]{40}$/)
81 81
    |> validate_format(:artifact_digest, ~r/^[0-9a-f]{64}$/)
82 82
    |> validate_format(:manifest_digest, ~r/^[0-9a-f]{64}$/)
83
    |> validate_length(:modules, max: 512)
83
    |> validate_length(:modules, max: 2_048)
84 84
    |> validate_length(:nodes, max: 100)
85 85
    |> validate_length(:expected_nodes, max: 100)
86 86
    |> validate_length(:canary, max: 255)
lib/openagents/forge/sync.ex modified +29

@@ -77,11 +77,40 @@ defmodule OpenAgents.Forge.Sync do

77 77
    |> Enum.filter(fn entry -> entry["seq"] > applied end)
78 78
    |> Enum.each(fn entry -> apply_entry!(repo, entry) end)
79 79
80
    rebuild_if_objects_missing!(repo, index, default_branch)
80 81
    converge_refs(repo, index)
81 82
    Repos.set_default_branch!(repo, default_branch)
82 83
    :ok
83 84
  end
84 85
86
  defp rebuild_if_objects_missing!(repo, index, default_branch) do
87
    unless refs_materialized?(repo, index) do
88
      Logger.warning("forge_sync_cache_rebuild repo=#{repo} code=missing_ref_object")
89
      :ok = Repos.delete_repo(repo)
90
      Repos.ensure_repo!(repo, default_branch)
91
92
      index
93
      |> WAL.entries()
94
      |> Enum.each(fn entry -> apply_entry!(repo, entry) end)
95
96
      unless refs_materialized?(repo, index) do
97
        raise "forge cache rebuild did not materialize every authoritative ref"
98
      end
99
    end
100
  end
101
102
  defp refs_materialized?(repo, index) do
103
    path = Repos.bare_path(repo)
104
105
    index
106
    |> WAL.refs()
107
    |> Map.values()
108
    |> Enum.uniq()
109
    |> Enum.all?(fn sha ->
110
      match?({_output, 0}, Repos.git(path, ["cat-file", "-e", sha]))
111
    end)
112
  end
113
85 114
  defp apply_entry!(repo, %{"seq" => seq, "object" => object} = entry) do
86 115
    path = Repos.bare_path(repo)
87 116
test/openagents/forge/sync_test.exs modified +8 -1

@@ -62,7 +62,14 @@ defmodule OpenAgents.Forge.SyncTest do

62 62
    assert String.trim(git_bare!(Repos.bare_path("storage-key"), ["symbolic-ref", "HEAD"])) ==
63 63
             "refs/heads/trunk"
64 64
65
    File.rm_rf!(Repos.bare_path("storage-key"))
65
    bare_path = Repos.bare_path("storage-key")
66
    File.rm_rf!(Path.join(bare_path, "objects"))
67
    File.mkdir_p!(Path.join(bare_path, "objects"))
68
69
    assert :ok = Sync.ensure_fresh("storage-key", "trunk")
70
    assert String.trim(git_bare!(bare_path, ["show", "trunk:README.md"])) == "durable import"
71
72
    File.rm_rf!(bare_path)
66 73
    repository = %Repository{storage_key: "storage-key", default_branch: "trunk"}
67 74
68 75
    assert {:ok, ^sha} = Browse.head(repository)
test/openagents/forge/target_lifecycle_test.exs modified +28 -2

@@ -227,6 +227,27 @@ defmodule OpenAgents.Forge.TargetLifecycleTest do

227 227
             Targets.finish_rolling_replacement(target.id, rolling_result(sha, "live"))
228 228
  end
229 229
230
  test "rolling replacement records a complete large module inventory", %{sha: sha} do
231
    {:ok, target} = Targets.promote("demo", sha, "operator:test")
232
    {:ok, _building} = Targets.advance(target.id, "building")
233
    {:ok, _built} = Targets.advance(target.id, "built")
234
    {:ok, _rolling} = Targets.advance(target.id, "needs_rolling_replace")
235
236
    modules = Enum.map(1..600, &"Elixir.OpenAgents.Generated.Module#{&1}")
237
238
    insert_build_receipt!(
239
      target,
240
      %{"classification" => "needs_rolling_replace", "source_sha" => sha},
241
      String.duplicate("a", 64),
242
      modules
243
    )
244
245
    assert {:ok, %{receipt: receipt}} =
246
             Targets.finish_rolling_replacement(target.id, rolling_result(sha, "live"))
247
248
    assert receipt.modules == modules
249
  end
250
230 251
  test "rolling replacement settlement refuses a superseded target", %{sha: sha} do
231 252
    {:ok, first} = Targets.promote("demo", sha, "operator:first")
232 253
    {:ok, _building} = Targets.advance(first.id, "building")

@@ -326,7 +347,12 @@ defmodule OpenAgents.Forge.TargetLifecycleTest do

326 347
    end
327 348
  end
328 349
329
  defp insert_build_receipt!(target, manifest, artifact_digest) do
350
  defp insert_build_receipt!(
351
         target,
352
         manifest,
353
         artifact_digest,
354
         modules \\ ["Elixir.OpenAgents.BuildInfo"]
355
       ) do
330 356
    %BuildReceipt{}
331 357
    |> BuildReceipt.changeset(%{
332 358
      repo: target.repo,

@@ -334,7 +360,7 @@ defmodule OpenAgents.Forge.TargetLifecycleTest do

334 360
      target_id: target.id,
335 361
      status: "complete",
336 362
      manifest: manifest,
337
      modules: ["Elixir.OpenAgents.BuildInfo"],
363
      modules: modules,
338 364
      artifact: "#{artifact_digest}.tar.gz",
339 365
      artifact_digest: artifact_digest,
340 366
      duration_ms: 1,

This page updates live while a promote is in flight · changelog