Cross-Repository CI Integration
This repository supports triggering integration tests from external repositories
via GitHub’s repository_dispatch mechanism. When CI runs on a PR in an
integrated repository, that repository dispatches an event here, which runs the
full integration test suite using the PR’s commit. Results are reported back as
status checks on the originating PR.
Integration testing is currently set up for the zallet, zebrad, and
[zaino] repositories.
How it works
The integration has two sides: the requesting repository (e.g.
ZcashFoundation/zebra) and this integration-tests repository
(zcash/integration-tests). Two GitHub Apps are used, one for each direction of
communication:
- Dispatch app: This is an app owned by the requesting org that is manually
installed on
zcash/integration-testsby repository administrators and is used by the requesting repository’s CI to trigger workflows here. Each requesting organization needs to create its own dispatch app. - Status-reporting app (
z3-integration-status-reporting, owned byzcash, public): Installed on the requesting repository. Used byintegration-testsCI to write status checks back to the requesting repository’s PR.
This two-app model follows the principle of least privilege: each app only has the permissions and credentials for the direction it serves, limiting the blast radius if either app’s credentials are compromised.
Requesting repository setup
The requesting repository’s CI workflow includes a job that dispatches to this repository:
trigger-integration:
name: Trigger integration tests
runs-on: ubuntu-latest
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.DISPATCH_APP_ID }}
private-key: ${{ secrets.DISPATCH_APP_PRIVATE_KEY }}
owner: zcash
repositories: integration-tests
- name: Trigger integration tests
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: >
gh api repos/zcash/integration-tests/dispatches
--field event_type="<project>-interop-request"
--field client_payload[sha]="$GITHUB_SHA"
This uses the dispatch app’s credentials to generate a token scoped to
zcash/integration-tests, then sends a repository_dispatch event. The
owner field must be zcash because the token needs write access to the
zcash/integration-tests repository.
The client_payload must include:
sha: The commit SHA to build and test from the requesting repository.
It may also include these optional fields:
platforms: A JSON array of platform names (e.g.["ubuntu-22.04", "mingw32"]) to run only a subset of platforms. Requested platforms are treated as required (must pass). The valid list of platform names is maintained here. Unrecognized platform names are reported as error statuses on the requesting PR. When omitted, all platforms run.test_sha: A commit SHA or ref in thezcash/integration-testsrepository itself. When provided, the test suite is checked out at this ref instead ofmain. This is useful for testing integration-tests changes alongside project changes.
Integration-tests repository setup
In the integration-tests repository CI, three things are configured:
-
ci.ymltrigger: The workflow’son.repository_dispatch.typesarray includes the event type (zebra-interop-request,zallet-interop-request, andzaino-interop-requestare currently supported). -
Build job: A build job checks out the requesting repository at the dispatched commit SHA:
- name: Use specified commit if: github.event.action == '<project>-interop-request' env: SHA: ${{ github.event.client_payload.sha }} run: echo "PROJECT_REF=${SHA}" >> $GITHUB_ENV - name: Use current main if: github.event.action != '<project>-interop-request' run: echo "PROJECT_REF=refs/heads/main" >> $GITHUB_ENV -
Status reporting: Four composite actions in
.github/actions/handle communication back to the requesting repository:interop-repo-idsmaps the event type (e.g.zebra-interop-request) to the requesting repository’s owner and name. This mapping is maintained as acaseexpression so that only known event types resolve to valid repositories.start-interopgenerates a token from thez3-integration-status-reportingapp scoped to the requesting repository and creates a pending status check on the dispatched commit.finish-interop(run withif: always()) updates that status to the job’s final result (success, failure, or error).notify-interop-errorreports error statuses for any requested platforms that are not recognized by the CI matrix (seeplatformsin client_payload).
Each job calls
interop-repo-idsfirst, then passes its outputs tostart-interopat the beginning andfinish-interopat the end.
Security model
Who can trigger integration test workflows? Two independent gates control this:
- App installation on
zcash/integration-tests: The dispatch app must be installed onzcash/integration-tests, which requires approval from azcashorg admin. Forzcash-internal repos, the org-private [z3-integration-dispatch] app is used, so no external organization can use it. For external repos, the requesting organization creates its own dispatch app, which azcashadmin must explicitly approve for installation. - Event type allowlist in
ci.yml: The workflow only responds to event types explicitly listed inon.repository_dispatch.types. Even if an app could dispatch an event, it would be ignored unless its type is listed.
Credential separation: Each app’s private key is stored only where it is
needed — the dispatch app’s key in the requesting repository, the
z3-integration-status-reporting key (a single key pair, since it is one app)
in zcash/integration-tests. If a dispatch app’s credentials are compromised,
an attacker could trigger integration test runs but could not write arbitrary
statuses. If the
status-reporting credentials are compromised, an attacker could write status
checks to repositories where the app is installed but could not trigger workflow
runs.
App permissions:
| App | Permission | Purpose |
|---|---|---|
z3-integration-dispatch | Contents: Read and write | Send repository_dispatch events to zcash/integration-tests |
z3-integration-status-reporting | Statuses: Read and write | Create and update commit status checks on requesting repos |
Setting up integration for a new repository
To add cross-repository integration for a new project, follow these steps. The
instructions below use Zebra (ZcashFoundation/zebra) as a running example.
1. Set up the dispatch app (requesting org side)
The requesting repository needs a dispatch app installed on
zcash/integration-tests.
The requesting organization creates a GitHub App with:
- Repository permissions:
Contents: Read and write
Then have a zcash org admin install it on zcash/integration-tests. Store the
app’s ID and private key as repository secrets in the requesting repository
(as DISPATCH_APP_ID and DISPATCH_APP_PRIVATE_KEY) so that they can be used
for CI configuration as described in the next step.
2. Update the requesting repository’s CI
Add a trigger-integration job to the requesting repository’s CI workflow (see
the example above). Use the event type
<project>-interop-request appropriate to the requesting repository where
<project> is one of zallet, zebra, or zaino.
3. Install z3-integration-status-reporting on the requesting repository
The z3-integration-status-reporting app must be installed on the requesting
repository so that integration-tests can write status updates back to the
PR or workflow run that triggered the test.
An admin of the requesting organization can install the app via https://github.com/apps/z3-integration-status-reporting/installations/new
During installation, select only the specific repository that needs integration
(e.g. zebra).
4. Verify
Open a test PR in the requesting repository and confirm that:
- The
trigger-integrationjob dispatches successfully. - Integration tests run in this repository.
- Status checks appear on the requesting repository’s PR commit.
- Both success and failure states propagate correctly.