Skip to content

Release v0.4.0

Release v0.4.0 #32

Workflow file for this run

# Manual distribution workflow. Push a v* tag first; `Release Prepare` runs the
# test suite and builds the server binaries. Then run this workflow with that
# tag and the successful prepare run ID.
#
# NOTHING HERE COMPILES. Every stage consumes the prepare run's artifacts, so a
# failure in one stage is retried by re-dispatching with only that stage
# enabled — the tests, the binaries, and the Docker image are never rebuilt to
# fix a Discord webhook or a crates.io timeout.
#
# Stages, in dependency order:
# publish_crates -> crates.io, 8 tiers
# docker -> per-arch images + multi-arch manifest (full releases only)
# github_release -> GitHub Release with the binary tarballs
# notify -> Discord announcement (full releases only)
#
# Each stage is idempotent: crates already on crates.io are skipped, image tags
# and the release are overwritten in place. Re-running everything is safe; the
# toggles exist to save time, not to protect against double-publishing.
#
# Required secrets: CARGO_REGISTRY_TOKEN, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
# DISCORD_RELEASE_WEBHOOK_URL.
name: Release
run-name: Release ${{ inputs.tag }}
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish, e.g. v0.2.0"
required: true
type: string
prepare_run_id:
description: "Successful Release Prepare run ID holding the binaries"
required: true
type: string
ref:
description: "Ref to take workflow/Dockerfile/scripts from. Defaults to the tag; override to pick up a distribution-only fix without re-tagging."
required: false
type: string
publish_crates:
description: "Publish crates to crates.io"
type: boolean
default: true
docker:
description: "Build and push Docker images"
type: boolean
default: true
github_release:
description: "Create the GitHub Release"
type: boolean
default: true
notify:
description: "Announce on Discord"
type: boolean
default: true
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# Always runs. Cheap, and it guarantees the dispatched tag still means what
# the prepare run assumed it meant.
validate-version:
uses: ./.github/workflows/release-validate.yml
with:
ref: ${{ inputs.tag }}
# ── crates.io ────────────────────────────────────────────────────────────────
# All 8 tiers in one job, with is_published checks and wait_for polling, so a
# re-run never double-publishes an already-indexed crate and never races the
# index for a dependency it just pushed.
publish-crates:
name: Publish to crates.io
needs: validate-version
if: inputs.publish_crates
runs-on: ubuntu-latest
environment: crates.io
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake clang libclang-dev pkg-config protobuf-compiler perl \
libcurl4-openssl-dev libsasl2-dev
- name: Set version from tag
run: bash scripts/ci/stamp_version.sh "${{ needs.validate-version.outputs.version }}"
- name: Publish crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
# Tier 1: no internal NodeDB dependencies
TIER1="nodedb-codec nodedb-wal nodedb-raft"
# Tier 2: depends on tier 1 only (nodedb-types depends on nodedb-codec)
TIER2="nodedb-types"
# Tier 3: depends on tier 2 only (nodedb-mem + crates that need only nodedb-types/nodedb-raft)
TIER3="nodedb-mem nodedb-bridge nodedb-crdt nodedb-strict nodedb-client nodedb-cluster nodedb-array"
# Tier 4: depends on nodedb-mem (nodedb-fts, nodedb-vector, nodedb-graph, nodedb-spatial, nodedb-columnar)
TIER4="nodedb-fts nodedb-vector nodedb-graph nodedb-spatial nodedb-columnar"
# Tier 5: depends on tier 4 (nodedb-query depends on nodedb-spatial, nodedb-fts)
TIER5="nodedb-query"
# Tier 6: depends on tier 5 (nodedb-sql depends on nodedb-query, nodedb-spatial)
TIER6="nodedb-sql"
# Tier 7: depends on tier 6 (nodedb-physical depends on nodedb-sql, -query, -graph, -array)
TIER7="nodedb-physical"
# Tier 8: depends on tier 7 (nodedb depends on all crates including nodedb-physical)
TIER8="nodedb"
is_published() {
curl -sf \
-H "User-Agent: nodedb-ci (github.com/NodeDB-Lab/nodedb)" \
"https://crates.io/api/v1/crates/$1/$2" > /dev/null 2>&1
}
wait_for() {
local crate="$1" version="$2"
echo -n " Waiting for $crate@$version..."
for i in $(seq 1 30); do
if is_published "$crate" "$version"; then
echo " ready"
return 0
fi
sleep 5
done
echo " timed out!"
return 1
}
publish_tier() {
local tier_name="$1"; shift
local crates=("$@")
local need_wait=()
echo "::group::Tier: $tier_name"
for crate in "${crates[@]}"; do
VERSION=$(cargo metadata --no-deps --format-version=1 \
| jq -r --arg name "$crate" '.packages[] | select(.name == $name) | .version')
if is_published "$crate" "$VERSION"; then
echo " $crate@$VERSION already published — skipping"
else
echo " Publishing $crate@$VERSION..."
cargo publish -p "$crate" --allow-dirty --no-verify
need_wait+=("$crate:$VERSION")
fi
done
for entry in "${need_wait[@]}"; do
wait_for "${entry%%:*}" "${entry##*:}"
done
echo "::endgroup::"
}
publish_tier "1 (no internal deps)" $TIER1
publish_tier "2 (depends on tier 1)" $TIER2
publish_tier "3 (depends on tier 2)" $TIER3
publish_tier "4 (depends on tier 3)" $TIER4
publish_tier "5 (depends on tier 4)" $TIER5
publish_tier "6 (depends on tier 5)" $TIER6
publish_tier "7 (depends on tier 6)" $TIER7
publish_tier "8 (depends on tier 7)" $TIER8
# ── Docker (full releases only) ──────────────────────────────────────────────
# Native runner per arch (no QEMU). The image is assembled from the binary the
# prepare run already built: `--build-context binary=` overrides the Dockerfile
# stage of the same name, so Buildx prunes the compile stages entirely and the
# build is a two-layer copy.
docker-build:
name: Docker build (${{ matrix.label }})
needs: validate-version
if: inputs.docker && needs.validate-version.outputs.is_full_release == 'true'
runs-on: ${{ matrix.runs-on }}
permissions:
contents: read
actions: read
strategy:
fail-fast: false
matrix:
include:
- runs-on: ubuntu-latest
platform: linux/amd64
label: amd64
binary: binary-linux-x64
- runs-on: ubuntu-24.04-arm
platform: linux/arm64
label: arm64
binary: binary-linux-arm64
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || inputs.tag }}
- name: Download prebuilt binary
uses: actions/download-artifact@v8
with:
name: ${{ matrix.binary }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./binctx
- name: Make binary executable
run: chmod +x ./binctx/nodedb
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push platform image
uses: docker/build-push-action@v7
with:
context: .
build-contexts: binary=./binctx
platforms: ${{ matrix.platform }}
push: true
tags: farhansyah/nodedb:${{ needs.validate-version.outputs.version }}-${{ matrix.label }}
sbom: true
provenance: mode=max
# The binary is now compiled on the runner (glibc 2.39) rather than inside
# the Debian builder stage, so the runtime base must be able to load it.
# Exercise the pushed per-arch tag before it can reach the multi-arch
# manifest: `latest` is only ever built from images that have run.
- name: Smoke-test pushed image
run: |
docker run --rm --pull=always \
farhansyah/nodedb:${{ needs.validate-version.outputs.version }}-${{ matrix.label }} \
--version
docker-manifest:
name: Docker manifest
needs: [validate-version, docker-build]
if: inputs.docker && needs.validate-version.outputs.is_full_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push multi-arch manifest
env:
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
docker buildx imagetools create \
--tag farhansyah/nodedb:${VERSION} \
--tag farhansyah/nodedb:latest \
farhansyah/nodedb:${VERSION}-amd64 \
farhansyah/nodedb:${VERSION}-arm64
# ── GitHub Release ───────────────────────────────────────────────────────────
# Deliberately independent of publish-crates and docker: a crates.io outage
# must not block cutting the release, and re-running this stage alone is the
# fix for a bad release body.
github-release:
name: Create GitHub Release
needs: validate-version
if: inputs.github_release
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Download binary artifacts
uses: actions/download-artifact@v8
with:
pattern: "server-*"
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
name: NodeDB ${{ needs.validate-version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: ${{ needs.validate-version.outputs.is_full_release != 'true' }}
files: artifacts/*
fail_on_unmatched_files: true
# ── Discord (full releases only) ─────────────────────────────────────────────
notify-discord:
name: Notify Discord
needs: [validate-version, github-release]
if: >-
always() &&
inputs.notify &&
needs.validate-version.outputs.is_full_release == 'true' &&
needs.github-release.result != 'failure' &&
needs.github-release.result != 'cancelled'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Build changelog and post to Discord
env:
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
VERSION: ${{ needs.validate-version.outputs.version }}
IS_FULL: ${{ needs.validate-version.outputs.is_full_release }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
TAG="v${VERSION}"
PREV_TAG=$(python3 .github/scripts/prev_tag.py "$TAG")
if [[ -n "$PREV_TAG" ]]; then
RANGE="${PREV_TAG}..${TAG}"
COMPARE_URL="https://github.com/${REPO}/compare/${PREV_TAG}...${TAG}"
else
RANGE="${TAG}"
COMPARE_URL="https://github.com/${REPO}/releases/tag/${TAG}"
fi
COMMITS=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges --max-count=25)
[[ -z "$COMMITS" ]] && COMMITS="- (no commits found)"
COUNT=$(git rev-list --count --no-merges "$RANGE" 2>/dev/null || echo 0)
if [[ "$IS_FULL" == "true" ]]; then
COLOR=3066993 # green
TITLE="🚀 New Release: NodeDB ${VERSION}"
else
COLOR=15844367 # amber
TITLE="🚧 New Pre-Release: NodeDB ${VERSION}"
fi
DESC=$(printf '**Changes since %s** (%s commits)\n%s\n\n[Full changelog](%s) · [Release notes](https://github.com/%s/releases/tag/%s)' \
"${PREV_TAG:-start}" "$COUNT" "$COMMITS" "$COMPARE_URL" "$REPO" "$TAG")
PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--arg desc "$DESC" \
--arg url "https://github.com/${REPO}/releases/tag/${TAG}" \
--argjson color "$COLOR" \
'{embeds:[{title:$title,url:$url,description:$desc,color:$color}]}')
curl -fsS -H "Content-Type: application/json" \
-X POST -d "$PAYLOAD" "$DISCORD_RELEASE_WEBHOOK_URL"