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
24 changes: 24 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
changelog:
exclude:
labels:
- upstream-sync
- duplicate
- invalid
- wontfix
- question
categories:
- title: Breaking Changes
labels:
- breaking-change
- title: Features
labels:
- enhancement
- title: Bug Fixes
labels:
- bug
- title: Documentation
labels:
- documentation
- title: Other Changes
labels:
- "*"
12 changes: 12 additions & 0 deletions .github/scripts/release/constants.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
SERVERS=(disruption-manager dominator filegen-server fleet-manager hypervisor
image-unpacker imageserver imaginator installer mdbd subd)

# Plain binary, no tarball. Trim this list to publish fewer clients.
CLIENTS=(ami-publisher builder-tool domtool filegen-client fs2objectcache
fsbench fsreadslow hyper-control imagetool list-cert-expirations logtool
mdb-relayd objecttool scan show-cert srpc-test subtool unpacker-tool
vm-control)

DIST_DIR="dist"
SUPPORTED_DESTINATIONS=(github jfrog)
62 changes: 62 additions & 0 deletions .github/scripts/release/destinations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/constants.sh"

is_supported_destination() {
local d="$1" s
for s in "${SUPPORTED_DESTINATIONS[@]}"; do [[ "$s" == "$d" ]] && return 0; done
return 1
}

# destinations feeds release.yml's fromJson() matrix, so it must always be valid JSON, even empty.
resolve_destinations() {
: "${RAW_TARGETS:?set PUBLISH_TARGETS (repo var) or the publish_targets dispatch input}"
: "${VERSION:?}"
# Newline-string, not an array -- empty array expansion is unbound-variable on bash 3.2.
local raw dest matched="" parts dest_json
IFS=',' read -ra parts <<<"$RAW_TARGETS"
for raw in "${parts[@]}"; do
read -r dest <<<"$raw"
[[ -z "$dest" ]] && continue
if ! is_supported_destination "$dest"; then
gha_error "unsupported publish destination '$dest' (supported: ${SUPPORTED_DESTINATIONS[*]})"
exit 1
fi
matched="${matched}${dest}"$'\n'
done
# Standalone assignment so set -e catches a failure here, not swallowed as gha_output's argument.
dest_json="$(printf '%s' "$matched" | sed '/^$/d' | sort -u | jq -R . | jq -sc .)"
gha_output version "$VERSION"
gha_output destinations "$dest_json"
}

# Missing/empty here means a build silently produced nothing -- fail loudly, not crash downstream.
require_dist_populated() {
local group
for group in tarballs binaries; do
if [[ -z "$(ls -A "$DIST_DIR/$group" 2>/dev/null)" ]]; then
gha_error "dist/$group is missing or empty -- nothing to publish"
exit 1
fi
done
}

# Must run before compute_checksums -- a downloaded file otherwise carries no version of its own.
stamp_version() {
: "${VERSION:?}"
require_dist_populated
local f
for f in "$DIST_DIR"/tarballs/*.tar.gz; do
mv "$f" "$(dirname "$f")/$(basename "$f" .tar.gz)-${VERSION}.tar.gz"
done
for f in "$DIST_DIR"/binaries/*; do
mv "$f" "${f}-${VERSION}"
done
}

# One combined file, not one per group -- GitHub release assets are a flat
# namespace, so a tarballs/SHA256SUMS and a binaries/SHA256SUMS would collide.
compute_checksums() {
require_dist_populated
(cd "$DIST_DIR" && sha256sum tarballs/* binaries/* | sed -E 's# (tarballs|binaries)/# #' >SHA256SUMS.tmp && mv SHA256SUMS.tmp SHA256SUMS)
cat "$DIST_DIR/SHA256SUMS"
}
67 changes: 67 additions & 0 deletions .github/scripts/release/package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/constants.sh"

# Catches a server added to one of SERVERS / Makefile .tarball targets but not the other.
verify_servers_list() {
local makefile_servers declared missing extra
makefile_servers="$(grep -oE '^[A-Za-z0-9_-]+\.tarball:' Makefile | sed 's/\.tarball:$//' | sort -u)"
declared="$(printf '%s\n' "${SERVERS[@]}" | sort -u)"
missing="$(comm -23 <(printf '%s\n' "$makefile_servers") <(printf '%s\n' "$declared"))"
extra="$(comm -13 <(printf '%s\n' "$makefile_servers") <(printf '%s\n' "$declared"))"
if [[ -n "$missing" || -n "$extra" ]]; then
gha_error "SERVERS in constants.sh is out of sync with Makefile .tarball targets (in Makefile but not SERVERS: ${missing:-none}; in SERVERS but no Makefile target: ${extra:-none})"
exit 1
fi
}

# Platform-suffixed so two build legs can merge into one dist/ without collisions.
# strict hard-fails on a missing binary instead of warning (Linux only).
collect_client_binaries() {
local src="$1" suffix="$2" strict="${3:-}" name
mkdir -p "$DIST_DIR/binaries"
for name in "${CLIENTS[@]}"; do
if [[ -f "$src/$name" ]]; then
cp -p "$src/$name" "$DIST_DIR/binaries/${name}-${suffix}"
elif [[ -n "$strict" ]]; then
gha_error "expected client binary '$name' not found in $src"
exit 1
else
echo "::warning::expected client binary '$name' not found in $src, skipping"
fi
done
}

build_linux() {
: "${PLATFORM:?}"
verify_servers_list
local out="/tmp/${LOGNAME:-runner}" gobin
GOPATH="$(go env GOPATH)"
export GOPATH
gobin="$GOPATH/bin"
mkdir -p "$out" "$DIST_DIR/tarballs" "$DIST_DIR/binaries" ssl
make all
# shellcheck disable=SC2046 # word splitting is the point: one make target per server
make $(printf '%s.tarball ' "${SERVERS[@]}")
cp "$out"/*.tar.gz "$DIST_DIR/tarballs/"
collect_client_binaries "$gobin" "$PLATFORM" strict
strip "$DIST_DIR"/binaries/* 2>/dev/null || true
cp lib/version/BUILD_INFO "$DIST_DIR/BUILD_INFO"
find "$DIST_DIR" -type f | sort
}

# Servers won't compile on darwin, so only non-server cmd/ packages are built here.
build_darwin() {
: "${PLATFORM:?}"
make generate # //go:embed needs BUILD_INFO before go build touches that package
local out="/tmp/${LOGNAME:-runner}-darwin" targets=() name
mkdir -p "$out"
for name in "${CLIENTS[@]}"; do
targets+=("./cmd/$name")
done
# Trailing slash on -o: go build with multiple packages discards binaries without it.
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -buildvcs=true -o "$out/" "${targets[@]}"
collect_client_binaries "$out" "$PLATFORM"
strip "$DIST_DIR"/binaries/* 2>/dev/null || true
cp lib/version/BUILD_INFO "$DIST_DIR/BUILD_INFO"
find "$DIST_DIR" -type f | sort
}
48 changes: 48 additions & 0 deletions .github/scripts/release/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/constants.sh"

publish_github() {
: "${VERSION:?}"
local files=("$DIST_DIR"/tarballs/* "$DIST_DIR"/binaries/* "$DIST_DIR/BUILD_INFO" "$DIST_DIR/SHA256SUMS")

if ! gh release view "$VERSION" >/dev/null 2>&1; then
gh release create "$VERSION" --title "$VERSION" --generate-notes --verify-tag --latest "${files[@]}"
return
fi

# Release exists already -- resume by uploading only missing assets, refuse if complete.
local existing_assets missing=() f base
existing_assets="$(gh release view "$VERSION" --json assets --jq '.assets[].name')"
for f in "${files[@]}"; do
base="$(basename "$f")"
grep -qxF "$base" <<<"$existing_assets" || missing+=("$f")
done

if [[ ${#missing[@]} -eq 0 ]]; then
echo "::notice::release $VERSION already has every expected asset -- nothing to publish"
return
fi
gh release upload "$VERSION" "${missing[@]}"
}

publish_jfrog() {
: "${VERSION:?}"
: "${JFROG_REPO:?set the JFROG_REPO repository variable}"
local match_count
match_count="$(jf rt search "${JFROG_REPO}/dominator/${VERSION}/BUILD_INFO" | jq 'length')"
if [[ "$match_count" != "0" ]]; then
echo "::notice::artifacts already exist under ${JFROG_REPO}/dominator/${VERSION}/ -- nothing to publish"
return
fi

local v
# --fail-no-op: jf rt upload otherwise exits 0 (success) even when the glob matches nothing.
for v in "$VERSION" latest; do
jf rt upload "$DIST_DIR/tarballs/*" "${JFROG_REPO}/dominator/${v}/tarballs/" --flat=true --fail-no-op
jf rt upload "$DIST_DIR/binaries/*" "${JFROG_REPO}/dominator/${v}/binaries/" --flat=true --fail-no-op
jf rt upload "$DIST_DIR/SHA256SUMS" "${JFROG_REPO}/dominator/${v}/" --flat=true --fail-no-op
done
# BUILD_INFO under $VERSION must be uploaded last -- it's what the existence check above trusts.
jf rt upload "$DIST_DIR/BUILD_INFO" "${JFROG_REPO}/dominator/latest/" --flat=true --fail-no-op
jf rt upload "$DIST_DIR/BUILD_INFO" "${JFROG_REPO}/dominator/${VERSION}/" --flat=true --fail-no-op
}
22 changes: 22 additions & 0 deletions .github/scripts/release/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euxo pipefail # -x: full command trace in the log
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$dir/utils.sh"
source "$dir/constants.sh"
source "$dir/package.sh"
source "$dir/destinations.sh"
source "$dir/publish.sh"

case "${1:-}" in
build-linux) build_linux ;;
build-darwin) build_darwin ;;
resolve-destinations) resolve_destinations ;;
stamp-version) stamp_version ;;
checksums) compute_checksums ;;
publish-github) publish_github ;;
publish-jfrog) publish_jfrog ;;
-h | --help | *)
echo "usage: $0 <build-linux|build-darwin|resolve-destinations|stamp-version|checksums|publish-github|publish-jfrog>" >&2
exit 1
;;
esac
22 changes: 22 additions & 0 deletions .github/scripts/release/utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

gha_output() {
local key="$1" value="$2"
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
echo " [output] $key=$value"
return
fi
if [[ "$value" == *$'\n'* ]]; then
{
echo "$key<<__DELIM__"
echo "$value"
echo "__DELIM__"
} >>"$GITHUB_OUTPUT"
else
echo "$key=$value" >>"$GITHUB_OUTPUT"
fi
}

gha_error() {
echo "::error::$1"
}
71 changes: 71 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Shared build job. Called by ci.yml (every PR/master push) and release.yml
# (every tag push), so both pipelines run identical build logic -- never two
# YAML copies that can silently drift apart.
#
# Linux is the gate: it's where tests run, and it builds both servers and
# clients. Darwin runs after Linux passes (never in parallel) and builds
# clients only (servers aren't cross-platform) -- its failure blocks CI and
# release alike, since a broken darwin compile is a real regression, not
# runner noise.
name: Build

on:
workflow_call:

jobs:
build-linux:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # full history for git describe

- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true

- name: Generate build info
run: make generate # //go:embed needs BUILD_INFO before go test/go build touch that package

- name: Run tests
run: make test

- name: Build & package
env:
PLATFORM: linux-amd64
run: .github/scripts/release/run.sh build-linux

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: github.ref_type == 'tag'
with:
name: dominator-build-linux-amd64
path: dist/
if-no-files-found: error

build-darwin:
needs: build-linux
runs-on: macos-15 # native arm64 -- avoids cross-compile/strip issues
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # full history for git describe

- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true

- name: Build & package
env:
PLATFORM: darwin-arm64
run: .github/scripts/release/run.sh build-darwin

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: github.ref_type == 'tag'
with:
name: dominator-build-darwin-arm64
path: dist/
if-no-files-found: error
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CI for Dominator. Runs the shared build+test (see build.yml) on every PR and
# master push -- nothing about releases here.
name: CI

on:
pull_request:
push:
branches: [master]
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
uses: ./.github/workflows/build.yml
Loading