Skip to content

Commit 5349602

Browse files
committed
feat(demo): one quickstart, three provider modes
## Summary ### Why? The repository had two divergent local paths, and the quickstart documented the wrong one. `make local-submitqueue-start` brings up an all-fake stack — fake change provider, fake CI, **noop merger** — and `doc/howto/QUICKSTART.md` (#591) described that. It reaches `landed` in seconds with no credentials, but nothing is ever pushed, so it proves the pipeline's choreography and nothing about merging. `make local-provider-start` runs the real thing, but was GitHub-only: it hardcoded the token-requiring overlay, so the credential-free provider configuration that already existed could not be reached by hand at all. `PROVIDER=local` failed at `docker compose up` on a missing `GITHUB_TOKEN`, and would have merged nothing even past that, since the overlay it needs bind-mounts a repository the other one has never heard of. The two are not variants of one thing. Faking every edge and merging into a real repository answer different questions, and calling both "local" is what made them hard to keep apart. ### What? `PROVIDER` now names three modes, and is the only thing that changes between them: | `PROVIDER` | A change is | Landing it | Needs | |---|---|---|---| | `fake` (default) | a URI, and nothing else | reports success without touching a repository | nothing | | `git` | a branch in a bare repository on disk | a real fetch, cherry-pick and push | nothing | | `github` | a real pull request | a real push to a real repository | a repository and a token | They are the tiers `PROVIDER-E2E.md` already named, so the documentation collapses into one ladder where every rung takes the same commands. **Provider directories.** `demo/provider/local/` becomes `demo/provider/git/` — both old names were "local" — and gains a `demo-queue` entry so the default `QUEUE` works there. It needs its own `checkoutPath`: Runway refuses two queues that share a checkout with differing merger configuration, and `demo-queue` squash-rebases where `e2e-git-queue` rebases. A new `demo/provider/fake/` spells out the all-fake mode rather than leaving it implied by absent configuration. `e2e-git-queue` is untouched, and `make e2e-git-test` is the gate that proves it. **Overlay per mode.** `PROVIDER_COMPOSE_FILE` becomes a map, since which overlay a mode needs is not something its two config files can express — `github` requires a credential, `git` requires the sandbox mounted, `fake` requires neither. A new `docker-compose.fake.yml` covers the third. **`make demo-pr` works without GitHub.** `createOne` was the only function that touched the provider; it now describes a change and hands it to a `changeSource`. Three implement it: `fakeSource` (no I/O at all — mints a reproducible `git://` URI), `gitSource` (pushes real branches with the pinned `@git//:git`, serializing its commands because one working tree cannot take concurrent checkouts), and `githubSource` (today's REST client, extracted unchanged). `GITHUB_TOKEN` is now read only when it is actually needed. **`tool/gitsandbox`** provisions the bare repository `PROVIDER=git` merges into, idempotently, so a restart keeps whatever landed. `platform/gitexec` locates git and strips the ambient environment, so a developer's hooks or signing key cannot fail a demo. Two fixes fell out of getting `PROVIDER=git` to work at all: - Runway's checkout directory moves to a named volume unless a path is given. It is where git clones, cherry-picks and commits, and on macOS a freshly written loose object read back over a bind mount can report `loose object … is corrupt` — which failed the first land against every new stack. The E2E still passes a path and still gets a bind mount. - The Runway image now creates `/var/runway/checkouts`, so a named volume mounted there starts with a mode the service can write. Without it the service fails at boot with `mkdir: permission denied` whenever it runs as a non-root user. ## Test Plan Every command in the rewritten quickstart was run by hand against a live stack, and the output quoted in it is what it printed. - ✅ `make local-provider-start` (defaults to `fake`) then `make demo-pr` — three changes, no repository, no token, all `landed` - ✅ `PROVIDER=git make local-provider-start` from a clean slate, then `PROVIDER=git make demo-pr` — real branches and multi-file commits, verified with `git -C /tmp/sq-sandbox/sandbox.git log --oneline main` - ✅ `PROVIDER=git make demo-pr STACKED=true` — three changes, exactly one more entry in the target's reflog, so the stack landed atomically - ✅ first land against a brand-new stack, repeatedly, which is the case the named volume fixes - ✅ `make land` by hand on both `fake` and `git`, confirming the fake run lands nothing and the git run puts a commit on the branch - ✅ `make e2e-git-test` after the rename and again after the compose change - ✅ `make test` (104 targets), `make gazelle`, `make fmt` `PROVIDER=github` is unchanged in behaviour but needs a token, so it has not been re-run here; the GitHub source is the previous code path moved behind the interface. ## Note for reviewers `make local-provider-start` with no arguments meant GitHub before this change and means `fake` after it.
1 parent c34c87f commit 5349602

31 files changed

Lines changed: 1701 additions & 234 deletions

Makefile

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ COMPOSE = docker-compose
66

77
# SubmitQueue compose files
88
COMPOSE_FILE = service/submitqueue/docker-compose.yml
9-
PROVIDER_COMPOSE_FILE = service/submitqueue/docker-compose.provider.yml
109
GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml
1110
ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml
1211

@@ -46,12 +45,33 @@ PROTO_PACKAGES = api/base/change api/base/mergestrategy api/base/messagequeue ap
4645
# Set REPO_ROOT for docker-compose
4746
export REPO_ROOT := $(shell pwd)
4847

49-
# Which provider the demo stack targets. Selects a configuration directory rather
50-
# than a code path, so adding a provider means adding a directory — see
48+
# Which provider the demo stack targets, and the only difference between a free
49+
# local run and a live one. Selects a configuration directory rather than a code
50+
# path, so adding a provider is mostly adding a directory — see
5151
# service/submitqueue/demo/provider/README.md.
52-
PROVIDER ?= github
52+
#
53+
# fake a change is a URI; nothing merges anywhere. Needs nothing.
54+
# git branches in a bare repository on disk; real fetch, cherry-pick, push.
55+
# github real pull requests. Needs a repository and GITHUB_TOKEN.
56+
PROVIDER ?= fake
5357
export SQ_PROVIDER_CONFIG_DIR ?= $(REPO_ROOT)/service/submitqueue/demo/provider/$(PROVIDER)
5458

59+
# Which compose overlay each mode needs. This cannot live in the provider
60+
# directory: the two config files say how the services are configured, not what
61+
# has to be mounted or which credential has to be present for them to start.
62+
PROVIDER_COMPOSE_FILE_fake = service/submitqueue/docker-compose.fake.yml
63+
PROVIDER_COMPOSE_FILE_git = service/submitqueue/docker-compose.git.yml
64+
PROVIDER_COMPOSE_FILE_github = service/submitqueue/docker-compose.provider.yml
65+
PROVIDER_COMPOSE_FILE = $(PROVIDER_COMPOSE_FILE_$(PROVIDER))
66+
67+
# Where PROVIDER=git keeps the bare repository it merges into. Outside the
68+
# repository, so a demo leaves nothing in a checkout, and bind-mounted rather
69+
# than kept in a volume so `git log` on the host can show what landed.
70+
#
71+
# SQ_RUNWAY_CHECKOUT_DIR is deliberately not set: unset, Runway's working trees
72+
# live in a named volume instead of on the host. See docker-compose.git.yml.
73+
export SQ_GIT_SANDBOX_DIR ?= /tmp/sq-sandbox
74+
5575
# Defaults for `make land` / `make demo-pr` against the provider demo stack.
5676
DEMO_REPO ?= behinddwalls/sq-demo
5777
COUNT ?= 3
@@ -77,7 +97,7 @@ define assert_clean
7797
fi
7898
endef
7999

80-
.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache tidy tidy-bazel tidy-go help
100+
.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-provider-clean local-provider-start local-provider-stop local-runway-start local-runway-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache tidy tidy-bazel tidy-go help
81101

82102

83103
build: ## Build all services and examples
@@ -172,9 +192,11 @@ clean-proto: ## Clean generated proto files
172192
@rm -f $(foreach p,$(PROTO_PACKAGES),$(p)/protopb/*.pb.go $(p)/protopb/*.pb.yarpc.go)
173193
@echo "Proto clean complete!"
174194

175-
demo-pr: ## Create N PRs in the demo repo, enqueue each as it is created, and watch (COUNT=3 FILES=3 CONCURRENCY=5; needs GITHUB_TOKEN)
195+
demo-pr: ## Create N changes, enqueue each as it is created, and watch (PROVIDER=fake|git|github COUNT=3 FILES=3 CONCURRENCY=5)
176196
@$(BAZEL) run //service/submitqueue/demo/pr -- \
197+
-provider $(PROVIDER) \
177198
-repo $(DEMO_REPO) \
199+
-sandbox-dir $(SQ_GIT_SANDBOX_DIR) \
178200
-count $(COUNT) \
179201
-files $(FILES) \
180202
-concurrency $(CONCURRENCY) \
@@ -301,25 +323,50 @@ local-submitqueue-gateway-stop: ## Stop Gateway service
301323
@$(COMPOSE) -f $(GATEWAY_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
302324
@echo "Gateway services stopped."
303325

304-
local-provider-start: build-all-linux ## Start the full stack against a real provider (PROVIDER=github; needs GITHUB_TOKEN)
326+
local-provider-start: build-all-linux ## Start the full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN)
305327
@echo "Starting full stack against provider '$(PROVIDER)' ($(SQ_PROVIDER_CONFIG_DIR))..."
306328
@test -f "$(SQ_PROVIDER_CONFIG_DIR)/merge.yaml" \
307329
|| { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/merge.yaml not found"; exit 2; }
308-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(PROVIDER_LOCAL_PROJECT) up -d --build --wait
330+
@test -n "$(PROVIDER_COMPOSE_FILE)" \
331+
|| { echo "Provider '$(PROVIDER)' has no compose overlay; add PROVIDER_COMPOSE_FILE_$(PROVIDER) to the Makefile"; exit 2; }
332+
@if [ "$(PROVIDER)" = "git" ]; then \
333+
$(BAZEL) run //tool/gitsandbox -- -sandbox-dir "$(SQ_GIT_SANDBOX_DIR)" || exit 1; \
334+
fi
335+
@# Rootless Docker maps container root to the host user; rootful Docker needs
336+
@# the host UID:GID explicitly, or the services write files into the sandbox
337+
@# bind mount that the host user cannot then read or remove. Resolved here
338+
@# rather than at parse time, so `make help` does not shell out to Docker.
339+
@set -e; \
340+
if docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'; then \
341+
export SQ_CONTAINER_USER=0:0; \
342+
else \
343+
export SQ_CONTAINER_USER=$$(id -u):$$(id -g); \
344+
fi; \
345+
$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(PROVIDER_LOCAL_PROJECT) up -d --build --wait
309346
@echo "Applying database schemas..."
310347
@$(MAKE) -s local-init-submitqueue-schemas SUBMITQUEUE_LOCAL_PROJECT=$(PROVIDER_LOCAL_PROJECT)
311348
@echo ""
312349
@echo "✅ Stack is running against provider '$(PROVIDER)'."
313350
@echo ""
314351
@echo "Gateway gRPC port: $$(docker port $(PROVIDER_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')"
352+
@if [ "$(PROVIDER)" = "git" ]; then \
353+
echo "Merge target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \
354+
fi
315355
@echo ""
316-
@echo "Land a change with:"
317-
@echo " make land PR=https://github.com/owner/repo/pull/7 GATEWAY_ADDR=localhost:<gateway port>"
356+
@echo "Generate traffic with:"
357+
@echo " make demo-pr GATEWAY_ADDR=localhost:<gateway port>"
318358

319-
local-provider-stop: ## Stop the provider demo stack
359+
local-provider-stop: ## Stop the provider demo stack (keeps PROVIDER=git's sandbox repository)
320360
@echo "Stopping provider stack..."
321361
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(PROVIDER_LOCAL_PROJECT) down
322362
@echo "Provider stack stopped."
363+
@if [ -d "$(SQ_GIT_SANDBOX_DIR)" ]; then \
364+
echo "Sandbox repository left at $(SQ_GIT_SANDBOX_DIR); remove it with 'make local-provider-clean'."; \
365+
fi
366+
367+
local-provider-clean: local-provider-stop ## Stop the provider stack and delete PROVIDER=git's sandbox repository
368+
@rm -rf "$(SQ_GIT_SANDBOX_DIR)"
369+
@echo "Removed $(SQ_GIT_SANDBOX_DIR)."
323370

324371
local-init-submitqueue-schemas: ## Manually apply all database schemas
325372
@echo "Applying storage schema to mysql-app..."

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,34 @@ Cross-domain Go code (errors, metrics, consumer framework, HTTP helpers, shared
1515

1616
## Quick Start
1717

18-
Land a change and watch it reach `landed`. Requires Docker and Docker Compose, and nothing else — no repository, no account, no token. See [Development Setup](doc/howto/DEVELOPMENT.md) for full prerequisites.
18+
Put traffic through the queue and watch it land. Requires Docker and Docker Compose, and nothing else — no repository, no account, no token. See [Development Setup](doc/howto/DEVELOPMENT.md) for full prerequisites.
1919

2020
```bash
2121
# Start the full stack (Gateway + Orchestrator + Runway + MySQL)
22-
make local-submitqueue-start
22+
make local-provider-start
2323

2424
# Compose publishes a random host port; the line above prints it, as does this
2525
make local-submitqueue-ps
2626
export GATEWAY_ADDR=localhost:<gateway port>
2727

28-
# Submit a change, and follow the receipt it returns
29-
make land QUEUE=test-queue \
30-
URI='git://git.example.com/demo/refs%2Fheads%2Ffeature-a/1111111111111111111111111111111111111111'
31-
make land-status QUEUE=test-queue SQID=test-queue/1
28+
# Create changes, enqueue each as it is created, and watch them settle
29+
make demo-pr
3230

3331
# Stop services
3432
make local-stop
3533
```
3634

37-
Every integration at the edges is faked — the change provider, CI, and the merge itself — so the run is free and finishes in seconds. The queue's own logic is real: validation, batching, conflict analysis, and speculation all run, and the request log records the full trail from `accepted` to `landed`. Nothing is pushed to any repository.
35+
`PROVIDER` decides where changes come from and what landing them does, and it is the only thing that changes between them:
3836

39-
[Quickstart](doc/howto/QUICKSTART.md) explains the change URI, how to make a change fail on demand, and what this does and does not prove. From there, `make e2e-git-test` adds a real git merge (still no credentials), and [PROVIDER-E2E.md](doc/howto/PROVIDER-E2E.md) adds a live provider. See [service/README.md](service/README.md) for running individual services and clients.
37+
| `PROVIDER` | A change is | Landing it | Needs |
38+
|---|---|---|---|
39+
| **`fake`** (default) | a URI, and nothing else | reports success without touching a repository | nothing |
40+
| **`git`** | a branch in a bare repository on disk | a real fetch, cherry-pick and push | nothing |
41+
| **`github`** | a real pull request | a real push to a real repository | a repository and a token |
42+
43+
The queue's own logic is real in all three: validation, batching, conflict analysis, speculation, and a request log recording the full trail from `accepted` to `landed`. `PROVIDER=git make local-provider-start` is the first rung where a commit actually reaches a branch, and it still needs no credential.
44+
45+
[Quickstart](doc/howto/QUICKSTART.md) walks the ladder — including proving a change landed with `git log`, and making one fail on demand. [PROVIDER-E2E.md](doc/howto/PROVIDER-E2E.md) covers the last rung against a live provider. See [service/README.md](service/README.md) for running individual services and clients.
4046

4147
## Documentation
4248

doc/howto/DEVELOPMENT.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,14 @@ make local-submitqueue-start
6464
make local-submitqueue-ps
6565
export GATEWAY_ADDR=localhost:<gateway port>
6666

67-
# 4. Land a change and follow it to a terminal status
68-
make land QUEUE=test-queue \
69-
URI='git://git.example.com/demo/refs%2Fheads%2Ffeature-a/1111111111111111111111111111111111111111'
70-
make land-status QUEUE=test-queue SQID=test-queue/1
67+
# 4. Create changes, enqueue them, and watch them land
68+
make demo-pr
7169

7270
# 5. Stop services
7371
make local-stop
7472
```
7573

76-
[QUICKSTART.md](QUICKSTART.md) walks through the same run in detail — what the change URI has to look like, how to make a change fail on demand, and which parts of the pipeline are faked.
74+
[QUICKSTART.md](QUICKSTART.md) walks through the same run in detail, and on to `PROVIDER=git`, which lands real commits into a repository on disk — still with no credential.
7775

7876
If any step fails, see [Troubleshooting](#troubleshooting) below.
7977

doc/howto/PROVIDER-E2E.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
How to run the whole pipeline against a live repository and watch a change actually land. This is the manual tier: it needs a scratch repository and a token, which is why it is not automated in CI.
44

5-
Two tiers below it run with no credentials at all and cover most of what can break:
5+
Two tiers below it run with no credentials at all and cover most of what can break. They are the same `PROVIDER` values the demo stack takes, so each tier is also something you can drive by hand — see [QUICKSTART.md](QUICKSTART.md):
66

7-
| | Command | Covers | Secrets |
8-
|---|---|---|---|
9-
| Tier 1 | `make e2e-test` | pipeline choreography, on the noop merger | none |
10-
| Tier 2 | `make e2e-git-test` | real git: provisioning, cherry-pick, atomic push, head-branch updates | none |
11-
| Tier 3 | this document | the change provider: reading metadata, its CI, changes marked merged | a token |
7+
| | Automated | By hand | Covers | Secrets |
8+
|---|---|---|---|---|
9+
| Tier 1 | `--test_filter=TestE2EIntegration` | `PROVIDER=fake` | pipeline choreography, on the noop merger | none |
10+
| Tier 2 | `make e2e-git-test` | `PROVIDER=git` | real git: provisioning, cherry-pick, atomic push, head-branch updates | none |
11+
| Tier 3 || `PROVIDER=github`, this document | the change provider: reading metadata, its CI, changes marked merged | a token |
12+
13+
`make e2e-test` is not tier 1. It runs `//test/e2e/...` unfiltered — every hermetic E2E in the repository, across all three domains, tier 2 included — which is what CI wants and more than you want while iterating on one tier. The two tiers above share a single test target and are told apart by their suite, so tier 1 alone is that target with `--test_filter=TestE2EIntegration`; `make e2e-git-test` is the same thing filtered to `TestGitMergeE2E`.
1214

1315
Run tier 2 first. If the merge machinery is broken, it will say so in under a minute and without a repository to clean up afterwards.
1416

@@ -51,7 +53,7 @@ export GITHUB_TOKEN=ghp_...
5153
make local-provider-start PROVIDER=github
5254
```
5355

54-
The stack refuses to start without the token rather than falling back to the fake integrations. That is deliberate: a stack that silently runs on fakes reports changes as landed without having gone near the provider, which is a much worse way to find out.
56+
`PROVIDER=github` is required here: the default is `fake`, which runs the same stack against nothing at all. The token is required too, rather than defaulted — a stack that silently falls back to the fake integrations reports changes as landed without having gone near the provider, which is a much worse way to find out.
5557

5658
`local-provider-start` prints the gateway's port. Export it so the commands below are shorter:
5759

@@ -94,14 +96,16 @@ The order of `PRS` is the stack order. All three land as **one push** to `main`
9496
Opening pull requests by hand gets old fast. `demo-pr` creates them, enqueues them, and shows you where each one is:
9597

9698
```bash
97-
make demo-pr # 3 independent PRs, each enqueued as it is created
98-
make demo-pr COUNT=8 # more traffic
99-
make demo-pr FILES=8 # wider changes, more files per PR
100-
make demo-pr CONCURRENCY=1 # create them one at a time
101-
make demo-pr STACKED=true # one stack, enqueued as a single request
102-
make demo-pr LAND=false # create only, print the land command
99+
PROVIDER=github make demo-pr # 3 independent PRs, each enqueued as it is created
100+
PROVIDER=github make demo-pr COUNT=8 # more traffic
101+
PROVIDER=github make demo-pr FILES=8 # wider changes, more files per PR
102+
PROVIDER=github make demo-pr CONCURRENCY=1 # create them one at a time
103+
PROVIDER=github make demo-pr STACKED=true # one stack, enqueued as a single request
104+
PROVIDER=github make demo-pr LAND=false # create only, print the land command
103105
```
104106

107+
`PROVIDER` selects how `demo-pr` creates changes as well as what the stack merges with, and the two must agree: the default `fake` mints URIs that point at nothing, which a stack wired to GitHub cannot fetch. Export it once for the session if repeating it grows tiresome.
108+
105109
Each pull request is enqueued the moment it exists, so the queue is already working on the first while the last is still being opened. That overlap is the point: a queue holding one request at a time never batches, never analyzes a conflict against another batch, and never speculates. Nothing is awaited until every request is in.
106110

107111
Independent pull requests are created **five at a time** by default (`CONCURRENCY`). Opening one is several round trips — a branch, a commit per file, the pull request itself — so creating them serially was most of what a large run spent its time on, and it delayed the overlap the demo exists to show. A stack ignores the setting: each of its changes is based on the branch before it, so the next cannot be cut until the previous head exists. Lower it if the provider starts refusing bursts.

0 commit comments

Comments
 (0)