Skip to content

Commit 5dd3ebb

Browse files
committed
test(submitqueue): land real GitHub PRs in CI e2e and integration tests
## Summary ### Why? The merger's unit tests use a fake, which only encodes our reading of GitHub's docs. These suites check the merger, and SubmitQueue's whole land path, against real github.com on every CI run. ### What? - `TestGitHubLandE2E`: gateway → orchestrator (GitHub change provider) → Runway (GitHub merger). It lands a single PR and a stack, and rejects PRs that are not a stack. - Merger integration test: a full stack with redelivery, a partial stack, non-stack lists, and a moved head. - Shared `test/testutil/githubtestrepo` fixtures, which skip unless `SQ_GITHUB_TOKEN` and `SQ_GITHUB_TEST_REPO` are set. - CI: the `e2e` job and a new `merger-integration-test` job take the `SQ_TEST_REPO_TOKEN` repository secret (limited to the test repo). The secrets guard allows it as the only exception. - `e2e-github-queue` is added to `queues.yaml` and to `MQ_TENANTS`. ## Test Plan ✅ `TestGitHubLandE2E` 3/3 and the merger integration test 4/4 against `behinddwalls/sq-demo`; no fixtures left behind ✅ `make lint check-gazelle check-tidy`
1 parent a595fca commit 5dd3ebb

15 files changed

Lines changed: 851 additions & 5 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ jobs:
120120
- uses: ./.github/actions/setup
121121

122122
- name: Run E2E tests
123+
env:
124+
# Lands real pull requests in the test repository; see the secrets
125+
# guard in workflow-security.
126+
SQ_GITHUB_TOKEN: ${{ secrets.SQ_TEST_REPO_TOKEN }}
127+
SQ_GITHUB_TEST_REPO: ${{ vars.SQ_GITHUB_TEST_REPO || 'behinddwalls/sq-demo' }}
128+
# Fail rather than skip when the secret is missing, except on fork PRs,
129+
# which GitHub never gives secrets to.
130+
SQ_GITHUB_TEST_REQUIRED: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
123131
run: make e2e-test
124132

125133
- name: Upload Bazel failure logs
@@ -197,6 +205,33 @@ jobs:
197205
with:
198206
target: //test/integration/extension/messagequeue/...
199207

208+
merger-integration-test:
209+
name: Merger Extension Test
210+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
211+
runs-on: ubuntu-latest
212+
steps:
213+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
214+
with:
215+
# This job executes untrusted PR code (make build/test/lint). Don't
216+
# leave the GITHUB_TOKEN in the workspace git config while it runs.
217+
persist-credentials: false
218+
- uses: ./.github/actions/setup
219+
220+
- name: Run merger extension tests
221+
env:
222+
# Lands real pull requests in the test repository; see the secrets
223+
# guard in workflow-security.
224+
SQ_GITHUB_TOKEN: ${{ secrets.SQ_TEST_REPO_TOKEN }}
225+
SQ_GITHUB_TEST_REPO: ${{ vars.SQ_GITHUB_TEST_REPO || 'behinddwalls/sq-demo' }}
226+
# Fail rather than skip when the secret is missing, except on fork PRs,
227+
# which GitHub never gives secrets to.
228+
SQ_GITHUB_TEST_REQUIRED: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
229+
run: make integration-test-runway-merger
230+
231+
- name: Upload Bazel failure logs
232+
if: ${{ failure() }}
233+
uses: ./.github/actions/upload-testlogs
234+
200235
storage-integration-test:
201236
name: Storage Extension Test
202237
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
@@ -272,11 +307,17 @@ jobs:
272307
# real defenses remain (a) secrets scoped to a main-only Environment so a
273308
# PR-triggered job cannot obtain them, and (b) CODEOWNERS review on
274309
# .github/. GITHUB_TOKEN (least-privilege, read-only here) is allowlisted.
310+
#
311+
# SQ_TEST_REPO_TOKEN is the one deliberate exception. The live GitHub
312+
# tests (e2e and merger extension) merge real pull requests in a separate
313+
# test repository, which GITHUB_TOKEN cannot reach. It is a fine-grained
314+
# token limited to that repository, so a PR that exfiltrated it could only
315+
# reach that repository.
275316
- name: Guard — no repository secrets on the untrusted-code path
276317
run: |
277318
hits="$(grep -rnE '\$\{\{[^}]*secrets\.' \
278319
.github/workflows/ci.yml .github/actions \
279-
| grep -vE 'secrets\.GITHUB_TOKEN' || true)"
320+
| grep -vE 'secrets\.(GITHUB_TOKEN|SQ_TEST_REPO_TOKEN)' || true)"
280321
if [ -n "$hits" ]; then
281322
echo "::error::Repository secret referenced on the untrusted-code CI path (ci.yml / composite actions):" >&2
282323
echo "$hits" >&2
@@ -308,6 +349,7 @@ jobs:
308349
- orchestrator-integration-test
309350
- counter-integration-test
310351
- queue-integration-test
352+
- merger-integration-test
311353
- storage-integration-test
312354
- consumer-integration-test
313355
- workflow-security

‎Makefile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ integration-test-extensions: ## Run extension integration tests (runs in paralle
295295
@echo "Running extension integration tests (parallel)..."
296296
@$(BAZEL) test //test/integration/submitqueue/extension/... //test/integration/extension/... --test_output=errors
297297

298+
integration-test-runway-merger: ## Run Runway merger extension tests (GitHub ones need SQ_GITHUB_TOKEN and SQ_GITHUB_TEST_REPO=owner/repo, else skip)
299+
@echo "Running Runway merger extension tests..."
300+
@$(BAZEL) test //test/integration/runway/... --test_output=errors
301+
298302
integration-test-submitqueue-gateway: ## Run Gateway integration tests
299303
@echo "Running Gateway integration tests..."
300304
@$(BAZEL) test //test/integration/submitqueue/gateway:go_default_test --test_output=streamed

‎doc/howto/TESTING.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ make build-all-linux # Build Linux binaries for the local docker-
8484
- Containers: Each suite's required services and dependencies; SubmitQueue E2E includes Gateway, Orchestrator, Runway, and MySQL
8585
- Tests end-to-end behavior, including cross-service communication where applicable
8686

87+
### Live GitHub Tests
88+
89+
Two suites land real pull requests on github.com: `TestGitHubLandE2E` (in `make e2e-test`) and the Runway GitHub merger extension test (`make integration-test-runway-merger`). They run only when `SQ_GITHUB_TOKEN` and `SQ_GITHUB_TEST_REPO=owner/repo` are set, and skip otherwise, so every other run is unaffected. The token needs write access to the test repository; everything the tests open is named under `sq-it/` and removed afterwards, while what they merge stays on its default branch.
90+
91+
```bash
92+
SQ_GITHUB_TOKEN=$(gh auth token) SQ_GITHUB_TEST_REPO=behinddwalls/sq-demo make e2e-test
93+
```
94+
95+
CI supplies the token from the `SQ_TEST_REPO_TOKEN` repository secret, a token limited to the test repository. Fork pull requests receive no secrets, so the suites skip there. Every other CI run sets `SQ_GITHUB_TEST_REQUIRED=true`, which turns a missing secret into a failure; an expired or revoked token fails regardless.
96+
8797
### How Automated Tests Work
8898

8999
Tests use **docker-compose** via `ComposeStack` to spin up containers automatically:

‎runway/extension/merger/github/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ Each step's strategy maps onto a GitHub merge method — `REBASE` to `rebase`, `
1212

1313
Each URI's output is the merge commit GitHub records for its pull request: the squash commit, the merge commit, or, for a rebase, the last commit the rebase created for that pull request. That is one output per URI, where the git merger reports one per created commit under `REBASE`. It is read from the pull request's `merged` issue event, because API version 2026-03-10 no longer reports `merge_commit_sha` on a merged pull request. GitHub reports a stack merge settled a moment before every pull request in it shows its merge, so the merger re-reads until each is recorded.
1414

15+
## Live tests
16+
17+
Two suites run against a real repository whenever `SQ_GITHUB_TOKEN` and `SQ_GITHUB_TEST_REPO=owner/repo` are set, and skip otherwise (see [`test/testutil/githubtestrepo`](../../../../test/testutil/githubtestrepo)). Both open, stack and merge throwaway pull requests and check what GitHub recorded:
18+
19+
- [`test/integration/runway/extension/merger/github`](../../../../test/integration/runway/extension/merger/github) drives this merger on its own (`make integration-test-runway-merger`).
20+
- `TestGitHubLandE2E` in [`test/e2e/submitqueue`](../../../../test/e2e/submitqueue) lands pull requests through the whole stack — gateway, orchestrator with the GitHub change provider, and Runway with this merger (`make e2e-test`).
21+
22+
CI runs both in its usual e2e and merger extension jobs, with the token from the `SQ_TEST_REPO_TOKEN` repository secret.
23+
1524
## What a step must be
1625

1726
The URIs of a step must be something GitHub will land as one stack onto the target, and anything else is refused as an invalid request:

‎service/submitqueue/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
exports_files(
22
[
33
"docker-compose.git.yml",
4+
"docker-compose.provider.yml",
45
"docker-compose.yml",
56
],
67
visibility = ["//visibility:public"],

‎service/submitqueue/docker-compose.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ services:
7777
# Level for the queue's own logs; info by default so its per-message
7878
# chatter does not bury the rest of the service at debug.
7979
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
80-
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
80+
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,e2e-github-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
8181
# Path to YAML queue configuration baked into the image
8282
- QUEUE_CONFIG_PATH=/app/queues.yaml
8383
# Stable subscriber name for the request-log consumer
@@ -110,7 +110,7 @@ services:
110110
# Level for the queue's own logs; info by default so its per-message
111111
# chatter does not bury the rest of the service at debug.
112112
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
113-
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
113+
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,e2e-github-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
114114
- HOSTNAME=orchestrator-dev
115115
# Consumer-gate state shared with the host (see header comment)
116116
- CONSUMER_GATE_DIR=/var/submitqueue/consumergate
@@ -143,7 +143,7 @@ services:
143143
# Level for the queue's own logs; info by default so its per-message
144144
# chatter does not bury the rest of the service at debug.
145145
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
146-
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
146+
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,e2e-github-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
147147
- HOSTNAME=runway-dev
148148
# Consumer-gate state shared with the host (see header comment)
149149
- CONSUMER_GATE_DIR=/var/submitqueue/consumergate

‎service/submitqueue/gateway/server/docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ services:
6666
# Level for the queue's own logs; info by default so its per-message
6767
# chatter does not bury the rest of the service at debug.
6868
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
69-
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
69+
- MQ_TENANTS=${MQ_TENANTS:-test-queue,e2e-test-queue,e2e-cancel-queue,e2e-chain-queue,e2e-redelivery-queue,e2e-strand-queue,e2e-conflict-error-queue,e2e-git-queue,e2e-github-queue,demo-queue,e2e-respeculate-queue,file-overlap-queue}
7070
# Path to YAML queue configuration baked into the image
7171
- QUEUE_CONFIG_PATH=/app/queues.yaml
7272
# Stable subscriber name for the request-log consumer

‎service/submitqueue/gateway/server/queues.yaml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ queues:
2424
# Used by the hermetic git E2E, where Runway is wired to a real git merger
2525
# against a bare repository. See service/submitqueue/demo/provider/git.
2626
- name: e2e-git-queue
27+
# Used by the GitHub E2E, which lands real pull requests in a test
28+
# repository through the GitHub API. Its provider configuration is generated
29+
# by the test (test/e2e/submitqueue/github_suite_test.go).
30+
- name: e2e-github-queue
2731
# Used by the provider demo stack (make local-submitqueue-start) in every mode —
2832
# fake, git, and github. See service/submitqueue/demo/provider and
2933
# doc/howto/QUICKSTART.md.

‎test/e2e/submitqueue/BUILD.bazel‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go_test(
55
srcs = [
66
"fake_demo_test.go",
77
"git_suite_test.go",
8+
"github_suite_test.go",
89
"harness_test.go",
910
"suite_test.go",
1011
],
@@ -13,6 +14,7 @@ go_test(
1314
"//platform/extension/messagequeue/mysql/schema",
1415
"//service/runway/server:docker_test_context",
1516
"//service/submitqueue:docker-compose.git.yml",
17+
"//service/submitqueue:docker-compose.provider.yml",
1618
"//service/submitqueue:docker-compose.yml",
1719
"//service/submitqueue/demo/provider/git:config",
1820
"//service/submitqueue/demo/requests",
@@ -30,6 +32,13 @@ go_test(
3032
env = {
3133
"SUBMITQUEUE_TEST_GIT": "$(location @git//:git)",
3234
},
35+
# The GitHub suite lands real pull requests in a test repository when
36+
# these are set, and skips otherwise (see test/testutil/githubtestrepo).
37+
env_inherit = [
38+
"SQ_GITHUB_TEST_REPO",
39+
"SQ_GITHUB_TEST_REQUIRED",
40+
"SQ_GITHUB_TOKEN",
41+
],
3342
tags = [
3443
"e2e",
3544
"integration",
@@ -58,6 +67,7 @@ go_test(
5867
"//submitqueue/orchestrator/core/batch:go_default_library",
5968
"//submitqueue/orchestrator/extension/storage/mysql:go_default_library",
6069
"//test/testutil:go_default_library",
70+
"//test/testutil/githubtestrepo:go_default_library",
6171
"@com_github_stretchr_testify//assert:go_default_library",
6272
"@com_github_stretchr_testify//require:go_default_library",
6373
"@com_github_stretchr_testify//suite:go_default_library",

0 commit comments

Comments
 (0)