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
41 changes: 41 additions & 0 deletions .github/packaging/Dockerfile.tgz
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Build container for linux tarball packaging of avalanchego and subnet-evm.
#
# Based on Ubuntu 22.04 (glibc 2.35) to match the jammy target users
# already consume. Source tree is bind-mounted at runtime, not COPY'd.
#
# Usage (via build-builder-image.sh, which fetches GO_CHECKSUM automatically):
# DOCKERFILE=Dockerfile.tgz .github/packaging/scripts/build-builder-image.sh
# docker run --rm -v .:/build -v ./build/tgz:/output avalanchego-tgz-builder ...

FROM ubuntu:22.04

ARG GO_VERSION=INVALID
ARG GO_CHECKSUM=INVALID
ARG TARGETARCH

ENV DEBIAN_FRONTEND=noninteractive

# Install build dependencies
# - gcc, libc6-dev: required for cgo (CGO_ENABLED=1 in scripts/constants.sh)
# - gnupg2: GPG signing of tarballs
# - git: version detection in build scripts
# - ca-certificates, curl: Go download
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gcc \
git \
gnupg2 \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*

# Install Go (with SHA256 verification)
RUN curl -fsSL -o /tmp/go.tar.gz \
"https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \
&& echo "${GO_CHECKSUM} /tmp/go.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf /tmp/go.tar.gz \
&& rm /tmp/go.tar.gz
ENV PATH="/usr/local/go/bin:${PATH}"

WORKDIR /build
64 changes: 62 additions & 2 deletions .github/packaging/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# RPM packaging tasks for avalanchego and subnet-evm.
# Packaging tasks for avalanchego and subnet-evm.
#
# Builds RPMs inside a Rocky Linux 9 container (glibc 2.34) with GPG signing.
# - RPMs build inside a Rocky Linux 9 container (glibc 2.34) with GPG signing.
# - Linux tarballs build inside an Ubuntu 22.04 container (glibc 2.35) with
# detached GPG signatures and validate in a fresh ubuntu:22.04 container.
# PACKAGING_TAG defaults to v0.0.0 for local testing; set for release builds.

version: '3'
Expand Down Expand Up @@ -31,6 +33,18 @@ vars:
PACKAGING_DOCKER_IMAGE: avalanchego-rpm-builder
PACKAGING_OUTPUT_DIR: '{{.REPO_ROOT}}/build/rpm'

# Map uname -m to deb/tarball arch names (aarch64 -> arm64).
PACKAGING_TGZ_HOST_ARCH:
sh: |
arch=$(uname -m)
case "${arch}" in
x86_64) echo "amd64" ;;
arm64|aarch64) echo "arm64" ;;
*) echo "${arch}" ;;
esac
PACKAGING_TGZ_DOCKER_IMAGE: avalanchego-tgz-builder
PACKAGING_TGZ_OUTPUT_DIR: '{{.REPO_ROOT}}/build/tgz'

tasks:
default:
desc: Lists available packaging tasks
Expand Down Expand Up @@ -114,3 +128,49 @@ tasks:
GIT_COMMIT: '{{.PACKAGING_GIT_COMMIT}}'
cmds:
- cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/validate-rpm.sh'

build-tgz-builder-docker-image:
desc: Builds the linux tarball builder Docker image
internal: true
env:
GO_VERSION: '{{.PACKAGING_GO_VERSION}}'
DOCKER_IMAGE: '{{.PACKAGING_TGZ_DOCKER_IMAGE}}'
DOCKERFILE: 'Dockerfile.tgz'
CONTEXT_DIR: '{{.REPO_ROOT}}/.github/packaging'
cmds:
- cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/build-builder-image.sh'

build-tarballs:
desc: Builds linux tarballs for avalanchego and subnet-evm in a container
vars:
TGZ_TAG: '{{.PACKAGING_TAG}}'
deps: [build-tgz-builder-docker-image]
cmds:
- cmd: mkdir -p {{.PACKAGING_TGZ_OUTPUT_DIR}}
- cmd: >-
docker run --rm
--platform linux/{{.PACKAGING_TGZ_HOST_ARCH}}
-v {{.REPO_ROOT}}:/build
-v {{.PACKAGING_TGZ_OUTPUT_DIR}}:/output
{{if .GPG_KEY_FILE}}-v {{.GPG_KEY_FILE}}:{{.GPG_KEY_FILE}}:ro{{end}}
-e PACKAGING_TAG={{.TGZ_TAG}}
-e OUTPUT_DIR=/output
-e AVALANCHEGO_COMMIT={{.PACKAGING_GIT_COMMIT}}
{{if .GPG_KEY_FILE}}-e GPG_KEY_FILE{{end}}
{{if .GPG_PASSPHRASE}}-e GPG_PASSPHRASE{{end}}
{{.PACKAGING_TGZ_DOCKER_IMAGE}}
.github/packaging/scripts/build-tgz.sh

validate-tarballs:
desc: Validates tarballs by verifying signatures and smoke testing in a fresh container
cmds:
- cmd: >-
TAG={{.PACKAGING_TAG}}
GIT_COMMIT={{.PACKAGING_GIT_COMMIT}}
{{.REPO_ROOT}}/.github/packaging/scripts/validate-tgz.sh

test-build-tarballs:
desc: Builds and validates linux tarballs end-to-end
cmds:
- task: build-tarballs
- task: validate-tarballs
16 changes: 14 additions & 2 deletions .github/packaging/scripts/build-builder-image.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash

# Build the RPM builder Docker image with Go checksum verification.
# Build a packaging builder Docker image with Go checksum verification.
#
# Fetches the SHA256 checksum for the Go tarball from go.dev release
# metadata and passes it to the Docker build for integrity verification.
Expand All @@ -9,19 +9,24 @@
# GO_VERSION - Go version to install (e.g., "1.24.12")
# DOCKER_IMAGE - Name for the built Docker image
# CONTEXT_DIR - Path to the Dockerfile directory
#
# Optional env vars:
# DOCKERFILE - Dockerfile name within CONTEXT_DIR (default: "Dockerfile")

set -euo pipefail

: "${GO_VERSION:?GO_VERSION must be set}"
: "${DOCKER_IMAGE:?DOCKER_IMAGE must be set}"
: "${CONTEXT_DIR:?CONTEXT_DIR must be set}"

DOCKERFILE="${DOCKERFILE:-Dockerfile}"

command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required but not found on PATH" >&2; exit 1; }

# Map host arch to Go's naming convention
arch=$(uname -m)
case "${arch}" in
x86_64) goarch="amd64" ;;
x86_64) goarch="amd64" ;;
aarch64|arm64) goarch="arm64" ;;
*) echo "Unsupported arch: ${arch}" >&2; exit 1 ;;
esac
Expand Down Expand Up @@ -50,8 +55,15 @@ if [[ "${build_driver}" == "docker-container" ]]; then
build_flags+=(--load)
fi

# Pin the build to the host arch we resolved goarch / GO_CHECKSUM for.
# Without this, DOCKER_DEFAULT_PLATFORM (commonly set on Apple Silicon)
# could make Docker's TARGETARCH diverge from the checksum we computed,
# causing the Dockerfile's Go SHA256 verification to fail.
build_flags+=(--platform "linux/${goarch}")

docker build "${build_flags[@]}" \
--build-arg GO_VERSION="${GO_VERSION}" \
--build-arg GO_CHECKSUM="${checksum}" \
-f "${CONTEXT_DIR}/${DOCKERFILE}" \
-t "${DOCKER_IMAGE}" \
"${CONTEXT_DIR}"
159 changes: 159 additions & 0 deletions .github/packaging/scripts/build-tgz.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env bash

# Build and sign linux tarballs for avalanchego and subnet-evm inside the container.
#
# Required env vars:
# PACKAGING_TAG - Git tag (e.g., "v1.14.1")
# OUTPUT_DIR - Directory for the output tarballs (bind-mounted from host)
#
# Optional env vars:
# GPG_KEY_FILE - Path to GPG private key for signing
# GPG_PASSPHRASE - Passphrase for the GPG key
# AVALANCHEGO_COMMIT - Git commit hash (auto-detected if not set)
#
# Target architecture is derived from `uname -m` inside the container.
# The container runs at the platform pinned by the docker run --platform
# flag (host arch), so the produced filenames always match the binaries.

set -euo pipefail

: "${PACKAGING_TAG:?PACKAGING_TAG must be set}"
: "${OUTPUT_DIR:?OUTPUT_DIR must be set}"

REPO_ROOT="/build"
TAG="${PACKAGING_TAG}"

# Map uname -m to deb-style arch (aarch64 -> arm64). Computed inside the
# container, so it reflects the actual platform the binaries are built
# for, regardless of any caller-supplied env vars.
host_arch=$(uname -m)
case "${host_arch}" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) echo "Unsupported arch: ${host_arch}" >&2; exit 1 ;;
esac

mkdir -p "${OUTPUT_DIR}"

# Remove stale tarballs, signatures, and the exported public key from
# any previous run. Tar would overwrite same-tag tarballs, but on
# persistent runners or after a failed cleanup, build/tgz can carry
# over tarballs from a different tag — and the workflow's S3 upload
# step matches *.tar.gz with a wildcard, so stale archives would be
# republished alongside the current release.
rm -f "${OUTPUT_DIR}"/*.tar.gz "${OUTPUT_DIR}"/*.tar.gz.sig "${OUTPUT_DIR}/GPG-KEY-avalanchego"

echo "=== Building tarballs for ${ARCH} (tag: ${TAG}) ==="

# ── Step 1: Build binaries ────────────────────────────────────────

# In CI, the bind-mounted source tree is owned by the host user. Mark it
# as safe so that git works inside the container (needed by older build
# scripts that resolve the commit hash via git rather than AVALANCHEGO_COMMIT).
if ! git -C "${REPO_ROOT}" rev-parse HEAD &>/dev/null; then
git config --global --add safe.directory "${REPO_ROOT}"
fi

# shellcheck disable=SC1091
source "${REPO_ROOT}/scripts/constants.sh"
# shellcheck disable=SC1091
source "${REPO_ROOT}/scripts/git_commit.sh"

# shellcheck disable=SC2154
echo "Git commit: ${git_commit}"

# Disable Go's automatic VCS stamping — the commit hash is passed
# explicitly via AVALANCHEGO_COMMIT and -ldflags instead.
export GOFLAGS="${GOFLAGS:-} -buildvcs=false"

echo "Building avalanchego..."
"${REPO_ROOT}/scripts/build.sh"
# shellcheck disable=SC2154
AVALANCHEGO_BINARY="${avalanchego_path}"

echo "Building subnet-evm..."
SUBNET_EVM_BINARY="${REPO_ROOT}/build/subnet-evm"
# Build from subnet-evm directory — build.sh uses relative glob "plugin/"*.go
(cd "${REPO_ROOT}/graft/subnet-evm" && ./scripts/build.sh "${SUBNET_EVM_BINARY}")

echo "Binaries built:"
echo " avalanchego: ${AVALANCHEGO_BINARY}"
echo " subnet-evm: ${SUBNET_EVM_BINARY}"

# ── Step 2: GPG setup ─────────────────────────────────────────────

# Tri-state behavior:
# - GPG_KEY_FILE unset → unsigned build (local dev OK)
# - GPG_KEY_FILE set but empty → CI signing secret missing/blank,
# fail closed (no silent unsigned
# release artifacts)
# - GPG_KEY_FILE set and non-empty → sign each archive

if [[ -z "${GPG_KEY_FILE:-}" ]]; then
echo "No GPG key provided, skipping tarball signing."
sign_archive() { :; }
GPG_SIGNING_ENABLED=false
elif [[ ! -s "${GPG_KEY_FILE}" ]]; then
echo "ERROR: GPG_KEY_FILE is set (${GPG_KEY_FILE}) but the file is empty." >&2
echo " Refusing to produce unsigned release artifacts." >&2
echo " Verify that the GPG signing secret (e.g. RPM_GPG_PRIVATE_KEY) is configured." >&2
exit 1
else
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
trap 'gpgconf --kill gpg-agent 2>/dev/null || true; rm -rf "${GNUPGHOME}"' EXIT

echo "Importing GPG key for tarball signing..."
gpg --batch --import "${GPG_KEY_FILE}"

sign_archive() {
local archive="$1"
echo "Signing ${archive}..."
printf '%s' "${GPG_PASSPHRASE:-}" | gpg --batch --yes --detach-sign \
--pinentry-mode loopback \
--passphrase-fd 0 \
"${archive}"
echo "Verifying signature for ${archive}..."
gpg --batch --verify "${archive}.sig" "${archive}"
}

GPG_SIGNING_ENABLED=true
fi

# ── Step 3: Stage and tar each binary ─────────────────────────────

STAGE_DIR=$(mktemp -d)

stage_and_tar() {
local package="$1"
local binary="$2"
local archive="${OUTPUT_DIR}/${package}-linux-${ARCH}-${TAG}.tar.gz"
local stage="${STAGE_DIR}/${package}-${TAG}"

mkdir -p "${stage}"
cp "${binary}" "${stage}/"

echo "Creating ${archive}..."
(cd "${STAGE_DIR}" && tar -czvf "${archive}" "${package}-${TAG}")
sign_archive "${archive}"
}

stage_and_tar "avalanchego" "${AVALANCHEGO_BINARY}"
stage_and_tar "subnet-evm" "${SUBNET_EVM_BINARY}"

rm -rf "${STAGE_DIR}"

# ── Step 4: Export public key (for validation container only) ─────
#
# The public key is used by validate-tgz.sh to verify signatures in a
# fresh container. It is NOT uploaded to S3 or as a GitHub artifact —
# the public key is distributed via the existing S3 location only.

if [[ "${GPG_SIGNING_ENABLED}" == "true" ]]; then
PUB_KEY_FILE="${OUTPUT_DIR}/GPG-KEY-avalanchego"
gpg --batch --armor --export "security@avalabs.org" > "${PUB_KEY_FILE}"
echo "GPG public key exported to: ${PUB_KEY_FILE} (validation use only)"
fi

echo "=== Tarball build complete ==="
ls -la "${OUTPUT_DIR}"
Loading
Loading