Skip to content

Commit 5c62b0b

Browse files
committed
add casa21 CI runner for integration tests
1 parent b6bd142 commit 5c62b0b

2 files changed

Lines changed: 234 additions & 5 deletions

File tree

.github/workflows/integration-tests.yaml

Lines changed: 221 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ on:
99
name: Integration Tests
1010

1111
jobs:
12-
ci:
12+
# Standard integration path on GitHub-hosted runners.
13+
# Runs on both Ubuntu and macOS.
14+
integration_github_runner:
15+
name: integration-github-runner (${{ matrix.os }})
1316
runs-on: ${{ matrix.os }}
1417
strategy:
1518
matrix:
1619
os:
1720
- ubuntu-latest
1821
- macos-latest
19-
include:
20-
- os: ubuntu-latest
21-
target: x86_64-unknown-linux-gnu
2222

2323
steps:
2424
- name: Checkout repository
@@ -40,4 +40,220 @@ jobs:
4040

4141
- name: Integration Tests
4242
run: |
43-
RUST_BACKTRACE=1 RUST_LOG=debug cargo nextest run --manifest-path=integration-tests/Cargo.toml --nocapture
43+
RUST_BACKTRACE=1 RUST_LOG=debug cargo nextest run --manifest-path=integration-tests/Cargo.toml --nocapture
44+
45+
# High-performance integration path on the casa21 self-hosted runner.
46+
# Used only for trusted PR merge gating.
47+
# Trust boundary for self-hosted execution:
48+
# - on pull_request, only run for authors whose association is MEMBER/OWNER
49+
# (strict org-membership gate; COLLABORATOR is intentionally excluded)
50+
integration_casa21_runner:
51+
name: integration-casa21-runner
52+
if: >
53+
github.event_name == 'pull_request' &&
54+
contains(fromJSON('["MEMBER","OWNER"]'), github.event.pull_request.author_association)
55+
runs-on:
56+
- self-hosted
57+
- Linux
58+
- X64
59+
- casa21
60+
steps:
61+
- name: Checkout repository
62+
uses: actions/checkout@v4
63+
64+
- name: Add user cargo bin to PATH
65+
run: echo "/home/sri/.cargo/bin" >> "$GITHUB_PATH"
66+
67+
- name: Verify preinstalled Rust toolchain
68+
run: |
69+
# Casa21 runner is expected to have Rust preinstalled (managed out-of-band on the VPS).
70+
rustc --version
71+
cargo --version
72+
rustup show active-toolchain
73+
rustup run 1.85.0 rustc --version | grep -q "1.85.0"
74+
75+
- name: Verify preinstalled cargo-nextest
76+
run: |
77+
# Casa21 runner is expected to have cargo-nextest preinstalled.
78+
cargo nextest --version
79+
cargo nextest --version | grep -q "0.9.100"
80+
81+
- name: Cleanup stale test processes and temp data
82+
run: |
83+
# Self-hosted runners are persistent. Clean leftovers from prior runs.
84+
pkill -f "[b]itcoin-node|[s]v2-tp" || true
85+
rm -rf /tmp/sv2-integration-tests
86+
87+
- name: Integration Tests
88+
env:
89+
# Use 24 build jobs to parallelize first-build compilation on casa21.
90+
CARGO_BUILD_JOBS: 24
91+
# Reuse build artifacts across runs on the persistent self-hosted runner.
92+
CARGO_TARGET_DIR: /home/sri/.cache/sv2-apps/target-integration-tests
93+
run: |
94+
# Tests are auto-discovered from integration-tests/tests; contributors do not need workflow edits.
95+
# NOTE: do not pass --nocapture with nextest; it serializes test execution.
96+
RUST_BACKTRACE=1 RUST_LOG=info cargo nextest run --manifest-path=integration-tests/Cargo.toml --config-file=integration-tests/.config/nextest.casa21.toml
97+
98+
# Single branch-protection target that implements the full truth table.
99+
# Configure branch protection to require ONLY this check.
100+
#
101+
# Truth table:
102+
# | integration_github_runner | integration_casa21_runner | integration_required |
103+
# |---------------------------|----------------------------------|-------------|
104+
# | passed ✅ | passed ✅ | unblocked ✅ |
105+
# | failed ❌ | passed ✅ | unblocked ✅ |
106+
# | running 💫 | passed ✅ | unblocked ✅ |
107+
# | passed ✅ | failed ❌ | blocked ❌ |
108+
# | failed ❌ | failed ❌ | blocked ❌ |
109+
# | running 💫 | failed ❌ | blocked ❌ |
110+
# | passed ✅ | absent (PR is not from org member) | unblocked ✅ |
111+
# | failed ❌ | absent (PR is not from org member) | blocked ❌ |
112+
# | running 💫 | absent (PR is not from org member) | waiting 💫 |
113+
#
114+
# Why this polls via API instead of `needs` fan-in:
115+
# - with `needs`, GitHub keeps the final job pending until *all* upstream jobs settle,
116+
# even when one path is irrelevant for the current trust mode.
117+
# - this implementation gates only on the relevant path:
118+
# * trusted PR -> integration-casa21-runner
119+
# * all other events -> both integration-github-runner matrix jobs
120+
integration_required:
121+
name: integration-required
122+
if: always()
123+
runs-on: ubuntu-latest
124+
# Minimum token scope for the github-script gate:
125+
# - actions:read -> list jobs in this workflow run via Actions API
126+
# - contents:read -> baseline read access for repository metadata/content APIs
127+
permissions:
128+
actions: read
129+
contents: read
130+
steps:
131+
- name: Enforce integration truth-table gate
132+
uses: actions/github-script@v7
133+
with:
134+
script: |
135+
// This is the single required status check used by branch protection.
136+
//
137+
// Design goals:
138+
// 1) Select the relevant integration path based on trust mode.
139+
// 2) Pass/fail/wait exactly as defined by the truth table above.
140+
// 3) Avoid `needs` fan-in deadlocks on irrelevant jobs.
141+
//
142+
// Trust mode:
143+
// - trusted: PRs authored by MEMBER/OWNER
144+
// - untrusted: all other events (including push and non-member PRs)
145+
//
146+
// Required path by mode:
147+
// - trusted -> integration-casa21-runner (single job)
148+
// - untrusted -> integration-github-runner matrix (ubuntu + macos)
149+
150+
// Associations that are considered trusted for self-hosted execution.
151+
const trustedAssociations = ['MEMBER', 'OWNER'];
152+
153+
// Detect trust mode from event payload.
154+
const trusted =
155+
context.eventName === 'pull_request' &&
156+
trustedAssociations.includes(
157+
context.payload.pull_request?.author_association || ''
158+
);
159+
160+
// Canonical job names as shown in the Actions UI for matrix legs.
161+
// Keep these in sync with `integration_github_runner.name`.
162+
const githubMatrixJobs = [
163+
'integration-github-runner (ubuntu-latest)',
164+
'integration-github-runner (macos-latest)',
165+
];
166+
167+
// Canonical job name for trusted path.
168+
const casa21JobName = 'integration-casa21-runner';
169+
170+
// Human-readable target used in logs.
171+
const targetLabel = trusted
172+
? casa21JobName
173+
: githubMatrixJobs.join(', ');
174+
175+
// Polling parameters:
176+
// - interval: how often to refresh job states
177+
// - timeout: hard stop to avoid an infinite wait on API/job anomalies
178+
const timeoutMs = 45 * 60 * 1000;
179+
const intervalMs = 10 * 1000;
180+
const start = Date.now();
181+
182+
core.info(`Gating merge status on path: ${targetLabel}`);
183+
184+
while (true) {
185+
// Fetch jobs for THIS workflow run.
186+
// We poll instead of wiring `needs` so this gate can ignore irrelevant paths.
187+
const { data } = await github.rest.actions.listJobsForWorkflowRun({
188+
owner: context.repo.owner,
189+
repo: context.repo.repo,
190+
run_id: context.runId,
191+
per_page: 100,
192+
});
193+
194+
if (trusted) {
195+
// Trusted path semantics:
196+
// - Pass only when casa21 is completed+success.
197+
// - Fail immediately on any non-success completion.
198+
// - Wait while in-progress or not yet visible.
199+
const job = data.jobs.find((j) => j.name === casa21JobName);
200+
201+
if (job) {
202+
core.info(`Observed ${casa21JobName}: status=${job.status}, conclusion=${job.conclusion}`);
203+
204+
if (job.status === 'completed') {
205+
if (job.conclusion === 'success') {
206+
core.info(`Passing: ${casa21JobName}=success`);
207+
return;
208+
}
209+
210+
core.setFailed(`Blocking: ${casa21JobName}=${job.conclusion}`);
211+
return;
212+
}
213+
} else {
214+
// Eventual consistency in job listing can briefly hide newly-started jobs.
215+
core.info(`Job ${casa21JobName} not visible yet; waiting...`);
216+
}
217+
} else {
218+
// Untrusted path semantics:
219+
// - Require BOTH github matrix legs.
220+
// - Fail fast if any leg completes with non-success.
221+
// - Pass only when all legs are completed+success.
222+
// - Otherwise keep waiting.
223+
const jobs = githubMatrixJobs.map((name) => data.jobs.find((j) => j.name === name));
224+
const allPresent = jobs.every(Boolean);
225+
226+
if (!allPresent) {
227+
core.info('Not all integration-github-runner matrix jobs are visible yet; waiting...');
228+
} else {
229+
for (const job of jobs) {
230+
core.info(`Observed ${job.name}: status=${job.status}, conclusion=${job.conclusion}`);
231+
}
232+
233+
const failedJob = jobs.find(
234+
(job) => job.status === 'completed' && job.conclusion !== 'success'
235+
);
236+
237+
if (failedJob) {
238+
core.setFailed(`Blocking: ${failedJob.name}=${failedJob.conclusion}`);
239+
return;
240+
}
241+
242+
const allCompleted = jobs.every((job) => job.status === 'completed');
243+
if (allCompleted) {
244+
core.info('Passing: all integration-github-runner matrix jobs=success');
245+
return;
246+
}
247+
}
248+
}
249+
250+
// Safety valve: fail closed if we waited too long.
251+
// This prevents a permanently pending required check.
252+
if (Date.now() - start > timeoutMs) {
253+
core.setFailed(`Timed out waiting for required integration path: ${targetLabel}`);
254+
return;
255+
}
256+
257+
// Sleep before next poll.
258+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
259+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[profile.default]
2+
3+
# Same retry policy as local integration test runs.
4+
retries = { backoff = "fixed", count = 3, delay = "2s" }
5+
6+
# SRI VM on Casa21 Server has 24 cores and 32 GB RAM.
7+
test-threads = 24
8+
9+
# Match default timeout behavior to avoid changing failure semantics.
10+
slow-timeout = { period = "60s", terminate-after = 2 }
11+
12+
status-level = "all"
13+
final-status-level = "all"

0 commit comments

Comments
 (0)