010add4642
Single Gitea Actions workflow triggered only by a push to main (no per-commit or per-PR CI). It runs the Go test suite as a gate, then builds and pushes the container image tagged latest and the unified HEAD-commit date (yyyy.MM.dd.HHmm), and cuts a Gitea release with the linux/amd64 binary attached. Defaults to the built-in Gitea registry; REGISTRY_* secrets override to an external one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
219 lines
9.0 KiB
YAML
219 lines
9.0 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 + 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 (used for the
|
|
# dockerized test gate and the image build/push). This is the same label the
|
|
# sibling repos use for their image jobs.
|
|
runs-on: ubuntu-host
|
|
|
|
env:
|
|
# 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. No manual secret needed.
|
|
BUILTIN_TOKEN: ${{ secrets.GITEA_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
|
|
|
|
# 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, and the binary's embedded version identical.
|
|
- 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)"
|
|
{
|
|
echo "version=$VERSION"
|
|
echo "git_commit=$GIT_COMMIT"
|
|
echo "git_commit_short=$GIT_COMMIT_SHORT"
|
|
echo "build_time=$BUILD_TIME"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "OrchestrAD version: $VERSION ($GIT_COMMIT_SHORT)"
|
|
|
|
# Test gate: run the Go test suite inside the same toolchain image the
|
|
# build uses. Running it in a container means the runner needs only Docker
|
|
# (no host Go/gcc), and a failure here stops the release before anything
|
|
# is published. CGO is on because the SQLite driver requires it.
|
|
- name: Test
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
docker run --rm -v "$PWD/backend:/src" -w /src \
|
|
-e CGO_ENABLED=1 golang:1.24-alpine \
|
|
sh -c "apk add --no-cache gcc musl-dev >/dev/null && go test ./..."
|
|
|
|
# jq is used to JSON-encode the release body safely. Install it if the
|
|
# self-hosted runner does not already have it.
|
|
- name: Ensure tooling (jq)
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "Installing jq..."
|
|
sudo apt-get update -y && sudo apt-get install -y jq
|
|
fi
|
|
jq --version
|
|
|
|
- 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"
|
|
|
|
# Pull the linux/amd64 binary back out of the freshly built image so the
|
|
# release carries a ready-to-run artifact, not just an image reference.
|
|
- name: Extract release binary
|
|
env:
|
|
IMAGE: ${{ steps.reg.outputs.image }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
CID="$(docker create "${IMAGE}:${VERSION}")"
|
|
docker cp "$CID:/app/orchestrad" "dist/orchestrad"
|
|
docker rm "$CID" >/dev/null
|
|
tar -C dist -czf "dist/orchestrad-${VERSION}-linux-amd64.tar.gz" orchestrad
|
|
( cd dist && sha256sum "orchestrad-${VERSION}-linux-amd64.tar.gz" > "orchestrad-${VERSION}-linux-amd64.tar.gz.sha256" )
|
|
ls -la dist
|
|
|
|
# Create the Gitea release for this version and attach the binary. Uses
|
|
# the auto-injected token; no manual secret required.
|
|
- name: Create Gitea release
|
|
shell: bash
|
|
env:
|
|
API_URL: ${{ github.api_url }}
|
|
REPO: ${{ github.repository }}
|
|
TOKEN: ${{ secrets.GITEA_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 if a release for this tag already exists (e.g. a re-run).
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
"${API_URL}/repos/${REPO}/releases/tags/${VERSION}")"
|
|
if [ "$code" = "200" ]; then
|
|
echo "Release ${VERSION} already exists; skipping."
|
|
exit 0
|
|
fi
|
|
|
|
# Markdown release notes, JSON-encoded with jq so any characters are
|
|
# safely escaped.
|
|
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' \
|
|
"$VERSION" "$VERSION" "$GIT_COMMIT_SHORT" "$SERVER_URL" "$REPO" "$GIT_COMMIT" "$IMAGE" "$VERSION" "$IMAGE")"
|
|
|
|
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"
|
|
|
|
for asset in dist/orchestrad-${VERSION}-linux-amd64.tar.gz dist/orchestrad-${VERSION}-linux-amd64.tar.gz.sha256; 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."
|