From c71388e14844265d2e5c4efa043262de219c1b32 Mon Sep 17 00:00:00 2001 From: BMAD CI Fix Agent Date: Fri, 4 Sep 2026 18:48:16 -0500 Subject: [PATCH] feat: add linux package artifact --- .github/workflows/package.yml | 24 +++++++ .gitignore | 1 + Makefile | 52 ++++++++++++++- README.md | 12 +++- cmd/devrail-router/main.go | 2 +- docs/packaging.md | 45 +++++++++++++ packaging/linux/install.sh | 84 ++++++++++++++++++++++++ packaging/systemd/devrail-router.service | 1 + 8 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/package.yml create mode 100644 packaging/linux/install.sh diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..cccff3e --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,24 @@ +# Package workflow builds and smoke-tests the installable tarball. +name: Package + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Smoke-test package + run: make package-smoke GOOS=linux GOARCH=amd64 + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: devrail-router-linux-amd64 + path: dist/*.tar.gz diff --git a/.gitignore b/.gitignore index 25c9531..e949a49 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ override.tf.json # --- DevRail --- .devrail-output/ +bin/ # --- Secrets --- .env diff --git a/Makefile b/Makefile index 799a953..8175d62 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,12 @@ DEVRAIL_IMAGE ?= ghcr.io/devrail-dev/dev-toolchain:v1 DEVRAIL_FAIL_FAST ?= 0 DEVRAIL_LOG_FORMAT ?= json +VERSION ?= 0.1.0-dev +GOOS ?= $(shell go env GOOS 2>/dev/null || echo linux) +GOARCH ?= $(shell go env GOARCH 2>/dev/null || echo amd64) +DIST_DIR ?= dist +BIN_DIR ?= bin +PACKAGE_NAME := devrail-router_$(VERSION)_$(GOOS)_$(GOARCH) DOCKER_RUN := docker run --rm \ -v "$$(pwd):/workspace" \ @@ -45,7 +51,7 @@ HAS_RUST := $(filter rust,$(LANGUAGES)) # --------------------------------------------------------------------------- # .PHONY declarations # --------------------------------------------------------------------------- -.PHONY: help lint format fix test security scan docs changelog check install-hooks init +.PHONY: help build clean lint format fix package package-smoke test security scan docs changelog check install-hooks init .PHONY: _lint _format _fix _test _security _scan _docs _changelog _check _check-config _init # =========================================================================== @@ -58,12 +64,23 @@ help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +build: ## Build the devrail-router binary for GOOS/GOARCH + @mkdir -p "$(BIN_DIR)" + CGO_ENABLED=0 GOOS="$(GOOS)" GOARCH="$(GOARCH)" go build \ + -trimpath \ + -ldflags "-s -w -X main.version=$(VERSION)" \ + -o "$(BIN_DIR)/devrail-router" \ + ./cmd/devrail-router + changelog: ## Generate CHANGELOG.md from conventional commits $(DOCKER_RUN) make _changelog check: ## Run all checks (lint, format, test, security, scan, docs) $(DOCKER_RUN) make _check +clean: ## Remove build and package outputs + rm -rf "$(BIN_DIR)" "$(DIST_DIR)" + docs: ## Generate documentation $(DOCKER_RUN) make _docs @@ -101,6 +118,39 @@ install-hooks: ## Install pre-commit hooks lint: ## Run all linters $(DOCKER_RUN) make _lint +package: build ## Build a Linux/macOS tarball package + @rm -rf "$(DIST_DIR)/$(PACKAGE_NAME)" + @mkdir -p "$(DIST_DIR)/$(PACKAGE_NAME)" + cp "$(BIN_DIR)/devrail-router" "$(DIST_DIR)/$(PACKAGE_NAME)/devrail-router" + cp LICENSE README.md CHANGELOG.md "$(DIST_DIR)/$(PACKAGE_NAME)/" + mkdir -p "$(DIST_DIR)/$(PACKAGE_NAME)/configs" \ + "$(DIST_DIR)/$(PACKAGE_NAME)/docs" \ + "$(DIST_DIR)/$(PACKAGE_NAME)/packaging/systemd" \ + "$(DIST_DIR)/$(PACKAGE_NAME)/packaging/linux" + cp configs/router.example.yaml "$(DIST_DIR)/$(PACKAGE_NAME)/configs/" + cp docs/architecture.md docs/packaging.md "$(DIST_DIR)/$(PACKAGE_NAME)/docs/" + cp packaging/systemd/devrail-router.service "$(DIST_DIR)/$(PACKAGE_NAME)/packaging/systemd/" + cp packaging/linux/install.sh "$(DIST_DIR)/$(PACKAGE_NAME)/packaging/linux/" + chmod 0755 "$(DIST_DIR)/$(PACKAGE_NAME)/devrail-router" \ + "$(DIST_DIR)/$(PACKAGE_NAME)/packaging/linux/install.sh" + (cd "$(DIST_DIR)" && tar -czf "$(PACKAGE_NAME).tar.gz" "$(PACKAGE_NAME)") + +package-smoke: package ## Build and smoke-test the tarball package + @set -e; \ + tmp_dir=$$(mktemp -d); \ + trap 'rm -rf "$$tmp_dir"' EXIT; \ + tar -xzf "$(DIST_DIR)/$(PACKAGE_NAME).tar.gz" -C "$$tmp_dir"; \ + host_goos=$$(go env GOHOSTOS); \ + host_goarch=$$(go env GOHOSTARCH); \ + if [ "$(GOOS)" = "$$host_goos" ] && [ "$(GOARCH)" = "$$host_goarch" ]; then \ + "$$tmp_dir/$(PACKAGE_NAME)/devrail-router" version; \ + "$$tmp_dir/$(PACKAGE_NAME)/devrail-router" check -config "$$tmp_dir/$(PACKAGE_NAME)/configs/router.example.yaml"; \ + else \ + echo "skipping binary execution for cross-built package $(GOOS)/$(GOARCH) on $$host_goos/$$host_goarch"; \ + fi; \ + bash -n "$$tmp_dir/$(PACKAGE_NAME)/packaging/linux/install.sh"; \ + test -f "$$tmp_dir/$(PACKAGE_NAME)/packaging/systemd/devrail-router.service" + scan: ## Run universal scanners (trivy, gitleaks) $(DOCKER_RUN) make _scan diff --git a/README.md b/README.md index ca7f53f..6aa3a63 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ This repository is in early foundation work. The current service supports: - OpenAI-compatible `/v1/*` request proxying - model alias rewriting - YAML configuration -- Linux/systemd packaging notes +- Linux tarball packaging +- Linux/systemd install script and unit Routing policy, auth, telemetry, LM Studio lifecycle integration, and Omarchy integration are planned next. @@ -61,6 +62,15 @@ curl http://127.0.0.1:8080/v1/chat/completions \ }' ``` +Build and smoke-test an installable Linux package: + +```sh +make package-smoke GOOS=linux GOARCH=amd64 +``` + +The package is written to `dist/` and includes the binary, example config, +systemd unit, and Linux installer. + ## Configuration See `configs/router.example.yaml`. diff --git a/cmd/devrail-router/main.go b/cmd/devrail-router/main.go index dc8a4ba..5c1ea53 100644 --- a/cmd/devrail-router/main.go +++ b/cmd/devrail-router/main.go @@ -15,7 +15,7 @@ import ( "github.com/devrail-dev/devrail-router/internal/server" ) -const version = "0.1.0-dev" +var version = "0.1.0-dev" func main() { os.Exit(run(os.Args[1:])) diff --git a/docs/packaging.md b/docs/packaging.md index b1fa518..7f8613c 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -15,6 +15,51 @@ The primary Linux installation target is: The systemd unit lives at `packaging/systemd/devrail-router.service`. +Build a tarball for the current platform: + +```sh +make package +``` + +Build a Linux AMD64 tarball and run package smoke checks: + +```sh +make package-smoke GOOS=linux GOARCH=amd64 +``` + +The generated archive is written to `dist/` with this layout: + +```text +devrail-router___/ + devrail-router + configs/router.example.yaml + packaging/linux/install.sh + packaging/systemd/devrail-router.service + docs/ + README.md + CHANGELOG.md + LICENSE +``` + +Install from an unpacked tarball: + +```sh +sudo ./packaging/linux/install.sh +``` + +The installer creates the `devrail-router` service user and group when missing, +installs the binary, installs a default config only if `/etc/devrail/router.yaml` +does not already exist, installs the systemd unit, reloads systemd, and enables +the service. It does not start the service unless `START_SERVICE=1` is set. + +Useful installer flags: + +```sh +DRY_RUN=1 ./packaging/linux/install.sh +FORCE_CONFIG=1 sudo ./packaging/linux/install.sh +START_SERVICE=1 sudo ./packaging/linux/install.sh +``` + ## Container Image A container image is useful for proxy-only deployments and CI smoke tests. It is diff --git a/packaging/linux/install.sh b/packaging/linux/install.sh new file mode 100644 index 0000000..0610f57 --- /dev/null +++ b/packaging/linux/install.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +SERVICE_NAME="devrail-router" +SERVICE_USER="${SERVICE_USER:-devrail-router}" +SERVICE_GROUP="${SERVICE_GROUP:-devrail-router}" +PREFIX="${PREFIX:-/usr/local}" +BIN_DIR="${BIN_DIR:-${PREFIX}/bin}" +CONFIG_DIR="${CONFIG_DIR:-/etc/devrail}" +STATE_DIR="${STATE_DIR:-/var/lib/devrail-router}" +SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}" +ENABLE_SERVICE="${ENABLE_SERVICE:-1}" +START_SERVICE="${START_SERVICE:-0}" +FORCE_CONFIG="${FORCE_CONFIG:-0}" +DRY_RUN="${DRY_RUN:-0}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +package_root="$(cd -- "${script_dir}/../.." && pwd)" +binary_src="${DEVRAIL_ROUTER_BINARY:-${package_root}/devrail-router}" +config_src="${DEVRAIL_ROUTER_CONFIG:-${package_root}/configs/router.example.yaml}" +unit_src="${DEVRAIL_ROUTER_SYSTEMD_UNIT:-${package_root}/packaging/systemd/devrail-router.service}" + +if [[ $EUID -eq 0 ]]; then + sudo_cmd=() +else + sudo_cmd=(sudo) +fi + +run() { + printf '+ %q' "$@" + printf '\n' + if [[ "${DRY_RUN}" == "1" ]]; then + return 0 + fi + "${sudo_cmd[@]}" "$@" +} + +need_file() { + local path="$1" + if [[ ! -f "${path}" ]]; then + echo "missing required file: ${path}" >&2 + exit 1 + fi +} + +need_file "${binary_src}" +need_file "${config_src}" +need_file "${unit_src}" + +if ! getent group "${SERVICE_GROUP}" >/dev/null 2>&1; then + run groupadd --system "${SERVICE_GROUP}" +fi + +if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then + run useradd --system --gid "${SERVICE_GROUP}" --home-dir "${STATE_DIR}" --shell /usr/sbin/nologin "${SERVICE_USER}" +fi + +run install -d -m 0755 "${BIN_DIR}" "${CONFIG_DIR}" "${SYSTEMD_DIR}" +run install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_GROUP}" "${STATE_DIR}" +run install -m 0755 "${binary_src}" "${BIN_DIR}/devrail-router" + +if [[ ! -f "${CONFIG_DIR}/router.yaml" || "${FORCE_CONFIG}" == "1" ]]; then + run install -m 0640 -o root -g "${SERVICE_GROUP}" "${config_src}" "${CONFIG_DIR}/router.yaml" +else + echo "keeping existing ${CONFIG_DIR}/router.yaml" +fi + +run install -m 0644 "${unit_src}" "${SYSTEMD_DIR}/${SERVICE_NAME}.service" + +if command -v systemctl >/dev/null 2>&1; then + run systemctl daemon-reload + if [[ "${ENABLE_SERVICE}" == "1" ]]; then + run systemctl enable "${SERVICE_NAME}.service" + fi + if [[ "${START_SERVICE}" == "1" ]]; then + run systemctl restart "${SERVICE_NAME}.service" + fi +else + echo "systemctl not found; installed files but did not enable service" +fi + +echo "DevRail Router installed." +echo "Config: ${CONFIG_DIR}/router.yaml" +echo "Binary: ${BIN_DIR}/devrail-router" diff --git a/packaging/systemd/devrail-router.service b/packaging/systemd/devrail-router.service index 30d0bbe..ee82963 100644 --- a/packaging/systemd/devrail-router.service +++ b/packaging/systemd/devrail-router.service @@ -11,6 +11,7 @@ Group=devrail-router ExecStart=/usr/local/bin/devrail-router serve -config /etc/devrail/router.yaml Restart=on-failure RestartSec=5s +StateDirectory=devrail-router NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict