Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/.gitea/workflows/release.yml
Fimeg ff2f30f47a v0.2.9.3: device classification + ARM support — Pixel 3 lands
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.
2026-07-06 18:21:23 -04:00

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"