Operations
How to operate the repo day-to-day. The Architecture file says what the system is; this file says what you do to it.
Read
CLAUDE.mdfirst if you have not.
Daily commands
# Full local CI before opening any PR (fmt, both clippy passes, deny, tests;
# the Postgres suites need Docker)
make ci
# Run the server against in-memory SQLite (no Docker)
MAIDAN_ALLOW_INSECURE_DEV_KEK=1 DATABASE_URL=sqlite::memory: \
MAIDAN_SESSION_SECRET=dev-session-secret-change-me-0123456789 cargo run --bin maidan-server
# Run the prod-style stack (postgres + minio + server)
docker compose --profile full up
curl http://localhost:8080/health
# Two-instance federation push smoke (postgres + maidan-a + maidan-b)
docker compose --profile federation up -d
bash scripts/federation-smoke.sh
# Build the published docs site (mdBook 0.4.40, mdbook-linkcheck 0.7.7,
# mdbook-mermaid 0.14.1). sync-docs.sh copies docs/ into book/src first.
mdbook-mermaid install book
bash book/sync-docs.sh
mdbook build book && ./scripts/check-docs-presentation.sh
mdbook serve book # preview at http://127.0.0.1:3000
# The tracked MCP reference is regenerated from the tool catalog:
cargo run -p maidan-mcp --bin gen-mcp-reference
Kill switches (operator levers)
When something is going wrong — a runaway agent, a leaked token, a traffic spike,
an untrusted egress target — these are the levers to pull. Two shapes: a
per-member freeze (a runtime API, no restart) and MAIDAN_* env flags
(set-and-restart).
Freeze a member
Freezing a member drops their active leases (their claimed threads return to the
queue) and makes claim_next refuse them; they stay frozen until an explicit
unfreeze. It is not a thread/workspace pause — it stops one member. Requires
token:admin.
# freeze (optionally with an audit reason); returns the freeze + how many claims were released
curl -sX POST "$BASE/members/$MEMBER_ID/freeze" -H "Authorization: Bearer $ADMIN" \
-H 'content-type: application/json' -d '{"reason":"compromised token"}'
curl -s "$BASE/workspaces/$WS/frozen-members" -H "Authorization: Bearer $ADMIN" # who is frozen
curl -sX DELETE "$BASE/members/$MEMBER_ID/freeze" -H "Authorization: Bearer $ADMIN" # unfreeze
MCP twins: freeze_member / unfreeze_member / list_frozen_members (also
token:admin). Freeze/unfreeze are written to the audit log
(member.freeze / member.unfreeze).
MAIDAN_* env flags (set + restart)
| Flag | Lever |
|---|---|
MAIDAN_ALLOW_INSECURE_NO_AUTH (+ AUTH_DISABLED) | Auth is fail-closed: disabling it needs this explicit ack, and never in production. Leave unset in prod. |
MAIDAN_RATE_LIMIT_MAX | Per-client request ceiling (per bearer/IP over 60 s). Unset ⇒ a built-in 1200/60 s floor on the server binary; 0 disables. Lower it to throttle a spike. |
MAIDAN_MAX_BODY_BYTES | Max request body (default 2 MiB); oversized ⇒ 413. |
MAIDAN_SECRET_EGRESS_ALLOWLIST | Instance ceiling on the hosts a workspace may trust with its secret values. Each workspace lists its own hosts (/workspaces/:wid/secret-egress-hosts, secret:admin + secret:read); set, a host outside this comma-separated list never receives a substituted value and cannot be added. Unset ⇒ no ceiling. Set empty ⇒ no host receives a value (the off switch). |
FEDERATION_DISABLED | Stops the outbound federation pull worker. Ingress (POST /a2a/v1/events) keeps serving; revoke the peer to stop it. |
MAIDAN_DB_STATEMENT_TIMEOUT_MS | Per-connection Postgres statement timeout (default 30 s) — caps a runaway query. |
MAIDAN_DB_IDLE_IN_TRANSACTION_TIMEOUT_MS | Ends a connection idle inside an open transaction (default 60 s), so it cannot hold back vacuum. |
MAIDAN_MAX_CONCURRENT_REQUESTS | In-flight HTTP request ceiling (default 1024, 0 off); past it, an immediate 503 with Retry-After. Lower it to protect the database under a spike. Gauge maidan_http_in_flight_requests, counter maidan_http_shed_total. |
MAIDAN_MAX_WS_CONNECTIONS | Ceiling on live /ws/subscribe connections (default 10 000; past it, 503). Gauges: maidan_ws_connections, maidan_mcp_streamable_sessions. |
MAIDAN_DB_LOCK_TIMEOUT_MS | Fails a statement waiting on a lock after 10 s by default; migrations exempt themselves. |
MAIDAN_CLAIM_REAP_TICK_SECS | Seconds between claim-reaper ticks (default 5, 0 off). Each tick frees claims whose lease lapsed, charges their worked time to the thread's wall budget, and emits ClaimExpired (ClaimFailed and a DLQ entry when that leaves the thread over budget); counter maidan_claims_reaped_total. |
MAIDAN_CLAIM_ACK_TIMEOUT_SECS | How long a leased claim may go unacknowledged before the claim reaper emits one ClaimUnacknowledged (default 120, 0 off); counter maidan_claims_unacknowledged_total. |
MAIDAN_CLAIM_DEFAULT_LEASE_SECS | The lease a claim_next_thread claim gets when the caller names none (default 600; 1 s to 7 days, an invalid value keeps the default and is logged). |
MAIDAN_CHANGE_SWEEP_SECS | Seconds between change sweeps (default 120, 0 off). Each sweep reads from GitHub the agent pull requests not yet seen closed (at most 25 a workspace, the longest unread first) and settles each close once: a merge closes the change's thread with ThreadLanded, so its dependents become claimable; a close without a merge leaves the thread open with one notice. A merge also gives each live claim whose footprint covers a merged path one rebase_needed notice. |
MAIDAN_STALE_BASE_COMMITS | Commits behind its branch at which a delivered round's base is stale (default 50; an unparsable or non-positive value keeps it). Such a delivery gets one stale_base notice on its thread. |
Opt-in workers: MAIDAN_SCHEDULER_TICK_SECS, MAIDAN_WAIT_SWEEP_TICK_SECS, MAIDAN_DIGEST_TICK_SECS | Unset ⇒ the worker never starts. Unset one to stop that background activity (scheduled tasks / wait escalations / digests). |
Mail worker: MAIDAN_MAIL_WORKER_TICK_SECS | Runs whenever SMTP is configured; the tick defaults to 5 s. |
Retention: MAIDAN_RETENTION_SWEEP_SECS | The sweeper starts when any MAIDAN_RETENTION_*_DAYS is set; the sweep interval defaults to 86400 s. |
MAIDAN_RETENTION_*_DAYS (messages/events/audit/deliveries/notifications) | With the retention sweeper on, per-table age cutoffs; the event log is floored at the min at-least-once cursor so a lagging consumer never loses an undelivered event. Deliveries covers every finished delivery row but never a pending one, an egress/mail dead letter, or a held workspace's rows. Notifications, off unless MAIDAN_RETENTION_NOTIFICATIONS_DAYS is set, deletes read notifications older than that and leaves unread and snoozed ones. Messages, off unless MAIDAN_RETENTION_MESSAGES_DAYS is set, are erased past that age in every workspace that is not held; a workspace may set a shorter policy and not a longer one. The usage ledger is not pruned. See Production — Retention. |
Federation peer secrets and the secret store share the FEDERATION_ENCRYPTION_KEY
keyring; rotate with FEDERATION_DECRYPT_KEYS. See
Production.md for the full config surface.
Load & soak testing
scripts/loadgen.sh drives concurrent REST traffic (post message / read thread
/ search) at the server and prints per-op latency percentiles (p50/p95/p99) +
throughput — the baseline the rest of Arc D's optimizations are measured
against. The measurement is the #[ignore]d load_baseline test
(crates/maidan-server/tests/loadgen.rs), so it never runs as a pass/fail CI
gate (a hard latency floor would flake across runner hardware); the percentile
math is pure and unit-tested and does run in CI.
# in-process server (SQLite), defaults (8 workers × 50 iterations):
scripts/loadgen.sh
# tune concurrency + switch to a timed soak:
MAIDAN_LOADGEN_CONCURRENCY=32 MAIDAN_LOADGEN_DURATION_SECS=60 scripts/loadgen.sh
# point at a live/scaled deployment (bring your own ids + bearer):
MAIDAN_LOADGEN_URL=http://localhost:8080 \
MAIDAN_LOADGEN_BEARER=<token> \
MAIDAN_LOADGEN_IDS='<workspace>|<channel>|<thread>|<member>' \
scripts/loadgen.sh
The report is one row per op kind with count/min/mean/p50/p95/p99/max (ms) and
an overall ops/s. Capture a baseline before an Arc D optimization and re-run
after to show the change.
PR flow (the long version)
1. Pick the next item
The ranked list at the top of docs/Open Work.md is the
plan. A planned arc of work gets a cluster plan in docs/Clusters/Cluster N.md
(PR ladder, ordering, exit criteria, risks) before its first PR; a single fix
or improvement is just a PR against its Open Work row.
2. Branch and commit
git fetch origin
git checkout -b <kind>/<scope>-<slug> origin/main
kind ∈ {feat, fix, perf, refactor, test, docs, ci, build, chore}; see
docs/Conventions.md. Commit with a
Conventional Commits title that reads
as a release note.
Before pushing, run make ci (fmt, both clippy passes, deny, tests), and
bash scripts/check-agent-contract.sh if you touched a contract.
3. Open the PR
git push -u origin <branch>
gh pr create --base main --title "..." --body-file <body.md>
Start the body from the PR template. The Retrospective (PR-level) section is mandatory. The squash commit keeps the branch's commit messages, not the PR body, so a deferral or decision a later reader needs also goes into Open Work or Decisions in the same PR.
For stacked work, open the child PR against its parent's branch. After the
parent squash-merges, rebase the child onto main, dropping the parent's
commits (git rebase --onto origin/main <old-parent-tip> <child>). Don't
merge a parent with --delete-branch while a child is stacked on it: GitHub
closes the child for good.
4. Watch CI
gh pr checks <num> # one-shot
gh pr checks <num> --watch # to completion
The eight required jobs are listed in CLAUDE.md
and the full matrix in docs/Conventions.md. A
conflicting PR runs no CI at all: rebase it first. If a job goes red, fix it on
the branch; the common failures are in "Debugging CI" below.
4a. Address the CodeRabbit review
CodeRabbit reviews every non-draft PR to main and each new push
(.coderabbit.yaml). Before merging, address every comment, including those it
lists outside the diff in its review body: fix it, or reply with the reason it
does not apply. It is advisory, not a required check, so a wrong comment is
answered, not obeyed. @coderabbitai review asks for a fresh review.
5. Merge
Merge only when the eight required checks are green and every CodeRabbit comment is addressed.
gh pr merge <num> -R david-engelmann/maidan --squash --admin
--admin is intentional (docs/Decisions.md, "Admin-merge
instead of local-first push"). Branch protection does not apply to admins, so
--admin will merge over a red required check: that is only done with the
maintainer's explicit go-ahead for that PR.
Closing a cluster
When a cluster's PRs have merged, its close record is one PR (branch
docs/cluster-N-close) that:
- Writes
docs/Retros/Cluster N.mdin the shape ofdocs/Retros/README.md: what shipped (one line per PR), what was deferred and to where, surprises, decisions, risks still open. - Prepends a source record to
docs/Capabilities.md:## Cluster N (source record; no \vN.0.0` tag) —`, a table of what shipped and where it lives. - Adds the cluster's entries to
CHANGELOG.mdunder[Unreleased](Added / Changed / Fixed / Security). - Updates
docs/Open Work.md: shipped rows removed, deferrals added where they belong in the ranked list. - Adds the retro to
docs/Retros/README.md, and updatesdocs/Architecture.mdif the shape of the system changed.
Cutting a release
Tagging is the maintainer's call, not part of closing a cluster. To cut
vX.0.0:
-
Land a release-record PR: a
## [vX.0.0]section indocs/Capabilities.md(linking the GitHub release) naming what it contains, a## [X.0.0]section inCHANGELOG.md(the[Unreleased]entries move under it), thelatest **\vX.0.0`**line inCLAUDE.md, and the image pins inREADME.md.bash scripts/check-release-records.sh --tag vX.0.0` must pass. -
Tag
mainand push the tag:git checkout main && git pull --ff-only git tag -a vX.0.0 -m "vX.0.0: <theme>" git push origin vX.0.0Tags are annotated and unsigned: no GPG key is configured. Release artifacts are signed keylessly with cosign.
Push the tag the day the record PR merges. A release record with no tag reads as a release that is not there, which is what happened to
v411.0.0. If you decide not to tag after all, the next PR renames the CHANGELOG heading to## [X.0.0] — (never tagged; ships in Y.0.0)and the Capabilities section to a source record, as #1078 did for 411. -
The push runs
.github/workflows/release.yml:release record matches tagre-runs the record check against the tag;- binaries for
x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu(viacross) andaarch64-apple-darwin;x86_64-apple-darwinis the manualrelease-darwin-x86.yml; - multi-arch images
ghcr.io/david-engelmann/maidan-server,maidan-cliandmaidan-postgres; - a blocking trivy scan of each image, then
cosign signof each image digest andcosign attestof that image's CycloneDX SBOM to the same digest (generate image SBOMs: cargo-cyclonedx for the server and CLI; trivy for Postgres, one SBOM per platform, each attested to that platform's manifest digest); published server + CLI boot smokeagainst the published images;- the GitHub Release, with the four SBOMs (
maidan-server.cdx.json,maidan-cli.cdx.json,maidan-postgres.linux-amd64.cdx.json,maidan-postgres.linux-arm64.cdx.json) and a.cosign.bundlebeside every artifact.
-
Verify the release, and see "Debugging the release workflow" below if a job fails. Anyone can verify signatures:
cosign verify-blob --bundle maidan-<target>.tar.gz.cosign.bundle \ --certificate-identity-regexp '^https://github\.com/david-engelmann/maidan/\.github/workflows/release\.yml@refs/(tags/v[0-9]+\.[0-9]+\.[0-9]+|heads/main)$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ maidan-<target>.tar.gz cosign verify ghcr.io/david-engelmann/maidan-server:vX.0.0 \ --certificate-identity-regexp '^https://github\.com/david-engelmann/maidan/\.github/workflows/release\.yml@refs/(tags/v[0-9]+\.[0-9]+\.[0-9]+|heads/main)$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com cosign verify-attestation --type cyclonedx ghcr.io/david-engelmann/maidan-server:vX.0.0 \ --certificate-identity-regexp '^https://github\.com/david-engelmann/maidan/\.github/workflows/release\.yml@refs/(tags/v[0-9]+\.[0-9]+\.[0-9]+|heads/main)$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comThe SBOM and attestation steps first run on the first tag after
v412.0.0. On that release, runverify-attestationfor all three images before announcing it.
Clusters that were never tagged ship in the next release; their Capabilities sections say "source record; no tag".
Debugging CI
lint fails
cargo fmt --checkfailed: runcargo fmtlocally, commit, push.clippy -D warningsfailed: read the lint, fix it. If a lint is wrong, use#[allow(clippy::...)]with a// reason: ...comment explaining why.cargo deny checkfailed:unmaintainedadvisory: if it's a dev-dep with no production impact, add todeny.toml's[advisories] ignorewith a rationale comment.wildcarderror: workspace path deps needpublish.workspace = trueon the crate andpublish = falsein workspace.package.vulnerability: check if a fixed version exists; bump deps or ignore with rationale if the vulnerability is not reachable in our code path.
secrets fails
trufflehog found a verified secret. Treat as a real incident:
- Rotate the secret immediately at the issuer.
- Force-push a history rewrite to remove it (or contact GitHub
support if it's already on
main). - Investigate how it got committed; fix the discipline gap.
If trufflehog itself is broken (the action API changed): pin to a
specific commit SHA in ci.yml.
unit tests fails
Run cargo nextest run --workspace --lib --bins --profile ci locally with
the same toolchain (cargo test --workspace --lib --bins runs the same
tests without nextest). The job uploads a junit-unit artifact naming each
failure. The toolchain pin is in rust-toolchain.toml; if a deep
transitive dep needs a newer rustc, bump the pin.
integration (testcontainers) fails
Run cargo nextest run --workspace --tests --profile ci locally with
Docker running; the job uploads a junit-integration artifact. The ci
profile in .config/nextest.toml kills a test after 3 minutes and retries
only the quarantined tests listed there; a quarantined test that passes on a
retry is reported as FLAKY, not hidden. Common failures:
- "syntax error at or near
(": a migration uses syntax that the testcontainer's Postgres major doesn't support. Verify the test is pinned topgvector/pgvector:pg17(notpostgres:17-alpine); the pg17 image supports everything pg16 supports plus thevectorextension. - "cannot DELETE from contentless fts5 table": the FTS5 schema was
reverted to
content=''. It must stay non-contentless. - "the container failed to start with a Docker daemon running": Docker answered, so the test does not skip. The image could not be pulled or its readiness message never came; the error after the colon says which. A test skips (printing "skipping: no Docker daemon") only when no daemon answers, which is not the case on CI.
s3_roundtripors3_multipartfails at "start the S3 container": the image incrates/maidan-artifacts/tests/common/mod.rscould not be pulled or did not logAPI:. These tests skip only when no Docker daemon answers, so a pull failure fails them instead of passing withs3.rsuntested. Keep the image pinned to the digestcompose.yamluses.
loom fails
The job model-checks every interleaving of the sharded bus and the presence hub with loom. Run it locally:
cargo test -p maidan-bus --features loom --release --lib loom
cargo test -p maidan-server --features loom --release --lib loom
In the test build the loom feature swaps the locks for loom's, so it only
builds the models (the normal tests are compiled out). To see the failing interleaving, rerun
the failing model with LOOM_LOG=trace LOOM_LOCATION=1.
tla fails
The job model-checks the TLA+ specs in specs/tla with TLC (pinned
tla2tools.jar, Java 21). Run scripts/tla.sh locally. Each spec has a
passing config and one that turns off a mechanism (ClaimNoReset.cfg,
EventLogUnordered.cfg), where TLC must find the named invariant violated.
A failure prints a counterexample trace that breaks the invariant.
osv scan fails
The job runs scripts/osv-scan.sh (osv-scanner 2.6.0, pinned by SHA-256)
over the lockfiles cargo-deny does not read. Run it locally; its table names
the advisory, the package and the lockfile. A new advisory can turn it red
with no change here. If a fixed version exists, bump that lockfile
(npm install in ui-tests/, cargo update -p <crate> in fuzz/). If the
advisory does not apply, add an IgnoredVulns entry to
.config/osv-scanner.toml with the lockfile and the reason. A dependency
added to the TypeScript or Python SDK needs a committed lockfile; the script
says so.
cargo vet fails
The job runs cargo vet --locked (cargo-vet 0.10.2) over the root
Cargo.lock. It fails when a crate version has neither an audit nor an
exemption in supply-chain/, which follows a lockfile change, or an edit to
supply-chain/ (audits.toml, config.toml, imports.lock) that drops or
narrows an audit or exemption: it names each crate, the version and the
criteria it is missing. Run cargo vet locally, then certify or exempt what
it lists, as Conventions ("Dependencies") describes, and
commit supply-chain/ with the change.
NOTIFY floor simulation fails
the_floor_delivers_every_committed_event_under_faults in maidan-bus
prints the failing seed and its last steps. Replay it with the full trace:
MAIDAN_SIM_SEED=<seed> cargo test -p maidan-bus --lib notify_floor::sim -- --nocapture
MAIDAN_SIM_SEEDS=5000 runs more seeds.
coverage (llvm-cov) fails
The job runs the whole suite under cargo llvm-cov nextest and then
scripts/coverage-floors.py, which fails when the workspace or any crate is
under its line-coverage floor in .config/coverage-floors.toml, when a crate
has no floor, or when a floor names a crate that no longer exists. It is not a
required check.
-
Reproduce locally (Docker running, so the Postgres suites count):
cargo llvm-cov clean --workspace cargo llvm-cov nextest --workspace --profile ci --no-report cargo llvm-cov report --lcov --output-path lcov.info python3 scripts/coverage-floors.py lcov.infoThe same table is in the job's step summary, and
lcov.infois in itscoverageartifact. Skip thecleanand the report also counts test binaries left over from an older build, whose lines show as uncovered: a crate far under its floor with more lines than its source has is that. -
A crate under its floor lost tested lines: add tests, or say in the PR why that code no longer needs them and lower the floor there.
-
A new crate needs a floor in the same PR. Take its measured coverage from the job, subtract one point and round down to the half point.
-
Raise a floor when a crate's coverage rises, the same way, citing the run.
Codecov (optional)
When CODECOV_TOKEN is configured as a repository secret, the coverage job
uploads lcov.info via codecov/codecov-action. Fork PRs and local runs skip
the upload step. The upload does not fail CI when Codecov is unreachable.
Subscribe delivery troubleshooting (v6.0.0)
- Reproduce lag locally:
cargo test -p maidan-server subscribe_emits_replay_hint_when_bus_subscriber_lags -- --nocapture. - Scrape metrics:
curl -s localhost:8080/metrics | rg 'maidan_(bus_lag|subscribe_replay)'. - No workspace filter — subscribers without
filter.workspace_idonly getreplay_hint, not auto-replay; see Production — Delivery reliability metrics. - Truncation loop — sustained
replay_truncatedmeans the client must advanceafter_iduntil the frame stops. - Postgres LISTEN —
maidan_bus_listener_okand/health/readybusfield; listener errors incrementmaidan_bus_listener_errors_total. - Indexer lag — set
INDEXER_STALE_SECS(e.g.300) when embeddings are on; readiness degrades only while message events wait unhandled past it, never for an idle instance. Watchmaidan_indexer_pending_age_seconds(0 when caught up) and/healthindexer_last_event_at;maidan_indexer_last_event_age_secondsgrows on any quiet instance.
Bus hydrate troubleshooting (v8.0.0)
- Reproduce missing row:
cargo test -p maidan-bus pointer_notify_for_missing_log_id_increments_not_found_hydrate_stat -- --nocapture(requires Docker). - Scrape metrics:
curl -s localhost:8080/metrics | rg 'maidan_bus_notify_hydrate'. - Spike in
not_found— confirm HTTP mutations callappend_eventbeforebus.publish; check for federation or scripts callingpg_notifydirectly. - Spike in
invalid_payload— inspect NOTIFY payloads in logs (drop notify payload); legacy full-envelope path still requires valid JSON. - Subscriber gaps with flat hydrate counters — use subscribe replay metrics (Production — Delivery reliability metrics); hydrate failures are listener-side only.
Authorization-denial troubleshooting (v410.0.0)
- Scrape
maidan_authorization_decisions_total. Its labels are closed vocabularies:surface(rest|mcp), capabilityaction,outcome, and resource kind. Principal, workspace, subject, and future delegation grant IDs never become metric labels. MaidanAuthorizationDenialsElevatedfires above one denial/second for five minutes. Split the counter bysurfaceandactionto distinguish a stale integration grant from broad credential probing.- Detail warnings are content-free and sampled 1-in-64. They carry identity and resource IDs for correlation, but never request/response bodies, prompts, messages, tool arguments, secrets, or provider payloads.
- Anonymous and undelegated denials are not written to
maidan_audit: this lane is aggregate observability, the bounded-write decision from Cluster 182. A delegated token's decisions, allowed or refused, are recorded asauthorization.decisionaudit rows.
docker compose smoke fails
compose upfailed:docker compose --profile full up -d --waittimed out on a service health check. Read thecompose logs on failurestep, usually a migration failure or Postgres not accepting connections yet. If a health check race recurs, raise that service'sretriesincompose.yaml.health reports okfailed:/healthanswered but itsstatuswas notok; the JSON names the failing check.
Debugging the release workflow
If the release workflow runs but doesn't produce a GitHub Release:
-
Check the per-matrix-job status:
gh run list --repo david-engelmann/maidan --workflow=release.yml --limit 5 gh run view <run-id> --repo david-engelmann/maidan --log-failed | tail -40 -
The
bundlejob downloads the threemaidan-*matrix artifacts by name, flattens them into onerelease-assetsartifact, and thegithub releasejob downloads only that bundle. Docker push is separate — a slow or failed image build no longer blocks GitHub Release assets. -
Common failures:
download-artifactfails after some artifacts succeed: the release job was pulling every workflow artifact (including Docker GHA cache blobs). Fixed by bundling namedmaidan-*artifacts first.maidan-serverdocker exceeded 2h (historical): sequential multi-arch in one job. The workflow now buildslinux/amd64andlinux/arm64in parallel, then merges withdocker buildx imagetools.- Workflow stuck hours on
macos-13: Intel Mac builds moved to.github/workflows/release-darwin-x86.yml(workflow_dispatchonly). They are not part of the tag release path. - macOS x86_64 build red on
macos-latest: the runner is arm64 now. Userelease-darwin-x86.ymlonmacos-13. See PR #36. - Docker push fails on auth: check that the runner has
packages: writepermission inrelease.yml. softprops/action-gh-releasefails onfail_on_unmatched_files: one or more matrix builds didn't produce an artifact. Fix the matrix entry that failed.generate image SBOMsfails: signing, attestation and the GitHub Release all wait on it, so no image is signed. Amvofcrates/<crate>/sbom.jsonfailing means cargo-cyclonedx changed where it writes (it is pinned in the job); thejqcheck failing means an SBOM came out empty. Fix the job and rerun withworkflow_dispatch.
-
To retry a release without re-tagging:
gh workflow run release.yml --repo david-engelmann/maidan \ -f tag=v0.X.0 -
To create a release manually after the workflow already failed:
gh release create v0.X.0 --repo david-engelmann/maidan \ --title "v0.X.0 — Cluster X: <theme>" \ --notes-file <(echo "...")
Branch protection state
main is protected:
- 8 required status checks:
lint (fmt + clippy + deny),secrets scan,unit tests,integration (testcontainers),docker compose smoke,scale-out smoke(required since themaidan-scale-1.0gate),promtool (alert rules)andotlp smoke(required since Cluster 124). - 1 required approving review, required conversation resolution, linear history (squash only), no force pushes, no deletions.
strict: false: a PR need not be up to date withmainto merge. Whether to require it is F-43 in Open Work, the maintainer's call.enforce_admins: false: an admin can push tomainand merge over red checks. The rule against both is policy, not a technical control.
To inspect:
gh api /repos/david-engelmann/maidan/branches/main/protection | jq
When the repo is in a half-state
If something breaks mid-cluster (e.g., the user interrupts a long session):
- Check
git statusandgit log --oneline -10. - Read the most recent retro for context.
- Read the most recent open PR's body for what was in flight.
- Read
docs/Open Work.mdfor what's queued. - If a branch was left uncommitted, decide:
- Squash into a new commit and finish the PR.
- Reset the branch (
git reset --hard origin/<branch>) if the work is unwanted.
Never force-push to main. Branch resets are fine.