Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ jobs:
- uses: ./.github/actions/setup

- name: Run E2E tests
env:
# Lands real pull requests in the test repository; see the secrets
# guard in workflow-security.
SQ_GITHUB_TOKEN: ${{ secrets.SQ_TEST_REPO_TOKEN }}
SQ_GITHUB_TEST_REPO: ${{ vars.SQ_GITHUB_TEST_REPO }}
# Fail rather than skip when the secret is missing, but only in a
# repository that set up a test repo (forks without one skip), and
# not on fork PRs, which GitHub never gives secrets to.
SQ_GITHUB_TEST_REQUIRED: ${{ vars.SQ_GITHUB_TEST_REPO != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
run: make e2e-test

- name: Upload Bazel failure logs
Expand Down Expand Up @@ -197,6 +206,34 @@ jobs:
with:
target: //test/integration/extension/messagequeue/...

merger-integration-test:
name: Merger Extension Test
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# This job executes untrusted PR code (make build/test/lint). Don't
# leave the GITHUB_TOKEN in the workspace git config while it runs.
persist-credentials: false
- uses: ./.github/actions/setup

- name: Run merger extension tests
env:
# Lands real pull requests in the test repository; see the secrets
# guard in workflow-security.
SQ_GITHUB_TOKEN: ${{ secrets.SQ_TEST_REPO_TOKEN }}
SQ_GITHUB_TEST_REPO: ${{ vars.SQ_GITHUB_TEST_REPO }}
# Fail rather than skip when the secret is missing, but only in a
# repository that set up a test repo (forks without one skip), and
# not on fork PRs, which GitHub never gives secrets to.
SQ_GITHUB_TEST_REQUIRED: ${{ vars.SQ_GITHUB_TEST_REPO != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
run: make integration-test-runway-merger

- name: Upload Bazel failure logs
if: ${{ failure() }}
uses: ./.github/actions/upload-testlogs

storage-integration-test:
name: Storage Extension Test
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
Expand Down Expand Up @@ -272,11 +309,17 @@ jobs:
# real defenses remain (a) secrets scoped to a main-only Environment so a
# PR-triggered job cannot obtain them, and (b) CODEOWNERS review on
# .github/. GITHUB_TOKEN (least-privilege, read-only here) is allowlisted.
#
# SQ_TEST_REPO_TOKEN is the one deliberate exception. The live GitHub
# tests (e2e and merger extension) merge real pull requests in a separate
# test repository, which GITHUB_TOKEN cannot reach. It is a fine-grained
# token limited to that repository, so a PR that exfiltrated it could only
# reach that repository.
- name: Guard — no repository secrets on the untrusted-code path
run: |
hits="$(grep -rnE '\$\{\{[^}]*secrets\.' \
.github/workflows/ci.yml .github/actions \
| grep -vE 'secrets\.GITHUB_TOKEN' || true)"
| grep -vE 'secrets\.(GITHUB_TOKEN|SQ_TEST_REPO_TOKEN)' || true)"
if [ -n "$hits" ]; then
echo "::error::Repository secret referenced on the untrusted-code CI path (ci.yml / composite actions):" >&2
echo "$hits" >&2
Expand Down Expand Up @@ -308,6 +351,7 @@ jobs:
- orchestrator-integration-test
- counter-integration-test
- queue-integration-test
- merger-integration-test
- storage-integration-test
- consumer-integration-test
- workflow-security
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ integration-test-extensions: ## Run extension integration tests (runs in paralle
@echo "Running extension integration tests (parallel)..."
@$(BAZEL) test //test/integration/submitqueue/extension/... //test/integration/extension/... --test_output=errors

integration-test-runway-merger: ## Run Runway merger extension tests (GitHub ones need SQ_GITHUB_TOKEN and SQ_GITHUB_TEST_REPO=owner/repo, else skip)
@echo "Running Runway merger extension tests..."
@$(BAZEL) test //test/integration/runway/... --test_output=errors

integration-test-submitqueue-gateway: ## Run Gateway integration tests
@echo "Running Gateway integration tests..."
@$(BAZEL) test //test/integration/submitqueue/gateway:go_default_test --test_output=streamed
Expand Down
21 changes: 21 additions & 0 deletions doc/howto/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ make build-all-linux # Build Linux binaries for the local docker-
- Containers: Each suite's required services and dependencies; SubmitQueue E2E includes Gateway, Orchestrator, Runway, and MySQL
- Tests end-to-end behavior, including cross-service communication where applicable

### Live GitHub Tests

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.

```bash
SQ_GITHUB_TOKEN=$(gh auth token) SQ_GITHUB_TEST_REPO=<owner>/<repo> make e2e-test
```

CI takes the repository from the `SQ_GITHUB_TEST_REPO` repository variable and the token from the `SQ_TEST_REPO_TOKEN` repository secret. In a repository that sets the variable, every CI run except a fork pull request sets `SQ_GITHUB_TEST_REQUIRED=true`, which turns a missing secret into a failure; an expired or revoked token fails regardless. Fork pull requests receive no secrets, and repositories without the variable skip the suites.

#### Use your own test repository

Any contributor can run these suites against a repository of their own:

1. Create a repository on github.com with a default branch (an initial commit is enough). It must allow squash and rebase merges, and its default branch must not require reviews or status checks, since the tests merge directly.
2. Create a token that can write to it: a fine-grained token limited to that repository with read and write access to contents, pull requests and issues, or simply `gh auth token`.
3. Run locally with `SQ_GITHUB_TOKEN` and `SQ_GITHUB_TEST_REPO` set, as above.
4. For CI in your fork, set the `SQ_GITHUB_TEST_REPO` repository variable and the `SQ_TEST_REPO_TOKEN` repository secret in the fork's Actions settings. Runs in the fork then exercise your test repository.

Every run merges a few small files under `sq-it/` into the test repository's default branch, so use a repository that exists for this.

### How Automated Tests Work

Tests use **docker-compose** via `ComposeStack` to spin up containers automatically:
Expand Down
9 changes: 9 additions & 0 deletions runway/extension/merger/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ Each step's strategy maps onto a GitHub merge method — `REBASE` to `rebase`, `

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.

## Live tests

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:

- [`test/integration/runway/extension/merger/github`](../../../../test/integration/runway/extension/merger/github) drives this merger on its own (`make integration-test-runway-merger`).
- `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`).

CI runs both in its usual e2e and merger extension jobs, with the token from the `SQ_TEST_REPO_TOKEN` repository secret.

## What a step must be

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:
Expand Down
1 change: 1 addition & 0 deletions service/submitqueue/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
exports_files(
[
"docker-compose.git.yml",
"docker-compose.provider.yml",
"docker-compose.yml",
],
visibility = ["//visibility:public"],
Expand Down
6 changes: 3 additions & 3 deletions service/submitqueue/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ services:
# Level for the queue's own logs; info by default so its per-message
# chatter does not bury the rest of the service at debug.
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
- 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}
- 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}
# Path to YAML queue configuration baked into the image
- QUEUE_CONFIG_PATH=/app/queues.yaml
# Stable subscriber name for the request-log consumer
Expand Down Expand Up @@ -110,7 +110,7 @@ services:
# Level for the queue's own logs; info by default so its per-message
# chatter does not bury the rest of the service at debug.
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
- 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}
- 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}
- HOSTNAME=orchestrator-dev
# Consumer-gate state shared with the host (see header comment)
- CONSUMER_GATE_DIR=/var/submitqueue/consumergate
Expand Down Expand Up @@ -143,7 +143,7 @@ services:
# Level for the queue's own logs; info by default so its per-message
# chatter does not bury the rest of the service at debug.
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
- 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}
- 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}
- HOSTNAME=runway-dev
# Consumer-gate state shared with the host (see header comment)
- CONSUMER_GATE_DIR=/var/submitqueue/consumergate
Expand Down
2 changes: 1 addition & 1 deletion service/submitqueue/gateway/server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ services:
# Level for the queue's own logs; info by default so its per-message
# chatter does not bury the rest of the service at debug.
- QUEUE_LOG_LEVEL=${QUEUE_LOG_LEVEL:-}
- 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}
- 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}
# Path to YAML queue configuration baked into the image
- QUEUE_CONFIG_PATH=/app/queues.yaml
# Stable subscriber name for the request-log consumer
Expand Down
4 changes: 4 additions & 0 deletions service/submitqueue/gateway/server/queues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ queues:
# Used by the hermetic git E2E, where Runway is wired to a real git merger
# against a bare repository. See service/submitqueue/demo/provider/git.
- name: e2e-git-queue
# Used by the GitHub E2E, which lands real pull requests in a test
# repository through the GitHub API. Its provider configuration is generated
# by the test (test/e2e/submitqueue/github_suite_test.go).
- name: e2e-github-queue
# Used by the provider demo stack (make local-submitqueue-start) in every mode —
# fake, git, and github. See service/submitqueue/demo/provider and
# doc/howto/QUICKSTART.md.
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/submitqueue/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go_test(
srcs = [
"fake_demo_test.go",
"git_suite_test.go",
"github_suite_test.go",
"harness_test.go",
"suite_test.go",
],
Expand All @@ -13,6 +14,7 @@ go_test(
"//platform/extension/messagequeue/mysql/schema",
"//service/runway/server:docker_test_context",
"//service/submitqueue:docker-compose.git.yml",
"//service/submitqueue:docker-compose.provider.yml",
"//service/submitqueue:docker-compose.yml",
"//service/submitqueue/demo/provider/git:config",
"//service/submitqueue/demo/requests",
Expand All @@ -30,6 +32,13 @@ go_test(
env = {
"SUBMITQUEUE_TEST_GIT": "$(location @git//:git)",
},
# The GitHub suite lands real pull requests in a test repository when
# these are set, and skips otherwise (see test/testutil/githubtestrepo).
env_inherit = [
"SQ_GITHUB_TEST_REPO",
"SQ_GITHUB_TEST_REQUIRED",
"SQ_GITHUB_TOKEN",
],
tags = [
"e2e",
"integration",
Expand Down Expand Up @@ -58,6 +67,7 @@ go_test(
"//submitqueue/orchestrator/core/batch:go_default_library",
"//submitqueue/orchestrator/extension/storage/mysql:go_default_library",
"//test/testutil:go_default_library",
"//test/testutil/githubtestrepo:go_default_library",
"@com_github_stretchr_testify//assert:go_default_library",
"@com_github_stretchr_testify//require:go_default_library",
"@com_github_stretchr_testify//suite:go_default_library",
Expand Down
Loading
Loading