Context
The live Cloud integration workflow is expensive, mutates a shared test organization, and currently runs on every matching PR update. This becomes particularly noisy and unreliable when refreshing a stack of PRs.
Example failure: https://github.com/ClickHouse/clickhousectl/actions/runs/31696362617/job/94434938976?pr=393
That job failed on a one-shot query-endpoint upsert with 503 SERVICE_UNAVAILABLE. It was not related to the PR changes. During the same stacked-PR refresh, many Cloud Integration jobs entered the shared organization together and produced:
429 TOO_MANY_REQUESTS while creating API keys, updating services, and cleaning up.
- Repeated
503 SERVICE_UNAVAILABLE responses from query-endpoint upserts.
- A transient Query API
404 saying the ClickHouse service was currently unavailable.
- Postgres
500 Internal error responses.
The linked job spent roughly four minutes compiling, failed about one minute into the live test, and then spent another 2.5 minutes cleaning up before reporting failure. Rerunning the job repeats all of that work.
Current behavior
.github/workflows/cloud-integration.yml runs five suites sequentially in one job:
- ClickHouse service lifecycle.
- Postgres lifecycle.
- Organization lifecycle.
- Postgres CDC ClickPipe.
- Optional ClickPipe smoke tests.
The workflow currently triggers for every change under crates/clickhousectl/src/cloud/**, but it runs only cargo test -p clickhouse-cloud-api. CLI-only changes therefore launch live API tests that neither compile nor execute the changed CLI code.
The API source is now split into domain modules, but every API-domain change still launches every live suite. ClickStack, UDF, and backup changes also launch the workflow even though it has no live suite for those domains.
Existing resilience is limited:
poll_until retries polling checks at a fixed interval, without jitter or error classification.
retry_api_call exists but is used only for the known Postgres read-replica "no backups yet" race.
- Direct management operations normally get one attempt.
- Cleanup mutations normally get one attempt and can themselves fail with
429 or 5xx.
- The API test client has no explicit request timeout.
- Workflow concurrency is scoped to one PR ref, so it does not limit concurrent jobs from different PRs in a stack.
Goals
- Keep ordinary build, unit-test, clippy, and formatting CI automatic.
- Make live Cloud integration opt-in rather than running on every PR update.
- Allow one full run on the top PR of a stack to validate the complete inherited stack.
- Avoid running unrelated live suites when a targeted domain run is sufficient.
- Absorb temporary API throttling and service failures without rerunning a complete job.
- Keep cleanup reliable when the original test operation fails.
Proposed work
1. Harden live tests
- Add bounded exponential backoff with jitter for
429, retryable 5xx, and transport timeouts.
- Back off more aggressively after transient polling errors instead of keeping every job on the same fixed cadence.
- Fail immediately on permanent API errors rather than waiting for a polling timeout.
- Retry only safe reads and idempotent updates, upserts, state changes, and deletes.
- Apply the same retry policy to cleanup operations.
- Add an explicit management-request timeout.
- Do not blindly retry resource-creation POSTs after ambiguous transport or server failures. Reconcile by deterministic run tags/name first so retries cannot create duplicate resources.
- Add mock coverage for transient-success, terminal-error, timeout, and cleanup cases.
2. Select suites by affected domain
Keep one workflow with a small changed-path classifier rather than duplicating the complete secret-bearing job across several workflows.
| Changed domain |
Live suites |
| Services |
Service, ClickPipe CDC |
| Postgres |
Postgres, ClickPipe CDC |
| ClickPipes |
ClickPipe CDC, smoke |
| Organizations, activity, members, RBAC |
Organization |
| API keys |
Service, Organization |
| ClickStack, UDFs, backups |
None until a corresponding live suite exists |
| CLI Cloud modules |
None; regular CLI CI owns these |
| Shared client, error, models, test support, manifests |
All |
Scheduled and explicitly requested full runs should select every suite. Unknown new API source paths should fail closed by selecting all suites.
Selected suites can remain sequential in one job to reuse compilation and avoid creating another traffic burst. Each selected step should still run if an earlier selected suite fails, followed by one final result step that reports the aggregate failure.
3. Make live CI on-demand
The leading option is a maintainer-applied label such as can be tested or run-cloud-integration.
Recommended semantics:
- The expensive live workflow responds to the
pull_request labeled event, not every synchronize event.
- Applying the full-suite label is a one-shot trigger for the PR head SHA at that moment.
- A maintainer can apply it only to the top PR of a stack. Because that branch contains every lower stack commit, a full run validates the combined stack.
- New commits do not automatically launch another live run. Remove and reapply the label when another run is wanted.
- The workflow may remove the trigger label after enqueueing so it behaves like a button, although that requires narrowly scoped
pull-requests: write permission.
- The full-suite label must override changed-path selection. The top PR diff is relative to its immediate stacked base and does not list changes made by lower PRs, even though those commits are present in the tested checkout.
- Keep the weekday scheduled run as a full sweep against
main.
GitHub label permissions provide the intended approval boundary: users need triage/write access to apply repository labels. Fork PRs still do not receive repository secrets under pull_request; do not switch to pull_request_target and execute untrusted PR code. A maintainer-owned mirror branch is the safe route when a fork needs live validation.
Other trigger options
Manual workflow dispatch
Add workflow_dispatch inputs for PR/ref and scope (all, affected, service, postgres, org, or clickpipes). This is secure and explicit for maintainers, but less discoverable from the PR page and requires care to ensure the exact intended SHA is tested.
PR comment command
A command such as /run-cloud-ci all is convenient and can support scopes, but authorization and secret handling are more complex. An issue_comment/pull_request_target workflow must only validate the commenter and dispatch a separate trusted workflow; it must never directly check out and execute PR code with secrets.
Persistent approval label
Treat the label as approval and continue running on every later push while it remains attached. This resembles the main ClickHouse repository model, but it is less suitable here because repeated pushes to a labeled PR can recreate the same load. One-shot label semantics are preferable for this repository.
Environment approval
Trigger workflows automatically but hold the live job behind a protected GitHub environment. This prevents unapproved API access but leaves many pending workflow runs and becomes awkward during stack refreshes. It is not preferred.
Ready-for-review or merge-ready trigger
Run once when a PR leaves draft or receives approval. This is lightweight but less explicit, and review changes do not always correspond to the exact moment a live Cloud run is useful. It could complement, but should not replace, a manual trigger.
Suggested rollout
- Add retry/backoff, request timeouts, and cleanup hardening.
- Remove CLI-only live triggers and add domain-aware suite selection.
- Add a one-shot maintainer label for a full run, retaining manual and scheduled full runs.
- Optionally add scoped labels or
workflow_dispatch inputs for targeted reruns.
- Observe retry logs, API status distribution, run frequency, and leaked-resource counts before changing concurrency further.
Acceptance criteria
- A CLI-only, ClickStack-only, UDF-only, or backup-only PR update does not launch live Cloud tests.
- A domain-targeted invocation runs only the mapped suites.
- Applying the full-suite label to the top PR of a stack tests that exact head SHA and runs all suites once.
- Subsequent pushes do not automatically rerun the one-shot labeled workflow.
- Transient
429 and retryable 5xx responses show bounded backoff and can recover within the same test run.
- Cleanup uses the same transient retry policy and reports any final failure without losing the original test error.
- Scheduled runs against
main continue to exercise the full suite.
Context
The live Cloud integration workflow is expensive, mutates a shared test organization, and currently runs on every matching PR update. This becomes particularly noisy and unreliable when refreshing a stack of PRs.
Example failure: https://github.com/ClickHouse/clickhousectl/actions/runs/31696362617/job/94434938976?pr=393
That job failed on a one-shot query-endpoint upsert with
503 SERVICE_UNAVAILABLE. It was not related to the PR changes. During the same stacked-PR refresh, many Cloud Integration jobs entered the shared organization together and produced:429 TOO_MANY_REQUESTSwhile creating API keys, updating services, and cleaning up.503 SERVICE_UNAVAILABLEresponses from query-endpoint upserts.404saying the ClickHouse service was currently unavailable.500 Internal errorresponses.The linked job spent roughly four minutes compiling, failed about one minute into the live test, and then spent another 2.5 minutes cleaning up before reporting failure. Rerunning the job repeats all of that work.
Current behavior
.github/workflows/cloud-integration.ymlruns five suites sequentially in one job:The workflow currently triggers for every change under
crates/clickhousectl/src/cloud/**, but it runs onlycargo test -p clickhouse-cloud-api. CLI-only changes therefore launch live API tests that neither compile nor execute the changed CLI code.The API source is now split into domain modules, but every API-domain change still launches every live suite. ClickStack, UDF, and backup changes also launch the workflow even though it has no live suite for those domains.
Existing resilience is limited:
poll_untilretries polling checks at a fixed interval, without jitter or error classification.retry_api_callexists but is used only for the known Postgres read-replica "no backups yet" race.429or5xx.Goals
Proposed work
1. Harden live tests
429, retryable5xx, and transport timeouts.2. Select suites by affected domain
Keep one workflow with a small changed-path classifier rather than duplicating the complete secret-bearing job across several workflows.
Scheduled and explicitly requested full runs should select every suite. Unknown new API source paths should fail closed by selecting all suites.
Selected suites can remain sequential in one job to reuse compilation and avoid creating another traffic burst. Each selected step should still run if an earlier selected suite fails, followed by one final result step that reports the aggregate failure.
3. Make live CI on-demand
The leading option is a maintainer-applied label such as
can be testedorrun-cloud-integration.Recommended semantics:
pull_requestlabeledevent, not everysynchronizeevent.pull-requests: writepermission.main.GitHub label permissions provide the intended approval boundary: users need triage/write access to apply repository labels. Fork PRs still do not receive repository secrets under
pull_request; do not switch topull_request_targetand execute untrusted PR code. A maintainer-owned mirror branch is the safe route when a fork needs live validation.Other trigger options
Manual workflow dispatch
Add
workflow_dispatchinputs for PR/ref and scope (all,affected,service,postgres,org, orclickpipes). This is secure and explicit for maintainers, but less discoverable from the PR page and requires care to ensure the exact intended SHA is tested.PR comment command
A command such as
/run-cloud-ci allis convenient and can support scopes, but authorization and secret handling are more complex. Anissue_comment/pull_request_targetworkflow must only validate the commenter and dispatch a separate trusted workflow; it must never directly check out and execute PR code with secrets.Persistent approval label
Treat the label as approval and continue running on every later push while it remains attached. This resembles the main ClickHouse repository model, but it is less suitable here because repeated pushes to a labeled PR can recreate the same load. One-shot label semantics are preferable for this repository.
Environment approval
Trigger workflows automatically but hold the live job behind a protected GitHub environment. This prevents unapproved API access but leaves many pending workflow runs and becomes awkward during stack refreshes. It is not preferred.
Ready-for-review or merge-ready trigger
Run once when a PR leaves draft or receives approval. This is lightweight but less explicit, and review changes do not always correspond to the exact moment a live Cloud run is useful. It could complement, but should not replace, a manual trigger.
Suggested rollout
workflow_dispatchinputs for targeted reruns.Acceptance criteria
429and retryable5xxresponses show bounded backoff and can recover within the same test run.maincontinue to exercise the full suite.