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
73 changes: 69 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,16 +255,81 @@ jobs:
path: dist/*
if-no-files-found: error

# EACH CHANNEL DEPLOYS ITS OWN RELAY BEFORE ITS BINARIES PUBLISH. dev updates
# caf-fabric-dev, staging updates caf-fabric-staging, and main (rc) or a
# stable dispatch updates caf-fabric once RELAY_DEPLOY_PRODUCTION is true. A
# failed deploy or post-deploy proof rolls the Worker back when a prior
# version existed and stops publish; a rollback cannot undo a one-way storage
# upgrade, which is why every relay change reaches dev first and staging a
# week later.
relay:
needs: prepare
permissions:
contents: read
concurrency:
group: relay-${{ (needs.prepare.outputs.channel == 'stable' || needs.prepare.outputs.channel == 'rc') && 'production' || needs.prepare.outputs.channel }}
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check production relay deploy gate
id: gate
env:
CHANNEL: ${{ needs.prepare.outputs.channel }}
RELAY_DEPLOY_PRODUCTION: ${{ vars.RELAY_DEPLOY_PRODUCTION }}
run: |
set -euo pipefail
if [ "$CHANNEL" = "stable" ] || [ "$CHANNEL" = "rc" ]; then
if [ "$RELAY_DEPLOY_PRODUCTION" != "true" ]; then
echo "Production relay deploys are off; set repository variable RELAY_DEPLOY_PRODUCTION=true to enable caf-fabric deploys."
echo "deploy=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
echo "deploy=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.gate.outputs.deploy == 'true'
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.sha }}
- uses: actions/setup-go@v5
if: steps.gate.outputs.deploy == 'true'
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@v4
if: steps.gate.outputs.deploy == 'true'
with:
node-version: "22"
cache: npm
cache-dependency-path: relay/hosted/package-lock.json
- name: Install hosted relay dependencies
if: steps.gate.outputs.deploy == 'true'
working-directory: relay/hosted
run: npm ci
- name: Deploy and verify channel relay
if: steps.gate.outputs.deploy == 'true'
env:
CHANNEL: ${{ needs.prepare.outputs.channel }}
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
RELAY_DEPLOY_PRODUCTION: ${{ vars.RELAY_DEPLOY_PRODUCTION }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CODEAF_TELEMETRY: "off"
DO_NOT_TRACK: "1"
run: bash scripts/relay-deploy.sh

# PUBLISH SAYS always() FOR THE SAME REASON build DOES. GitHub skips a job
# whose dependency chain contains a skipped job unless the job's own condition
# says otherwise, and the chain here always contains one: `test` is skipped on
# every dev build by design. The first live run on 2026-09-11 (dev, ef36c3770)
# proved it — prepare and build green, eight minutes of binaries uploaded, and
# publish skipped without a word, so no dev release existed. The two explicit
# results below are the whole gate: a red build still publishes nothing.
# publish skipped without a word, so no dev release existed. The three
# explicit results below are the whole gate: a red build or relay deploy still
# publishes nothing.
publish:
needs: [prepare, build]
if: always() && needs.prepare.result == 'success' && needs.build.result == 'success'
needs: [prepare, build, relay]
if: always() && needs.prepare.result == 'success' && needs.build.result == 'success' && needs.relay.result == 'success'
permissions:
contents: write
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ test-tooling:
bash scripts/touched-verdict_test.sh
bash scripts/one-suite_test.sh
bash scripts/shard-test_test.sh
bash scripts/relay-fingerprint_test.sh
bash scripts/relay-wrangler-config_test.sh
bash scripts/relay-deploy_test.sh
bash bench/canary/lib/repo_test.sh

manual-gates:
Expand Down
7 changes: 4 additions & 3 deletions cmd/codeaf-release/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
err = runPrune(args[1:], stdin, stdout)
case "service":
err = runService(args[1:], stdout)
case "relay-url":
err = runRelayURL(args[1:], stdout)
case "promotion-plan":
err = runPromotionPlan(args[1:], stdout)
case "promotion-message":
Expand Down Expand Up @@ -93,9 +95,8 @@ func usage(w io.Writer) {
Print stable, rc, dev, staging, or other.
prune --channel dev|staging
Read <tag><tab><RFC3339 createdAt> lines and print expired tags.
service [--url <sync service>]
Fail unless the live sync service (the hosted one by default) promises
every capability this build needs.
service [--url <sync service>] | relay-url --channel stable|rc|staging|dev
Check a live sync service's capabilities, or print a channel's hosted URL.
promotion-plan|promotion-decision|promotion-recheck|promotion-release|promotion-published|promotion-jobs|promotion-push-error|promotion-message
Plan, check, and report the staging promotion. See docs/rules/promotion.md.
`)
Expand Down
19 changes: 19 additions & 0 deletions cmd/codeaf-release/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,22 @@ func describe(missing []directory.Capability) string {
}
return strings.Join(parts, ", ")
}

// runRelayURL prints the hosted sync service address for one release channel so
// CI scripts read internal/syncsetup/relayurl.go instead of spelling URLs twice.
func runRelayURL(args []string, stdout io.Writer) error {
if len(args) != 2 || args[0] != "--channel" {
return usageErr("relay-url takes --channel <stable|rc|staging|dev>")
}
switch args[1] {
case "stable", "rc":
fmt.Fprintln(stdout, syncsetup.ProductionRelayURL)
case "staging":
fmt.Fprintln(stdout, syncsetup.StagingRelayURL)
case "dev":
fmt.Fprintln(stdout, syncsetup.DevRelayURL)
default:
return usageErr("unknown channel %q (expected stable, rc, staging, or dev)", args[1])
}
return nil
}
23 changes: 23 additions & 0 deletions cmd/codeaf-release/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/Agent-Field/codeaf/internal/directory"
"github.com/Agent-Field/codeaf/internal/syncsetup"
)

// TestServiceGateFailsAheadOfTheService pins #2072's release gate: a build
Expand All @@ -28,3 +29,25 @@ func TestServiceGateFailsAheadOfTheService(t *testing.T) {
t.Fatalf("a complete service failed the gate: %q", errs.String())
}
}

func TestRelayURLPrintsEachChannelConstant(t *testing.T) {
cases := map[string]string{
"stable": syncsetup.ProductionRelayURL,
"rc": syncsetup.ProductionRelayURL,
"staging": syncsetup.StagingRelayURL,
"dev": syncsetup.DevRelayURL,
}
for ch, want := range cases {
var out, errs bytes.Buffer
if code := run([]string{"relay-url", "--channel", ch}, nil, &out, &errs); code != 0 {
t.Fatalf("relay-url --channel %s exited %d: %s", ch, code, errs.String())
}
if got := strings.TrimSpace(out.String()); got != want {
t.Fatalf("relay-url --channel %s = %q, want %q", ch, got, want)
}
}
var out, errs bytes.Buffer
if code := run([]string{"relay-url", "--channel", "unknown"}, nil, &out, &errs); code != 2 {
t.Fatalf("unknown channel exited %d, want 2", code)
}
}
16 changes: 16 additions & 0 deletions docs/changes/unreleased/2079-relay-per-channel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
kind: changed
title: Each release channel syncs through its own relay, and CI deploys a relay when its code changes
pr: 2079
surface: [remote, build, docs]
invalidates:
- "Every build synced through https://codeaf.agentfield.ai/fabric by default; now only stable and rc do, staging builds use https://caf-fabric-staging.instrument-santosh.workers.dev, and dev tags, make build and tests use https://caf-fabric-dev.instrument-santosh.workers.dev. Channels do not sync with each other. CODEAF_SYNC_URL and a pairing's saved relay still win."
- "syncsetup.HostedRelayURL was one constant; it is now the channel's address, chosen from the build's revision, and the three addresses are ProductionRelayURL, StagingRelayURL and DevRelayURL in internal/syncsetup/relayurl.go."
- "pair.LinkOrigin was the constant https://codeaf.agentfield.ai; it is now set at startup to the channel's relay origin, and pairing links from any channel's origin are recognised."
- "Nothing deployed the hosted relay, so the client could move ahead of it unnoticed; the release workflow's relay job now deploys caf-fabric-dev on dev pushes and caf-fabric-staging on staging pushes when the relay's fingerprint changed, proves each deploy, and rolls back on failure. A failed relay job stops that channel's publish."
- "The production relay caf-fabric is still deployed by hand; CI deploys it only once the repository variable RELAY_DEPLOY_PRODUCTION is true, which it is not."
---

A relay change now reaches the dev relay with the dev build that needs it, and
staging a week later, before production is ever touched. A Worker rollback
cannot undo a one-way storage upgrade, which is why that order matters.
16 changes: 13 additions & 3 deletions docs/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,19 @@ of tests exempted from passing on a clean checkout.
`Full check` (`ci-full.yml`) runs nightly and validates promotion candidates.
It runs full tests, cross builds and container-based remote checks; binary size
is reported separately. `Promote to staging` requires a successful fresh Full
check on its exact candidate before moving the pointer. `Release` guards branch
ancestry, tests the applicable release surface and publishes platform builds.
Inspect workflow definitions and live branch rules for current required checks.
check on its exact candidate before moving the pointer. `Release` (`release.yml`)
guards branch ancestry, tests the applicable release surface, builds platform
binaries, and runs the `relay` job before `publish`. The `relay` job hashes the
hosted relay's inputs with `scripts/relay-fingerprint.sh`, skips when the live
Worker's deployment tag already matches, otherwise generates the channel's
Wrangler configuration with `scripts/relay-wrangler-config.sh`, creates the
channel's R2 bucket if needed, deploys the channel's Worker (`caf-fabric-dev` on
`dev`, `caf-fabric-staging` on `staging`, `caf-fabric` on `main` and `stable`
when the repository variable `RELAY_DEPLOY_PRODUCTION` is `true`), proves the
live service with `internal/relay/hosted_test.go` (`CODEAF_RELAY_HOSTED_URL`)
and `codeaf-release service --url`, and rolls back to the recorded prior version
id and stops `publish` if verification fails. Inspect workflow definitions and
live branch rules for current required checks.

CI does not run the race detector. Run `go test -race` on the affected package
when investigating a concurrency issue. A green ordinary test run does not
Expand Down
16 changes: 16 additions & 0 deletions docs/rules/promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,19 @@ make changelog VERSION=v0.2.0
Promote that commit through staging to main. Never write a release-only commit
directly onto either pointer. To roll back, publish a forward repair or select
an earlier stable release as Latest; do not rewrite branch history.

## Channel relays

Each release channel syncs through its own hosted relay Worker, deployed
automatically by `Release`'s `relay` job after `prepare` and before `publish`:

- `dev` deploys `caf-fabric-dev` (`https://caf-fabric-dev.instrument-santosh.workers.dev`).
- `staging` deploys `caf-fabric-staging` (`https://caf-fabric-staging.instrument-santosh.workers.dev`).
- `main` (`rc`) and `stable` dispatch deploy `caf-fabric` (`https://codeaf.agentfield.ai/fabric`) only when the repository variable `RELAY_DEPLOY_PRODUCTION` is `true`; while unset or anything else, the `relay` job logs that production relay deploys are off and how to enable them, contacts nothing, and lets `publish` proceed against the currently live relay.

Each deploy is gated by `scripts/relay-fingerprint.sh` (`--tag <first 16 hex>`
and `--message <commit sha>`), verified live with `internal/relay/hosted_test.go`
and `codeaf-release service --url`, and rolled back with `wrangler rollback` if
verification fails, blocking `publish`. Because a Worker code rollback cannot
undo a one-way Durable Object or storage upgrade, every relay change lands on
`dev` first and soaks on `staging` for a week before reaching `main`.
2 changes: 1 addition & 1 deletion internal/atlas/pairing.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var Pairing = &Map{
Files: []FileRef{{Path: "cmd/codeaf/pair.go", Symbols: []string{"runPair"}}}},
// ── services ──────────────────────────────────────────────────────
{ID: "hosted", Label: "hosted relay", Short: "default", Kind: KindService, X: 0.38, Y: 0.1,
Summary: "The default service at https://codeaf.agentfield.ai/fabric: a Cloudflare Worker with Durable Objects and R2. It holds the directory, the encrypted blobs and pairing requests, and it cannot decrypt any of them.",
Summary: "The default hosted sync service selected per release channel in internal/syncsetup/relayurl.go: a Cloudflare Worker with Durable Objects and R2. It holds the directory, the encrypted blobs and pairing requests, and it cannot decrypt any of them.",
Details: []string{
"Limits: 5 GiB/identity, 1200 req/min/identity, 600/min/device",
"Pairing: 10-minute TTL, 10 link creates/hr, 3 pending per IP",
Expand Down
2 changes: 1 addition & 1 deletion internal/manual/chat/add-or-remove-a-computer.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Check number: 4821 — the other device shows the same number.
Waiting for approval (10 minutes). Press ctrl+c to cancel.
```

A device with no screen or no browser, such as a server, does the same: run `codeaf pair` there and approve from your laptop. Two things name the same request: the link `https://codeaf.agentfield.ai/p/<code>#<key>`, which opens in a browser and hands over to the installed app (the part after `#` never leaves the browser), and the bare token `<code>.<key>`, for a terminal where nobody can click. `codeaf pair approve` takes either one, and so does pasting into home. When it is approved it says how many workspaces it can now reach:
A device with no screen or no browser, such as a server, does the same: run `codeaf pair` there and approve from your laptop. Two things name the same request: the link (`https://codeaf.agentfield.ai/p/<code>#<key>` on stable releases and release candidates, `https://caf-fabric-staging.instrument-santosh.workers.dev/p/<code>#<key>` on staging builds, or `https://caf-fabric-dev.instrument-santosh.workers.dev/p/<code>#<key>` on dev and local source builds), which opens in a browser and hands over to the installed app (the part after `#` never leaves the browser), and the bare token `<code>.<key>`, for a terminal where nobody can click. Channels do not sync with each other: pair computers on the same release channel (or with the same `CODEAF_SYNC_URL`). `codeaf pair approve` takes either form, and so does pasting into home. When it is approved it says how many workspaces it can now reach:

```
Paired. 3 workspaces available.
Expand Down
4 changes: 2 additions & 2 deletions internal/manual/chat/relay-hosted-or-your-own.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hosted relay or your own relay

## Hosted relay or my own — which one do I use, and where does sync go by default
## Hosted relay or my own — which one do I use, where does sync go by default, and which relay each channel uses

Moving chats and pairing are available by default on Linux, macOS and native Windows on every build with a compatible engine. Pair both computers first.

Expand All @@ -13,7 +13,7 @@ A relay is the small service your computers sync through. There are two kinds. B

Either way the relay sees only ciphertext and the short list on "What can the relay see".

**Where sync goes by default.** Unset, a computer syncs through codeaf's hosted relay at `https://codeaf.agentfield.ai/fabric`, once this build carries that address. The address lives in one place in the program, `HostedRelayURL`; a build whose value is empty has no default, and then nothing is copied and pairing says `no sync address is set, so there is nowhere to pair through: set CODEAF_SYNC_URL to a sync address, or pass --via <address>`. A new home sends no relay requests before completing a pairing or offering a code or link. Reading the add-device instructions does not start sync. Older identities without the quiet marker retain their earlier behavior: see "Is my code sent anywhere before I pair" on the page *Pairing your chats with a second computer*. To use your own relay instead, set `CODEAF_SYNC_URL` as below. Turn sync off with `CODEAF_SYNC_URL=off`; pairing cannot override that with `--via`.
**Where sync goes by default.** Unset, each release channel syncs through its own hosted relay: stable releases and release candidates use `https://codeaf.agentfield.ai/fabric`, staging builds use `https://caf-fabric-staging.instrument-santosh.workers.dev`, and dev builds and local builds from source use `https://caf-fabric-dev.instrument-santosh.workers.dev`. Channels do not sync with each other: computers must run the same channel (or set the same `CODEAF_SYNC_URL`) to pair and share chats. A build whose `HostedRelayURL` is empty has no default, and then nothing is copied and pairing says `no sync address is set, so there is nowhere to pair through: set CODEAF_SYNC_URL to a sync address, or pass --via <address>`. A new home sends no relay requests before completing a pairing or offering a code or link. Reading the add-device instructions does not start sync. Older identities without the quiet marker retain their earlier behavior: see "Is my code sent anywhere before I pair" on the page *Pairing your chats with a second computer*. To use your own relay instead, set `CODEAF_SYNC_URL` as below. Turn sync off with `CODEAF_SYNC_URL=off`; pairing cannot override that with `--via`.

## Point codeaf at a relay — CODEAF_SYNC_URL, url or off

Expand Down
Loading
Loading