diff --git a/.github/packaging/Dockerfile b/.github/packaging/Dockerfile.rpm similarity index 96% rename from .github/packaging/Dockerfile rename to .github/packaging/Dockerfile.rpm index 3a7d729c05d9..024ac73562bd 100644 --- a/.github/packaging/Dockerfile +++ b/.github/packaging/Dockerfile.rpm @@ -15,10 +15,12 @@ ARG TARGETARCH # Install build dependencies # - gcc: required for cgo (CGO_ENABLED=1 in scripts/constants.sh) +# - gettext: envsubst for nfpm config template expansion # - gnupg2: GPG signing of RPM packages # - git: version detection in build scripts RUN dnf install -y \ gcc \ + gettext \ gnupg2 \ git \ && dnf clean all diff --git a/.github/packaging/Taskfile.yml b/.github/packaging/Taskfile.yml index 8d1def282896..43c332221039 100644 --- a/.github/packaging/Taskfile.yml +++ b/.github/packaging/Taskfile.yml @@ -50,14 +50,17 @@ tasks: GO_VERSION: '{{.PACKAGING_GO_VERSION}}' DOCKER_IMAGE: '{{.PACKAGING_DOCKER_IMAGE}}' CONTEXT_DIR: '{{.REPO_ROOT}}/.github/packaging' + DOCKERFILE: Dockerfile.rpm cmds: - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/build-builder-image.sh' build-avalanchego-rpm: desc: Builds RPM for avalanchego vars: - RPM_ARCH: '{{.RPM_ARCH | default .PACKAGING_HOST_ARCH}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' RPM_TAG: '{{.PACKAGING_TAG}}' + env: + GPG_KEY_PASSPHRASE: '{{.GPG_KEY_PASSPHRASE}}' deps: [build-builder-docker-image] cmds: - cmd: mkdir -p {{.PACKAGING_OUTPUT_DIR}} @@ -65,23 +68,26 @@ tasks: docker run --rm -v {{.REPO_ROOT}}:/build -v {{.PACKAGING_OUTPUT_DIR}}:/output - {{if .RPM_GPG_KEY_FILE}}-v {{.RPM_GPG_KEY_FILE}}:{{.RPM_GPG_KEY_FILE}}:ro{{end}} + {{if .GPG_KEY_FILE}}-v {{.GPG_KEY_FILE}}:{{.GPG_KEY_FILE}}:ro{{end}} + -e PKG_FORMAT=RPM -e PACKAGE=avalanchego -e VERSION={{trimPrefix "v" .RPM_TAG}} -e TAG={{.RPM_TAG}} - -e RPM_ARCH={{.RPM_ARCH}} + -e PACKAGE_ARCH={{.PACKAGE_ARCH}} -e OUTPUT_DIR=/output -e AVALANCHEGO_COMMIT={{.PACKAGING_GIT_COMMIT}} - {{if .RPM_GPG_KEY_FILE}}-e RPM_GPG_KEY_FILE={{.RPM_GPG_KEY_FILE}}{{end}} - {{if .NFPM_RPM_PASSPHRASE}}-e NFPM_RPM_PASSPHRASE={{.NFPM_RPM_PASSPHRASE}}{{end}} + {{if .GPG_KEY_FILE}}-e GPG_KEY_FILE={{.GPG_KEY_FILE}}{{end}} + {{if .GPG_KEY_PASSPHRASE}}-e GPG_KEY_PASSPHRASE{{end}} {{.PACKAGING_DOCKER_IMAGE}} - .github/packaging/scripts/build-rpm.sh + .github/packaging/scripts/build-package.sh build-subnet-evm-rpm: desc: Builds RPM for subnet-evm vars: - RPM_ARCH: '{{.RPM_ARCH | default .PACKAGING_HOST_ARCH}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' RPM_TAG: '{{.PACKAGING_TAG}}' + env: + GPG_KEY_PASSPHRASE: '{{.GPG_KEY_PASSPHRASE}}' deps: [build-builder-docker-image] cmds: - cmd: mkdir -p {{.PACKAGING_OUTPUT_DIR}} @@ -89,17 +95,18 @@ tasks: docker run --rm -v {{.REPO_ROOT}}:/build -v {{.PACKAGING_OUTPUT_DIR}}:/output - {{if .RPM_GPG_KEY_FILE}}-v {{.RPM_GPG_KEY_FILE}}:{{.RPM_GPG_KEY_FILE}}:ro{{end}} + {{if .GPG_KEY_FILE}}-v {{.GPG_KEY_FILE}}:{{.GPG_KEY_FILE}}:ro{{end}} + -e PKG_FORMAT=RPM -e PACKAGE=subnet-evm -e VERSION={{trimPrefix "v" .RPM_TAG}} -e TAG={{.RPM_TAG}} - -e RPM_ARCH={{.RPM_ARCH}} + -e PACKAGE_ARCH={{.PACKAGE_ARCH}} -e OUTPUT_DIR=/output -e AVALANCHEGO_COMMIT={{.PACKAGING_GIT_COMMIT}} - {{if .RPM_GPG_KEY_FILE}}-e RPM_GPG_KEY_FILE={{.RPM_GPG_KEY_FILE}}{{end}} - {{if .NFPM_RPM_PASSPHRASE}}-e NFPM_RPM_PASSPHRASE={{.NFPM_RPM_PASSPHRASE}}{{end}} + {{if .GPG_KEY_FILE}}-e GPG_KEY_FILE={{.GPG_KEY_FILE}}{{end}} + {{if .GPG_KEY_PASSPHRASE}}-e GPG_KEY_PASSPHRASE{{end}} {{.PACKAGING_DOCKER_IMAGE}} - .github/packaging/scripts/build-rpm.sh + .github/packaging/scripts/build-package.sh test-build-rpms: desc: Builds and validates RPMs end-to-end @@ -112,5 +119,6 @@ tasks: env: TAG: '{{.PACKAGING_TAG}}' GIT_COMMIT: '{{.PACKAGING_GIT_COMMIT}}' + PACKAGE_ARCH: '{{.PACKAGE_ARCH | default .PACKAGING_HOST_ARCH}}' cmds: - cmd: '{{.REPO_ROOT}}/.github/packaging/scripts/validate-rpm.sh' diff --git a/.github/packaging/nfpm/avalanchego.yml b/.github/packaging/nfpm/avalanchego-rpm.yml similarity index 62% rename from .github/packaging/nfpm/avalanchego.yml rename to .github/packaging/nfpm/avalanchego-rpm.yml index 30247b703f25..34db9ff7f878 100644 --- a/.github/packaging/nfpm/avalanchego.yml +++ b/.github/packaging/nfpm/avalanchego-rpm.yml @@ -1,5 +1,5 @@ name: avalanchego -arch: "${RPM_ARCH}" +arch: "${PACKAGE_ARCH}" version: "${VERSION}" maintainer: "Ava Labs " description: "AvalancheGo node — the official Avalanche protocol implementation" @@ -8,14 +8,12 @@ license: "BSD-3-Clause" depends: - "glibc >= 2.34" contents: - - src: "${AVALANCHEGO_BINARY}" + - src: "${BINARY_PATH}" dst: /var/opt/avalanchego/bin/avalanchego - expand: true file_info: mode: 0755 -# changelog and key_file paths are set to well-known locations by build-rpm.sh -changelog: "/build/build/nfpm-changelog.yml" +changelog: "${NFPM_CHANGELOG}" rpm: compression: zstd signature: - key_file: "/build/build/gpg/signing-key.asc" + key_file: "${NFPM_SIGNING_KEY}" diff --git a/.github/packaging/nfpm/subnet-evm.yml b/.github/packaging/nfpm/subnet-evm-rpm.yml similarity index 65% rename from .github/packaging/nfpm/subnet-evm.yml rename to .github/packaging/nfpm/subnet-evm-rpm.yml index 7e129a3e54dc..a27a837f9adf 100644 --- a/.github/packaging/nfpm/subnet-evm.yml +++ b/.github/packaging/nfpm/subnet-evm-rpm.yml @@ -1,5 +1,5 @@ name: subnet-evm -arch: "${RPM_ARCH}" +arch: "${PACKAGE_ARCH}" version: "${VERSION}" maintainer: "Ava Labs " description: "Subnet-EVM plugin for AvalancheGo" @@ -8,15 +8,13 @@ license: "BSD-3-Clause" depends: - "glibc >= 2.34" contents: - - src: "${SUBNET_EVM_BINARY}" + - src: "${BINARY_PATH}" # SUBNET_EVM_VM_ID is sourced from graft/subnet-evm/scripts/constants.sh dst: /var/opt/avalanchego/plugins/${SUBNET_EVM_VM_ID} - expand: true file_info: mode: 0755 -# changelog and key_file paths are set to well-known locations by build-rpm.sh -changelog: "/build/build/nfpm-changelog.yml" +changelog: "${NFPM_CHANGELOG}" rpm: compression: zstd signature: - key_file: "/build/build/gpg/signing-key.asc" + key_file: "${NFPM_SIGNING_KEY}" diff --git a/.github/packaging/scripts/build-builder-image.sh b/.github/packaging/scripts/build-builder-image.sh index f134922fa6cf..f91cb5c40682 100755 --- a/.github/packaging/scripts/build-builder-image.sh +++ b/.github/packaging/scripts/build-builder-image.sh @@ -9,12 +9,14 @@ # 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 +# DOCKERFILE - Dockerfile name (e.g., "Dockerfile.rpm") 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 must be set (e.g. Dockerfile.rpm)}" command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required but not found on PATH" >&2; exit 1; } @@ -45,7 +47,7 @@ build_flags=() build_driver=$( docker buildx inspect 2>/dev/null \ | awk '/^Driver:/ { print $2; exit }' -) +) || true if [[ "${build_driver}" == "docker-container" ]]; then build_flags+=(--load) fi @@ -53,5 +55,6 @@ fi docker build "${build_flags[@]}" \ --build-arg GO_VERSION="${GO_VERSION}" \ --build-arg GO_CHECKSUM="${checksum}" \ + -f "${CONTEXT_DIR}/${DOCKERFILE}" \ -t "${DOCKER_IMAGE}" \ "${CONTEXT_DIR}" diff --git a/.github/packaging/scripts/build-package.sh b/.github/packaging/scripts/build-package.sh new file mode 100755 index 000000000000..37c0f6123b01 --- /dev/null +++ b/.github/packaging/scripts/build-package.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# Build and sign a Linux package inside the container. + +set -euo pipefail + +: "${PACKAGE:?PACKAGE must be set (avalanchego or subnet-evm)}" +: "${VERSION:?VERSION must be set (semver without v prefix, e.g. 1.14.1)}" +: "${TAG:?TAG must be set (git tag, e.g. v1.14.1)}" +: "${PACKAGE_ARCH:?PACKAGE_ARCH must be set (x86_64 or aarch64)}" +: "${OUTPUT_DIR:?OUTPUT_DIR must be set (bind-mounted output dir)}" + +: "${PKG_FORMAT:?PKG_FORMAT must be set (RPM or DEB)}" +pkg_format_lower="${PKG_FORMAT,,}" + +REPO_ROOT="/build" +PACKAGING_DIR="${REPO_ROOT}/.github/packaging" + +# shellcheck disable=SC1091 +source "${PACKAGING_DIR}/scripts/lib-build-common.sh" + +# Well-known paths referenced by nfpm configs +export NFPM_CHANGELOG="${REPO_ROOT}/build/nfpm-changelog.yml" +export NFPM_SIGNING_KEY="${REPO_ROOT}/build/gpg/signing-key.asc" + +echo "=== Building ${PACKAGE} ${PKG_FORMAT} for ${PACKAGE_ARCH} (tag: ${TAG}) ===" + +init_build_env +build_binary "${PACKAGE}" +generate_changelog "${VERSION}" + +# ── GPG signing ─────────────────────────────────────────────────── + +GPG_KEY_FILE="${GPG_KEY_FILE:-}" +GPG_PUBLIC_KEY="${OUTPUT_DIR}/GPG-KEY-avalanchego" + +# nfpm reads the signing passphrase from a packager-specific env var +# (NFPM_RPM_PASSPHRASE, NFPM_DEB_PASSPHRASE, ...); mirror our format- +# agnostic GPG_KEY_PASSPHRASE into the name nfpm expects. +nfpm_passphrase_var="NFPM_${PKG_FORMAT}_PASSPHRASE" +export "${nfpm_passphrase_var}=${GPG_KEY_PASSPHRASE:-}" + +# Ephemeral keys use a known throwaway passphrase so local and CI builds +# exercise passphrase handling without release credentials. +if [[ -z "${GPG_KEY_FILE}" ]]; then + use_ephemeral_gpg_passphrase "${nfpm_passphrase_var}" +fi + +setup_gpg "${GPG_KEY_FILE}" "${GPG_PUBLIC_KEY}" "${PKG_FORMAT}" + +# ── Package with nfpm ───────────────────────────────────────────── + +export VERSION PACKAGE_ARCH BINARY_PATH + +PKG_FILENAME="${PACKAGE}-${TAG}-${PACKAGE_ARCH}.${pkg_format_lower}" +PKG_PATH="${OUTPUT_DIR}/${PKG_FILENAME}" + +run_nfpm_package \ + "${PACKAGING_DIR}/nfpm/${PACKAGE}-${pkg_format_lower}.yml" \ + "${REPO_ROOT}/build/${PACKAGE}-${pkg_format_lower}-resolved.yml" \ + "${pkg_format_lower}" \ + "${PKG_PATH}" + +echo "${PKG_FORMAT} built: ${PKG_PATH}" diff --git a/.github/packaging/scripts/build-rpm.sh b/.github/packaging/scripts/build-rpm.sh deleted file mode 100755 index 4f996e03fb9b..000000000000 --- a/.github/packaging/scripts/build-rpm.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -# Build and validate an RPM package inside the container. -# -# Required env vars: -# PACKAGE - "avalanchego" or "subnet-evm" -# VERSION - Semantic version without "v" prefix (e.g., "1.14.1") -# TAG - Git tag (e.g., "v1.14.1") -# RPM_ARCH - RPM architecture name ("x86_64" or "aarch64") -# OUTPUT_DIR - Directory for the output RPM (bind-mounted from host) -# -# Optional env vars: -# RPM_GPG_KEY_FILE - Path to GPG private key for signing -# NFPM_RPM_PASSPHRASE - Passphrase for the GPG key -# AVALANCHEGO_COMMIT - Git commit hash (auto-detected if not set) - -set -euo pipefail - -: "${PACKAGE:?PACKAGE must be set (avalanchego or subnet-evm)}" -: "${VERSION:?VERSION must be set}" -: "${TAG:?TAG must be set}" -: "${RPM_ARCH:?RPM_ARCH must be set}" -: "${OUTPUT_DIR:?OUTPUT_DIR must be set}" - -REPO_ROOT="/build" -PACKAGING_DIR="${REPO_ROOT}/.github/packaging" - -# Well-known paths referenced by nfpm configs -NFPM_CHANGELOG="${REPO_ROOT}/build/nfpm-changelog.yml" -NFPM_SIGNING_KEY="${REPO_ROOT}/build/gpg/signing-key.asc" - -echo "=== Building ${PACKAGE} RPM for ${RPM_ARCH} (tag: ${TAG}) ===" - -# ── Step 1: Build binary ────────────────────────────────────────── - -# 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" - -case "${PACKAGE}" in - avalanchego) - echo "Building avalanchego..." - "${REPO_ROOT}/scripts/build.sh" - # shellcheck disable=SC2154 - BINARY_PATH="${avalanchego_path}" - ;; - subnet-evm) - echo "Building subnet-evm..." - # Source VM ID from constants.sh (canonical definition) - SUBNET_EVM_VM_ID=$( - grep '^DEFAULT_VM_ID=' "${REPO_ROOT}/graft/subnet-evm/scripts/constants.sh" \ - | cut -d'"' -f2 - ) - export SUBNET_EVM_VM_ID - echo "Subnet-EVM VM ID: ${SUBNET_EVM_VM_ID}" - - 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}") - BINARY_PATH="${SUBNET_EVM_BINARY}" - ;; - *) - echo "Unknown package: ${PACKAGE}" >&2 - exit 1 - ;; -esac - -echo "Binary built at: ${BINARY_PATH}" - -# ── Step 2: Generate changelog ──────────────────────────────────── - -cat > "${NFPM_CHANGELOG}" < - changes: - - note: "See https://github.com/ava-labs/avalanchego/releases/tag/v${VERSION}" -EOF - -# ── Step 3: Set up GPG signing ──────────────────────────────────── - -GPG_WORKDIR="${REPO_ROOT}/build/gpg" -mkdir -p "${GPG_WORKDIR}" -GPG_PUBLIC_KEY="${OUTPUT_DIR}/RPM-GPG-KEY-avalanchego" - -if [[ -n "${RPM_GPG_KEY_FILE:-}" ]]; then - echo "Using provided GPG key for signing" - gpg --batch --import "${RPM_GPG_KEY_FILE}" - # Copy to well-known path for nfpm config - cp "${RPM_GPG_KEY_FILE}" "${NFPM_SIGNING_KEY}" -elif [[ -f "${NFPM_SIGNING_KEY}" ]]; then - # Reuse ephemeral key from a previous build (e.g., avalanchego built before subnet-evm) - echo "Reusing existing ephemeral GPG key" - gpg --batch --import "${NFPM_SIGNING_KEY}" - export NFPM_RPM_PASSPHRASE="" -else - echo "Generating ephemeral GPG key for signing" - - gpg --batch --gen-key < "${NFPM_SIGNING_KEY}" - export NFPM_RPM_PASSPHRASE="" -fi - -# Export public key for verification -gpg --batch --armor --export "security@avalabs.org" > "${GPG_PUBLIC_KEY}" -echo "GPG public key exported to: ${GPG_PUBLIC_KEY}" - -# ── Step 4: Package with nfpm ───────────────────────────────────── - -RPM_FILENAME="${PACKAGE}-${TAG}-${RPM_ARCH}.rpm" -RPM_PATH="${OUTPUT_DIR}/${RPM_FILENAME}" -mkdir -p "${OUTPUT_DIR}" - -# Set binary path env var for nfpm config (contents use expand: true) -case "${PACKAGE}" in - avalanchego) export AVALANCHEGO_BINARY="${BINARY_PATH}" ;; - subnet-evm) export SUBNET_EVM_BINARY="${BINARY_PATH}" ;; -esac - -export VERSION RPM_ARCH - -echo "Packaging ${RPM_FILENAME}..." -nfpm package \ - --config "${PACKAGING_DIR}/nfpm/${PACKAGE}.yml" \ - --packager rpm \ - --target "${RPM_PATH}" - -echo "RPM built: ${RPM_PATH}" diff --git a/.github/packaging/scripts/lib-build-common.sh b/.github/packaging/scripts/lib-build-common.sh new file mode 100644 index 000000000000..0242e7925afd --- /dev/null +++ b/.github/packaging/scripts/lib-build-common.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash + +# Shared functions for RPM and DEB package build scripts. +# +# Sourced (not executed) by build-package.sh. + +: "${REPO_ROOT:?REPO_ROOT must be set by the caller}" + +readonly PACKAGER_NAME="Ava Labs" +readonly PACKAGER_EMAIL="security@avalabs.org" +readonly EPHEMERAL_GPG_PASSPHRASE="avalanchego-ephemeral-gpg-passphrase" + +use_ephemeral_gpg_passphrase() { + local passphrase_env="${1:?passphrase env var required}" + + declare -gx "${passphrase_env}=${EPHEMERAL_GPG_PASSPHRASE}" +} + +# Initialize the build environment inside the container. +# Marks the bind-mounted source tree as git-safe, sources project +# scripts (constants.sh, git_commit.sh), and disables Go VCS stamping. +init_build_env() { + 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 # git_commit is set by git_commit.sh sourced above + 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" +} + +# Build the binary for the specified package. +# Sets BINARY_PATH (global) as a side effect. +# +# Args: package_name ("avalanchego" or "subnet-evm") +build_binary() { + local package="${1:?package name required}" + + case "${package}" in + avalanchego) + echo "Building avalanchego..." + "${REPO_ROOT}/scripts/build.sh" + # avalanchego_path is set by scripts/constants.sh, sourced in init_build_env + # shellcheck disable=SC2154 + BINARY_PATH="${avalanchego_path}" + ;; + subnet-evm) + echo "Building subnet-evm..." + resolve_subnet_evm_vm_id + echo "Subnet-EVM VM ID: ${SUBNET_EVM_VM_ID}" + + BINARY_PATH="${REPO_ROOT}/build/subnet-evm" + (cd "${REPO_ROOT}/graft/subnet-evm" && ./scripts/build.sh "${BINARY_PATH}") + ;; + *) + echo "Unknown package: ${package}" >&2 + return 1 + ;; + esac + + echo "Binary built at: ${BINARY_PATH}" +} + +# Resolve the subnet-evm VM ID from the canonical constants file. +# Sets SUBNET_EVM_VM_ID (global) as a side effect. +resolve_subnet_evm_vm_id() { + SUBNET_EVM_VM_ID="$( + { + # shellcheck disable=SC1091 + source "${REPO_ROOT}/graft/subnet-evm/scripts/constants.sh" + # shellcheck disable=SC2154 + : "${DEFAULT_VM_ID:?DEFAULT_VM_ID must be set by constants.sh}" + } >&2 + echo "${DEFAULT_VM_ID}" + )" + export SUBNET_EVM_VM_ID +} + +# Generate the nfpm changelog file. +# +# Args: version (semver without "v" prefix) +generate_changelog() { + local version="${1:?version required}" + : "${NFPM_CHANGELOG:?NFPM_CHANGELOG must be set by the caller}" + + mkdir -p "$(dirname "${NFPM_CHANGELOG}")" + cat > "${NFPM_CHANGELOG}" < + changes: + - note: "See https://github.com/ava-labs/avalanchego/releases/tag/v${version}" +EOF + + # Sanity-check the heredoc wrote the version line (catches silent + # substitution failures: empty file, unresolved ${version}, etc.) + if ! grep -q "^- semver: ${version}\$" "${NFPM_CHANGELOG}"; then + echo "ERROR: generated changelog ${NFPM_CHANGELOG} missing 'semver: ${version}'" >&2 + return 1 + fi +} + +# Set up GPG signing (import provided key, reuse ephemeral, or generate new). +# +# Args: +# gpg_key_file - path to GPG private key, or empty for ephemeral +# public_key_out - output path for exported public key +# key_label - label for ephemeral key ("RPM" or "DEB") +setup_gpg() { + local gpg_key_file="${1:-}" + local public_key_out="${2:?public key output path required}" + local key_label="${3:-Package}" + : "${NFPM_SIGNING_KEY:?NFPM_SIGNING_KEY must be set by the caller}" + + mkdir -p "$(dirname "${NFPM_SIGNING_KEY}")" + + if [[ -n "${gpg_key_file}" ]]; then + echo "Using provided GPG key for signing" + gpg --batch --import "${gpg_key_file}" + cp "${gpg_key_file}" "${NFPM_SIGNING_KEY}" + elif [[ -f "${NFPM_SIGNING_KEY}" ]]; then + # avalanchego and subnet-evm builds run in separate docker invocations + # but share the on-disk key via the bind-mounted build/gpg directory so + # the validator's single exported public key verifies both RPMs. + echo "Reusing existing ephemeral GPG key" + gpg --batch --import "${NFPM_SIGNING_KEY}" + else + echo "Generating ephemeral GPG key for signing" + gpg --batch --pinentry-mode loopback --gen-key < "${NFPM_SIGNING_KEY}" + fi + + gpg --batch --armor --export "${PACKAGER_EMAIL}" > "${public_key_out}" + echo "GPG public key exported to: ${public_key_out}" +} + +# Package with nfpm after envsubst preprocessing. +# +# nfpm does not expand env vars in top-level fields (changelog, +# signature.key_file). Preprocess the config template with envsubst +# so all ${VAR} references resolve before nfpm sees them. +run_nfpm_package() { + # path to nfpm YAML template (with ${VAR} placeholders) + local config_template="${1:?config template path required}" + # output path for the preprocessed config + local resolved_path="${2:?resolved config path required}" + # nfpm packager name ("rpm" or "deb") + local packager="${3:?packager name required}" + # output path for the built package + local target_path="${4:?target path required}" + + mkdir -p "$(dirname "${target_path}")" + + envsubst < "${config_template}" > "${resolved_path}" + + echo "Packaging $(basename "${target_path}")..." + nfpm package \ + --config "${resolved_path}" \ + --packager "${packager}" \ + --target "${target_path}" +} + +# Verify that all expected package files exist in a directory. +# +# Args: pkg_dir file1 [file2 ...] +assert_files_exist() { + local pkg_dir="${1:?package directory required}"; shift + local missing=() + for f in "$@"; do + [[ -f "${pkg_dir}/${f}" ]] || missing+=("${pkg_dir}/${f}") + done + if (( ${#missing[@]} > 0 )); then + printf 'ERROR: expected file not found: %s\n' "${missing[@]}" >&2 + exit 1 + fi +} diff --git a/.github/packaging/scripts/smoke-test.sh b/.github/packaging/scripts/smoke-test.sh new file mode 100755 index 000000000000..bcb69db5361d --- /dev/null +++ b/.github/packaging/scripts/smoke-test.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Smoke test installed avalanchego and subnet-evm packages. +# +# Runs inside a validation container after packages have been installed. +# Verifies that binaries are present, executable, and report the expected +# version/commit information. + +set -euxo pipefail + +AVAGO_BIN="${1:?avalanchego binary path required}" +SUBNET_EVM_BIN="${2:?subnet-evm plugin binary path required}" +GIT_COMMIT="${3:?expected full git commit hash required}" + +# ── Smoke test avalanchego ──────────────────────────────────────── + +output=$("${AVAGO_BIN}" --version) +echo "avalanchego --version: ${output}" +if [[ "${output}" != avalanchego/* ]]; then + echo "ERROR: --version output does not start with avalanchego/" >&2 + exit 1 +fi +if [[ "${output}" != *"${GIT_COMMIT}"* ]]; then + echo "ERROR: avalanchego --version output does not contain expected commit ${GIT_COMMIT}" >&2 + echo "Output: ${output}" >&2 + exit 1 +fi + +# ── Verify subnet-evm plugin ───────────────────────────────────── + +if [[ ! -x "${SUBNET_EVM_BIN}" ]]; then + echo "ERROR: subnet-evm plugin not found or not executable at ${SUBNET_EVM_BIN}" >&2 + exit 1 +fi + +evm_output=$("${SUBNET_EVM_BIN}" --version) +echo "subnet-evm --version: ${evm_output}" +if [[ "${evm_output}" != *"${GIT_COMMIT}"* ]]; then + echo "ERROR: subnet-evm --version output does not contain expected commit ${GIT_COMMIT}" >&2 + echo "Output: ${evm_output}" >&2 + exit 1 +fi + +echo "All package validations passed" diff --git a/.github/packaging/scripts/validate-rpm.sh b/.github/packaging/scripts/validate-rpm.sh index 12ae1d2a4087..8b5a1dd30384 100755 --- a/.github/packaging/scripts/validate-rpm.sh +++ b/.github/packaging/scripts/validate-rpm.sh @@ -4,97 +4,50 @@ # # Validates locally-built RPMs by running a fresh rockylinux:9 # container to verify signature, install, and smoke test. -# -# Required env vars: -# TAG - Git tag (e.g., "v1.14.1") -# GIT_COMMIT - Full git commit hash used to build the binaries -# -# Optional env vars: -# RPM_ARCH - RPM architecture ("x86_64" or "aarch64"), defaults to host set -euo pipefail -: "${TAG:?TAG must be set}" -: "${GIT_COMMIT:?GIT_COMMIT must be set}" - -if [[ -z "${RPM_ARCH:-}" ]]; then - arch=$(uname -m) - case "${arch}" in - x86_64) RPM_ARCH="x86_64" ;; - arm64) RPM_ARCH="aarch64" ;; - *) RPM_ARCH="${arch}" ;; - esac -fi +: "${TAG:?TAG must be set (Git tag, e.g. v1.14.1)}" +: "${GIT_COMMIT:?GIT_COMMIT must be set (full git commit hash used to build the binaries)}" +: "${PACKAGE_ARCH:?PACKAGE_ARCH must be set (x86_64 or aarch64)}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" RPM_DIR="${REPO_ROOT}/build/rpm" +SCRIPTS_DIR="${REPO_ROOT}/.github/packaging/scripts" -# Source VM ID from constants.sh (canonical definition) -SUBNET_EVM_VM_ID=$( - grep '^DEFAULT_VM_ID=' "${REPO_ROOT}/graft/subnet-evm/scripts/constants.sh" \ - | cut -d'"' -f2 -) +# shellcheck disable=SC1091 +source "${SCRIPTS_DIR}/lib-build-common.sh" -# Verify expected files exist -for f in \ - "avalanchego-${TAG}-${RPM_ARCH}.rpm" \ - "subnet-evm-${TAG}-${RPM_ARCH}.rpm" \ -; do - if [[ ! -f "${RPM_DIR}/${f}" ]]; then - echo "ERROR: expected file not found: ${RPM_DIR}/${f}" >&2 - exit 1 - fi -done +resolve_subnet_evm_vm_id + +assert_files_exist "${RPM_DIR}" \ + "avalanchego-${TAG}-${PACKAGE_ARCH}.rpm" \ + "subnet-evm-${TAG}-${PACKAGE_ARCH}.rpm" echo "=== Validating RPMs in fresh Rocky Linux 9 container ===" docker run --rm \ -v "${RPM_DIR}:/rpms:ro" \ + -v "${SCRIPTS_DIR}/smoke-test.sh:/smoke-test.sh:ro" \ rockylinux:9 \ bash -euxc ' - # Import GPG key and verify signatures if available - if [[ -f /rpms/RPM-GPG-KEY-avalanchego ]]; then - rpm --import /rpms/RPM-GPG-KEY-avalanchego - rpm -K "/rpms/avalanchego-'"${TAG}"'-'"${RPM_ARCH}"'.rpm" - rpm -K "/rpms/subnet-evm-'"${TAG}"'-'"${RPM_ARCH}"'.rpm" - else - echo "Skipping GPG verification (unsigned build)" - fi - - # Install both packages - rpm -ivh "/rpms/avalanchego-'"${TAG}"'-'"${RPM_ARCH}"'.rpm" - rpm -ivh "/rpms/subnet-evm-'"${TAG}"'-'"${RPM_ARCH}"'.rpm" - - # Smoke test avalanchego - full_commit="'"${GIT_COMMIT}"'" - output=$(/var/opt/avalanchego/bin/avalanchego --version) - echo "avalanchego --version: ${output}" - if [[ "${output}" != avalanchego/* ]]; then - echo "ERROR: --version output does not start with avalanchego/" >&2 - exit 1 - fi - if [[ "${output}" != *"${full_commit}"* ]]; then - echo "ERROR: avalanchego --version output does not contain expected commit ${full_commit}" >&2 - echo "Output: ${output}" >&2 - exit 1 - fi - - # Verify subnet-evm plugin - plugin="/var/opt/avalanchego/plugins/'"${SUBNET_EVM_VM_ID}"'" - if [[ ! -x "${plugin}" ]]; then - echo "ERROR: subnet-evm plugin not found or not executable" >&2 + # Import GPG key and verify signatures (always produced by the build). + if [[ ! -f /rpms/GPG-KEY-avalanchego ]]; then + echo "ERROR: GPG-KEY-avalanchego not found; build did not export a key" >&2 exit 1 fi + rpm --import /rpms/GPG-KEY-avalanchego + rpm -K "/rpms/avalanchego-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" + rpm -K "/rpms/subnet-evm-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" - # Smoke test subnet-evm version and commit - evm_output=$("${plugin}" --version) - echo "subnet-evm --version: ${evm_output}" - if [[ "${evm_output}" != *"${full_commit}"* ]]; then - echo "ERROR: subnet-evm --version output does not contain expected commit ${full_commit}" >&2 - echo "Output: ${evm_output}" >&2 - exit 1 - fi - - echo "All RPM validations passed" + # Install both packages + rpm -ivh "/rpms/avalanchego-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" + rpm -ivh "/rpms/subnet-evm-'"${TAG}"'-'"${PACKAGE_ARCH}"'.rpm" + + # Run shared smoke test + bash /smoke-test.sh \ + /var/opt/avalanchego/bin/avalanchego \ + "/var/opt/avalanchego/plugins/'"${SUBNET_EVM_VM_ID}"'" \ + "'"${GIT_COMMIT}"'" ' echo "=== RPM validation complete ===" diff --git a/.github/packaging/scripts/workflow-setup-packaging.sh b/.github/packaging/scripts/workflow-setup-packaging.sh new file mode 100755 index 000000000000..c7becef4efa3 --- /dev/null +++ b/.github/packaging/scripts/workflow-setup-packaging.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Set up packaging workflow environment: resolve tag and import GPG key. +# +# Called directly from workflow YAML (workflow-*.sh scripts are CI glue, +# not developer entrypoints, and are exempt from the run_task.sh policy). +# +# Writes key=value outputs to $GITHUB_OUTPUT when running in CI, or to +# stdout when running locally (for inspection / testing). + +set -euo pipefail + +: "${GITHUB_REF:?GITHUB_REF must be set (Git ref, e.g. refs/tags/v1.14.1; locally: any ref string)}" +: "${GITHUB_SHA:?GITHUB_SHA must be set (Git commit SHA; locally: any SHA)}" + +# Optional env vars (defaulted below): +# GITHUB_OUTPUT - step-output file path; empty/unset writes to stdout +# TAG_INPUT - explicit tag from workflow_dispatch (empty: auto-detect) +# GPG_PRIVATE_KEY - signing key content (empty: ephemeral PR/local signing) +# RELEASE - non-empty marks a release build (missing key is then +# fatal instead of falling back to ephemeral signing) +OUTPUT="${GITHUB_OUTPUT:-/dev/stdout}" + +# ── Resolve tag ────────────────────────────────────────────────── + +TAG_INPUT="${TAG_INPUT:-}" + +if [[ -n "${TAG_INPUT}" ]]; then + TAG="${TAG_INPUT}" +elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG="${GITHUB_REF/refs\/tags\//}" +else + TAG="v0.0.0-pr.${GITHUB_SHA::8}" +fi + +echo "tag=${TAG}" >> "${OUTPUT}" +echo "Resolved tag: ${TAG}" >&2 + +# ── Import GPG key ─────────────────────────────────────────────── + +GPG_PRIVATE_KEY="${GPG_PRIVATE_KEY:-}" +RELEASE="${RELEASE:-}" + +if [[ -n "${GPG_PRIVATE_KEY}" ]]; then + GPG_KEY_FILE="$(mktemp)" + chmod 600 "${GPG_KEY_FILE}" + printf '%s' "${GPG_PRIVATE_KEY}" > "${GPG_KEY_FILE}" + echo "gpg-key-file=${GPG_KEY_FILE}" >> "${OUTPUT}" + echo "GPG key written to temporary file" >&2 +elif [[ -n "${RELEASE}" ]]; then + echo "ERROR: release build requires GPG_PRIVATE_KEY but none was provided" >&2 + exit 1 +else + echo "gpg-key-file=" >> "${OUTPUT}" +fi diff --git a/.github/workflows/build-rpm-release.yml b/.github/workflows/build-rpm-release.yml index 2dbbf9b22ff9..ee52a6be11a5 100644 --- a/.github/workflows/build-rpm-release.yml +++ b/.github/workflows/build-rpm-release.yml @@ -37,51 +37,45 @@ jobs: with: ref: ${{ github.event.inputs.tag || github.ref }} - # When building an older tag via workflow_dispatch, the tag's tree - # may not contain the packaging scripts. Overlay them from the - # workflow branch so that packaging:test-build-rpms is available. + # When building an older tag via workflow_dispatch, the tag's + # .github/packaging tree may be stale or absent. Replace it + # wholesale with the workflow branch's version so that the build + # uses the current packaging:test-build-rpms task and scripts. + # Split into two steps because actions/checkout (which provides + # the master-side tree) and the cp (which applies it) cannot be + # combined. - name: Overlay packaging scripts from workflow branch if: github.event.inputs.tag uses: actions/checkout@v5 with: path: .packaging-overlay sparse-checkout: .github/packaging + - name: Apply packaging overlay if: github.event.inputs.tag run: | + rm -rf .github/packaging cp -r .packaging-overlay/.github/packaging .github/packaging rm -rf .packaging-overlay shell: bash - uses: ./.github/actions/setup-go-for-project - - name: Get tag - run: | - if [[ -n "${{ github.event.inputs.tag }}" ]]; then - echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV" - elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> "$GITHUB_ENV" - else - # PR builds: synthetic tag for smoke testing the build pipeline - echo "TAG=v0.0.0-pr.${GITHUB_SHA::8}" >> "$GITHUB_ENV" - fi - shell: bash - - - name: Import GPG key - if: github.event_name != 'pull_request' - run: | - GPG_KEY_FILE="$(mktemp)" - chmod 600 "${GPG_KEY_FILE}" - printf '%s' "${{ secrets.RPM_GPG_PRIVATE_KEY }}" > "${GPG_KEY_FILE}" - printf 'GPG_KEY_FILE=%s\n' "${GPG_KEY_FILE}" >> "$GITHUB_ENV" + - name: Set up packaging environment + id: setup + run: ./.github/packaging/scripts/workflow-setup-packaging.sh shell: bash + env: + TAG_INPUT: ${{ github.event.inputs.tag }} + GPG_PRIVATE_KEY: ${{ github.event_name != 'pull_request' && secrets.RPM_GPG_PRIVATE_KEY || '' }} + RELEASE: ${{ github.event_name != 'pull_request' && 'true' || '' }} - name: Build and validate RPMs run: ./scripts/run_task.sh --taskfile .github/packaging/Taskfile.yml test-build-rpms env: - PACKAGING_TAG: ${{ env.TAG }} - RPM_GPG_KEY_FILE: ${{ env.GPG_KEY_FILE }} - NFPM_RPM_PASSPHRASE: ${{ secrets.RPM_GPG_PASSPHRASE }} + PACKAGING_TAG: ${{ steps.setup.outputs.tag }} + GPG_KEY_FILE: ${{ steps.setup.outputs.gpg-key-file }} + GPG_KEY_PASSPHRASE: ${{ github.event_name != 'pull_request' && secrets.RPM_GPG_PASSPHRASE || '' }} - name: Upload RPMs as artifacts if: github.event_name != 'pull_request' @@ -89,15 +83,16 @@ jobs: with: name: rpms-${{ matrix.rpm_arch }} path: | - build/rpm/avalanchego-${{ env.TAG }}-${{ matrix.rpm_arch }}.rpm - build/rpm/subnet-evm-${{ env.TAG }}-${{ matrix.rpm_arch }}.rpm - build/rpm/RPM-GPG-KEY-avalanchego + build/rpm/avalanchego-${{ steps.setup.outputs.tag }}-${{ matrix.rpm_arch }}.rpm + build/rpm/subnet-evm-${{ steps.setup.outputs.tag }}-${{ matrix.rpm_arch }}.rpm + build/rpm/GPG-KEY-avalanchego - name: Cleanup if: always() run: | - if [[ -n "${GPG_KEY_FILE:-}" ]]; then - rm -f "${GPG_KEY_FILE}" + if [[ -n "${{ steps.setup.outputs.gpg-key-file }}" ]]; then + rm -f "${{ steps.setup.outputs.gpg-key-file }}" fi rm -rf build/rpm + sudo rm -rf build/gpg shell: bash diff --git a/scripts/actionlint.sh b/scripts/actionlint.sh index 3d166f7cf757..b6d41bfa6f57 100755 --- a/scripts/actionlint.sh +++ b/scripts/actionlint.sh @@ -11,8 +11,10 @@ for file in "${AVALANCHE_PATH}"/.github/workflows/*.{yml,yaml}; do # Skip if no matches found (in case one of the extensions doesn't exist) [[ -f "$file" ]] || continue - # Search for scripts/* except for scripts/run_task.sh - MATCHES=$(grep -H -n -P "scripts/(?!run_task\.sh)" "$file" || true) + # Search for scripts/* except for: + # - scripts/run_task.sh (the approved launcher for developer entrypoints) + # - workflow-*.sh (CI-only glue scripts, not developer entrypoints) + MATCHES=$(grep -H -n -P "scripts/(?!run_task\.sh|workflow-)" "$file" || true) if [[ -n "${MATCHES}" ]]; then echo "${MATCHES}" SCRIPT_USAGE=1 @@ -21,6 +23,7 @@ done if [[ -n "${SCRIPT_USAGE}" ]]; then echo "Error: the lines listed above must be converted to use scripts/run_task.sh to ensure local reproducibility." + echo " CI-only helpers may use the workflow-*.sh naming convention to bypass this check." exit 1 fi