Skip to content
Merged
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
11 changes: 7 additions & 4 deletions .github/workflows/mirror-to-dockerhub.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Mirror Images to Docker Hub

# Copies published GHCR tags to Docker Hub without rebuilding. A rebuild is not
# equivalent: the Dockerfile fetches token-bridge-contracts and apt packages at
# build time, so it lands a different digest than the tag it replaces.
# Promotes published GHCR tags to public Docker Hub without rebuilding. A rebuild
# is not equivalent: the Dockerfile fetches token-bridge-contracts and apt
# packages at build time, so it lands a different digest than the tag it
# replaces. Releases publish privately to GHCR; this is the deliberate step that
# makes a verified version public.
#
# The tag list is read from the source registry, not derived from
# resolvePublishMatrix: that function describes what a release publishes now, so
Expand Down Expand Up @@ -41,7 +43,7 @@ on:
source-repository:
description: "GHCR repository to copy from"
required: false
default: "ghcr.io/offchainlabs/arbitrum-testnode-ci"
default: "ghcr.io/offchainlabs/arbitrum-litro"
type: string
dockerhub-repository:
description: "Docker Hub repository to mirror into"
Expand Down Expand Up @@ -125,6 +127,7 @@ jobs:

- name: Mirror tags
env:
ALIASES: ${{ steps.tags.outputs.aliases }}
DST_REPOSITORY: ${{ inputs.dockerhub-repository }}
SRC_REPOSITORY: ${{ inputs.source-repository }}
TAGS: ${{ steps.tags.outputs.list }}
Expand Down
135 changes: 57 additions & 78 deletions .github/workflows/release-testnode-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ name: Publish Testnode
# GENERATED inline in CI by running `init` (no pre-built snapshot release
# bundles, nothing to publish out of band). The intended (variant x version)
# matrix and the init flags per combo come from runtime.mjs resolvePublishMatrix.
#
# Publishing lands in GHCR only by default, where the package is private. Docker
# Hub is public and its tags are permanent in practice, so getting there is a
# separate promotion: verify the GHCR images, then dispatch Mirror Images to
# Docker Hub, which copies the digests rather than rebuilding.
on:
push:
tags:
Expand All @@ -30,6 +35,14 @@ on:
- l3-custom-16
- l3-custom-18
- l3-custom-20
registries:
description: "Where to publish. Docker Hub is public and permanent; promote to it with Mirror Images to Docker Hub instead unless a direct publish is intended."
required: true
default: "ghcr"
type: choice
options:
- ghcr
- ghcr,dockerhub
dockerhub-repository:
description: "Docker Hub repository to publish to (retarget for a dry run)"
required: false
Expand All @@ -46,11 +59,24 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
registries: ${{ steps.registries.outputs.selected }}
version: ${{ steps.ver.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4

# Resolved once so the build and alias jobs cannot disagree about where a
# release went. Dispatch inputs do not exist on a tag push, so a tag falls
# through to the private registry.
- name: Resolve target registries
id: registries
env:
REGISTRIES: ${{ inputs.registries }}
run: |
set -euo pipefail
echo "selected=${REGISTRIES:-ghcr}" >> "$GITHUB_OUTPUT"
echo "publishing to ${REGISTRIES:-ghcr}"

- name: Resolve image version
id: ver
run: |
Expand Down Expand Up @@ -129,6 +155,23 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

# Before the snapshot below, which takes tens of minutes: a taken tag or a
# missing credential should fail in a minute, not after the build.
- name: Resolve image refs
id: refs
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: >-
node scripts/ci/resolve-publish-refs.mjs
--registries "${{ needs.resolve-publish-matrix.outputs.registries }}"
--owner "${{ github.repository_owner }}"
--dockerhub-repository "${{ inputs.dockerhub-repository || 'offchainlabs/arbitrum-litro' }}"
--version "${{ needs.resolve-publish-matrix.outputs.version }}"
--variant "${{ matrix.variant }}"
--contracts-version "${{ matrix.contractsVersion }}"
${{ inputs.overwrite && '--overwrite' || '' }}

- name: Install Foundry (forge for token-bridge build)
uses: foundry-rs/foundry-toolchain@v1

Expand Down Expand Up @@ -268,85 +311,21 @@ jobs:
--variant "${{ matrix.variant }}"
--snapshot-id "${{ matrix.snapshotId }}"

# Both refs are derived from one suffix so the registries cannot disagree
# about what a version means. The suffix shape must match
# buildTestnodeImageRef in packages/testnode/src/runtime.mjs -- that is
# what consumers resolve, so a drift here publishes tags nothing pulls.
- name: Resolve image refs
id: refs
env:
CONTRACTS_VERSION: ${{ matrix.contractsVersion }}
# Empty on tag pushes (dispatch inputs do not exist there), so a
# release always lands in the real repository.
DOCKERHUB_REPOSITORY: ${{ inputs.dockerhub-repository || 'offchainlabs/arbitrum-litro' }}
OWNER: ${{ github.repository_owner }}
VARIANT: ${{ matrix.variant }}
VERSION: ${{ needs.resolve-publish-matrix.outputs.version }}
run: |
set -euo pipefail
suffix="${VERSION}-nc${CONTRACTS_VERSION#v}-${VARIANT}"
owner="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')"
{
echo "ghcr=ghcr.io/${owner}/arbitrum-litro:${suffix}"
echo "dockerhub=${DOCKERHUB_REPOSITORY}:${suffix}"
} >> "$GITHUB_OUTPUT"
echo "publishing ${suffix} to ghcr.io/${owner}/arbitrum-litro and ${DOCKERHUB_REPOSITORY}"

# Docker Hub tags are mutable and public: without this, a re-run silently
# replaces a shipped image. Queried through the Hub API rather than the
# registry because Docker Hub answers 401 for an unknown repository, which
# is indistinguishable from bad credentials. Assumes a public repository
# (a private one 404s whether or not the tag exists).
- name: Refuse to overwrite a published Docker Hub tag
if: ${{ !inputs.overwrite }}
env:
REF: ${{ steps.refs.outputs.dockerhub }}
run: |
set -euo pipefail
repository="${REF%:*}"
tag="${REF##*:}"
status="$(curl -sS -o /dev/null -w '%{http_code}' \
"https://hub.docker.com/v2/repositories/${repository}/tags/${tag}")"
case "$status" in
404)
echo "$REF is free"
;;
200)
echo "$REF already exists; re-run with overwrite=true to replace it" >&2
exit 1
;;
*)
echo "could not determine whether $REF exists (HTTP $status)" >&2
exit 1
;;
esac

# Supplies the container driver the gha cache below needs; the default
# docker driver cannot export one.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: ${{ contains(needs.resolve-publish-matrix.outputs.registries, 'ghcr') }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# A release must not degrade to GHCR-only when the credential is missing:
# half-published versions are worse than a failed run.
- name: Check Docker Hub credentials
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: |
set -euo pipefail
if [ -z "${DOCKERHUB_USERNAME}" ] || [ -z "${DOCKERHUB_TOKEN}" ]; then
echo "DOCKERHUB_USERNAME and DOCKERHUB_TOKEN are required to publish" >&2
exit 1
fi

- name: Log in to Docker Hub
if: ${{ contains(needs.resolve-publish-matrix.outputs.registries, 'dockerhub') }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand All @@ -362,23 +341,24 @@ jobs:
build-args: |
BUNDLE_VERSION=${{ needs.resolve-publish-matrix.outputs.version }}
BUNDLE_VARIANT=${{ matrix.variant }}
IMAGE_SOURCE=${{ github.server_url }}/${{ github.repository }}
NITRO_CONTRACTS_REF=${{ env.NITRO_CONTRACTS_REF }}
NITRO_CONTRACTS_COMMIT=${{ env.NITRO_CONTRACTS_COMMIT }}
TOKENBRIDGE_REF=${{ env.TOKEN_BRIDGE_REF }}
TOKENBRIDGE_COMMIT=${{ env.TOKEN_BRIDGE_COMMIT }}
push: true
# One build, two pushes -- a second build could diverge from the first.
tags: |
${{ steps.refs.outputs.ghcr }}
${{ steps.refs.outputs.dockerhub }}
# One build, one push per registry -- a second build could diverge from
# the first, giving the same tag a different digest per registry.
tags: ${{ steps.refs.outputs.tags }}
# Unscoped on purpose: the token-bridge-contracts stage is identical
# across matrix rows (same pinned ref), so sharing one scope is where
# the saving is. Rows overwrite each other's entry; reads still hit.
cache-from: type=gha
cache-to: type=gha,mode=max

# Moves `latest-<variant>` onto this release, in both registries so the alias
# means the same thing wherever it is pulled from.
# Moves `latest-<variant>` onto this release, in every registry the release was
# pushed to. Registries this run skipped pick the alias up when the version is
# mirrored to them, so the alias never names a version that registry lacks.
publish-latest-bundle:
if: ${{ github.ref_type == 'tag' }}
needs: [resolve-publish-matrix, publish-testnode-image]
Expand All @@ -400,27 +380,26 @@ jobs:
tar -xzf /tmp/crane.tar.gz -C /usr/local/bin crane

- name: Log in to GHCR
if: ${{ contains(needs.resolve-publish-matrix.outputs.registries, 'ghcr') }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Log in to Docker Hub
if: ${{ contains(needs.resolve-publish-matrix.outputs.registries, 'dockerhub') }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Lowercase owner
id: owner
run: echo "name=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"

- name: Publish latest bundle aliases
env:
MATRIX: ${{ needs.resolve-publish-matrix.outputs.matrix }}
VERSION: ${{ needs.resolve-publish-matrix.outputs.version }}
run: >-
node scripts/ci/publish-latest-aliases.mjs
--repository "ghcr.io/${{ steps.owner.outputs.name }}/arbitrum-litro"
--repository "offchainlabs/arbitrum-litro"
--registries "${{ needs.resolve-publish-matrix.outputs.registries }}"
--owner "${{ github.repository_owner }}"
--dockerhub-repository "${{ inputs.dockerhub-repository || 'offchainlabs/arbitrum-litro' }}"
101 changes: 101 additions & 0 deletions .github/workflows/verify-published-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Verify Published Image

# Boots a published image through the action, so a version can be checked in the
# private registry before it is promoted to public Docker Hub. GHCR needs a
# credential, which is what makes this distinct from test-action.yml's
# credential-free check of the public default.
on:
workflow_dispatch:
inputs:
version:
description: "Published version to verify (e.g. v0.3.0)"
required: true
type: string
image-repository:
description: "Repository holding the published image"
required: false
default: "ghcr.io/offchainlabs/arbitrum-litro"
type: string
nitro-contracts-version:
description: "Nitro contracts version the tag was published for"
required: true
default: "v3.2"
type: choice
options:
- v3.2
- v2.1

jobs:
# One row per shape a consumer boots: an L2-only chain and a full L1/L2/L3
# stack. Both resolve their tag through the action, so a tag the action cannot
# name fails here rather than in a consumer's repository.
verify:
strategy:
fail-fast: false
matrix:
include:
- variant: l2
l3-enabled: "false"
- variant: l3-eth
l3-enabled: "true"
name: verify (${{ matrix.variant }})
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4

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

- name: Run action against the published image
id: action
uses: ./
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
image-repository: ${{ inputs.image-repository }}
l3-enabled: ${{ matrix.l3-enabled }}
nitro-contracts-version: ${{ inputs.nitro-contracts-version }}
version: ${{ inputs.version }}

- name: Validate chains respond
env:
L1_RPC_URL: ${{ steps.action.outputs.l1-rpc-url }}
L2_RPC_URL: ${{ steps.action.outputs.l2-rpc-url }}
L3_RPC_URL: ${{ steps.action.outputs.l3-rpc-url }}
run: |
set -euo pipefail
for url in "$L1_RPC_URL" "$L2_RPC_URL" "${L3_RPC_URL:-}"; do
if [ -z "$url" ]; then
continue
fi
echo "checking $url"
curl -sfS -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' "$url"
done

- name: Validate network config is served
env:
LOCAL_NETWORK_PATH: ${{ steps.action.outputs.local-network-path }}
run: |
set -euo pipefail
test -f "$LOCAL_NETWORK_PATH"
curl -sfS http://127.0.0.1:8080/health

- name: Validate bundle provenance
run: >-
node scripts/ci/assert-bundle-labels.mjs
--image-ref "${{ steps.action.outputs.image-ref }}"
--variant "${{ matrix.variant }}"
--version "${{ inputs.version }}"
Loading
Loading