Cross-platform binaries, MSI, service mgmt, pure-Go SQLite, centralized logging #3
@@ -0,0 +1,39 @@
|
||||
# Keep the Docker build context small and reproducible. The image builds the
|
||||
# frontend and Go binary from source inside the stages, so none of the local
|
||||
# build output, dependencies, or VCS metadata should be shipped into the build.
|
||||
|
||||
# VCS / CI
|
||||
.git
|
||||
.gitea
|
||||
.github
|
||||
|
||||
# Frontend: rebuilt by the image (npm ci + next build)
|
||||
frontend/node_modules
|
||||
frontend/.next
|
||||
frontend/out
|
||||
|
||||
# Staged UI embed output: the image regenerates and stages this itself
|
||||
backend/internal/webui/dist/*
|
||||
!backend/internal/webui/dist/.gitignore
|
||||
|
||||
# Go / build artifacts
|
||||
binaries
|
||||
**/*.exe
|
||||
**/*.test
|
||||
|
||||
# Local runtime data
|
||||
data
|
||||
**/*.db
|
||||
**/*.db-shm
|
||||
**/*.db-wal
|
||||
**/*.log
|
||||
|
||||
# Local scratch space and docs (not needed to build the app)
|
||||
Scratch
|
||||
docs
|
||||
|
||||
# Editor / OS noise
|
||||
.idea
|
||||
.vscode
|
||||
**/.DS_Store
|
||||
**/Thumbs.db
|
||||
+166
-72
@@ -2,7 +2,8 @@ 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.
|
||||
# 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.
|
||||
@@ -20,9 +21,9 @@ on:
|
||||
|
||||
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.
|
||||
# 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
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
# 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.
|
||||
# the release tag, the binary version, and the MSI version aligned.
|
||||
- name: Compute version
|
||||
id: ver
|
||||
shell: bash
|
||||
@@ -66,38 +67,157 @@ jobs:
|
||||
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 ($GIT_COMMIT_SHORT)"
|
||||
echo "OrchestrAD version: $VERSION (msi $MSI_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 ./..."
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- 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'
|
||||
|
||||
# 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
|
||||
command -v jq >/dev/null 2>&1 || { sudo apt-get update -y && sudo apt-get install -y jq; }
|
||||
jq --version
|
||||
|
||||
# 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@latest
|
||||
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
|
||||
@@ -151,24 +271,7 @@ jobs:
|
||||
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.
|
||||
# Create the Gitea release and attach every artifact in dist/.
|
||||
- name: Create Gitea release
|
||||
shell: bash
|
||||
env:
|
||||
@@ -182,44 +285,35 @@ jobs:
|
||||
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}' \
|
||||
# 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
|
||||
echo "Release ${VERSION} already exists; skipping."
|
||||
exit 0
|
||||
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
|
||||
|
||||
# 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
|
||||
for asset in dist/*; do
|
||||
name="$(basename "$asset")"
|
||||
echo "Uploading $name"
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
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."
|
||||
echo "Release ${VERSION} published with $(ls dist | wc -l) assets."
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Windows resource objects generated from versioninfo.json by goversioninfo
|
||||
# (icon + version metadata). Regenerated during the build, not tracked.
|
||||
*.syso
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
|
||||
+3
-2
@@ -25,7 +25,8 @@ RUN npm run build
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
# Pure-Go SQLite (modernc.org/sqlite) means no C toolchain is needed.
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -44,7 +45,7 @@ ARG VERSION=dev
|
||||
ARG BUILD_TIME=unknown
|
||||
ARG GIT_COMMIT=unknown
|
||||
|
||||
RUN CGO_ENABLED=1 go build \
|
||||
RUN CGO_ENABLED=0 go build \
|
||||
-ldflags "-s -w \
|
||||
-X github.com/Grace-Solutions/OrchestrAD/internal/version.Version=${VERSION} \
|
||||
-X github.com/Grace-Solutions/OrchestrAD/internal/version.BuildTime=${BUILD_TIME} \
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/cli"
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/version"
|
||||
)
|
||||
|
||||
@@ -18,70 +19,59 @@ func main() {
|
||||
|
||||
cmd := os.Args[1]
|
||||
|
||||
// version and help are plain CLI output, not log events, so they bypass the
|
||||
// centralized logger. Everything else dispatches to a handler and routes any
|
||||
// error through the centralized logger for one consistent format.
|
||||
switch cmd {
|
||||
case "version", "-v", "--version":
|
||||
fmt.Printf("OrchestrAD %s\n", version.Version)
|
||||
case "init":
|
||||
if err := cli.RunInit(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "run":
|
||||
if err := cli.RunForeground(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "install":
|
||||
if err := cli.RunInstall(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "uninstall":
|
||||
if err := cli.RunUninstall(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "start":
|
||||
if err := cli.RunStart(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "stop":
|
||||
if err := cli.RunStop(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "migrate":
|
||||
if err := cli.RunMigrate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "backup":
|
||||
if err := cli.RunBackup(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "restore":
|
||||
if len(os.Args) < 4 || os.Args[2] != "--file" {
|
||||
fmt.Fprintf(os.Stderr, "Usage: orchestrad restore --file <path>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := cli.RunRestore(os.Args[3]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "doctor":
|
||||
if err := cli.RunDoctor(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
case "help", "-h", "--help":
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
|
||||
var run func() error
|
||||
switch cmd {
|
||||
case "init":
|
||||
run = cli.RunInit
|
||||
case "run":
|
||||
run = cli.RunForeground
|
||||
case "initialize":
|
||||
run = cli.RunInitialize
|
||||
case "remove":
|
||||
run = cli.RunRemove
|
||||
case "install":
|
||||
run = cli.RunInstall
|
||||
case "uninstall":
|
||||
run = cli.RunUninstall
|
||||
case "start":
|
||||
run = cli.RunStart
|
||||
case "stop":
|
||||
run = cli.RunStop
|
||||
case "migrate":
|
||||
run = cli.RunMigrate
|
||||
case "backup":
|
||||
run = cli.RunBackup
|
||||
case "restore":
|
||||
if len(os.Args) < 4 || os.Args[2] != "--file" {
|
||||
logging.Error("CLI", "Usage: orchestrad restore --file <path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
path := os.Args[3]
|
||||
run = func() error { return cli.RunRestore(path) }
|
||||
case "doctor":
|
||||
run = cli.RunDoctor
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
||||
logging.Error("CLI", "Unknown command: %s", cmd)
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := run(); err != nil {
|
||||
logging.Error("CLI", "%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
@@ -90,10 +80,12 @@ func printUsage() {
|
||||
Usage: orchestrad <command> [options]
|
||||
|
||||
Commands:
|
||||
init Initialize database, apply migrations, install and start service
|
||||
run Run in foreground mode
|
||||
install Install as system service
|
||||
uninstall Stop and uninstall service
|
||||
init Initialize database and apply migrations
|
||||
run Run in foreground mode (also the service entry point)
|
||||
initialize Install and start the service (idempotent)
|
||||
remove Stop and remove the service (idempotent)
|
||||
install Alias for initialize
|
||||
uninstall Alias for remove
|
||||
start Start installed service
|
||||
stop Stop installed service
|
||||
migrate Apply database migrations
|
||||
|
||||
+11
-5
@@ -1,8 +1,6 @@
|
||||
module github.com/Grace-Solutions/OrchestrAD
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
@@ -10,17 +8,25 @@ require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/golang-migrate/migrate/v4 v4.17.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/kardianos/service v1.3.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
golang.org/x/crypto v0.48.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
modernc.org/sqlite v1.58.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
modernc.org/libc v1.75.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
+50
-4
@@ -5,6 +5,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1L
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
@@ -15,6 +17,8 @@ github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baD
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -24,6 +28,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
@@ -36,12 +42,18 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
|
||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -52,11 +64,45 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
|
||||
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.75.6 h1:yKk8qo+Di4gkmvRboK8ocCqH22FiUCR6jRy2OwtCRus=
|
||||
modernc.org/libc v1.75.6/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.58.0 h1:38u40/bwkfM7f0Myhosl+SEMltSDxnGdQf8o6Kjmys0=
|
||||
modernc.org/sqlite v1.58.0/go.mod h1:rsD2CckafgObKC4DhBlGBf+RiHxkc3hINGt1Xw32tVY=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
+52
-33
@@ -5,10 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/audit"
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/auth"
|
||||
@@ -31,7 +28,7 @@ func RunInit() error {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
|
||||
logger := logging.New(cfg.Logging)
|
||||
logger := logging.Init(cfg.Logging)
|
||||
logger.Info("CLI", "Initializing OrchestrAD...")
|
||||
|
||||
// Initialize database
|
||||
@@ -54,15 +51,28 @@ func RunInit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunForeground runs the application in foreground mode
|
||||
// RunForeground runs the application. It always goes through the service
|
||||
// framework (service.Run): interactively this starts the server and blocks
|
||||
// until an interrupt; under a service manager (Windows SCM, systemd, launchd)
|
||||
// it speaks the manager's control protocol. Docker and manual `run` invocations
|
||||
// take the interactive path; the installed service launches `run` too, so both
|
||||
// share exactly one startup path.
|
||||
func RunForeground() error {
|
||||
return runService()
|
||||
}
|
||||
|
||||
// runServer contains the actual server bring-up: config, database, migrations,
|
||||
// bootstrap, services, scheduler, and the HTTP server. It blocks until ctx is
|
||||
// cancelled (by an interrupt when interactive, or by the service manager's stop
|
||||
// request), then returns after a graceful shutdown.
|
||||
func runServer(ctx context.Context) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
|
||||
logger := logging.New(cfg.Logging)
|
||||
logger.Info("CLI", "Starting OrchestrAD in foreground mode...")
|
||||
logger := logging.Init(cfg.Logging)
|
||||
logger.Info("CLI", "Starting OrchestrAD...")
|
||||
|
||||
// Initialize database
|
||||
database, err := db.New(cfg.Database, logger)
|
||||
@@ -116,50 +126,59 @@ func RunForeground() error {
|
||||
"mode": "foreground",
|
||||
})
|
||||
|
||||
// Setup graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Start the scheduler so enabled rules fire automatically
|
||||
// Start the scheduler so enabled rules fire automatically. It is bound to
|
||||
// the run context so it stops when the server is asked to shut down.
|
||||
sched := scheduler.New(database.Conn(), ruleRunner, logger)
|
||||
if err := sched.Start(ctx); err != nil {
|
||||
return fmt.Errorf("starting scheduler: %w", err)
|
||||
}
|
||||
defer sched.Stop()
|
||||
|
||||
// Create HTTP server
|
||||
// Create and run the HTTP server. srv.Run blocks until ctx is cancelled,
|
||||
// which happens on an interactive interrupt or a service stop request.
|
||||
srv := server.New(cfg, database, deps, logger)
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
logger.Info("CLI", "Shutdown signal received, stopping server...")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
return srv.Run(ctx)
|
||||
}
|
||||
|
||||
// RunInstall installs the application as a system service
|
||||
// RunInitialize idempotently installs and starts the system service (Windows
|
||||
// service, systemd/upstart/sysv unit, or launchd daemon). Safe to re-run.
|
||||
func RunInitialize() error {
|
||||
return initializeService()
|
||||
}
|
||||
|
||||
// RunRemove idempotently stops and removes the system service. Safe to re-run.
|
||||
func RunRemove() error {
|
||||
return removeService()
|
||||
}
|
||||
|
||||
// RunInstall is an alias for RunInitialize: it idempotently installs and starts
|
||||
// the service.
|
||||
func RunInstall() error {
|
||||
return fmt.Errorf("service install not yet implemented for this platform")
|
||||
return initializeService()
|
||||
}
|
||||
|
||||
// RunUninstall removes the system service
|
||||
// RunUninstall is an alias for RunRemove: it idempotently stops and removes the
|
||||
// service.
|
||||
func RunUninstall() error {
|
||||
return fmt.Errorf("service uninstall not yet implemented for this platform")
|
||||
return removeService()
|
||||
}
|
||||
|
||||
// RunStart starts the installed service
|
||||
// RunStart starts the installed service.
|
||||
func RunStart() error {
|
||||
return fmt.Errorf("service start not yet implemented for this platform")
|
||||
if err := controlService("start"); err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Service", "Service started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunStop stops the installed service
|
||||
// RunStop stops the installed service.
|
||||
func RunStop() error {
|
||||
return fmt.Errorf("service stop not yet implemented for this platform")
|
||||
if err := controlService("stop"); err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Service", "Service stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunMigrate applies database migrations
|
||||
@@ -169,7 +188,7 @@ func RunMigrate() error {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
|
||||
logger := logging.New(cfg.Logging)
|
||||
logger := logging.Init(cfg.Logging)
|
||||
logger.Info("CLI", "Running database migrations...")
|
||||
|
||||
database, err := db.New(cfg.Database, logger)
|
||||
@@ -203,7 +222,7 @@ func RunDoctor() error {
|
||||
return fmt.Errorf("config validation failed: %w", err)
|
||||
}
|
||||
|
||||
logger := logging.New(cfg.Logging)
|
||||
logger := logging.Init(cfg.Logging)
|
||||
logger.Info("Doctor", "Running system health checks...")
|
||||
|
||||
// Check database
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Service lifecycle management (install / uninstall / start / stop) plus the
|
||||
// service-hosted run loop. Uses kardianos/service so the same binary runs as a
|
||||
// Windows service (SCM), a systemd/upstart/sysv daemon on Linux, or a launchd
|
||||
// daemon on macOS, and can also run interactively (foreground / container).
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceName = "OrchestrAD"
|
||||
serviceDisplayName = "OrchestrAD"
|
||||
serviceDescription = "OrchestrAD - Active Directory Rule Automation Platform"
|
||||
)
|
||||
|
||||
// program adapts the server run loop to the service.Interface contract. Start
|
||||
// must not block, so the server runs on a goroutine whose lifetime is bound to
|
||||
// a context cancelled by Stop.
|
||||
type program struct {
|
||||
cancel context.CancelFunc
|
||||
done chan error
|
||||
}
|
||||
|
||||
// Start is called by the service manager (or by Run when interactive). It
|
||||
// launches the server without blocking.
|
||||
func (p *program) Start(s service.Service) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancel = cancel
|
||||
p.done = make(chan error, 1)
|
||||
go func() {
|
||||
p.done <- runServer(ctx)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop is called on service stop / interactive interrupt. It cancels the run
|
||||
// context and waits for the server to unwind so the process exits cleanly.
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
if p.done != nil {
|
||||
return <-p.done
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// serviceConfig builds the platform service definition. WorkingDirectory is
|
||||
// pinned to the executable's directory so a relative ORCHESTRAD_DATA_PATH
|
||||
// (default ./data) resolves next to the installed binary rather than wherever
|
||||
// the service manager happens to launch it from. Arguments ["run"] make the
|
||||
// service manager start the process in run mode, which routes back through
|
||||
// service.Run.
|
||||
func serviceConfig() (*service.Config, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving executable path: %w", err)
|
||||
}
|
||||
return &service.Config{
|
||||
Name: serviceName,
|
||||
DisplayName: serviceDisplayName,
|
||||
Description: serviceDescription,
|
||||
Arguments: []string{"run"},
|
||||
WorkingDirectory: filepath.Dir(exe),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newService constructs the service handle and its program.
|
||||
func newService() (service.Service, error) {
|
||||
cfg, err := serviceConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service.New(&program{}, cfg)
|
||||
}
|
||||
|
||||
// runService runs the server through the service framework. When launched by a
|
||||
// service manager it speaks the manager's control protocol (required on
|
||||
// Windows); when launched interactively it runs Start, blocks until an
|
||||
// interrupt, then runs Stop. This is the single entry point used by the `run`
|
||||
// command so foreground, container, and service execution share one path.
|
||||
func runService() error {
|
||||
s, err := newService()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
return s.Run()
|
||||
}
|
||||
|
||||
// controlService applies a single service control action (start or stop) using
|
||||
// the platform service manager.
|
||||
func controlService(action string) error {
|
||||
s, err := newService()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
if err := service.Control(s, action); err != nil {
|
||||
return fmt.Errorf("%s service: %w", action, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeService idempotently ensures the service is installed and running.
|
||||
// Re-running it is safe: an already-installed service is not reinstalled, and an
|
||||
// already-running one is left alone. This backs the `initialize` (and `install`)
|
||||
// command so provisioning is repeatable.
|
||||
func initializeService() error {
|
||||
s, err := newService()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
status, err := s.Status()
|
||||
if errors.Is(err, service.ErrNotInstalled) {
|
||||
if err := s.Install(); err != nil {
|
||||
return fmt.Errorf("installing service: %w", err)
|
||||
}
|
||||
logging.Info("Service", "Service installed")
|
||||
status = service.StatusStopped
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("querying service status: %w", err)
|
||||
} else {
|
||||
logging.Info("Service", "Service already installed")
|
||||
}
|
||||
|
||||
if status == service.StatusRunning {
|
||||
logging.Info("Service", "Service already running")
|
||||
return nil
|
||||
}
|
||||
if err := s.Start(); err != nil {
|
||||
return fmt.Errorf("starting service: %w", err)
|
||||
}
|
||||
logging.Info("Service", "Service started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeService idempotently ensures the service is stopped and removed.
|
||||
// Re-running it is safe: a not-installed service is treated as already removed,
|
||||
// and a stop failure on an already-stopped service does not block removal. This
|
||||
// backs the `remove` (and `uninstall`) command.
|
||||
func removeService() error {
|
||||
s, err := newService()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating service: %w", err)
|
||||
}
|
||||
|
||||
status, err := s.Status()
|
||||
if errors.Is(err, service.ErrNotInstalled) {
|
||||
logging.Info("Service", "Service not installed; nothing to remove")
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("querying service status: %w", err)
|
||||
}
|
||||
|
||||
if status == service.StatusRunning {
|
||||
if err := s.Stop(); err != nil {
|
||||
logging.Warn("Service", "Could not stop service before removal: %v", err)
|
||||
} else {
|
||||
logging.Info("Service", "Service stopped")
|
||||
}
|
||||
}
|
||||
if err := s.Uninstall(); err != nil {
|
||||
return fmt.Errorf("removing service: %w", err)
|
||||
}
|
||||
logging.Info("Service", "Service removed")
|
||||
return nil
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/config"
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
@@ -28,10 +28,12 @@ type DB struct {
|
||||
func New(cfg config.DatabaseConfig, logger *logging.Logger) (*DB, error) {
|
||||
logger.Info("Database", "Opening database at %s", cfg.Path)
|
||||
|
||||
// Build connection string with pragmas
|
||||
dsn := fmt.Sprintf("file:%s?_journal_mode=WAL&_foreign_keys=on&_busy_timeout=5000", cfg.Path)
|
||||
// Build connection string with pragmas. modernc.org/sqlite applies these
|
||||
// _pragma directives on every connection the pool opens, so WAL mode,
|
||||
// foreign-key enforcement, and the busy timeout hold for all connections.
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=foreign_keys(on)&_pragma=busy_timeout(5000)", cfg.Path)
|
||||
|
||||
conn, err := sql.Open("sqlite3", dsn)
|
||||
conn, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening database: %w", err)
|
||||
}
|
||||
@@ -77,13 +79,13 @@ func (db *DB) Migrate() error {
|
||||
}
|
||||
|
||||
// Create migration driver
|
||||
driver, err := sqlite3.WithInstance(db.conn, &sqlite3.Config{})
|
||||
driver, err := sqlite.WithInstance(db.conn, &sqlite.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating migration driver: %w", err)
|
||||
}
|
||||
|
||||
// Create migrator
|
||||
m, err := migrate.NewWithInstance("iofs", source, "sqlite3", driver)
|
||||
m, err := migrate.NewWithInstance("iofs", source, "sqlite", driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating migrator: %w", err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Grace-Solutions/OrchestrAD/internal/config"
|
||||
@@ -24,6 +25,53 @@ type Logger struct {
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// std is the process-wide centralized logger. Every entry point initializes it
|
||||
// once via Init and shares the returned instance through dependency injection;
|
||||
// code that cannot easily receive an injected logger (main, the service
|
||||
// wrapper) uses the package-level Default/Info/Warn/Error/Debug helpers, which
|
||||
// resolve to this same instance. This keeps all output on one writer, one
|
||||
// rotation policy, and the single [UTC] - [Component] - [Level] - Message format.
|
||||
var (
|
||||
stdMu sync.RWMutex
|
||||
std *Logger
|
||||
)
|
||||
|
||||
// Init creates the centralized logger from cfg, stores it as the process-wide
|
||||
// default, and returns it so callers can also inject it explicitly. The last
|
||||
// Init wins; typically it is called exactly once per process at startup.
|
||||
func Init(cfg config.LoggingConfig) *Logger {
|
||||
l := New(cfg)
|
||||
stdMu.Lock()
|
||||
std = l
|
||||
stdMu.Unlock()
|
||||
return l
|
||||
}
|
||||
|
||||
// Default returns the centralized logger. If Init has not run yet (e.g. an
|
||||
// early top-level error before config is loaded), it lazily creates a
|
||||
// console-only logger so logging never panics and the format stays consistent.
|
||||
func Default() *Logger {
|
||||
stdMu.RLock()
|
||||
l := std
|
||||
stdMu.RUnlock()
|
||||
if l != nil {
|
||||
return l
|
||||
}
|
||||
stdMu.Lock()
|
||||
defer stdMu.Unlock()
|
||||
if std == nil {
|
||||
std = New(config.LoggingConfig{Level: "info", EnableConsole: true})
|
||||
}
|
||||
return std
|
||||
}
|
||||
|
||||
// Package-level helpers delegate to the centralized logger so callers without
|
||||
// an injected Logger still emit the standard format.
|
||||
func Debug(component, msg string, args ...any) { Default().Debug(component, msg, args...) }
|
||||
func Info(component, msg string, args ...any) { Default().Info(component, msg, args...) }
|
||||
func Warn(component, msg string, args ...any) { Default().Warn(component, msg, args...) }
|
||||
func Error(component, msg string, args ...any) { Default().Error(component, msg, args...) }
|
||||
|
||||
// New creates a new Logger with the given configuration
|
||||
func New(cfg config.LoggingConfig) *Logger {
|
||||
level := parseLevel(cfg.Level)
|
||||
|
||||
@@ -109,9 +109,14 @@ func (s *Server) setupMiddleware() {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
// Health check (unauthenticated)
|
||||
// Health check (unauthenticated). Register HEAD as well as GET: container
|
||||
// health probes and reverse proxies commonly issue HEAD (e.g. the Docker
|
||||
// HEALTHCHECK's `wget --spider`), and chi returns 405 for an unregistered
|
||||
// method rather than falling back to the GET handler.
|
||||
s.router.Get("/health", s.handleHealth)
|
||||
s.router.Head("/health", s.handleHealth)
|
||||
s.router.Get("/api/health", s.handleHealth)
|
||||
s.router.Head("/api/health", s.handleHealth)
|
||||
|
||||
// API v1 routes
|
||||
s.router.Route("/api/v1", func(r chi.Router) {
|
||||
@@ -120,6 +125,7 @@ func (s *Server) setupRoutes() {
|
||||
// interrogate the server before any user has signed in.
|
||||
r.Get("/version", s.handleVersion)
|
||||
r.Get("/health", s.handleHealth)
|
||||
r.Head("/health", s.handleHealth)
|
||||
|
||||
// Auth endpoints. Login / logout / csrf are public by design; /me
|
||||
// requires a valid session so clients can resolve the current user.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": { "Major": 0, "Minor": 0, "Patch": 0, "Build": 0 },
|
||||
"ProductVersion": { "Major": 0, "Minor": 0, "Patch": 0, "Build": 0 },
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags": "00",
|
||||
"FileOS": "040004",
|
||||
"FileType": "01",
|
||||
"FileSubType": "00"
|
||||
},
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "Grace Solutions",
|
||||
"FileDescription": "OrchestrAD - Active Directory Rule Automation Platform",
|
||||
"InternalName": "orchestrad",
|
||||
"LegalCopyright": "Copyright (c) Grace Solutions",
|
||||
"OriginalFilename": "orchestrad.exe",
|
||||
"ProductName": "OrchestrAD",
|
||||
"ProductVersion": "dev"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
"Translation": { "LangID": "0409", "CharsetID": "04B0" }
|
||||
},
|
||||
"IconPath": "../resources/icons/orchestrad.ico",
|
||||
"ManifestPath": ""
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
OrchestrAD Windows installer (WiX v4+ / built with WiX 7).
|
||||
|
||||
Installs orchestrad.exe to "Program Files\OrchestrAD" and registers +
|
||||
starts the Windows service by invoking the binary's own idempotent
|
||||
`initialize` command. Uninstall runs `remove` (idempotent stop + delete)
|
||||
before the files are deleted, so no orphaned service is left behind.
|
||||
|
||||
Build:
|
||||
wix build installer/OrchestrAD.wxs \
|
||||
-d Version=<x.y.z> -d BinDir=<dir containing orchestrad.exe> \
|
||||
-d IconPath=<path to orchestrad.ico> -o OrchestrAD.msi
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package
|
||||
Name="OrchestrAD"
|
||||
Manufacturer="Grace Solutions"
|
||||
Version="$(Version)"
|
||||
UpgradeCode="41EC0251-FC95-4C00-8C91-EC3F9BE1F278"
|
||||
Scope="perMachine"
|
||||
Compressed="yes">
|
||||
|
||||
<SummaryInformation Description="OrchestrAD - Active Directory Rule Automation Platform" />
|
||||
|
||||
<!-- Block downgrades; replace older/equal on upgrade. -->
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of OrchestrAD is already installed." />
|
||||
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<!-- Add/Remove Programs metadata. -->
|
||||
<Icon Id="OrchestradIcon" SourceFile="$(IconPath)" />
|
||||
<Property Id="ARPPRODUCTICON" Value="OrchestradIcon" />
|
||||
<Property Id="ARPNOMODIFY" Value="1" />
|
||||
|
||||
<!-- Install tree: Program Files\OrchestrAD -->
|
||||
<StandardDirectory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="OrchestrAD" />
|
||||
</StandardDirectory>
|
||||
|
||||
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
|
||||
<Component Id="OrchestradExe" Bitness="always64">
|
||||
<File Id="OrchestradExe" Name="orchestrad.exe" Source="$(BinDir)/orchestrad.exe" KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
<Feature Id="Main" Title="OrchestrAD" Level="1">
|
||||
<ComponentGroupRef Id="ProductComponents" />
|
||||
</Feature>
|
||||
|
||||
<!--
|
||||
Service lifecycle via the binary's own idempotent CLI. Deferred, no-impersonate
|
||||
custom actions run as LocalSystem, which can create/delete services.
|
||||
- Install/upgrade: after the files land, run `initialize` (install + start).
|
||||
- Uninstall: before the files are removed, run `remove` (stop + delete).
|
||||
-->
|
||||
<CustomAction Id="InitializeService"
|
||||
FileRef="OrchestradExe"
|
||||
ExeCommand="initialize"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Return="check" />
|
||||
|
||||
<CustomAction Id="RemoveService"
|
||||
FileRef="OrchestradExe"
|
||||
ExeCommand="remove"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Return="ignore" />
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<!-- Remove the service before its binary is deleted, only when uninstalling. -->
|
||||
<Custom Action="RemoveService" Before="RemoveFiles" Condition="REMOVE="ALL"" />
|
||||
<!-- Install and start the service after files are present, when not uninstalling. -->
|
||||
<Custom Action="InitializeService" After="InstallFiles" Condition="NOT REMOVE="ALL"" />
|
||||
</InstallExecuteSequence>
|
||||
</Package>
|
||||
</Wix>
|
||||
@@ -1,2 +0,0 @@
|
||||
# Placeholder for application icons
|
||||
# Windows icon (orchestrad.ico) should be placed here for embedding in Windows builds
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 364 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,38 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#5b5ef0"/>
|
||||
<stop offset="1" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="0.32" cy="0.26" r="0.9">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.28"/>
|
||||
<stop offset="0.55" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#1e1b4b" flood-opacity="0.35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Badge -->
|
||||
<rect x="8" y="8" width="240" height="240" rx="52" fill="url(#bg)"/>
|
||||
<rect x="8" y="8" width="240" height="240" rx="52" fill="url(#glow)"/>
|
||||
|
||||
<!-- Orchestration graph: a central hub conducting four directory nodes -->
|
||||
<g stroke="#ffffff" stroke-width="12" stroke-linecap="round" opacity="0.95">
|
||||
<line x1="128" y1="128" x2="128" y2="66"/>
|
||||
<line x1="128" y1="128" x2="190" y2="128"/>
|
||||
<line x1="128" y1="128" x2="128" y2="190"/>
|
||||
<line x1="128" y1="128" x2="66" y2="128"/>
|
||||
</g>
|
||||
|
||||
<g filter="url(#soft)">
|
||||
<!-- Satellite nodes -->
|
||||
<circle cx="128" cy="60" r="20" fill="#ffffff"/>
|
||||
<circle cx="196" cy="128" r="20" fill="#ffffff"/>
|
||||
<circle cx="128" cy="196" r="20" fill="#ffffff"/>
|
||||
<circle cx="60" cy="128" r="20" fill="#ffffff"/>
|
||||
<!-- Central hub -->
|
||||
<circle cx="128" cy="128" r="30" fill="#ffffff"/>
|
||||
<circle cx="128" cy="128" r="14" fill="#6d40e8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
+32
-55
@@ -105,6 +105,32 @@ if (-not $All) {
|
||||
# LDFlags for version injection
|
||||
$LDFlags = "-s -w -X github.com/Grace-Solutions/OrchestrAD/internal/version.Version=$Version -X github.com/Grace-Solutions/OrchestrAD/internal/version.BuildTime=$BuildTime -X github.com/Grace-Solutions/OrchestrAD/internal/version.GitCommit=$GitCommit"
|
||||
|
||||
# Generate the Windows icon/version resource so the windows/* binaries embed the
|
||||
# OrchestrAD icon. goversioninfo is a pure-Go tool and runs on any host; the
|
||||
# arch-suffixed .syso files are only linked into their matching GOARCH build.
|
||||
$BuildWindows = $All -or ($Platforms | Where-Object { $_.OS -eq "windows" })
|
||||
if ($BuildWindows) {
|
||||
Write-Host "`nGenerating Windows resource (icon + version metadata)..." -ForegroundColor Cyan
|
||||
$GoVersionInfo = Join-Path (Join-Path $env:USERPROFILE "go\bin") "goversioninfo.exe"
|
||||
if (-not (Test-Path $GoVersionInfo)) {
|
||||
Write-Host " Installing goversioninfo..." -ForegroundColor Yellow
|
||||
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
|
||||
}
|
||||
$vp = $Version -split '\.'
|
||||
Push-Location $BackendDir
|
||||
try {
|
||||
foreach ($a in @(@{arch='amd64';flags=@('-64')}, @{arch='arm64';flags=@('-64','-arm')})) {
|
||||
$out = "cmd\orchestrad\resource_windows_$($a.arch).syso"
|
||||
& $GoVersionInfo @($a.flags) `
|
||||
"-ver-major=$([int]$vp[0])" "-ver-minor=$([int]$vp[1])" "-ver-patch=$([int]$vp[2])" "-ver-build=$([int]$vp[3])" `
|
||||
"-product-version=$([int]$vp[0]).$([int]$vp[1]).$([int]$vp[2])" `
|
||||
"-o" $out "versioninfo.json"
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
Push-Location $BackendDir
|
||||
|
||||
try {
|
||||
@@ -126,65 +152,16 @@ try {
|
||||
|
||||
Write-Host "`nBuilding for $OS/$Arch..." -ForegroundColor Green
|
||||
|
||||
# Pure-Go build (modernc.org/sqlite): CGO is disabled, so every target
|
||||
# cross-compiles from any host with no C toolchain. Windows binaries pick
|
||||
# up the icon/version resource generated into cmd/orchestrad before the
|
||||
# loop (resource_windows_<arch>.syso).
|
||||
$env:GOOS = $OS
|
||||
$env:GOARCH = $Arch
|
||||
$env:CGO_ENABLED = "1"
|
||||
$PlatformLDFlags = $LDFlags
|
||||
$SavedPath = $env:PATH
|
||||
$env:CGO_ENABLED = "0"
|
||||
|
||||
# For cross-compilation, CGO needs special handling
|
||||
if ($OS -ne "windows" -and $env:OS -eq "Windows_NT") {
|
||||
$env:CGO_ENABLED = "0"
|
||||
Write-Host " Note: CGO disabled for cross-compilation (SQLite will use pure Go driver)" -ForegroundColor Yellow
|
||||
} elseif ($OS -eq "windows") {
|
||||
# Produce a single-file binary with zero runtime DLL dependencies by
|
||||
# statically linking the mingw-w64 C runtime. Auto-discover a gcc
|
||||
# that targets $Arch from common install locations.
|
||||
$GccName = if ($Arch -eq "arm64") { "aarch64-w64-mingw32-gcc.exe" } else { "gcc.exe" }
|
||||
$env:CC = ""
|
||||
if ($Arch -eq "arm64") {
|
||||
$CcCandidates = @(
|
||||
"C:\ProgramData\mingw64\aarch64-w64-mingw32\bin",
|
||||
"C:\msys64\clangarm64\bin"
|
||||
)
|
||||
foreach ($Dir in $CcCandidates) {
|
||||
if (Test-Path (Join-Path $Dir $GccName)) { $env:CC = (Join-Path $Dir $GccName); break }
|
||||
}
|
||||
if (-not $env:CC) {
|
||||
Write-Host " Skipping windows/arm64: no aarch64-w64-mingw32-gcc on PATH (install an aarch64 mingw-w64 toolchain to enable)." -ForegroundColor Yellow
|
||||
$env:PATH = $SavedPath
|
||||
$env:CC = ""
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if (-not (Get-Command gcc -ErrorAction SilentlyContinue)) {
|
||||
$GccCandidates = @(
|
||||
"C:\ProgramData\mingw64\mingw64\bin",
|
||||
"C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin",
|
||||
"C:\msys64\ucrt64\bin",
|
||||
"C:\msys64\mingw64\bin",
|
||||
"C:\TDM-GCC-64\bin"
|
||||
)
|
||||
foreach ($GccDir in $GccCandidates) {
|
||||
if (Test-Path (Join-Path $GccDir "gcc.exe")) {
|
||||
$env:PATH = "$GccDir;$env:PATH"
|
||||
Write-Host " Using gcc from $GccDir" -ForegroundColor DarkGray
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not (Get-Command gcc -ErrorAction SilentlyContinue)) {
|
||||
throw "gcc is required on PATH for a Windows CGO build (mattn/go-sqlite3). Install mingw-w64 via 'choco install mingw' or point PATH at an existing gcc.exe."
|
||||
}
|
||||
}
|
||||
$PlatformLDFlags = "$LDFlags -linkmode external -extldflags `"-static`""
|
||||
}
|
||||
go build -ldflags "$LDFlags" -o "$OutputFile" ./cmd/orchestrad
|
||||
|
||||
go build -ldflags "$PlatformLDFlags" -o "$OutputFile" ./cmd/orchestrad
|
||||
|
||||
$env:PATH = $SavedPath
|
||||
$env:CC = ""
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$FileSize = (Get-Item $OutputFile).Length / 1MB
|
||||
Write-Host " Built: $OutputFile ($([math]::Round($FileSize, 2)) MB)" -ForegroundColor Green
|
||||
|
||||
Reference in New Issue
Block a user