Skip to content

Commit 353ed5d

Browse files
authored
Merge pull request #45 from input-output-hk/devnet-e2e-smoke
ci: prove the Yaci devnet role end-to-end
2 parents 3322c00 + 57c024a commit 353ed5d

9 files changed

Lines changed: 288 additions & 0 deletions

File tree

.github/workflows/devnet-smoke.yml

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# End-to-end devnet smoke gate.
2+
#
3+
# Purpose: prove the one build-green role that no other job exercises — the
4+
# Yaci DevKit *devnet* — actually works end to end, and that the devnet↔off-chain
5+
# `.env` seam connects. Every other gate leaves this role untested:
6+
# • scheduled-smoke.yml intentionally EXCLUDES yaci (its `just` targets that a
7+
# no-op runner sees only echo), so nothing builds/tests the devnet role.
8+
# • The template degrades gracefully — with no yaci-devkit installed, every
9+
# layer skips and exits 0. Green, but proving nothing.
10+
#
11+
# This job installs yaci-devkit for real, scaffolds a fullstack project
12+
# (aiken on-chain → blueprint → meshjs off-chain → yaci devnet), and runs the
13+
# whole thing under CARDANO_INIT_REQUIRE_DEVNET=1, which turns every graceful
14+
# skip into a hard failure. So a green run here MEANS: the devnet came up, the
15+
# indexer served the chain, and the off-chain mint→lock→redeem round-trip
16+
# executed against it — never a vacuous skip.
17+
#
18+
# Like scheduled-smoke/installer-recipes, it depends on the live world (an npm
19+
# release, a cardano-node download), so it runs on a schedule + on demand, never
20+
# as an every-commit gate, and files a tracking issue on scheduled failure.
21+
22+
name: devnet-smoke
23+
24+
on:
25+
schedule:
26+
- cron: "0 8 * * 1" # Mondays 08:00 UTC, after scheduled-smoke (06:00) and installer-recipes (07:00)
27+
workflow_dispatch: {} # allow manual runs
28+
# Opt-in per-PR run: add the `run-devnet-smoke` label to a PR to run this
29+
# (heavy, world-dependent) gate against that PR's code. Re-add to re-run.
30+
pull_request:
31+
types: [labeled]
32+
33+
permissions:
34+
contents: read
35+
36+
jobs:
37+
devnet:
38+
name: yaci devnet end-to-end
39+
# Schedule and manual runs always run; PR runs only for our opt-in label
40+
# (a `labeled` event fires for every label, so filter to ours).
41+
if: github.event_name != 'pull_request' || github.event.label.name == 'run-devnet-smoke'
42+
runs-on: ubuntu-latest
43+
timeout-minutes: 30
44+
steps:
45+
- uses: actions/checkout@v5
46+
47+
- name: Install Rust
48+
uses: dtolnay/rust-toolchain@stable
49+
50+
- name: Install just
51+
uses: extractions/setup-just@v4
52+
53+
# Off-chain (meshjs) + the yaci-devkit CLI both run on Node.
54+
- name: Install Node
55+
uses: actions/setup-node@v5
56+
with:
57+
node-version: 20
58+
59+
# yaci-cli / yaci-store are JVM services; the devkit needs a JDK on PATH.
60+
- name: Install JDK
61+
uses: actions/setup-java@v4
62+
with:
63+
distribution: temurin
64+
java-version: 21
65+
66+
- name: Install Aiken
67+
uses: aiken-lang/setup-aiken@v1
68+
with:
69+
# Keep this in lockstep with templates/aiken/on-chain/aiken.toml.jinja.
70+
version: v1.1.21
71+
72+
- name: Install yaci-devkit
73+
run: npm install -g @bloxbean/yaci-devkit
74+
75+
# The first `yaci-devkit up` downloads a cardano-node into ~/.yaci-cli;
76+
# cache it so subsequent runs skip the (large) download.
77+
- name: Cache yaci-cli node download
78+
uses: actions/cache@v4
79+
with:
80+
path: ~/.yaci-cli
81+
key: yaci-cli-${{ runner.os }}
82+
83+
- name: Scaffold a fullstack project in isolation
84+
run: cargo run --quiet -- --name devnet-smoke --on-chain aiken --off-chain meshjs --devnet yaci
85+
86+
- name: Build & run the end-to-end devnet smoke
87+
working-directory: devnet-smoke
88+
# CARDANO_INIT_REQUIRE_DEVNET=1 makes every graceful skip a hard failure,
89+
# so this cannot pass vacuously. `just build` produces the on-chain
90+
# blueprint and installs off-chain deps; `just test` builds the blueprint
91+
# first, then the devnet component spins up an ephemeral devnet and runs
92+
# its integration test + the off-chain round-trip against it (guaranteed
93+
# teardown via trap).
94+
env:
95+
CARDANO_INIT_REQUIRE_DEVNET: "1"
96+
run: |
97+
just build
98+
just test
99+
100+
notify-on-failure:
101+
needs: devnet
102+
# Only file an issue for the unattended scheduled run, not manual debugging.
103+
if: failure() && github.event_name == 'schedule'
104+
runs-on: ubuntu-latest
105+
permissions:
106+
issues: write
107+
steps:
108+
- uses: actions/github-script@v8
109+
with:
110+
script: |
111+
const { owner, repo } = context.repo;
112+
const label = "devnet-smoke";
113+
const title = "Devnet smoke failed: the Yaci devnet no longer works end to end";
114+
const today = new Date().toISOString().slice(0, 10);
115+
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
116+
const body = [
117+
`The weekly devnet end-to-end smoke failed on ${today}.`,
118+
``,
119+
`A scaffolded fullstack project's devnet↔off-chain seam stopped working —`,
120+
`likely a breaking yaci-devkit release, a cardano-node download/boot`,
121+
`failure, or upstream drift in the off-chain provider. The run log shows`,
122+
`which stage failed (devnet boot, indexer liveness, or the round-trip).`,
123+
``,
124+
`Run: ${runUrl}`,
125+
].join("\n");
126+
127+
// De-dupe: comment on the existing open issue instead of spamming a
128+
// new one every Monday. (Creating an issue auto-creates the label.)
129+
const existing = await github.rest.issues.listForRepo({
130+
owner, repo, state: "open", labels: label,
131+
});
132+
if (existing.data.length === 0) {
133+
await github.rest.issues.create({ owner, repo, title, body, labels: [label] });
134+
} else {
135+
await github.rest.issues.createComment({
136+
owner, repo, issue_number: existing.data[0].number, body,
137+
});
138+
}

src/scaffold/snapshots/aiken_meshjs_kupo_ogmios.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,24 @@ const indexerUrl = (process.env.INDEXER_URL ?? "").trim();
13081308
const adminUrl = (process.env.YACI_ADMIN_URL ?? "http://localhost:10000").trim();
13091309
const canRun = indexerUrl !== "" && hasBundledBlueprint();
13101310
1311+
// When set (by the end-to-end CI smoke test), a skip is a failure — but only
1312+
// once a devnet is actually live (INDEXER_URL set). The top-level `just test`
1313+
// also runs this suite standalone with no devnet (blank INDEXER_URL) before the
1314+
// devnet phase; that skip is expected and must stay green. So strict mode fails
1315+
// only when the devnet is up yet the round-trip still can't run (e.g. the
1316+
// on-chain blueprint wasn't bundled) — a real, actionable breakage.
1317+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
1318+
1319+
if (requireDevnet && indexerUrl !== "" && !hasBundledBlueprint()) {
1320+
describe("GiftCard round-trip on a Yaci devnet", () => {
1321+
it("must run under CARDANO_INIT_REQUIRE_DEVNET (devnet is live)", () => {
1322+
throw new Error(
1323+
"Devnet is live but no bundled blueprint — build the on-chain component first.",
1324+
);
1325+
});
1326+
});
1327+
}
1328+
13111329
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
13121330
13131331
/** Poll `fn` until `ok(result)` or we run out of tries. */

src/scaffold/snapshots/aiken_meshjs_nix.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,24 @@ const indexerUrl = (process.env.INDEXER_URL ?? "").trim();
12981298
const adminUrl = (process.env.YACI_ADMIN_URL ?? "http://localhost:10000").trim();
12991299
const canRun = indexerUrl !== "" && hasBundledBlueprint();
13001300
1301+
// When set (by the end-to-end CI smoke test), a skip is a failure — but only
1302+
// once a devnet is actually live (INDEXER_URL set). The top-level `just test`
1303+
// also runs this suite standalone with no devnet (blank INDEXER_URL) before the
1304+
// devnet phase; that skip is expected and must stay green. So strict mode fails
1305+
// only when the devnet is up yet the round-trip still can't run (e.g. the
1306+
// on-chain blueprint wasn't bundled) — a real, actionable breakage.
1307+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
1308+
1309+
if (requireDevnet && indexerUrl !== "" && !hasBundledBlueprint()) {
1310+
describe("GiftCard round-trip on a Yaci devnet", () => {
1311+
it("must run under CARDANO_INIT_REQUIRE_DEVNET (devnet is live)", () => {
1312+
throw new Error(
1313+
"Devnet is live but no bundled blueprint — build the on-chain component first.",
1314+
);
1315+
});
1316+
});
1317+
}
1318+
13011319
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
13021320
13031321
/** Poll `fn` until `ok(result)` or we run out of tries. */

src/scaffold/snapshots/aiken_meshjs_yaci.snap

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,24 @@ const indexerUrl = (process.env.INDEXER_URL ?? "").trim();
13151315
const adminUrl = (process.env.YACI_ADMIN_URL ?? "http://localhost:10000").trim();
13161316
const canRun = indexerUrl !== "" && hasBundledBlueprint();
13171317
1318+
// When set (by the end-to-end CI smoke test), a skip is a failure — but only
1319+
// once a devnet is actually live (INDEXER_URL set). The top-level `just test`
1320+
// also runs this suite standalone with no devnet (blank INDEXER_URL) before the
1321+
// devnet phase; that skip is expected and must stay green. So strict mode fails
1322+
// only when the devnet is up yet the round-trip still can't run (e.g. the
1323+
// on-chain blueprint wasn't bundled) — a real, actionable breakage.
1324+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
1325+
1326+
if (requireDevnet && indexerUrl !== "" && !hasBundledBlueprint()) {
1327+
describe("GiftCard round-trip on a Yaci devnet", () => {
1328+
it("must run under CARDANO_INIT_REQUIRE_DEVNET (devnet is live)", () => {
1329+
throw new Error(
1330+
"Devnet is live but no bundled blueprint — build the on-chain component first.",
1331+
);
1332+
});
1333+
});
1334+
}
1335+
13181336
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
13191337
13201338
/** Poll `fn` until `ok(result)` or we run out of tries. */
@@ -1590,7 +1608,15 @@ const fileEnv = readEnv(envPath);
15901608
const indexerUrl = (process.env.INDEXER_URL ?? fileEnv.INDEXER_URL ?? "").trim();
15911609
const network = process.env.CARDANO_NETWORK || fileEnv.CARDANO_NETWORK || "preview";
15921610
1611+
// When set, a devnet is mandatory: every graceful skip below becomes a hard
1612+
// failure. The end-to-end CI smoke test sets this so a pass proves the seam
1613+
// actually ran (see scripts/devnet-test.sh).
1614+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
1615+
15931616
if (!indexerUrl) {
1617+
if (requireDevnet) {
1618+
fail("No INDEXER_URL, but CARDANO_INIT_REQUIRE_DEVNET is seta live devnet was required.");
1619+
}
15941620
skip("No INDEXER_URL in ../.env — no local devnet configured.");
15951621
console.log(" Start one with: just dev (writes INDEXER_URL into ../.env)");
15961622
console.log(" Skipping the devnet integration test.");
@@ -1617,6 +1643,9 @@ async function getJson(path) {
16171643
}
16181644
}
16191645
// Still unreachable: the URL is likely stale (devnet not running). Degrade.
1646+
if (requireDevnet) {
1647+
fail(`Devnet required (CARDANO_INIT_REQUIRE_DEVNET) but unreachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
1648+
}
16201649
skip(`Devnet not reachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
16211650
console.log(" `just test` starts a devnet automatically; or run one with `just dev`.");
16221651
console.log(" Skipping the devnet integration test.");
@@ -1672,6 +1701,10 @@ main().catch((err) => {
16721701
# the per-tool build/test smoke gate) stays green: the integration test then
16731702
# runs with no devnet and skips itself.
16741703
#
1704+
# Set CARDANO_INIT_REQUIRE_DEVNET=1 to turn every graceful skip into a hard
1705+
# failure. The end-to-end CI smoke test uses this so a green run proves the
1706+
# devnet actually came up and the seam ran — never a vacuous skip.
1707+
#
16751708
# Invoked via `sh scripts/devnet-test.sh` (no exec bit; see the interface
16761709
# contract). Yaci DevKit supports Linux x64 and macOS arm64 (not Windows).
16771710
set -u
@@ -1680,6 +1713,11 @@ API_URL="http://localhost:8080/api/v1/"
16801713
LOG=".yaci-devnet.log"
16811714
16821715
if ! command -v yaci-devkit >/dev/null 2>&1; then
1716+
if [ -n "${CARDANO_INIT_REQUIRE_DEVNET:-}" ]; then
1717+
echo "✗ yaci-devkit is not installed, but CARDANO_INIT_REQUIRE_DEVNET is set."
1718+
echo " Install it: npm install -g @bloxbean/yaci-devkit"
1719+
exit 1
1720+
fi
16831721
echo "• yaci-devkit not installed — running the test without a devnet (it will skip)."
16841722
echo " Install it for the full integration test:"
16851723
echo " npm install -g @bloxbean/yaci-devkit"

src/scaffold/snapshots/offchain_only.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,24 @@ const indexerUrl = (process.env.INDEXER_URL ?? "").trim();
10681068
const adminUrl = (process.env.YACI_ADMIN_URL ?? "http://localhost:10000").trim();
10691069
const canRun = indexerUrl !== "" && hasBundledBlueprint();
10701070
1071+
// When set (by the end-to-end CI smoke test), a skip is a failure — but only
1072+
// once a devnet is actually live (INDEXER_URL set). The top-level `just test`
1073+
// also runs this suite standalone with no devnet (blank INDEXER_URL) before the
1074+
// devnet phase; that skip is expected and must stay green. So strict mode fails
1075+
// only when the devnet is up yet the round-trip still can't run (e.g. the
1076+
// on-chain blueprint wasn't bundled) — a real, actionable breakage.
1077+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
1078+
1079+
if (requireDevnet && indexerUrl !== "" && !hasBundledBlueprint()) {
1080+
describe("GiftCard round-trip on a Yaci devnet", () => {
1081+
it("must run under CARDANO_INIT_REQUIRE_DEVNET (devnet is live)", () => {
1082+
throw new Error(
1083+
"Devnet is live but no bundled blueprint — build the on-chain component first.",
1084+
);
1085+
});
1086+
});
1087+
}
1088+
10711089
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
10721090
10731091
/** Poll `fn` until `ok(result)` or we run out of tries. */

src/scaffold/snapshots/yaci_devnet_only.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,15 @@ const fileEnv = readEnv(envPath);
302302
const indexerUrl = (process.env.INDEXER_URL ?? fileEnv.INDEXER_URL ?? "").trim();
303303
const network = process.env.CARDANO_NETWORK || fileEnv.CARDANO_NETWORK || "preview";
304304
305+
// When set, a devnet is mandatory: every graceful skip below becomes a hard
306+
// failure. The end-to-end CI smoke test sets this so a pass proves the seam
307+
// actually ran (see scripts/devnet-test.sh).
308+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
309+
305310
if (!indexerUrl) {
311+
if (requireDevnet) {
312+
fail("No INDEXER_URL, but CARDANO_INIT_REQUIRE_DEVNET is seta live devnet was required.");
313+
}
306314
skip("No INDEXER_URL in ../.env — no local devnet configured.");
307315
console.log(" Start one with: just dev (writes INDEXER_URL into ../.env)");
308316
console.log(" Skipping the devnet integration test.");
@@ -329,6 +337,9 @@ async function getJson(path) {
329337
}
330338
}
331339
// Still unreachable: the URL is likely stale (devnet not running). Degrade.
340+
if (requireDevnet) {
341+
fail(`Devnet required (CARDANO_INIT_REQUIRE_DEVNET) but unreachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
342+
}
332343
skip(`Devnet not reachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
333344
console.log(" `just test` starts a devnet automatically; or run one with `just dev`.");
334345
console.log(" Skipping the devnet integration test.");
@@ -384,6 +395,10 @@ main().catch((err) => {
384395
# the per-tool build/test smoke gate) stays green: the integration test then
385396
# runs with no devnet and skips itself.
386397
#
398+
# Set CARDANO_INIT_REQUIRE_DEVNET=1 to turn every graceful skip into a hard
399+
# failure. The end-to-end CI smoke test uses this so a green run proves the
400+
# devnet actually came up and the seam ran — never a vacuous skip.
401+
#
387402
# Invoked via `sh scripts/devnet-test.sh` (no exec bit; see the interface
388403
# contract). Yaci DevKit supports Linux x64 and macOS arm64 (not Windows).
389404
set -u
@@ -392,6 +407,11 @@ API_URL="http://localhost:8080/api/v1/"
392407
LOG=".yaci-devnet.log"
393408
394409
if ! command -v yaci-devkit >/dev/null 2>&1; then
410+
if [ -n "${CARDANO_INIT_REQUIRE_DEVNET:-}" ]; then
411+
echo "✗ yaci-devkit is not installed, but CARDANO_INIT_REQUIRE_DEVNET is set."
412+
echo " Install it: npm install -g @bloxbean/yaci-devkit"
413+
exit 1
414+
fi
395415
echo "• yaci-devkit not installed — running the test without a devnet (it will skip)."
396416
echo " Install it for the full integration test:"
397417
echo " npm install -g @bloxbean/yaci-devkit"

templates/meshjs/off-chain/src/contract.integration.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ const indexerUrl = (process.env.INDEXER_URL ?? "").trim();
2626
const adminUrl = (process.env.YACI_ADMIN_URL ?? "http://localhost:10000").trim();
2727
const canRun = indexerUrl !== "" && hasBundledBlueprint();
2828

29+
// When set (by the end-to-end CI smoke test), a skip is a failure — but only
30+
// once a devnet is actually live (INDEXER_URL set). The top-level `just test`
31+
// also runs this suite standalone with no devnet (blank INDEXER_URL) before the
32+
// devnet phase; that skip is expected and must stay green. So strict mode fails
33+
// only when the devnet is up yet the round-trip still can't run (e.g. the
34+
// on-chain blueprint wasn't bundled) — a real, actionable breakage.
35+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
36+
37+
if (requireDevnet && indexerUrl !== "" && !hasBundledBlueprint()) {
38+
describe("GiftCard round-trip on a Yaci devnet", () => {
39+
it("must run under CARDANO_INIT_REQUIRE_DEVNET (devnet is live)", () => {
40+
throw new Error(
41+
"Devnet is live but no bundled blueprint — build the on-chain component first.",
42+
);
43+
});
44+
});
45+
}
46+
2947
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
3048

3149
/** Poll `fn` until `ok(result)` or we run out of tries. */

templates/yaci/devnet/integration.test.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,15 @@ const fileEnv = readEnv(envPath);
5555
const indexerUrl = (process.env.INDEXER_URL ?? fileEnv.INDEXER_URL ?? "").trim();
5656
const network = process.env.CARDANO_NETWORK || fileEnv.CARDANO_NETWORK || "preview";
5757

58+
// When set, a devnet is mandatory: every graceful skip below becomes a hard
59+
// failure. The end-to-end CI smoke test sets this so a pass proves the seam
60+
// actually ran (see scripts/devnet-test.sh).
61+
const requireDevnet = (process.env.CARDANO_INIT_REQUIRE_DEVNET ?? "").trim() !== "";
62+
5863
if (!indexerUrl) {
64+
if (requireDevnet) {
65+
fail("No INDEXER_URL, but CARDANO_INIT_REQUIRE_DEVNET is set — a live devnet was required.");
66+
}
5967
skip("No INDEXER_URL in ../.env — no local devnet configured.");
6068
console.log(" Start one with: just dev (writes INDEXER_URL into ../.env)");
6169
console.log(" Skipping the devnet integration test.");
@@ -82,6 +90,9 @@ async function getJson(path) {
8290
}
8391
}
8492
// Still unreachable: the URL is likely stale (devnet not running). Degrade.
93+
if (requireDevnet) {
94+
fail(`Devnet required (CARDANO_INIT_REQUIRE_DEVNET) but unreachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
95+
}
8596
skip(`Devnet not reachable at ${url} (${lastErr?.code ?? lastErr?.name ?? "error"}).`);
8697
console.log(" `just test` starts a devnet automatically; or run one with `just dev`.");
8798
console.log(" Skipping the devnet integration test.");

0 commit comments

Comments
 (0)