Skip to content

Commit ec0235b

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

2 files changed

Lines changed: 237 additions & 5 deletions

File tree

.github/workflows/integration-tests.yaml

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