@toon-protocol/rig 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -459,10 +459,58 @@ rig ci serve --relay wss://<relay> --repo <owner-npub>/<repo-id> [--repo …]
459
459
  | `--gateway <url>` | `RIG_ARWEAVE_GATEWAY`, else rig's preferred Arweave gateway | the gateway that serves the store's raw bytes: prefix for log/artifact URLs (`<gateway>/raw/<txId>`) **and** the first gateway the coordinator reads a commit's objects from (same `/raw/` route), ahead of the shared public list — name a private or local gateway (the sandbox's) and the coordinator needs nothing else to read |
460
460
  | `--act-bin <path>` | `act` on PATH (or `RIG_ACT_BIN`) | the act binary |
461
461
  | `--platform <label>=<image>` | `ubuntu-latest=catthehacker/ubuntu:act-latest` | `runs-on` label → Docker image (repeatable) |
462
+ | `--pull` / `--no-pull` | act's default (pull before each run) | whether act pulls the runner image named by `--platform` before each run |
462
463
  | `--workdir <dir>` | `<state-dir>/work` | where commits are materialized |
463
464
  | `--once` | | stop after the first run concludes (its 9842 and final 39842 are on the relay) — one `rig ci trigger` answered by one bounded serve; ignored or refused triggers do not count |
464
465
  | `--json` | | one JSON document with the coordinator, relay, repos and state dir on start |
465
466
 
467
+ **Two act behaviours decide whether a real workflow passes**, and a workflow
468
+ whose steps only `echo` hides both:
469
+
470
+ - **Write `actions/checkout@v4`, exactly as you would on GitHub.** act copies
471
+ the materialized tree into the job container *at that step*, not when the
472
+ container starts, and short-circuits it to a local `docker cp` — so the step
473
+ needs no network and no node in the image. A workflow without it
474
+ runs its tools against an **empty** container — the steps execute, the files
475
+ are not there:
476
+
477
+ ```
478
+ error: could not find `Cargo.toml` in `/…/work/<repo>-<sha>-1` or any parent directory
479
+ ```
480
+
481
+ That path is the checkout the coordinator materialized under `--workdir`
482
+ (`<repo-id>-<short sha>-<n>`), as the job container sees it.
483
+ - **The default image is not GitHub's hosted runner.** `ubuntu-latest` maps to
484
+ `catthehacker/ubuntu:act-latest`, which carries no language toolchain, so a
485
+ workflow that expects a preinstalled `cargo`, `go` or JDK fails at its first
486
+ step. Either point `--platform` at an image that has one
487
+ (`--platform ubuntu-latest=rust:1-bookworm`) or install it in a step. The
488
+ official `rust` images ship neither rustfmt nor clippy —
489
+ `rustup component add rustfmt clippy` is a step, not a given — and an image
490
+ you built locally needs `--no-pull` (below).
491
+
492
+ A workflow that survives both, run by a coordinator started with
493
+ `--platform ubuntu-latest=rust:1-bookworm`:
494
+
495
+ ```yaml
496
+ # .github/workflows/ci.yml
497
+ name: ci
498
+ on: [push]
499
+ jobs:
500
+ test:
501
+ runs-on: ubuntu-latest
502
+ steps:
503
+ - uses: actions/checkout@v4 # act resolves this to the local copy
504
+ - run: rustup component add rustfmt clippy
505
+ - run: cargo fmt --check
506
+ - run: cargo clippy -- -D warnings
507
+ - run: cargo test
508
+ ```
509
+
510
+ A runner image built or loaded on the coordinator host and never pushed to a
511
+ registry needs `--no-pull`: without it act force-pulls the image named by
512
+ `--platform` and every run fails in *Set up job* with `pull access denied`.
513
+
466
514
  On start the coordinator publishes an **Advertisement** (19843: runner family
467
515
  `act`, its selectors, admission `maintainer-request`, execution
468
516
  `request-required`, billing `out-of-band`, and a fresh `secrets-key`) and renews
@@ -510,9 +558,10 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v rig-ci-state:/st
510
558
  The entrypoint is `rig`, so every verb works (`docker run --rm rig-ci --version`),
511
559
  and every environment variable rig honours means the same inside the image
512
560
  (`TOON_CLIENT_HOME` is `/state`). act launches each job as a container on the
513
- daemon behind the mounted socket and copies the checkout in, so no host path
514
- has to match. Against the [sandbox](#dogfooding-rigs-own-ciyml-on-the-sandbox)
515
- on the same host, run on the host network with the sandbox's own `localhost`
561
+ daemon behind the mounted socket and copies the checkout in at the
562
+ `actions/checkout` step, so no host path has to match. Against the
563
+ [sandbox](#dogfooding-rigs-own-ciyml-on-the-sandbox) on the same host, run on
564
+ the host network with the sandbox's own `localhost`
516
565
  addresses — a client dials the endpoint a node **advertises**, and the sandbox
517
566
  hub advertises `http://127.0.0.1:3200/ilp` so host-run smokes can reach it
518
567
  (sandbox-only; a real connector advertises its public URL, which the default
@@ -565,6 +614,11 @@ git commit --allow-empty -m 'ci: dogfood' && rig push --yes
565
614
  rig ci status HEAD --json | jq .summary
566
615
  ```
567
616
 
617
+ `RIG_ARWEAVE_GATEWAY` in that common env also steers the READ path: `rig clone`
618
+ and `rig fetch` try `http://localhost:3000/raw/<txId>` first, so a repo pushed to
619
+ the sandbox clones back out of it even though the public gateways never see those
620
+ objects (see [Reading through your own gateway](#reading-through-your-own-gateway)).
621
+
568
622
  The same flow, end to end and asserted, is
569
623
  `src/__integration__/ci-sandbox.integration.test.ts`; it runs only with
570
624
  `RIG_CI_SANDBOX=1` and skips itself unless the sandbox's relay, hub, store edge
@@ -774,6 +828,37 @@ clone never leaves a partial repo. `rig fetch [remote]` is the same pipeline as
774
828
  delta: only locally-missing objects are downloaded, and `refs/remotes/<remote>/*`
775
829
  (tags → `refs/tags/*`) move with a `git fetch`-style report.
776
830
 
831
+ ### Reading through your own gateway
832
+
833
+ By default both commands read from rig's shared public gateway list. A repository
834
+ whose objects live only on a **private, local or air-gapped** gateway is invisible
835
+ there — so `--gateway <url>` (or `RIG_ARWEAVE_GATEWAY`, the same variable `rig push`,
836
+ `rig site` and `rig ci serve` honour) puts that gateway **first**, on the store's
837
+ raw-bytes route `<url>/raw/<txId>`, with the public list kept behind it as the
838
+ fallback. It is the read-path twin of `rig ci serve --gateway`.
839
+
840
+ ```sh
841
+ export RIG_ARWEAVE_GATEWAY=http://localhost:3000 # the dev sandbox's gateway
842
+ rig clone ws://localhost:7100 <owner-npub>/<repo-id>
843
+ rig fetch # or: rig fetch --gateway http://localhost:3000
844
+ ```
845
+
846
+ `/raw/` and not `/<txId>`: an ar.io node serves raw transaction bytes there whether
847
+ or not it does sandboxed-subdomain redirects, and the TOON store's own gateway
848
+ answers on that route alone. Public gateways serve the same bytes on both, so with
849
+ nothing configured the list is exactly as it always was.
850
+
851
+ The **`Git-SHA` lookup follows the gateway too**. A kind:30618 object map is capped,
852
+ so a SHA it does not carry is resolved by asking a gateway's GraphQL endpoint which
853
+ transaction carries that object — and a configured gateway is asked at
854
+ `<url>/graphql`, never `arweave.net` (which would answer about a *different*
855
+ network, or leak the query off an air-gapped stack). Object bytes are
856
+ content-addressed and SHA-verified, so any mirror may serve them; a tag query is a
857
+ statement about one network, so it goes only to the network you named. Configure
858
+ nothing and it is the shared `arweave.net` resolver, unchanged. A gateway that does
859
+ not serve GraphQL therefore resolves nothing — name one that does (every ar.io node
860
+ does), or leave the variable unset.
861
+
777
862
  `rig issue list|show` and `rig pr list|show` are pure relay reads (kind:1621/1617 by
778
863
  the repo `#a` tag; state from kind:1630-1633, latest wins; comments under `show` —
779
864
  NIP-22 kind:1111 matched on the uppercase `E` root tag, merged with legacy kind:1622
@@ -12,7 +12,7 @@ import {
12
12
  parseMaintainers,
13
13
  queryRelay,
14
14
  resolveIdentity
15
- } from "./chunk-JBFBD7HV.js";
15
+ } from "./chunk-S3KKQ4IC.js";
16
16
  import {
17
17
  defaultDaemonPort,
18
18
  standaloneForced,
@@ -1677,10 +1677,16 @@ var CI_MANUAL_TRIGGER_KIND = 9840;
1677
1677
  var CI_JOB_RESULT_KIND = 9841;
1678
1678
  var CI_WORKFLOW_RESULT_KIND = 9842;
1679
1679
  var CI_WORKFLOW_PROGRESS_KIND = 39842;
1680
+ var CI_LIVE_LOG_TAIL_KIND = 39841;
1680
1681
  var CI_RUNNER_FAMILY = "act";
1681
1682
  var CI_SOFTWARE = "rig";
1682
1683
  var CI_MAX_ADVERTISEMENT_TTL = 30 * 60;
1683
1684
  var CI_MAX_PROGRESS_TTL = 30 * 60;
1685
+ var CI_MAX_LIVE_LOG_TAIL_TTL = 30 * 60;
1686
+ var CI_LIVE_LOG_TAIL_INTERVAL_MS = 1e4;
1687
+ var CI_LIVE_LOG_TAIL_JOB_BYTES = 16 * 1024;
1688
+ var CI_LIVE_LOG_TAIL_MAX_BYTES = 64 * 1024;
1689
+ var CI_RUNNER_CHANNEL_KEY = "~runner";
1684
1690
  var REPO_ANNOUNCEMENT_KIND = 30617;
1685
1691
  var CONCLUSIONS = /* @__PURE__ */ new Set([
1686
1692
  "success",
@@ -2412,6 +2418,138 @@ function parseCiWorkflowProgress(ev) {
2412
2418
  if (provenance) result.provenance = provenance;
2413
2419
  return result;
2414
2420
  }
2421
+ function liveLogTailBudget(entries) {
2422
+ if (!Number.isFinite(entries) || entries <= 1) {
2423
+ return CI_LIVE_LOG_TAIL_JOB_BYTES;
2424
+ }
2425
+ const share = Math.floor(CI_LIVE_LOG_TAIL_MAX_BYTES / entries);
2426
+ return Math.max(1, Math.min(CI_LIVE_LOG_TAIL_JOB_BYTES, share));
2427
+ }
2428
+ function liveLogTailEventBudget(runTimeoutMs) {
2429
+ if (!Number.isFinite(runTimeoutMs) || runTimeoutMs <= 0) return 1;
2430
+ return Math.ceil(runTimeoutMs / CI_LIVE_LOG_TAIL_INTERVAL_MS) + 1;
2431
+ }
2432
+ function sliceLogTail(text, maxBytes) {
2433
+ const bytes = Buffer.from(text, "utf8");
2434
+ if (bytes.length <= maxBytes) return { tail: text, omitted: 0 };
2435
+ if (maxBytes <= 0) return { tail: "", omitted: bytes.length };
2436
+ let start = bytes.length - maxBytes;
2437
+ while (start < bytes.length && (bytes[start] & 192) === 128) {
2438
+ start += 1;
2439
+ }
2440
+ return { tail: bytes.subarray(start).toString("utf8"), omitted: start };
2441
+ }
2442
+ function assertOmitted(omitted, what) {
2443
+ if (!Number.isSafeInteger(omitted) || omitted < 0) {
2444
+ throw new Error(`${what}: omitted must be a non-negative byte count`);
2445
+ }
2446
+ }
2447
+ function buildCiLiveLogTail(input, createdAt = now()) {
2448
+ if (input.runId === "") throw new Error("a live log tail needs a run id");
2449
+ assertExpiration(createdAt, input.expiresAt, CI_MAX_LIVE_LOG_TAIL_TTL);
2450
+ const seen = /* @__PURE__ */ new Set();
2451
+ for (const job of input.jobs) {
2452
+ if (job.job === "") throw new Error("a live log tail job needs a job id");
2453
+ if (job.job === CI_RUNNER_CHANNEL_KEY) {
2454
+ throw new Error(
2455
+ `the runner channel is not a job: ${CI_RUNNER_CHANNEL_KEY} may not appear in jobs`
2456
+ );
2457
+ }
2458
+ if (seen.has(job.job)) {
2459
+ throw new Error(`job ${job.job} appears twice in one live log tail`);
2460
+ }
2461
+ seen.add(job.job);
2462
+ assertOmitted(job.omitted, `job ${job.job}`);
2463
+ }
2464
+ if (input.runner) assertOmitted(input.runner.omitted, "runner channel");
2465
+ const content = {
2466
+ jobs: input.jobs.map((j) => ({
2467
+ job: j.job,
2468
+ tail: j.tail,
2469
+ omitted: j.omitted
2470
+ }))
2471
+ };
2472
+ if (input.runner) {
2473
+ content.runner = {
2474
+ tail: input.runner.tail,
2475
+ omitted: input.runner.omitted
2476
+ };
2477
+ }
2478
+ return {
2479
+ kind: CI_LIVE_LOG_TAIL_KIND,
2480
+ content: JSON.stringify(content),
2481
+ tags: [
2482
+ ...commonTriggerTags(input.trigger),
2483
+ ["d", input.runId],
2484
+ ["expiration", String(input.expiresAt)]
2485
+ ],
2486
+ created_at: createdAt
2487
+ };
2488
+ }
2489
+ function isRecord(value) {
2490
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2491
+ }
2492
+ function parseOmitted(value) {
2493
+ if (value === void 0) return 0;
2494
+ if (!Number.isSafeInteger(value) || value < 0) return null;
2495
+ return value;
2496
+ }
2497
+ function parseTailChannel(value) {
2498
+ if (!isRecord(value)) return null;
2499
+ const omitted = parseOmitted(value["omitted"]);
2500
+ const tail = value["tail"];
2501
+ if (typeof tail !== "string" || omitted === null) return null;
2502
+ return { tail, omitted };
2503
+ }
2504
+ function parseCiLiveLogTail(ev) {
2505
+ if (ev.kind !== CI_LIVE_LOG_TAIL_KIND) return null;
2506
+ const { tags } = ev;
2507
+ const trigger = parseCiTriggerContext(tags);
2508
+ if (!trigger) return null;
2509
+ const d = single(tags, "d");
2510
+ const exp = single(tags, "expiration");
2511
+ if (!d || !d[1] || !exp) return null;
2512
+ const expiresAt = Number(exp[1]);
2513
+ if (!Number.isInteger(expiresAt) || expiresAt <= ev.created_at || expiresAt - ev.created_at > CI_MAX_LIVE_LOG_TAIL_TTL) {
2514
+ return null;
2515
+ }
2516
+ let content;
2517
+ try {
2518
+ content = JSON.parse(ev.content);
2519
+ } catch {
2520
+ return null;
2521
+ }
2522
+ if (!isRecord(content) || !Array.isArray(content["jobs"])) return null;
2523
+ const jobs = [];
2524
+ const seen = /* @__PURE__ */ new Set();
2525
+ for (const entry of content["jobs"]) {
2526
+ if (!isRecord(entry)) return null;
2527
+ const job = entry["job"];
2528
+ if (typeof job !== "string" || job === "") return null;
2529
+ if (job === CI_RUNNER_CHANNEL_KEY || seen.has(job)) return null;
2530
+ const channel = parseTailChannel(entry);
2531
+ if (!channel) return null;
2532
+ seen.add(job);
2533
+ jobs.push({ job, tail: channel.tail, omitted: channel.omitted });
2534
+ }
2535
+ let runner;
2536
+ if (content["runner"] !== void 0) {
2537
+ const parsed = parseTailChannel(content["runner"]);
2538
+ if (!parsed) return null;
2539
+ runner = parsed;
2540
+ }
2541
+ const result = {
2542
+ eventId: ev.id,
2543
+ pubkey: ev.pubkey.toLowerCase(),
2544
+ createdAt: ev.created_at,
2545
+ trigger,
2546
+ runId: d[1],
2547
+ jobs,
2548
+ expiresAt
2549
+ };
2550
+ if (runner) result.runner = runner;
2551
+ return result;
2552
+ }
2415
2553
 
2416
2554
  // src/ci/secrets.ts
2417
2555
  import { v2 as nip44 } from "nostr-tools/nip44";
@@ -2651,7 +2789,7 @@ var WORKFLOW_FILE_RE = /\.ya?ml$/i;
2651
2789
  function sha256Hex(bytes) {
2652
2790
  return createHash2("sha256").update(bytes).digest("hex");
2653
2791
  }
2654
- function isRecord(value) {
2792
+ function isRecord2(value) {
2655
2793
  return value !== null && typeof value === "object" && !Array.isArray(value);
2656
2794
  }
2657
2795
  function stringList(value) {
@@ -2662,7 +2800,7 @@ function stringList(value) {
2662
2800
  return void 0;
2663
2801
  }
2664
2802
  function refFilters(value) {
2665
- if (!isRecord(value)) return {};
2803
+ if (!isRecord2(value)) return {};
2666
2804
  const out = {};
2667
2805
  const branches = stringList(value["branches"]);
2668
2806
  const tags = stringList(value["tags"]);
@@ -2680,17 +2818,17 @@ function parseTriggers(on) {
2680
2818
  };
2681
2819
  if (typeof on === "string") enable(on, void 0);
2682
2820
  else if (Array.isArray(on)) for (const name of on) enable(name, void 0);
2683
- else if (isRecord(on)) {
2821
+ else if (isRecord2(on)) {
2684
2822
  for (const [name, options] of Object.entries(on)) enable(name, options);
2685
2823
  }
2686
2824
  return triggers;
2687
2825
  }
2688
2826
  function parseJobs(jobs) {
2689
- if (!isRecord(jobs)) return [];
2827
+ if (!isRecord2(jobs)) return [];
2690
2828
  const out = [];
2691
2829
  for (const [id, spec] of Object.entries(jobs)) {
2692
2830
  const job = { id };
2693
- if (isRecord(spec)) {
2831
+ if (isRecord2(spec)) {
2694
2832
  if (typeof spec["name"] === "string") job.name = spec["name"];
2695
2833
  const runsOn = stringList(spec["runs-on"]);
2696
2834
  if (runsOn) job.runsOn = runsOn;
@@ -2716,7 +2854,7 @@ function parseWorkflow(path, content) {
2716
2854
  parseError: err instanceof Error ? err.message : String(err)
2717
2855
  };
2718
2856
  }
2719
- if (!isRecord(doc)) {
2857
+ if (!isRecord2(doc)) {
2720
2858
  return { ...base, parseError: "workflow is not a YAML mapping" };
2721
2859
  }
2722
2860
  const on = doc["on"] ?? doc["true"];
@@ -4098,10 +4236,16 @@ export {
4098
4236
  CI_JOB_RESULT_KIND,
4099
4237
  CI_WORKFLOW_RESULT_KIND,
4100
4238
  CI_WORKFLOW_PROGRESS_KIND,
4239
+ CI_LIVE_LOG_TAIL_KIND,
4101
4240
  CI_RUNNER_FAMILY,
4102
4241
  CI_SOFTWARE,
4103
4242
  CI_MAX_ADVERTISEMENT_TTL,
4104
4243
  CI_MAX_PROGRESS_TTL,
4244
+ CI_MAX_LIVE_LOG_TAIL_TTL,
4245
+ CI_LIVE_LOG_TAIL_INTERVAL_MS,
4246
+ CI_LIVE_LOG_TAIL_JOB_BYTES,
4247
+ CI_LIVE_LOG_TAIL_MAX_BYTES,
4248
+ CI_RUNNER_CHANNEL_KEY,
4105
4249
  isCiConclusion,
4106
4250
  repoAddress,
4107
4251
  parseRepoAddress,
@@ -4124,6 +4268,11 @@ export {
4124
4268
  parseCiWorkflowResult,
4125
4269
  buildCiWorkflowProgress,
4126
4270
  parseCiWorkflowProgress,
4271
+ liveLogTailBudget,
4272
+ liveLogTailEventBudget,
4273
+ sliceLogTail,
4274
+ buildCiLiveLogTail,
4275
+ parseCiLiveLogTail,
4127
4276
  SECRET_NAME_RE,
4128
4277
  RESERVED_SECRET_NAMES,
4129
4278
  MAX_SECRET_NAMES,
@@ -4174,4 +4323,4 @@ export {
4174
4323
  assembleCiStatus,
4175
4324
  runCiStatus
4176
4325
  };
4177
- //# sourceMappingURL=chunk-DCGV6ALH.js.map
4326
+ //# sourceMappingURL=chunk-3IVO2MSZ.js.map