Skip to content

feat: add Docker support for running upgrade scripts#65

Open
yahgwai wants to merge 45 commits intomainfrom
feat/380-dockerize
Open

feat: add Docker support for running upgrade scripts#65
yahgwai wants to merge 45 commits intomainfrom
feat/380-dockerize

Conversation

@yahgwai
Copy link
Collaborator

@yahgwai yahgwai commented Feb 5, 2026

Add Dockerfile and CI workflow to enable running orbit-actions commands in a containerized environment without requiring local installation of Foundry and Node.js.

  • Adds a CLI which can be used locally - this adds reusable commands like 'deploy-execute-verify'
  • Adds dockerfile so CLI can be called via docker
  • Adds dockerhub integration
  • Adds verify scripts for contract upgrade

fixes:

Add Dockerfile and CI workflow to enable running orbit-actions
commands in a containerized environment without requiring local
installation of Foundry and Node.js.

- Add Dockerfile with Node 18, Foundry, and pre-installed dependencies
- Add smoke tests to verify tools and scripts are accessible
- Add GitHub Actions workflow to build and test Docker image on PRs
- Update README with Docker usage instructions
@yahgwai yahgwai requested a review from Copilot February 5, 2026 17:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Docker support for running orbit-actions commands in a containerized environment, eliminating the need for local installation of Foundry and Node.js.

Changes:

  • Added Dockerfile with Node 18, Foundry installation, and dependency setup
  • Created comprehensive smoke test suite to verify Docker image functionality
  • Added GitHub Actions workflow for automated Docker image testing on PRs
  • Updated documentation with Docker usage examples and configuration instructions

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Dockerfile Defines containerized environment with Node 18, Foundry, and pre-built dependencies
.dockerignore Excludes unnecessary files from Docker build context for optimization
test/docker/test-docker.bash Implements smoke tests verifying tools, dependencies, and scripts in Docker image
.github/workflows/test-docker.yml Automates Docker image building and testing on pull requests
package.json Adds test:docker script for running Docker smoke tests
README.md Documents Docker usage with examples for commands, environment variables, and volume mounting

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

yahgwai and others added 5 commits February 5, 2026 17:32
The Docker build requires lib/ (forge-std, arbitrum-sdk) which comes
from git submodules. Update CI to checkout with submodules and document
the prerequisite for local builds.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@yahgwai yahgwai requested a review from Copilot February 5, 2026 17:49
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Add a path-based CLI that allows users to browse and execute scripts
from the foundry directory:

- Browse directories: `docker run orbit-actions contract-upgrades/1.2.1`
- View files: `docker run orbit-actions contract-upgrades/1.2.1/README.md`
- Run upgrades: `docker run orbit-actions contract-upgrades/1.2.1/deploy-execute-verify`

Structure:
- entrypoint.sh: thin shim that sources .env and delegates to router
- bin/router: path parsing, directory listing, command dispatch
- bin/contract-upgrade: deploy, execute, deploy-execute-verify commands
- bin/arbos-upgrade: deploy, execute, verify, deploy-execute-verify commands
- lib/common.sh: shared utilities for auth parsing and forge helpers

Upgrade commands read configuration from mounted .env file and accept
auth flags (--deploy-key, --execute-key, --ledger, etc.) for signing.
Add Verify*.s.sol Forge scripts to each contract-upgrade version folder,
replacing hardcoded verification logic with discoverable scripts.

- Add verify command to bin/contract-upgrade and bin/router
- Create Verify scripts for 1.2.1, 2.1.0, 2.1.2, 2.1.3
- Update READMEs to reference forge script verification
Publish offchainlabs/chain-actions image to Docker Hub:
- On push to main: tag as latest
- On release tags (v*): tag as version (e.g., 1.2.3, 1.2)
- Manual trigger: tag with branch name (for testing)

Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets.
@yahgwai yahgwai requested a review from Copilot February 9, 2026 11:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 21 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Replace bash CLI (bin/router, bin/contract-upgrade, bin/arbos-upgrade,
lib/common.sh) with TypeScript implementation in src/cli/.

- Add commander for argument parsing
- Add execa for subprocess execution
- Update Dockerfile to use node entrypoint directly
- Remove entrypoint.sh (no longer needed)
- Remove redundant comments that state the obvious
- Add named constants for ArbOS precompiles (ARB_OWNER_PUBLIC, ARB_SYS)
- Add ARBOS_VERSION_OFFSET constant with explanation
- Remove unused AuthArgs interface
- Remove unused getRepoRoot import
- Fix double findRepoRoot() call in loadEnv()
- Remove deprecated @typescript-eslint/tslint plugin from eslint config
- Update docker tests to use --entrypoint for tool access
- Apply prettier formatting to src/cli files
- Fix no-implicit-coercion lint errors (!! -> Boolean())
Non-Docker tests for the bin/router and related scripts.
Replace verbose Docker documentation with concise CLI section and
streamlined Docker examples. Move tooling documentation to end of
README to prioritize upgrade guides.
Remove fallback locations for .env files. Now only loads from
process.cwd(), matching Forge's behavior for transparency.
Extract executeUpgrade() and verifyUpgrade() helpers to eliminate
code duplication between standalone commands and deploy-execute-verify.
Remove redundant subcommand interface - all functionality is accessed
via the router's path-based syntax. Also extract deployAction helper
to reduce duplication in arbos-upgrade.
Pin foundryup to nightly-2026-02-09 for reproducible Docker builds.
- Pin to actual nightly release (2026-02-10) using commit hash
- Add dist/ to eslintignore
- Fix prettier formatting in arbos-upgrade.ts
The foundryup --version flag doesn't work correctly in Docker
when bootstrapping from the install script. Revert to unpinned
foundryup which installs the latest stable release.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 29 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +45 to +52
const result = await execa('forge', args, {
stdio: 'inherit',
env: process.env,
})

if (result.exitCode !== 0) {
die(`Forge script failed with exit code ${result.exitCode}`)
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execa rejects/throws by default on non-zero exit codes, so this code path likely never reaches the exitCode check (it will throw before returning result). Handle failures via try/catch + die(...), or set reject: false in the execa options and keep the explicit exitCode check.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +84
const result = await execa('cast', args, {
stdio: 'inherit',
env: process.env,
})

if (result.exitCode !== 0) {
die(`Cast send failed with exit code ${result.exitCode}`)
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as runForgeScript: execa throws on non-zero by default, so the exitCode branch likely won't run. Use try/catch to call die(...) with a clear message (and optionally surface stderr), or set reject: false and keep the exitCode handling.

Copilot uses AI. Check for mistakes.
args.push('--skip-simulation')
}

const verbosity = options.verbosity ?? 3
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If options.verbosity is set to 0 (or a negative number), this will generate an invalid flag like -. Clamp verbosity to a sane minimum (e.g., Math.max(1, verbosity)) before building the -v... argument.

Suggested change
const verbosity = options.verbosity ?? 3
const verbosity = Math.max(1, options.verbosity ?? 3)

Copilot uses AI. Check for mistakes.
.argument('[args...]', 'Additional arguments')
.allowUnknownOption(true)
.action(async (pathArg?: string, args?: string[]) => {
await router(pathArg, args)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes string[] | undefined into router's second parameter typed as string[] (with a default). TypeScript will typically reject this (even though runtime defaulting works). Fix by calling await router(pathArg, args ?? []), or by making args non-optional in the action signature.

Suggested change
await router(pathArg, args)
await router(pathArg, args ?? [])

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +185
console.error(`Error: ArbOS version required`)
console.error(
`Usage: arbos-upgrades/at-timestamp/${basename} <version> [options]`
)
process.exit(1)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bypasses the shared error-handling (die) used elsewhere in the CLI, which can lead to inconsistent formatting/output and duplicated behavior. Consider replacing these console.error + process.exit(1) calls with die(...) (including the usage string) for consistency.

Suggested change
console.error(`Error: ArbOS version required`)
console.error(
`Usage: arbos-upgrades/at-timestamp/${basename} <version> [options]`
)
process.exit(1)
die(
`Error: ArbOS version required
Usage: arbos-upgrades/at-timestamp/${basename} <version> [options]`
)

Copilot uses AI. Check for mistakes.
# --ignore-scripts: forge install runs separately after full copy
RUN yarn install --frozen-lockfile --ignore-scripts

COPY . .
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Docker build relies on Forge dependencies being present, but the Dockerfile never runs forge install (and yarn install --ignore-scripts skips the repo's prepare hook that would normally do it). This makes docker build fail unless the build context already contains lib/ from a prior host-side forge install (as your CI currently does). Add an explicit RUN forge install inside the Dockerfile (after COPY . . and before forge build) so the image builds reliably from a clean checkout.

Suggested change
COPY . .
COPY . .
RUN forge install

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +43
with:
images: offchainlabs/chain-actions
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publish workflow targets offchainlabs/chain-actions, but the PR description/README/Docker usage references offchainlabs/orbit-actions. If orbit-actions is the intended image name, update images: accordingly to avoid publishing to the wrong repository.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@godzillaba godzillaba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still doing review but wanted to leave a couple comments before logging off

Comment on lines +18 to +26
if (arg === '--private-key' || arg === '--account') {
const value = args[i + 1]
if (value) {
return `${arg} ${value}`
}
}
if (arg === '--ledger' || arg === '--interactive') {
return arg
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to support more than private key? i doubt people will use the others

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already had these args documented in the readme:

eg: https://github.com/OffchainLabs/orbit-actions/blob/main/scripts/foundry/contract-upgrades/1.2.1/README.md#how-to-use-it

so i assumed they were useful for something

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok gotcha, i guess i don't really have a preference either way

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove command line args now, use env vars only for foundry with the cli

env?: Record<string, string>
}

export async function runForgeScript(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should expose -g, --gas-limit-multiplier since forge scripts can be finicky with L2 gas accounting

A: try to add -g 1000 to the command

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only use env vars now

README.md Outdated

```bash
# Browse available scripts
yarn orbit-actions # List top-level directories
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this command doesn't work

yarn orbit-actions
yarn run v1.22.22
warning package.json: License should be a valid SPDX license expression
error Command "orbit-actions" not found.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeh, needed npm link. I've got rid of this now - can use yarn cli locally now

README.md Outdated

```bash
# Browse available scripts
yarn orbit-actions # List top-level directories
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also why is the default just ls? personally doesn't feel intuitive

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it because using docker you need to explore to find paths to versions and envs and readme's etc?

if so i think some kind of tree command would be a little better

Copy link
Collaborator Author

@yahgwai yahgwai Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added some more help text. we have this so that a user can run any of the scripts in docker via the cli - not hardcoded commands.

I agree that it's not very intuitive - but i dont think tree is great either since you'll still need to pass a path to run the script with that

return result.stdout.trim()
}

export function parseActionAddress(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function implies that any action deployment script MUST deploy the action last. which is probably true for most reasonable scripts but still seems like a subtle footgun that should at least be documented

yahgwai and others added 12 commits March 9, 2026 15:53
Add comments to Dockerfile and workflow, format PERFORM_SELECTOR
expression, use die() instead of manual console.error/process.exit
in router.
…v vars

Remove custom auth flag parsing (--private-key, --account, --ledger, etc.)
and the deploy-execute-verify combined command. Forge/cast auth and behavior
is now configured entirely via FOUNDRY_*/ETH_* env vars passed through
process.env.

- Delete src/cli/utils/auth.ts
- Simplify ForgeScriptOptions to {script, rpcUrl, env?}
- Remove authArgs from CastSendOptions
- Gate arbos cast send on FOUNDRY_BROADCAST env var
- Remove parseOptions and combined command routing
- Update env templates with FOUNDRY_* vars
- Update README with env var docs and separate-step workflow
- Log full forge args instead of truncating for easier debugging
- runCastCall uses die() on failure instead of returning 'N/A'
- Format scheduled upgrade output as (version, timestamp) tuple
- Add contextual help text at every CLI browsing level
…text

- Trim env templates to FOUNDRY_BROADCAST and ETH_PRIVATE_KEY, link to
  Foundry config docs for the full list of supported env vars
- Remove orbit-actions prefix from CLI help text so it reads correctly
  regardless of invocation method (yarn cli, Docker, linked binary)
- Update README env var table to match
The orbit-actions binary only worked via yarn link and isn't used
in Docker (entrypoint is node directly) or local dev (yarn cli).
The [orbit-actions] prefix added no value -- the user already knows
what tool they're running. Drop the wrapper and use console.log
throughout for consistent, unprefixed output.
Co-authored-by: Henry <11198460+godzillaba@users.noreply.github.com>
…xecute chaining

Execute commands now read the deployed action address from Forge's
broadcast output when UPGRADE_ACTION_ADDRESS is not set, enabling
deploy && execute chaining without manual .env edits. The env var
still takes precedence as an explicit override for multisig flows.
Extract duplicate resolveActionAddress from arbos-upgrade and
contract-upgrade into a shared function in forge.ts. Make
parseActionAddress private and document its last-CREATE assumption.
Test was checking for deploy-execute-verify which no longer exists.
Updated to check for deploy/execute/verify as separate commands.
Also fixes prettier formatting.
@yahgwai yahgwai requested a review from godzillaba March 11, 2026 14:40
Comment on lines +100 to +110
async function cmdDeploy(version: string): Promise<void> {
const rpcUrl = requireEnv('CHILD_CHAIN_RPC')
console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`)
await deployAction(version, rpcUrl)

const address = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl)
console.log(`Deployed action address: ${address}`)
console.log(
'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override'
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo this function should just short circuit if env.UPGRADE_ACTION_ADDRESS is defined. currently if it's set then it will run the deploy script, then instead of printing out the new contract, it'll say:

console.log(`Deployed action address: ${process.env.UPGRADE_ACTION_ADDRESS}`)
console.log(
  'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override'
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if broadcast isn't turned on, then i think this will also say "deployed to XXX ... run execute next ..." even though XXX has not been deployed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, updated

rpcUrl,
})

const address = await resolveActionAddress(deployScript, rpcUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same short circuit concern here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

console.log(`To: ${upgradeExecutor}`)
console.log(`Calldata: ${executeCalldata}`)
console.log('')
console.log('Submit this to your multisig/Safe to execute the upgrade')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also seems weird to say "submit to your mutlisig" right before going ahead and executing from an EOA

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just deleting that comment. useless ayway

Comment on lines +89 to +93

// Assumes the action contract is the last CREATE in the broadcast file.
// This holds for all current deploy scripts, which deploy dependencies first
// and the action contract last.
function parseActionAddress(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this assumption should also be documented in all the foundry action deployment scripts, since those will surely be used as a reference for future versions. couldn't hurt to include it in the top level readme as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments


export async function runCastCall(options: CastCallOptions): Promise<string> {
try {
const result = await execa('cast', [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is stderr visible to the user if this fails?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pass {stderr: 'inherit'}?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, updated

Comment on lines +66 to +71
await runCastSend({
to: upgradeExecutor,
sig: 'execute(address,bytes)',
args: [actionAddress, PERFORM_SELECTOR],
rpcUrl,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we've already generated the actual calldata so imo we should just pass it along directly to cast send.

this is the only place runCastSend is called so i think it's okay to change the function sig.

Suggested change
await runCastSend({
to: upgradeExecutor,
sig: 'execute(address,bytes)',
args: [actionAddress, PERFORM_SELECTOR],
rpcUrl,
})
await runCastSend({
to: upgradeExecutor,
data: executeCalldata,
rpcUrl,
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use data in send and call

yahgwai added 6 commits March 12, 2026 10:59
Skip action address resolution when not broadcasting - forge runs in
simulation mode and doesn't produce a broadcast file at the normal path.
Also early-return from deploy if UPGRADE_ACTION_ADDRESS is already set.
Remove unused getEnv helper from CLI env utils.
Replace cast calldata/sig-based interfaces with pre-encoded --data param.
Encode calldata and decode results with ethers Interface, removing the
async castCalldata shell-out entirely.
The CLI identifies the action contract by taking the last CREATE from
the broadcast file. Add comments to all deploy scripts noting this
constraint, and document it in the README for future script authors.
- Block path traversal in router (reject ".." in path args)
- Anchor version regex to prevent misrouting on nested paths
- Fix .dockerignore excluding .env.sample and env template files
- Remove incorrect 2.1.3 verify script (checked nativeTokenDecimals,
  a 2.1.2 concern; reverts on ETH-native chains)
- Add ROLLUP env var to 1.2.1 and 2.1.0 env templates for verify scripts
- Use node:22 and foundryup --version stable in Dockerfile
- Use submodules: recursive in CI instead of forge install
- Add broadcast guidance on arbos execute without FOUNDRY_BROADCAST
- Add missing deploy script null check in contract-upgrade cmdExecute
- Wrap getChainId with try/catch for consistent error handling
- Load .env from repo root instead of cwd for path consistency
- Warn on fallback to /app when repo root not found
- Drop isCategoryDir messaging in directory listing
foundryup was rewritten and --version now prints the tool version
instead of installing a specific Foundry version. The new flag is
--install.
@yahgwai yahgwai requested a review from godzillaba March 12, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants