Skip to content

ci: add concurrency limits to all workflows - #100

Merged
neekolas merged 1 commit into
mainfrom
04-08-concurrency_limits_on_actions
Apr 8, 2026
Merged

neekolas merged 1 commit into
mainfrom
04-08-concurrency_limits_on_actions

Conversation

@neekolas

@neekolas neekolas commented Apr 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Adds concurrency groups to all four CI workflows (buf, lint, test, push)
  • Only one run per workflow per PR branch at a time; new pushes cancel pending/in-progress runs
  • Main branch pushes use github.run_id as the group key so they run independently

Why

Without concurrency limits, every push to a PR branch queues a new set of workflow runs. This wastes CI resources on stale commits and can cause unnecessary load on shared runners.

How it works

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true
  • PR runs: github.head_ref is the branch name, so all runs for the same branch share one group per workflow. cancel-in-progress: true kills the older run.
  • Main branch pushes: github.head_ref is empty, so it falls back to github.run_id (unique per run), meaning main pushes never cancel each other.
  • Retries: Work correctly — a retry enters the same concurrency group and proceeds normally if no other run is active.

Test plan

  • Verify YAML syntax is valid across all four workflow files
  • Push multiple commits in quick succession on a PR and confirm only the latest run proceeds
  • Confirm main branch pushes still run independently after merge

🤖 Generated with Claude Code

Note

Add concurrency limits to CI workflows and add V4 notification listener with binary topic support

  • Adds concurrency groups with cancel-in-progress: true to all GitHub Actions workflows (buf.yml, lint.yml, push.yml, test.yml) to prevent redundant runs.
  • Introduces a V4Listener that subscribes to the xmtpv4 NotificationApi, alongside the existing V3 Listener; the active listener is selected at startup via a new LISTENER_TYPE env var (default v3).
  • Migrates subscription topic storage from TEXT to BYTEA in Postgres (migration 00004), converting existing rows and removing non-conforming entries.
  • Adds a payload_format column to installations (migration 00005) and propagates it through registration, delivery (APNS/FCM payloads), and JSON serialization.
  • Exposes a /readyz HTTP endpoint on the API server that returns 503 when the active listener is not ready.
  • Replaces internal XMTP node/validation containers in docker-compose.yml with an external xnet network; dev/up and dev/down now orchestrate via xnet-cli.
  • Risk: the binary topic migration deletes subscriptions with unrecognized topic formats and deduplicates by normalized topic bytes, which is irreversible without the down migration.
📊 Macroscope summarized baa3424. 41 files reviewed, 12 issues evaluated, 8 issues filtered, 0 comments posted

🗂️ Filtered Issues

cmd/server/main.go — 0 comments posted, 1 evaluated, 1 filtered
  • line 107: When opts.Api.Enabled is true but opts.Xmtp.ListenerEnabled is false, notifListener will be nil, so apiServer.SetReadyCheck is never called and s.readyCheck remains nil. The /readyz endpoint is always registered (line 67 in Start()), so if the handler calls s.readyCheck() without a nil check, it will panic. This path is reachable: run with --api but without --xmtp-listener, then request /readyz. [ Cross-file consolidated ]
pkg/delivery/http_test.go — 0 comments posted, 1 evaluated, 1 filtered
  • line 173: The test will fail because interfaces.SendRequest has all fields tagged with json:"-", meaning json.Marshal(req) produces {}. The assertion require.Equal(t, "v4", p["payload_format"]) will fail since p will be an empty map with no payload_format key. [ Out of scope (triage) ]
pkg/interfaces/interfaces.go — 0 comments posted, 1 evaluated, 1 filtered
  • line 90: ValidateForListener at line 90 only checks listenerType == ListenerTypeV3 (i.e., the string "v3"). In cmd/server/main.go, when opts.Xmtp.ListenerType is empty or any value other than "v4", the default branch creates a V3 listener, but interfaces.ListenerType(opts.Xmtp.ListenerType) is passed as-is (e.g., ListenerType("")) to the ApiServer. Since "" != ListenerTypeV3 ("v3"), the validation in ValidateForListener never triggers, allowing V4 payload format registrations on what is actually a V3 listener. This bypasses the intended format compatibility check. [ Out of scope ]
pkg/testutils/delivery.go — 0 comments posted, 1 evaluated, 1 filtered
  • line 47: GetSendRequests at line 47 reads m.Calls (a slice on mock.Mock) without holding the mock's mutex. In the test usage pattern, GetSendRequests is called after RequireEventuallySendCount confirms the counter reached the expected value, but the Run callback increments the atomic counter before mock.Mock internally appends to m.Calls (the append happens after Run returns). This means the counter can reach want while the last Call entry has not yet been written to m.Calls, causing GetSendRequests to return fewer entries than expected and the subsequent require.Len to fail intermittently. [ Out of scope ]
pkg/topics/topics.go — 0 comments posted, 1 evaluated, 1 filtered
  • line 15: strings.TrimPrefix returns the string unchanged when the prefix is absent, so ParseV3Topic silently parses topic strings that lack the required V3_PREFIX (e.g., "g-abc123/proto" without the leading /xmtp/mls/1/). The function does not verify the prefix was actually present, potentially allowing malformed topics to be parsed as valid V3 topics if a caller omits the prefix check. [ Posting failed ]
pkg/xmtp/v4_listener.go — 0 comments posted, 3 evaluated, 3 filtered
  • line 109: startEnvelopeListener reads l.v4Client at line 109 without holding l.connMu, while refreshV4Client writes l.v4Client at line 387 under l.connMu. Since refreshV4Client is called from both startEnvelopeListener (line 114) and consumeEnvelopeStream (line 146) — and the subsequent read of l.v4Client at line 109 happens outside the lock — this is a data race on a non-atomic field. The Go memory model does not guarantee the read at line 109 will see the value written at line 387 without proper synchronization. [ Failed validation ]
  • line 311: In buildV3SendRequest, when handling *envelopesProto.ClientEnvelope_GroupMessage, the code calls payload.GroupMessage.GetV1() at line 311 and immediately passes the result to convertGroupMessageToV3 without checking if v1Input is nil. If GetV1() returns nil (e.g., if the GroupMessage contains a different version variant), convertGroupMessageToV3 will panic when dereferencing input.Data, input.SenderHmac, and input.ShouldPush. In contrast, buildV4SendRequest passes the result to buildGroupMessageContext which does handle nil input. The V3 path should either check for nil before calling convertGroupMessageToV3 or return an appropriate error like ErrUnknownPayloadType. [ Posting failed ]
  • line 381: In refreshV4Client, the old connection l.v4Conn is closed at line 381 before verifying that NewV4Client succeeds. If NewV4Client returns an error at line 384, the function returns without updating l.v4Client or l.v4Conn, leaving l.v4Client pointing to a client backed by the now-closed connection. Subsequent calls to l.v4Client.SubscribeAllEnvelopes in startEnvelopeListener will fail until a retry eventually succeeds. The fix is to only close the old connection after successfully creating the new client. [ Posting failed ]

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Apr 8, 2026
@macroscopeapp

macroscopeapp Bot commented Apr 8, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: Unable to determine

Macroscope's correctness review was unable to post its findings for this PR. Approvability cannot proceed without a successful correctness review.

You can customize Macroscope's approvability policy. Learn more.

neekolas commented Apr 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Apr 8, 5:03 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 8, 5:12 PM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 8, 5:13 PM UTC: @neekolas merged this pull request with Graphite.

@neekolas
neekolas changed the base branch from 04-04-use_xnet_for_backend_startup to graphite-base/100 April 8, 2026 17:10
@macroscopeapp
macroscopeapp Bot dismissed their stale review April 8, 2026 17:10

Dismissing prior approval to re-evaluate baa3424

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Apr 8, 2026
@neekolas
neekolas changed the base branch from graphite-base/100 to main April 8, 2026 17:11
@macroscopeapp
macroscopeapp Bot dismissed their stale review April 8, 2026 17:11

Dismissing prior approval to re-evaluate baa3424

Cancel in-progress runs when a new push arrives on the same PR branch.
Uses github.run_id fallback for main branch pushes so they don't cancel each other.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@neekolas
neekolas force-pushed the 04-08-concurrency_limits_on_actions branch from baa3424 to dc17e2a Compare April 8, 2026 17:12
@neekolas
neekolas merged commit f5e1f38 into main Apr 8, 2026
9 checks passed
@neekolas
neekolas deleted the 04-08-concurrency_limits_on_actions branch April 8, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant