ef19487611
Repeated runs filled the self-hosted runner's disk, so npm ci failed with ENOSPC. Prune unused Docker images/layers and the apt cache at the start of the job (the image is rebuilt fresh later). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
363 lines
16 KiB
YAML
363 lines
16 KiB
YAML
name: Release
|
|
|
|
# Fires only on a merge into main (a push to the main branch). No per-commit
|
|
# or per-PR CI runs on other branches — this is the single pipeline that turns
|
|
# what lands on main into a release, cross-platform binaries, an MSI, and a
|
|
# container image.
|
|
#
|
|
# Docs-only merges are skipped: touching just README/docs/LICENSE does not
|
|
# produce a new build.
|
|
on:
|
|
# Allow an on-demand run from the Gitea Actions UI/API without a dummy commit.
|
|
workflow_dispatch:
|
|
push:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- 'README.md'
|
|
- 'LICENSE'
|
|
- 'docs/**'
|
|
- '.gitignore'
|
|
- '.gitattributes'
|
|
|
|
jobs:
|
|
release:
|
|
# Label must match a registered Linux runner that has Docker. Pure-Go builds
|
|
# (modernc SQLite, CGO off) mean every OS/arch cross-compiles here, and WiX v5
|
|
# builds the Windows MSI on Linux, so one runner produces every artifact.
|
|
runs-on: ubuntu-host
|
|
|
|
# Grant the auto-injected Actions token the scopes this job needs: push to
|
|
# the built-in container registry (packages) and create a release (contents).
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
env:
|
|
# Use exactly the toolchain provided by setup-go; never auto-download a
|
|
# different Go toolchain (a `go install ...@latest` otherwise pulled a
|
|
# newer toolchain that mismatched and broke the build).
|
|
GOTOOLCHAIN: local
|
|
# Optional EXTERNAL registry override. Leave these unset to publish to the
|
|
# Gitea instance's own built-in container registry (the default below).
|
|
REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USERNAME }}
|
|
REGISTRY_PASS: ${{ secrets.REGISTRY_PASSWORD }}
|
|
# Injected automatically by Gitea Actions; used for the built-in registry
|
|
# login and for creating the release. github.token is always populated
|
|
# (unlike secrets.GITEA_TOKEN, which is not defined on every instance).
|
|
BUILTIN_TOKEN: ${{ github.token }}
|
|
SERVER_URL: ${{ github.server_url }}
|
|
ACTOR: ${{ github.actor }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
|
|
steps:
|
|
# OrchestrAD is a SHA-1 repo, so actions/checkout works as-is. fetch-depth
|
|
# 0 is required so the HEAD commit date is available for the version.
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
# This self-hosted runner accumulates Docker layers/images and caches
|
|
# across runs, which can fill the disk (npm ci fails with ENOSPC). Reclaim
|
|
# space up front by pruning unused Docker data (the image is rebuilt fresh
|
|
# later anyway) and the apt cache.
|
|
- name: Free disk space
|
|
shell: bash
|
|
run: |
|
|
df -h / || true
|
|
docker system prune -af || true
|
|
docker builder prune -af || true
|
|
sudo apt-get clean || true
|
|
df -h / || true
|
|
|
|
# Version = the UTC date of the HEAD commit, formatted yyyy.MM.dd.HHmm
|
|
# (the project's documented version scheme). Deriving it from the commit
|
|
# rather than "now" makes re-runs reproducible and keeps the image tag,
|
|
# the release tag, the binary version, and the MSI version aligned.
|
|
- name: Compute version
|
|
id: ver
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="$(TZ=UTC git show -s --format=%cd --date=format-local:'%Y.%m.%d.%H%M' HEAD)"
|
|
GIT_COMMIT="$(git rev-parse HEAD)"
|
|
GIT_COMMIT_SHORT="$(git rev-parse --short HEAD)"
|
|
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
# MSI ProductVersion fields are bounded (major<=255, build<=65535), so
|
|
# yyyy.MM.dd.HHmm cannot be used directly. Map to major=yy, minor=month,
|
|
# build=day*1440+minute-of-day, which stays in range and increases
|
|
# monotonically over time for correct upgrade detection.
|
|
IFS=. read -r Y M D HM <<< "$VERSION"
|
|
Y=$((10#$Y)); M=$((10#$M)); D=$((10#$D)); HM=$((10#$HM))
|
|
HH=$((HM/100)); MM=$((HM%100))
|
|
MSI_VERSION="$((Y-2000)).$M.$((D*1440 + HH*60 + MM))"
|
|
|
|
{
|
|
echo "version=$VERSION"
|
|
echo "git_commit=$GIT_COMMIT"
|
|
echo "git_commit_short=$GIT_COMMIT_SHORT"
|
|
echo "build_time=$BUILD_TIME"
|
|
echo "msi_version=$MSI_VERSION"
|
|
echo "vy=$Y"; echo "vm=$M"; echo "vd=$D"; echo "vhm=$HM"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "OrchestrAD version: $VERSION (msi $MSI_VERSION, $GIT_COMMIT_SHORT)"
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
# Install exactly the version go.mod requires so GOTOOLCHAIN=local has
|
|
# a matching toolchain and never needs to download one.
|
|
go-version-file: backend/go.mod
|
|
|
|
# The self-hosted runner has a stale global GOROOT pointing at a different
|
|
# Go install, so `go` (from setup-go) invokes a mismatched `compile`
|
|
# ("compile: version ... does not match go tool version"). Pin GOROOT to
|
|
# the directory of the active go binary for all later steps, and clear the
|
|
# persistent build cache so objects recompile with this toolchain.
|
|
- name: Align Go toolchain
|
|
run: |
|
|
set -euo pipefail
|
|
GO_BIN="$(command -v go)"
|
|
GR="$(cd "$(dirname "$GO_BIN")/.." && pwd)"
|
|
echo "active go: $GO_BIN"
|
|
echo "env GOROOT (before): ${GOROOT:-unset}"
|
|
echo "resolved GOROOT: $GR"
|
|
echo "GOROOT=$GR" >> "$GITHUB_ENV"
|
|
GOROOT="$GR" go version
|
|
GOROOT="$GR" go tool compile -V || true
|
|
GOROOT="$GR" go clean -cache
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '8.0.x'
|
|
|
|
- name: Ensure tooling (jq, zip)
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
missing=""
|
|
command -v jq >/dev/null 2>&1 || missing="$missing jq"
|
|
command -v zip >/dev/null 2>&1 || missing="$missing zip"
|
|
if [ -n "$missing" ]; then
|
|
sudo apt-get update -y && sudo apt-get install -y $missing
|
|
fi
|
|
jq --version
|
|
zip --version | head -2
|
|
|
|
# Test gate: pure-Go, so no C toolchain is needed. A failure stops the
|
|
# release before anything is built or published.
|
|
- name: Test
|
|
working-directory: backend
|
|
env:
|
|
CGO_ENABLED: '0'
|
|
run: go test ./...
|
|
|
|
# Build the Next.js static export once on the host and stage it into the
|
|
# //go:embed dir so every standalone binary ships the real UI. (The
|
|
# container image builds its own copy via the Dockerfile.)
|
|
- name: Build frontend
|
|
working-directory: frontend
|
|
run: |
|
|
set -euo pipefail
|
|
npm ci --no-audit --no-fund
|
|
npm run build
|
|
|
|
- name: Stage UI into embed dir
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
dist=backend/internal/webui/dist
|
|
find "$dist" -mindepth 1 -not -name '.gitignore' -delete
|
|
cp -r frontend/out/* "$dist"/
|
|
test -f "$dist/index.html"
|
|
|
|
# Generate the Windows icon/version resource. goversioninfo is pure Go; the
|
|
# arch-suffixed .syso files are only linked into their matching build.
|
|
- name: Generate Windows resource
|
|
working-directory: backend
|
|
env:
|
|
VY: ${{ steps.ver.outputs.vy }}
|
|
VM: ${{ steps.ver.outputs.vm }}
|
|
VD: ${{ steps.ver.outputs.vd }}
|
|
VHM: ${{ steps.ver.outputs.vhm }}
|
|
run: |
|
|
set -euo pipefail
|
|
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@v1.7.0
|
|
GV="$(go env GOPATH)/bin/goversioninfo"
|
|
"$GV" -64 -ver-major="$VY" -ver-minor="$VM" -ver-patch="$VD" -ver-build="$VHM" \
|
|
-product-version="$VY.$VM.$VD" -o cmd/orchestrad/resource_windows_amd64.syso versioninfo.json
|
|
"$GV" -64 -arm -ver-major="$VY" -ver-minor="$VM" -ver-patch="$VD" -ver-build="$VHM" \
|
|
-product-version="$VY.$VM.$VD" -o cmd/orchestrad/resource_windows_arm64.syso versioninfo.json
|
|
|
|
# Cross-compile all six targets (CGO off) and package them: .zip for
|
|
# Windows, .tar.gz elsewhere, each with a .sha256. Stage the windows/amd64
|
|
# binary for the MSI.
|
|
- name: Build binaries
|
|
working-directory: backend
|
|
env:
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
GIT_COMMIT: ${{ steps.ver.outputs.git_commit }}
|
|
BUILD_TIME: ${{ steps.ver.outputs.build_time }}
|
|
run: |
|
|
set -euo pipefail
|
|
LDFLAGS="-s -w \
|
|
-X github.com/Grace-Solutions/OrchestrAD/internal/version.Version=${VERSION} \
|
|
-X github.com/Grace-Solutions/OrchestrAD/internal/version.BuildTime=${BUILD_TIME} \
|
|
-X github.com/Grace-Solutions/OrchestrAD/internal/version.GitCommit=${GIT_COMMIT}"
|
|
DIST="$GITHUB_WORKSPACE/dist"; mkdir -p "$DIST" "$GITHUB_WORKSPACE/msistage"
|
|
|
|
build() {
|
|
local goos="$1" goarch="$2" label="$3" ext="$4"
|
|
echo "Building $label..."
|
|
local work; work="$(mktemp -d)"
|
|
GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 \
|
|
go build -ldflags "$LDFLAGS" -o "$work/orchestrad$ext" ./cmd/orchestrad
|
|
local name="orchestrad-${VERSION}-${label}"
|
|
if [ "$goos" = "windows" ]; then
|
|
( cd "$work" && zip -q "$DIST/${name}.zip" "orchestrad$ext" )
|
|
( cd "$DIST" && sha256sum "${name}.zip" > "${name}.zip.sha256" )
|
|
else
|
|
( cd "$work" && tar -czf "$DIST/${name}.tar.gz" "orchestrad$ext" )
|
|
( cd "$DIST" && sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256" )
|
|
fi
|
|
if [ "$goos" = "windows" ] && [ "$goarch" = "amd64" ]; then
|
|
cp "$work/orchestrad.exe" "$GITHUB_WORKSPACE/msistage/orchestrad.exe"
|
|
fi
|
|
rm -rf "$work"
|
|
}
|
|
|
|
build windows amd64 windows-amd64 .exe
|
|
build windows arm64 windows-arm64 .exe
|
|
build darwin amd64 macos-amd64 ""
|
|
build darwin arm64 macos-arm64 ""
|
|
build linux amd64 linux-amd64 ""
|
|
build linux arm64 linux-arm64 ""
|
|
ls -la "$DIST"
|
|
|
|
# Build the Windows MSI with WiX v5 (cross-platform, no OSMF fee). Installs
|
|
# to Program Files\OrchestrAD and registers+starts the service via the
|
|
# binary's idempotent `initialize`/`remove` commands.
|
|
- name: Build MSI
|
|
env:
|
|
MSI_VERSION: ${{ steps.ver.outputs.msi_version }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
export PATH="$PATH:$HOME/.dotnet/tools"
|
|
command -v wix >/dev/null 2>&1 || dotnet tool install --global wix --version 5.0.2
|
|
wix build -arch x64 installer/OrchestrAD.wxs \
|
|
-d Version="$MSI_VERSION" \
|
|
-d BinDir="$GITHUB_WORKSPACE/msistage" \
|
|
-d IconPath="$GITHUB_WORKSPACE/resources/icons/orchestrad.ico" \
|
|
-o "dist/OrchestrAD-${VERSION}-x64.msi"
|
|
( cd dist && sha256sum "OrchestrAD-${VERSION}-x64.msi" > "OrchestrAD-${VERSION}-x64.msi.sha256" )
|
|
ls -la dist
|
|
|
|
- name: Resolve registry target
|
|
id: reg
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -n "${REGISTRY_HOST:-}" ]; then
|
|
HOST="$REGISTRY_HOST"; USER="$REGISTRY_USER"; PASS="$REGISTRY_PASS"
|
|
echo "Using external registry $HOST"
|
|
else
|
|
HOST="$(echo "$SERVER_URL" | sed -E 's#^https?://##; s#/$##')"
|
|
USER="$ACTOR"; PASS="$BUILTIN_TOKEN"
|
|
echo "Using the built-in Gitea registry at $HOST"
|
|
fi
|
|
OWNER_LC="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')"
|
|
{
|
|
echo "host=$HOST"
|
|
echo "user=$USER"
|
|
echo "image=${HOST}/${OWNER_LC}/orchestrad"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "::add-mask::$PASS"
|
|
echo "REGISTRY_LOGIN_PASSWORD=$PASS" >> "$GITHUB_ENV"
|
|
|
|
- name: Registry login
|
|
run: echo "$REGISTRY_LOGIN_PASSWORD" | docker login "${{ steps.reg.outputs.host }}" -u "${{ steps.reg.outputs.user }}" --password-stdin
|
|
|
|
# One multi-stage build compiles the Next.js UI, embeds it, and produces
|
|
# the Go binary. Tag both the immutable version and latest (main only).
|
|
- name: Build image
|
|
env:
|
|
IMAGE: ${{ steps.reg.outputs.image }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
GIT_COMMIT: ${{ steps.ver.outputs.git_commit }}
|
|
BUILD_TIME: ${{ steps.ver.outputs.build_time }}
|
|
run: |
|
|
set -euo pipefail
|
|
docker build \
|
|
--build-arg VERSION="$VERSION" \
|
|
--build-arg GIT_COMMIT="$GIT_COMMIT" \
|
|
--build-arg BUILD_TIME="$BUILD_TIME" \
|
|
-t "${IMAGE}:${VERSION}" \
|
|
-t "${IMAGE}:latest" \
|
|
.
|
|
|
|
- name: Push image
|
|
env:
|
|
IMAGE: ${{ steps.reg.outputs.image }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
docker push "${IMAGE}:${VERSION}"
|
|
docker push "${IMAGE}:latest"
|
|
echo "Published ${IMAGE}:${VERSION} and ${IMAGE}:latest"
|
|
|
|
# Create the Gitea release and attach every artifact in dist/.
|
|
- name: Create Gitea release
|
|
shell: bash
|
|
env:
|
|
API_URL: ${{ github.api_url }}
|
|
REPO: ${{ github.repository }}
|
|
TOKEN: ${{ github.token }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
GIT_COMMIT: ${{ steps.ver.outputs.git_commit }}
|
|
GIT_COMMIT_SHORT: ${{ steps.ver.outputs.git_commit_short }}
|
|
IMAGE: ${{ steps.reg.outputs.image }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Skip creation if a release for this tag already exists (re-run), but
|
|
# still (re)upload any assets that are missing below.
|
|
code="$(curl -sS -o /tmp/rel.json -w '%{http_code}' \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
"${API_URL}/repos/${REPO}/releases/tags/${VERSION}")"
|
|
if [ "$code" = "200" ]; then
|
|
rel_id="$(jq -r '.id' /tmp/rel.json)"
|
|
echo "Release ${VERSION} already exists (id=$rel_id); ensuring assets."
|
|
else
|
|
body="$(printf '**OrchestrAD %s**\n\n| Field | Value |\n| --- | --- |\n| Version | `%s` |\n| Commit | [`%s`](%s/%s/commit/%s) |\n\n## Container image\n```\ndocker pull %s:%s\ndocker pull %s:latest\n```\n\n## Downloads\n- Windows installer: `OrchestrAD-%s-x64.msi` (installs to Program Files and runs as a service)\n- Standalone binaries: windows/macos/linux, amd64/arm64\n' \
|
|
"$VERSION" "$VERSION" "$GIT_COMMIT_SHORT" "$SERVER_URL" "$REPO" "$GIT_COMMIT" "$IMAGE" "$VERSION" "$IMAGE" "$VERSION")"
|
|
payload="$(jq -n --arg tag "$VERSION" --arg sha "$GIT_COMMIT" \
|
|
--arg name "OrchestrAD $VERSION" --arg body "$body" \
|
|
'{tag_name:$tag, target_commitish:$sha, name:$name, body:$body, draft:false, prerelease:false}')"
|
|
rel="$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
-H "Content-Type: application/json" -d "$payload" \
|
|
"${API_URL}/repos/${REPO}/releases")"
|
|
rel_id="$(printf '%s' "$rel" | jq -r '.id')"
|
|
if [ -z "$rel_id" ] || [ "$rel_id" = "null" ]; then
|
|
echo "Failed to create release:"; echo "$rel"; exit 1
|
|
fi
|
|
echo "Created release id=$rel_id"
|
|
fi
|
|
|
|
for asset in dist/*; do
|
|
name="$(basename "$asset")"
|
|
echo "Uploading $name"
|
|
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
-F "attachment=@${asset}" \
|
|
"${API_URL}/repos/${REPO}/releases/${rel_id}/assets?name=${name}" >/dev/null
|
|
done
|
|
echo "Release ${VERSION} published with $(ls dist | wc -l) assets."
|