Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: Publish CI Runtime
name: Publish Testnode

on:
workflow_dispatch:
inputs:
version:
description: "Pinned runtime version tag"
description: "Pinned testnode image version tag"
required: true
type: string
snapshot-version:
description: "Tagged snapshot release version to install"
description: "Snapshot release tag to use for every selected variant"
required: true
type: string
snapshot-repo:
Expand All @@ -17,7 +17,7 @@ on:
default: ""
type: string
variant:
description: "Runtime variant to publish"
description: "Named variant to publish"
required: true
default: "l3-eth"
type: choice
Expand Down Expand Up @@ -45,33 +45,47 @@ jobs:
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build publish matrix
id: matrix
env:
INPUT_VARIANT: ${{ inputs.variant }}
INPUT_SNAPSHOT_VERSION: ${{ inputs.snapshot-version }}
INPUT_CONTRACTS_VERSION: ${{ inputs.nitro-contracts-version }}
run: >-
node --input-type=module -e
"const variants =
"import { VARIANTS } from './packages/action/src/lib.mjs';
const names =
process.env.INPUT_VARIANT === 'all'
? ['l2', 'l3-eth', 'l3-custom-6', 'l3-custom-16', 'l3-custom-18', 'l3-custom-20']
? Object.keys(VARIANTS)
: [process.env.INPUT_VARIANT];
const contractsVersions =
process.env.INPUT_CONTRACTS_VERSION === 'all'
? ['v2.1', 'v3.2']
: [process.env.INPUT_CONTRACTS_VERSION];
const include = [];
for (const variant of variants) {
for (const variant of names) {
const config = VARIANTS[variant];
if (!config) throw new Error(\`Unknown variant \${variant}\`);
const snapshotReleaseTag = process.env.INPUT_SNAPSHOT_VERSION;
if (!snapshotReleaseTag) throw new Error('snapshot-version is required');
for (const contractsVersion of contractsVersions) {
include.push({ variant, contractsVersion });
include.push({
variant,
contractsVersion,
snapshotId: config.snapshotId,
snapshotReleaseTag
});
}
}
const output = process.env.GITHUB_OUTPUT;
if (!output) throw new Error('GITHUB_OUTPUT is required');
const { appendFileSync } = await import('node:fs');
appendFileSync(output, 'matrix=' + JSON.stringify({ include }) + '\\n');"

publish-runtime-image:
publish-testnode-image:
needs: resolve-publish-matrix
runs-on: ubuntu-latest
strategy:
Expand All @@ -91,41 +105,24 @@ jobs:

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Resolve variant metadata
id: variant
run: >-
node --input-type=module -e
"import { VARIANTS } from './scripts/action/lib.mjs';
const variant = process.env['INPUT_VARIANT'];
const definition = VARIANTS[variant];
if (!definition) throw new Error(\`Unknown variant \${variant}\`);
const output = process.env['GITHUB_OUTPUT'];
if (!output) throw new Error('GITHUB_OUTPUT is required');
const { appendFileSync } = await import('node:fs');
appendFileSync(output, \`snapshot-id=\${definition.snapshotId}\\n\`);"
env:
INPUT_VARIANT: ${{ matrix.variant }}

- name: Install snapshot release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TESTNODE_SNAPSHOT_GH_REPO: ${{ inputs.snapshot-repo || github.repository }}
run: >-
pnpm dev snapshot install --force
--id "${{ steps.variant.outputs.snapshot-id }}"
--release-tag "${{ inputs.snapshot-version }}"
--id "${{ matrix.snapshotId }}"
--release-tag "${{ matrix.snapshotReleaseTag }}"

- name: Prepare runtime context
- name: Prepare testnode context
run: >-
node scripts/ci/prepare-runtime-context.mjs
node scripts/ci/prepare-testnode-context.mjs
--variant "${{ matrix.variant }}"
--snapshot-id "${{ steps.variant.outputs.snapshot-id }}"
--snapshot-id "${{ matrix.snapshotId }}"
--nitro-contracts-version "${{ matrix.contractsVersion }}"

- name: Lowercase owner
Expand All @@ -143,10 +140,10 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push runtime image
- name: Build and push testnode image
uses: docker/build-push-action@v6
with:
context: .
file: docker/ci-runtime.Dockerfile
file: docker/testnode.Dockerfile
push: true
tags: ghcr.io/${{ steps.lower.outputs.owner }}/arbitrum-testnode-ci:${{ inputs.version }}-${{ steps.nc.outputs.tag }}-${{ matrix.variant }}
tags: ghcr.io/${{ steps.lower.outputs.owner }}/arbitrum-testnode:${{ inputs.version }}-${{ steps.nc.outputs.tag }}-${{ matrix.variant }}
14 changes: 6 additions & 8 deletions .github/workflows/test-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ jobs:

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand All @@ -36,18 +34,18 @@ jobs:
TESTNODE_SNAPSHOT_GH_REPO: ${{ github.repository }}
run: pnpm dev snapshot install --force --release-tag "${{ inputs.snapshot-version || 'v0.1.6' }}"

- name: Prepare runtime context
run: node scripts/ci/prepare-runtime-context.mjs --variant l3-eth --snapshot-id default
- name: Prepare testnode context
run: node scripts/ci/prepare-testnode-context.mjs --variant l3-eth --snapshot-id default

- name: Build local runtime image
run: docker build -f docker/ci-runtime.Dockerfile -t local/arbitrum-testnode-ci:${{ github.sha }}-nc3.2-l3-eth .
- name: Build local testnode image
run: docker build -f docker/testnode.Dockerfile -t local/arbitrum-testnode:${{ github.sha }}-nc3.2-l3-eth .

- name: Run action
id: action
uses: ./
with:
image-repository: local/arbitrum-testnode-ci
l3-node: true
image-repository: local/arbitrum-testnode
l3-enabled: true
nitro-contracts-version: v3.2
version: ${{ github.sha }}

Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
node_modules
dist
config/*.json
!config/variants.json
config/*.yaml
config/anvil-state
config/runs/
config/snapshots/
.ci-runtime-context/
.testnode-context/
scratch/
config/l1-l2-admin/
config/l2-l3-admin/
*.log
*.tsbuildinfo
146 changes: 122 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,66 @@ A snapshot-backed Arbitrum testnode that boots L1 + L2 + L3 with token bridges i

## Quick Start

### One-Command Local L3

Use `start` when you want a disposable local `L1 + L2 + L3` stack from a published testnode image instead of rebuilding the full testnode locally. This is a local development path only; it does not deploy to Arbitrum Sepolia.

Minimal usage:

```bash
pnpm dev start --image-version v0.2.2
```

By default that resolves the `l3-eth` variant image:

```text
ghcr.io/offchainlabs/arbitrum-testnode:v0.2.2-nc3.2-l3-eth
```

Config-driven usage:

```json
{
"version": "v0.2.2",
"l3Enabled": true
}
```

Save that as `testnode.start.json`, then run:

```bash
pnpm dev start
```

Optional config fields:

| Field | Default | Description |
|-------|---------|-------------|
| `version` | — | Required testnode image release version |
| `l3Enabled` | `true` | Boot the L3-enabled testnode |
| `feeTokenDecimals` | — | Custom L3 fee token decimals (`6`, `16`, `18`, `20`) |
| `nitroContractsVersion` | `v3.2` | Nitro contracts version tag component |
| `imageRepository` | `ghcr.io/offchainlabs/arbitrum-testnode` | testnode image repository |
| `containerName` | `arbitrum-testnode-<variant>` | Docker container name override |
| `outputDir` | `./.arbitrum-testnode/<version>/<variant>` | Export directory for config files |
| `startupTimeoutSeconds` | `120` | RPC readiness timeout |
| `networkConfigPath` | — | One path or an array of paths to overwrite with `localNetwork.json` |

Start exports config under `outputDir/config` and boots these host RPCs:

| Chain | URL |
|------|-----|
| L1 | `http://127.0.0.1:8545` |
| L2 | `http://127.0.0.1:8547` |
| L3 | `http://127.0.0.1:3347` |

### GitHub Action

```yaml
- uses: OffchainLabs/arbitrum-testnode@v0.1.0
with:
version: v0.1.0
l3-node: true
l3-enabled: true
github-token: ${{ secrets.GITHUB_TOKEN }}
```

Expand All @@ -23,40 +76,65 @@ The action starts a fully initialized testnode and exports environment variables
| `ARBITRUM_TESTNODE_L3_RPC_URL` | L3 (Orbit) RPC endpoint |
| `ARBITRUM_TESTNODE_LOCAL_NETWORK_PATH` | Path to `localNetwork.json` with all deployed contract addresses |
| `ARBITRUM_TESTNODE_CONFIG_DIR` | Directory with all exported config files |
| `ARBITRUM_TESTNODE_VARIANT` | Resolved variant name, such as `l3-eth` |

### Local Development

```bash
pnpm install
pnpm dev start --image-version v0.2.2 # Boot the published testnode image
pnpm dev init # First run: deploys everything from scratch (~12 min)
pnpm dev init # Subsequent runs: restores from snapshot (~10 sec)
pnpm dev stop # Stop all services
pnpm dev start # Restart from saved state
pnpm dev clean # Remove containers and runtime data
pnpm dev clean # Remove containers and saved data
pnpm dev status # Show service and init state
```

## Architecture

```
src/
├── index.ts # CLI entry point
├── accounts.ts # Deterministic HD wallet accounts (official nitro-testnode mnemonic)
├── rpc.ts # Viem client factories and contract ABIs
├── state.ts # Init state persistence (JSON)
├── exec.ts # Shell helpers for external CLIs
├── docker.ts # Docker Compose helpers + RPC polling
├── snapshot.ts # Snapshot capture, restore, and verification
├── token-bridge.ts # Token bridge deployment (L1-L2 and L2-L3)
├── validator-wallet.ts # Validator wallet creation and staking
└── commands/
├── init.ts # 14-step init sequence with resume
├── start.ts # Start all Docker services
├── stop.ts # Stop all Docker services
├── clean.ts # Remove containers, volumes, config
└── status.ts # Show service and init state
apps/
└── cli/ # `testnode` CLI entry point and command parsing

packages/
├── core/ # Chain, Docker, snapshot, bridge, state, and init helpers
├── testnode/ # Image resolution and Docker launcher helpers
└── action/ # Composite GitHub Action Node scripts

docker/ # Testnode, token bridge, and compose assets
scripts/ci/ # Release image context preparation helpers
action.yml # Root composite action contract
```

## Published Variants

Published images are driven by `config/variants.json`. Each entry defines a named variant:

- `name`: the variant users select, and the final image tag suffix
- `snapshotId`: the local snapshot directory to install and bake into the image
- `hostPorts`: the host RPC ports exposed by `start` and the action
- `l3Enabled`: whether the image includes an L3 node

The `Publish Testnode` workflow can publish one variant or `all`. It builds image tags as:

```text
ghcr.io/<owner>/arbitrum-testnode:<version>-nc<contracts-version>-<variant>
```

The `snapshot-version` workflow input provides the snapshot release tag used for every selected variant.

Publish one variant image from GitHub Actions:

```text
workflow: Publish Testnode
version: v0.2.2
variant: l3-eth
nitro-contracts-version: v3.2
snapshot-version: v0.1.6
```

Publish every catalog entry by setting `variant` to `all`. Publish every supported Nitro contracts tag by setting `nitro-contracts-version` to `all`.

## Init Sequence

The `init` command runs 14 steps to deploy a complete L1 + L2 + L3 stack:
Expand Down Expand Up @@ -106,19 +184,39 @@ Derived from the official nitro-testnode mnemonic. All accounts are pre-funded o

| Input | Required | Default | Description |
|-------|----------|---------|-------------|
| `version` | Yes | — | Release version for the runtime image tag |
| `l3-node` | No | `false` | Boot the L3 node |
| `version` | Yes | — | Release version for the testnode image tag |
| `l3-enabled` | No | `false` | Boot the L3-enabled testnode |
| `github-token` | No | — | Token for GHCR authentication |
| `image-repository` | No | `ghcr.io/offchainlabs/arbitrum-testnode-ci` | Container image repository |
| `fee-token-decimals` | No | — | Custom fee token decimals (16, 18, or 20) |
| `image-repository` | No | `ghcr.io/offchainlabs/arbitrum-testnode` | Container image repository |
| `fee-token-decimals` | No | — | Custom fee token decimals (6, 16, 18, or 20) |
| `nitro-contracts-version` | No | `v3.2` | Nitro contracts version tag component |
| `output-dir` | No | — | Directory where exported config files should be written |
| `container-name` | No | — | Docker container name override |
| `startup-timeout-seconds` | No | `120` | Max wait time for RPC readiness |
| `network-config-path` | No | — | Comma-separated path(s) to overwrite with exported `localNetwork.json` |

## Action Outputs

| Output | Description |
|--------|-------------|
| `config-dir` | Directory containing exported config files |
| `local-network-path` | Path to `localNetwork.json` |
| `l1l2-network-path` | Path to `l1l2_network.json` |
| `l2l3-network-path` | Path to `l2l3_network.json` |
| `l1-bridge-ui-config-path` | Path to the L1/L2 bridge UI config |
| `l2-bridge-ui-config-path` | Path to the L2/L3 bridge UI config |
| `l1-rpc-url` | Host RPC URL for L1 |
| `l2-rpc-url` | Host RPC URL for L2 |
| `l3-rpc-url` | Host RPC URL for L3 |
| `variant` | Resolved variant name |
| `nitro-contracts-version` | Resolved Nitro contracts version |

## Development

```bash
pnpm install # Install dependencies
pnpm dev # Run CLI in dev mode (tsx)
pnpm build # Bundle to dist/index.mjs
pnpm build # Build all workspace packages
pnpm test:run # Run tests once
pnpm lint # Lint check (Biome)
pnpm lint:fix # Auto-fix lint issues
Expand Down
Loading
Loading