DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak hostname fallback. Hardware-bound IDs on DMI-less devices. DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from /sys signals — system battery (scope=Device peripherals excluded, UPS excluded), DRM connector state, framebuffer min-dimension for phone/tablet split. Reports device_type/device_model/os_distro in registration and system-info paths. SERVER-001: migration 061 — device_type, device_type_manual (operator override, never agent-written), device_model, os_distro on agents. effective_device_type computed into every serialized agent. SERVER-002: PUT /admin/agents/:id/device-type — set/clear override, enum-validated, journaled. WEB-001: device-type icons + fleet filter, device model in list, detail header badge with reclassify dropdown, os_distro surfaced. INSTALL-003: arm64 install path unblocked — helper (required manifest component) now cross-built aarch64-unknown-linux-musl via rust-lld in the server image, signed at boot (helperArches += arm64), listed in the release manifest. Install template already handled uname -m and pacman. Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
658 lines
29 KiB
YAML
658 lines
29 KiB
YAML
name: release
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
# Gate runs before any build. A tag that doesn't agree with the tree is a
|
|
# broken release waiting to happen — fail here, not after artifacts exist.
|
|
gate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Version gate
|
|
run: |
|
|
set -euo pipefail
|
|
TAG=${GITHUB_REF#refs/tags/v}
|
|
echo "Tag version: $TAG"
|
|
FAIL=0
|
|
|
|
# versions.go — anchored so MinAgentVersion does not match.
|
|
SERVER_VER=$(grep -P '^\s*AgentVersion\s*=' server/internal/version/versions.go | grep -oP '"\K[^"]+')
|
|
CONFIG_VER=$(grep -P '^\s*ConfigVersion\s*=' server/internal/version/versions.go | grep -oP '"\K[^"]+')
|
|
echo "versions.go: AgentVersion=$SERVER_VER ConfigVersion=$CONFIG_VER"
|
|
if [ "$TAG" != "$SERVER_VER" ]; then
|
|
echo "::error::tag=$TAG but versions.go AgentVersion=$SERVER_VER"
|
|
FAIL=1
|
|
fi
|
|
if [ "$TAG" != "$CONFIG_VER" ]; then
|
|
echo "::error::tag=$TAG but versions.go ConfigVersion=$CONFIG_VER"
|
|
FAIL=1
|
|
fi
|
|
|
|
# docker-compose.yml BUILD_VERSION default.
|
|
COMPOSE_VER=$(grep -oP '(?<=BUILD_VERSION:-)[0-9]+(\.[0-9]+){3}' docker-compose.yml)
|
|
echo "docker-compose: BUILD_VERSION=$COMPOSE_VER"
|
|
if [ "$TAG" != "$COMPOSE_VER" ]; then
|
|
echo "::error::tag=$TAG but docker-compose BUILD_VERSION=$COMPOSE_VER"
|
|
FAIL=1
|
|
fi
|
|
|
|
# helper/Cargo.toml — 3-part semver, compare against first 3 octets.
|
|
CARGO_VER=$(grep -m1 '^version' helper/Cargo.toml | cut -d'"' -f2)
|
|
TAG_SEMVER=$(echo "$TAG" | cut -d. -f1-3)
|
|
echo "Cargo.toml: version=$CARGO_VER (tag semver=$TAG_SEMVER)"
|
|
if [ "$TAG_SEMVER" != "$CARGO_VER" ]; then
|
|
echo "::error::tag=$TAG (semver=$TAG_SEMVER) but Cargo.toml=$CARGO_VER"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Desktop: optional component, but if present, its version must match.
|
|
# desktop/Cargo.toml is the single source of the desktop version (3-part
|
|
# semver); tauri.conf.json carries no version field and inherits it from
|
|
# the crate (a 4-octet there is invalid semver and Tauri's build refuses
|
|
# it), so there is nothing to cross-check on tauri.conf.json.
|
|
if [ -f desktop/Cargo.toml ]; then
|
|
DESKTOP_CARGO_VER=$(grep -m1 '^version' desktop/Cargo.toml | cut -d'"' -f2)
|
|
echo "desktop/Cargo.toml: version=$DESKTOP_CARGO_VER"
|
|
if [ "$TAG_SEMVER" != "$DESKTOP_CARGO_VER" ]; then
|
|
echo "::error::tag=$TAG (semver=$TAG_SEMVER) but desktop/Cargo.toml=$DESKTOP_CARGO_VER"
|
|
FAIL=1
|
|
fi
|
|
fi
|
|
|
|
# CHANGELOG must mention the version being released.
|
|
if ! grep -q "$TAG" CHANGELOG.md; then
|
|
echo "::error::CHANGELOG.md has no entry for $TAG"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Forward-only: the new tag must sort above every existing tag.
|
|
HIGHEST=$(git tag --list 'v*' --sort=-v:refname | head -1)
|
|
echo "Highest tag: $HIGHEST"
|
|
if [ "$HIGHEST" != "v$TAG" ]; then
|
|
echo "::error::v$TAG does not sort above existing tags (highest=$HIGHEST) — version must move forward"
|
|
FAIL=1
|
|
fi
|
|
|
|
# The tagged commit must be on public — no releases from stray branches.
|
|
if ! git rev-parse --verify --quiet origin/public >/dev/null; then
|
|
echo "::error::origin/public not found in checkout — cannot verify tag ancestry"
|
|
FAIL=1
|
|
elif ! git merge-base --is-ancestor "$GITHUB_SHA" origin/public; then
|
|
echo "::error::tagged commit $GITHUB_SHA is not on public"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Tag must be signed with an SSH key from the allowed signers file.
|
|
# This ensures the release was created by an authorized maintainer.
|
|
SIGNERS_FILE=".gitea/allowed_signers"
|
|
if [ ! -f "$SIGNERS_FILE" ]; then
|
|
echo "::error::allowed signers file not found at $SIGNERS_FILE"
|
|
FAIL=1
|
|
else
|
|
git config gpg.format ssh
|
|
git config gpg.ssh.allowedSignersFile "$SIGNERS_FILE"
|
|
if ! git tag -v "v$TAG" >/dev/null 2>&1; then
|
|
echo "::error::tag v$TAG is not signed or signature is invalid"
|
|
FAIL=1
|
|
else
|
|
echo "Tag signature verified"
|
|
fi
|
|
fi
|
|
|
|
exit $FAIL
|
|
|
|
- name: Component catalog gate
|
|
run: |
|
|
set -euo pipefail
|
|
TAG=${GITHUB_REF#refs/tags/v}
|
|
FAIL=0
|
|
|
|
# The component catalog in server/internal/services/release_manifest.go
|
|
# is the authoritative list. Every required component must have source in
|
|
# the tree — the build jobs will verify binaries and the publish job will
|
|
# verify artifact presence, but this gate catches "we said we build X but
|
|
# X's directory doesn't exist" before any compile time is spent.
|
|
|
|
echo "Checking component catalog coverage..."
|
|
|
|
# Agent: must have agent/cmd/ and go.mod
|
|
if [ ! -d agent/cmd/agent ] || [ ! -f agent/go.mod ]; then
|
|
echo "::error::component 'agent' required but agent/cmd/ or go.mod missing"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Server: must have server/cmd/ and go.mod
|
|
if [ ! -d server/cmd/server ] || [ ! -f server/go.mod ]; then
|
|
echo "::error::component 'server' required but server/cmd/ or go.mod missing"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Helper: must have helper/Cargo.toml
|
|
if [ ! -f helper/Cargo.toml ]; then
|
|
echo "::error::component 'helper' required but helper/Cargo.toml missing"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Web: must have web/package.json
|
|
if [ ! -f web/package.json ]; then
|
|
echo "::error::component 'web' required but web/package.json missing"
|
|
FAIL=1
|
|
fi
|
|
|
|
# Desktop: optional, but if present, verify its source exists
|
|
if [ -d desktop ]; then
|
|
if [ -f desktop/Cargo.toml ]; then
|
|
DESKTOP_VER=$(grep -m1 '^version' desktop/Cargo.toml | cut -d'"' -f2 || echo "")
|
|
echo "desktop/Cargo.toml: version=$DESKTOP_VER"
|
|
if [ -n "$DESKTOP_VER" ] && [ "$DESKTOP_VER" != "0.1.0" ]; then
|
|
echo "[INFO] [gate] desktop version is $DESKTOP_VER (not 0.1.0 stub — version lockstep effective)"
|
|
fi
|
|
fi
|
|
else
|
|
echo "[INFO] [gate] desktop component optional — no desktop/ directory, skipping"
|
|
fi
|
|
|
|
echo "Component catalog gate: OK"
|
|
exit $FAIL
|
|
|
|
# Build the web UI once — it's the same embed for every platform.
|
|
web:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: web/package-lock.json
|
|
- name: Build web UI
|
|
run: cd web && npm ci && npm run build
|
|
- name: Stage for embedding
|
|
run: |
|
|
rm -rf server/internal/webui/dist
|
|
cp -r web/dist server/internal/webui/dist
|
|
test -s server/internal/webui/dist/index.html
|
|
test -d server/internal/webui/dist/assets
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: webui-dist
|
|
path: server/internal/webui/dist
|
|
retention-days: 1
|
|
|
|
# Supply-chain gate with teeth: a release cannot ship with an un-accepted
|
|
# reachable dependency vulnerability. Runs the SAME scripts/dep-scan.sh as CI,
|
|
# plus --posture-out to emit the attested posture embedded into the server
|
|
# binary and signed into the release manifest. If this fails, `release` never
|
|
# builds (it is in `needs`).
|
|
dep-scan:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version-file: agent/go.mod
|
|
cache: true
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: web/package-lock.json
|
|
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
|
- name: Install scanners
|
|
run: |
|
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
cargo install cargo-audit --locked
|
|
- name: Dependency gate + posture
|
|
run: scripts/dep-scan.sh --posture-out server/internal/services/posture-build.json
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: supply-chain-posture
|
|
path: server/internal/services/posture-build.json
|
|
retention-days: 1
|
|
|
|
# Build binaries for every target platform. The webui-dist and supply-chain
|
|
# posture artifacts are downloaded into the embed paths before the server compile.
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
needs: [gate, web, dep-scan]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- goos: linux
|
|
goarch: amd64
|
|
rust_target: x86_64-unknown-linux-gnu
|
|
suffix: linux-amd64
|
|
linker: ""
|
|
use_zigbuild: false
|
|
skip_helper: false
|
|
- goos: linux
|
|
goarch: arm64
|
|
rust_target: aarch64-unknown-linux-gnu
|
|
suffix: linux-arm64
|
|
linker: gcc-aarch64-linux-gnu
|
|
use_zigbuild: false
|
|
skip_helper: false
|
|
- goos: windows
|
|
goarch: amd64
|
|
rust_target: ""
|
|
suffix: windows-amd64
|
|
linker: gcc-mingw-w64-x86-64
|
|
use_zigbuild: false
|
|
skip_helper: true
|
|
- goos: darwin
|
|
goarch: arm64
|
|
rust_target: aarch64-apple-darwin
|
|
suffix: darwin-arm64
|
|
linker: ""
|
|
use_zigbuild: true
|
|
skip_helper: false
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version-file: agent/go.mod
|
|
cache: true
|
|
|
|
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
|
with:
|
|
targets: ${{ matrix.rust_target }}
|
|
|
|
- name: Install cross-linker
|
|
if: matrix.linker != ''
|
|
run: sudo apt-get update && sudo apt-get install -y ${{ matrix.linker }}
|
|
|
|
- name: Install cargo-zigbuild
|
|
if: matrix.use_zigbuild
|
|
run: pip3 install --break-system-packages cargo-zigbuild
|
|
|
|
- name: Download web UI
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
name: webui-dist
|
|
path: server/internal/webui/dist
|
|
|
|
# Embed the attested supply-chain posture (replaces the committed
|
|
# attested:false stub) so the running server signs an honest posture.
|
|
- name: Download supply-chain posture
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
name: supply-chain-posture
|
|
path: server/internal/services
|
|
|
|
- name: Build server
|
|
env:
|
|
GOOS: ${{ matrix.goos }}
|
|
GOARCH: ${{ matrix.goarch }}
|
|
CGO_ENABLED: "0"
|
|
run: |
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
EXT=""
|
|
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
|
|
cd server && go build -ldflags "-s -w \
|
|
-X github.com/Fimeg/RedFlag/server/internal/version/versions.AgentVersion=$VERSION \
|
|
-X github.com/Fimeg/RedFlag/server/internal/version/versions.ConfigVersion=$VERSION" \
|
|
-o ../dist/redflag-server-${{ matrix.suffix }}${EXT} ./cmd/server/
|
|
|
|
- name: Build agent
|
|
env:
|
|
GOOS: ${{ matrix.goos }}
|
|
GOARCH: ${{ matrix.goarch }}
|
|
CGO_ENABLED: "0"
|
|
run: |
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
EXT=""
|
|
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
|
|
cd agent && go build -ldflags "-s -w \
|
|
-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$VERSION \
|
|
-X github.com/Fimeg/RedFlag/agent/internal/version.ConfigVersion=$VERSION \
|
|
-X github.com/Fimeg/RedFlag/agent/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
-o ../dist/redflag-agent-${{ matrix.suffix }}${EXT} ./cmd/agent/
|
|
|
|
- name: Build helper
|
|
if: "!matrix.skip_helper"
|
|
run: |
|
|
cd helper
|
|
if [ "${{ matrix.use_zigbuild }}" = "true" ]; then
|
|
cargo zigbuild --release --target ${{ matrix.rust_target }}
|
|
else
|
|
cargo build --release --target ${{ matrix.rust_target }}
|
|
fi
|
|
EXT=""
|
|
if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; fi
|
|
cp target/${{ matrix.rust_target }}/release/redflag-helper${EXT} ../dist/redflag-helper-${{ matrix.suffix }}${EXT}
|
|
|
|
- name: Build desktop (Tauri system tray)
|
|
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
|
|
# Tauri v2 system dependencies (webkit2gtk-4.1 for Ubuntu 24.04+).
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev 2>/dev/null || true
|
|
|
|
# Build the desktop frontend (Tauri's beforeBuildCommand, but we do it
|
|
# explicitly so the web build is deterministic).
|
|
cd web && npm ci --silent && npm run build:desktop && cd ..
|
|
|
|
# Build the desktop binary.
|
|
cd desktop
|
|
cargo build --release
|
|
cp target/release/redflag-desktop ../dist/redflag-desktop-${{ matrix.suffix }}
|
|
echo "Desktop binary built: $(ls -lh ../dist/redflag-desktop-${{ matrix.suffix }})"
|
|
|
|
# Verify it self-reports the tag version.
|
|
DESKTOP_VER=$(../dist/redflag-desktop-${{ matrix.suffix }} --version 2>/dev/null || echo "no-version")
|
|
echo "Desktop version: $DESKTOP_VER"
|
|
echo "$DESKTOP_VER" | grep -q "v$VERSION" || { echo "::error::desktop binary reports $DESKTOP_VER, expected v$VERSION"; exit 1; }
|
|
|
|
- name: Build desktop (Tauri system tray - Windows cross-compile)
|
|
if: matrix.goos == 'windows' && matrix.goarch == 'amd64'
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
|
|
# Tauri v2 host build scripts need webkit2gtk even when cross-compiling.
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev clang lld llvm 2>/dev/null || true
|
|
|
|
# Install cargo-xwin for MSVC cross-compilation.
|
|
rustup target add x86_64-pc-windows-msvc
|
|
cargo install --locked cargo-xwin
|
|
|
|
# Build the desktop frontend.
|
|
cd web && npm ci --silent && npm run build:desktop && cd ..
|
|
|
|
# Cross-compile the desktop binary (bundling disabled — raw exe).
|
|
cd desktop
|
|
cargo xwin build --release --target x86_64-pc-windows-msvc
|
|
cp target/x86_64-pc-windows-msvc/release/redflag-desktop.exe \
|
|
../dist/redflag-desktop-${{ matrix.suffix }}.exe
|
|
echo "Desktop binary built: $(ls -lh ../dist/redflag-desktop-${{ matrix.suffix }}.exe)"
|
|
# Cross-compiled binary can't run here for version check, but the gate
|
|
# job validates the manifest hash before publish.
|
|
|
|
- name: Build Windows installer (RedFlagSetup.msi)
|
|
if: matrix.goos == 'windows' && matrix.goarch == 'amd64'
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
# MSI's ProductVersion only carries 3 significant fields for
|
|
# upgrade detection — same 3-vs-4-part reconciliation bump-version.sh
|
|
# already does for desktop/Cargo.toml vs tauri.conf.json.
|
|
WIX_VERSION=$(echo "$VERSION" | cut -d. -f1-3)
|
|
|
|
# NOT the official WiX Toolset .NET CLI — `wix build` genuinely
|
|
# does not work when the compiler runs on Linux (reproduced: even
|
|
# a single-char Directory/@Name fails WIX0389 "not a relative
|
|
# path" on every WiX version 4.0.5 through 6.0.1; the tool's own
|
|
# output says "only supports Windows... undefined behavior"
|
|
# beyond that point). msitools' `wixl` is a from-scratch
|
|
# Linux-native reimplementation of the same MSI-building grammar,
|
|
# built for exactly this case — verified locally 2026-07-01
|
|
# (msiinfo confirms Directory/Component/ServiceInstall/Upgrade
|
|
# tables all populated correctly against a real cross-compiled
|
|
# server binary).
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq msitools
|
|
|
|
mkdir -p installer/windows/dist
|
|
cp dist/redflag-server-windows-amd64.exe installer/windows/dist/redflag-server-windows-amd64.exe
|
|
|
|
cd installer/windows
|
|
wixl Product.wxs \
|
|
-D RedFlagVersion="$WIX_VERSION" \
|
|
-o ../../dist/RedFlagSetup-${{ matrix.suffix }}.msi
|
|
echo "Installer built: $(ls -lh ../../dist/RedFlagSetup-${{ matrix.suffix }}.msi)"
|
|
|
|
# Did it make it in: linux-amd64 binaries self-report the tag version.
|
|
# Cross-compiled binaries can't run here (wrong arch/OS), but the native
|
|
# ones must match.
|
|
- name: Verify binary versions (native only)
|
|
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
SERVER_OUT=$(./dist/redflag-server-${{ matrix.suffix }} --version)
|
|
echo "$SERVER_OUT"
|
|
echo "$SERVER_OUT" | grep -q "v$VERSION" || { echo "::error::server binary reports wrong version"; exit 1; }
|
|
AGENT_OUT=$(./dist/redflag-agent-${{ matrix.suffix }} --version)
|
|
echo "$AGENT_OUT"
|
|
echo "$AGENT_OUT" | grep -q "v$VERSION" || { echo "::error::agent binary reports wrong version"; exit 1; }
|
|
|
|
- name: Package tarball
|
|
run: |
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
cd dist
|
|
ls -la
|
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
|
# Windows: zip (no helper — it's Unix-only). Desktop binary and
|
|
# the RedFlagSetup installer are both optional (only exist for
|
|
# windows-amd64 today, not e.g. windows-arm64).
|
|
ZIP_FILES="redflag-server-${{ matrix.suffix }}.exe redflag-agent-${{ matrix.suffix }}.exe"
|
|
if [ -f "redflag-desktop-${{ matrix.suffix }}.exe" ]; then
|
|
ZIP_FILES="$ZIP_FILES redflag-desktop-${{ matrix.suffix }}.exe"
|
|
fi
|
|
if [ -f "RedFlagSetup-${{ matrix.suffix }}.msi" ]; then
|
|
ZIP_FILES="$ZIP_FILES RedFlagSetup-${{ matrix.suffix }}.msi"
|
|
fi
|
|
zip redflag-$VERSION-${{ matrix.suffix }}.zip $ZIP_FILES
|
|
sha256sum redflag-$VERSION-${{ matrix.suffix }}.zip > checksums-$VERSION-${{ matrix.suffix }}.txt
|
|
else
|
|
tar czf redflag-$VERSION-${{ matrix.suffix }}.tar.gz redflag-*-${{ matrix.suffix }}
|
|
sha256sum redflag-$VERSION-${{ matrix.suffix }}.tar.gz > checksums-$VERSION-${{ matrix.suffix }}.txt
|
|
fi
|
|
|
|
- name: Generate manifest artifact snippet
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
cd dist
|
|
# Emit one JSON object per binary in this platform's tarball.
|
|
# The publish job merges these into the full manifest artifacts array.
|
|
echo '[]' > "release-${{ matrix.suffix }}.artifacts.json"
|
|
for bin in redflag-server redflag-agent redflag-helper redflag-desktop; do
|
|
for f in "${bin}-${{ matrix.suffix }}" "${bin}-${{ matrix.suffix }}.exe"; do
|
|
if [ -f "$f" ]; then
|
|
SHA=$(sha256sum "$f" | awk '{print $1}')
|
|
SIZE=$(stat -c%s "$f")
|
|
# Map binary prefix to manifest platform name.
|
|
case "$bin" in
|
|
redflag-server) PLAT="server-${{ matrix.goos }}" ;;
|
|
redflag-agent) PLAT="${{ matrix.goos }}" ;;
|
|
redflag-helper) PLAT="helper-${{ matrix.goos }}" ;;
|
|
redflag-desktop) PLAT="desktop-${{ matrix.goos }}" ;;
|
|
esac
|
|
jq --arg plat "$PLAT" --arg arch "${{ matrix.goarch }}" \
|
|
--arg file "$(basename "$f")" --arg sha "$SHA" \
|
|
--argjson size "$SIZE" \
|
|
'. + [{"platform":$plat,"architecture":$arch,"filename":$file,"sha256":$sha,"size":$size}]' \
|
|
"release-${{ matrix.suffix }}.artifacts.json" > tmp.json \
|
|
&& mv tmp.json "release-${{ matrix.suffix }}.artifacts.json"
|
|
fi
|
|
done
|
|
done
|
|
|
|
# RedFlagSetup.msi doesn't fit the redflag-<bin>-<suffix> naming
|
|
# convention above (no redflag- prefix, .msi not .exe) — handled
|
|
# separately. Windows-amd64 only for now.
|
|
if [ -f "RedFlagSetup-${{ matrix.suffix }}.msi" ]; then
|
|
SHA=$(sha256sum "RedFlagSetup-${{ matrix.suffix }}.msi" | awk '{print $1}')
|
|
SIZE=$(stat -c%s "RedFlagSetup-${{ matrix.suffix }}.msi")
|
|
jq --arg plat "installer-${{ matrix.goos }}" --arg arch "${{ matrix.goarch }}" \
|
|
--arg file "RedFlagSetup-${{ matrix.suffix }}.msi" --arg sha "$SHA" \
|
|
--argjson size "$SIZE" \
|
|
'. + [{"platform":$plat,"architecture":$arch,"filename":$file,"sha256":$sha,"size":$size}]' \
|
|
"release-${{ matrix.suffix }}.artifacts.json" > tmp.json \
|
|
&& mv tmp.json "release-${{ matrix.suffix }}.artifacts.json"
|
|
fi
|
|
echo "Artifact snippet:"
|
|
cat "release-${{ matrix.suffix }}.artifacts.json"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: release-${{ matrix.suffix }}
|
|
path: dist/redflag-*-${{ matrix.suffix }}*
|
|
retention-days: 1
|
|
|
|
# Publish: gather all platform artifacts, generate the component manifest,
|
|
# and create the Gitea release.
|
|
publish:
|
|
runs-on: ubuntu-latest
|
|
needs: [release]
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Generate component manifest
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
NOW=$(date -u +%s)
|
|
|
|
# Component catalog — kept in sync with server/internal/services/release_manifest.go
|
|
# (componentCatalog()). Every component listed here MUST have a built artifact
|
|
# verified by the release matrix jobs, or the gate fails this release.
|
|
cat > manifest.json <<MANIFEST
|
|
{
|
|
"version": "$VERSION",
|
|
"generated_at": $NOW,
|
|
"key_id": "",
|
|
"components": [
|
|
{"name": "server", "kind": "binary", "required": true, "version_cmd": "--version"},
|
|
{"name": "agent", "kind": "binary", "required": true, "version_cmd": "--version"},
|
|
{"name": "helper", "kind": "binary", "required": true, "version_cmd": "--version"},
|
|
{"name": "desktop", "kind": "binary", "required": false, "version_cmd": "--version",
|
|
"provisioning": ["autostart_entry", "redflag-local_group", "desktop_user_membership"]},
|
|
{"name": "web", "kind": "embedded","required": true},
|
|
{"name": "installer","kind": "binary", "required": false}
|
|
],
|
|
"artifacts": []
|
|
}
|
|
MANIFEST
|
|
|
|
# Populate artifacts from downloaded release bundles. Each platform job
|
|
# uploads manifest artifact snippets as release-<suffix>.artifacts.json.
|
|
for f in artifacts/release-*/release-*.artifacts.json; do
|
|
if [ -f "$f" ]; then
|
|
echo "Merging artifact entries from $(basename "$(dirname "$f")")/$(basename "$f")"
|
|
jq -s '.[0].artifacts + .[1].artifacts' manifest.json "$f" > manifest.tmp \
|
|
&& mv manifest.tmp manifest.json
|
|
fi
|
|
done
|
|
|
|
# Verify every required component has at least one artifact.
|
|
# web = embedded (verified by web job producing a non-empty dist/).
|
|
# server/agent/helper/desktop = binary artifacts, cross-compiled per
|
|
# platform by the release matrix. docker-compose-from-source remains
|
|
# a valid, separate install path for the server — it just isn't a
|
|
# signed release artifact, so it isn't in this manifest.
|
|
for comp in server agent helper; do
|
|
if ! jq -e --arg c "$comp" '.artifacts[] | select(.platform | test($c))' manifest.json > /dev/null; then
|
|
echo "::error::required component '$comp' has no artifacts in manifest"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo "Component completeness verified"
|
|
|
|
# Upload manifest so the release job below attaches it.
|
|
cp manifest.json artifacts/manifest.json
|
|
|
|
- name: Create Gitea release
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
API="${GITHUB_SERVER_URL}/api/v1"
|
|
|
|
# Alpha until v0.3.0. Anything sorting below the stable floor publishes
|
|
# as a prerelease; this auto-flips to a stable release at v0.3.0 with no
|
|
# manual toggle to forget.
|
|
STABLE_FLOOR="0.3.0"
|
|
if [ "$(printf '%s\n%s\n' "$VERSION" "$STABLE_FLOOR" | sort -V | head -1)" = "$VERSION" ] \
|
|
&& [ "$VERSION" != "$STABLE_FLOOR" ]; then
|
|
PRERELEASE=true
|
|
else
|
|
PRERELEASE=false
|
|
fi
|
|
echo "Release $VERSION prerelease=$PRERELEASE (stable floor v$STABLE_FLOOR)"
|
|
|
|
RESPONSE=$(curl -sf -X POST "$API/repos/${GITHUB_REPOSITORY}/releases" \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"tag_name\":\"v$VERSION\",\"name\":\"v$VERSION\",\"draft\":false,\"prerelease\":$PRERELEASE}")
|
|
RELEASE_ID=$(echo "$RESPONSE" | grep -oP '"id":\s*\K[0-9]+' | head -1)
|
|
if [ -z "$RELEASE_ID" ]; then
|
|
echo "::error::failed to parse release id from API response: $RESPONSE"
|
|
exit 1
|
|
fi
|
|
echo "Created release id=$RELEASE_ID"
|
|
|
|
# Upload every artifact (tarballs, zips, checksums, manifest).
|
|
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.txt' -o -name 'manifest.json' \) | while read f; do
|
|
echo "Uploading $(basename "$f")"
|
|
curl -sf -X POST "$API/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-F "attachment=@$f" > /dev/null
|
|
done
|
|
echo "Release v$VERSION published"
|
|
|
|
# Codeberg is the public distribution endpoint. Same artifacts as the Gitea
|
|
# release, minus the server image — the server ships as docker-compose build
|
|
# from source, not a pullable image. Skips cleanly if the token is unset.
|
|
- name: Publish release (Codeberg)
|
|
run: |
|
|
set -euo pipefail
|
|
TOKEN="${{ secrets.CODEBERG_TOKEN }}"
|
|
if [ -z "$TOKEN" ]; then
|
|
echo "[INFO] [release] CODEBERG_TOKEN not set — skipping codeberg publish"
|
|
exit 0
|
|
fi
|
|
VERSION=${GITHUB_REF#refs/tags/v}
|
|
API="https://codeberg.org/api/v1/repos/Fimeg/RedFlag"
|
|
|
|
# Same alpha-until-v0.3.0 rule as the Gitea release above.
|
|
STABLE_FLOOR="0.3.0"
|
|
if [ "$(printf '%s\n%s\n' "$VERSION" "$STABLE_FLOOR" | sort -V | head -1)" = "$VERSION" ] \
|
|
&& [ "$VERSION" != "$STABLE_FLOOR" ]; then
|
|
PRERELEASE=true
|
|
else
|
|
PRERELEASE=false
|
|
fi
|
|
|
|
# Codeberg must have the tag before a release can target it.
|
|
git push "https://Fimeg:${TOKEN}@codeberg.org/Fimeg/RedFlag.git" "refs/tags/v${VERSION}"
|
|
|
|
RESPONSE=$(curl -sf -X POST "$API/releases" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"tag_name\":\"v$VERSION\",\"name\":\"v$VERSION\",\"draft\":false,\"prerelease\":$PRERELEASE}")
|
|
RELEASE_ID=$(echo "$RESPONSE" | grep -oP '"id":\s*\K[0-9]+' | head -1)
|
|
if [ -z "$RELEASE_ID" ]; then
|
|
echo "::error::failed to parse codeberg release id from API response: $RESPONSE"
|
|
exit 1
|
|
fi
|
|
echo "Created codeberg release id=$RELEASE_ID"
|
|
|
|
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.txt' -o -name 'manifest.json' \) | while read f; do
|
|
echo "Uploading $(basename "$f") to codeberg"
|
|
curl -sf -X POST "$API/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-F "attachment=@$f" > /dev/null
|
|
done
|
|
echo "Codeberg release v$VERSION published"
|