Files
OwnCord/.github/workflows/release.yml
T
J3vb 9c9b8be669 feat(b2-2): protocol epoch and negotiation (slim) (#1438)
* feat(b2-2): declare protocol_epoch in the schema and generate both constants

protocol/schema.json gains protocol_epoch (1). genprotocol emits
ws.ProtocolEpoch and PROTOCOL_EPOCH from it; the contract test pins the Go
constant to the schema so a stale regeneration fails the required check.

* feat(b2-2): check the client's protocol epoch in the auth handshake

The auth payload gains epoch (absent = 0). Outside [minClientEpoch,
ProtocolEpoch] the server answers one auth_error with code
protocol_epoch_unsupported, the client/server/min epochs, and a message
naming which side to update, then closes 1008 like every other handshake
failure. minClientEpoch is 0 for epoch 1 only so alpha.4 clients keep
connecting; the epoch-1 fixtures are unchanged.

* feat(b2-2): send the protocol epoch and offer the update on a refused connect

ws.ts sends epoch: PROTOCOL_EPOCH in the auth frame (contract test extended
on purpose). On auth_error code protocol_epoch_unsupported with a newer
server the dispatcher records the host in ui.store.updateRequiredHost and
main.ts mounts the UpdateNotifier on the connect page, so a refused client
gets the same Update Now banner it would have had on the main page.

* feat(b2-2): withhold client releases newer than the server's protocol epoch

The signed server-update manifest gains protocol_epoch (release.yml reads it
from protocol/schema.json). Updater.ReleaseProtocolEpoch verifies the
manifest and reads it; the client-update endpoint answers 204 when the
release's epoch is newer than ws.ProtocolEpoch or the manifest does not
verify. Releases without a manifest are epoch 0 and advertised as before.
Docs: protocol.md Compatibility section, api.md, deployment.md, protocol
README, CHANGELOG Unreleased.

* docs(b2-2): record the slim B2-2 decision and evidence; fold B2-3/B2-4 into it

* ci: prove the protocol_epoch manifest read on every PR, not only at tag time

* fix(b2-2): offer the update on an already-mounted connect page and keep the credential on a protocol refusal

Codex P1: on a first login or startup auto-login no overlay exists before
auth_ok, so a refusal never re-rendered the connect page and the one-time
read of updateRequiredHost missed it. The connect page now subscribes to
it, and a later refusal replaces the banner.

Codex P2: a refusal on reconnect went through the generic logout and
deleted the stored credential although the token is still valid.
clearAuth gets a protocol_epoch reason; main.ts keeps the credential on it
(the skip-auto-login flag is still set and, being sessionStorage, does not
survive the relaunch the update triggers).
2026-08-29 07:23:06 +02:00

668 lines
28 KiB
YAML

name: Release
on:
push:
tags:
- "v*"
# A deleted-and-re-pushed tag (it has happened — see the checksum note in the
# publish job) must not race two publish runs: `gh release create` fails
# loudly on the second run, but the ghcr :latest push does not, and which run
# wins it would be arbitrary. Queue, never cancel — a half-cancelled release
# is worse than a slow one.
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
# R-09 / RL-16. ci.yml has no `tags:` trigger, so a tag push starts this
# workflow and nothing else — and this workflow re-runs none of the required
# checks. It builds, smokes and signs, which is a different question from
# "did the gate pass on this commit".
#
# It did not, at least once: v1.2.0-alpha.3 published from a commit whose
# `Server Build & Test (windows-latest)` had concluded failure. Nothing
# noticed, because nothing looked.
#
# The required set is read out of b0-dev-branch-protection.sh rather than
# restated here, so pinning a new check cannot leave this gate behind. The
# logic lives in a script with a --selftest that ci.yml runs on every PR:
# a step that exists only in this file first executes at tag time, which is
# the wrong place to discover its bugs.
gate-evidence:
name: Verify exact-SHA gate evidence
runs-on: ubuntu-latest
permissions:
contents: read
checks: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
- name: Required checks must be green on the tagged commit
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node scripts/verify-gate-evidence.mjs "${{ github.sha }}"
# The v1.1.0-alpha.4 release shipped clients still versioned 1.1.0-alpha.3
# because the client manifests weren't bumped before tagging — deployed
# clients then never saw the update. Fail fast on that mismatch, before any
# expensive build starts.
verify-versions:
name: Verify client version matches tag
# Every build job needs verify-versions, and both publishers need those, so
# one edge here gates the whole graph — nothing builds, pushes to GHCR, or
# creates a Release on a commit that did not pass.
needs: gate-evidence
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Compare tag with client manifests
shell: bash
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
TAURI_VERSION=$(node -p "require('./Client/src-tauri/tauri.conf.json').version")
NPM_VERSION=$(node -p "require('./Client/package.json').version")
CARGO_VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Client/src-tauri/Cargo.toml | head -1)
fail=0
for pair in "tauri.conf.json:$TAURI_VERSION" "package.json:$NPM_VERSION" "Cargo.toml:$CARGO_VERSION"; do
file="${pair%%:*}"; ver="${pair#*:}"
if [ "$ver" != "$TAG_VERSION" ]; then
echo "::error::Release tag v$TAG_VERSION does not match client version $ver in $file — bump the client version before tagging."
fail=1
fi
done
exit $fail
release-client-windows:
name: Build Tauri (Windows)
needs: verify-versions
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
cache-dependency-path: Client/package-lock.json
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: Client/src-tauri
- name: Install npm dependencies
working-directory: Client
run: npm ci
- name: Build Tauri app
working-directory: Client
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run tauri build
- name: Stage Windows release assets
shell: bash
run: |
mkdir -p release-staging
NSIS_DIR="Client/src-tauri/target/release/bundle/nsis"
INSTALLER=$(find "$NSIS_DIR" -name "*.exe" | head -1)
cp "$INSTALLER" release-staging/
NSIS_ZIP=$(find "$NSIS_DIR" -name "*_x64-setup.nsis.zip" ! -name "*.sig" | head -1)
if [ -n "$NSIS_ZIP" ] && [ -f "$NSIS_ZIP" ]; then cp "$NSIS_ZIP" release-staging/; fi
NSIS_SIG=$(find "$NSIS_DIR" -name "*_x64-setup.nsis.zip.sig" | head -1)
if [ -n "$NSIS_SIG" ] && [ -f "$NSIS_SIG" ]; then cp "$NSIS_SIG" release-staging/; fi
- name: Upload Windows release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: windows-release-assets
path: release-staging/
release-client-linux:
name: Build Tauri (Linux)
needs: verify-versions
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
cache-dependency-path: Client/package-lock.json
- name: Install Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
libsecret-1-dev \
libdbus-1-dev \
libasound2-dev \
libssl-dev \
patchelf \
librsvg2-dev \
xdg-utils
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: Client/src-tauri
- name: Install npm dependencies
working-directory: Client
run: npm ci
- name: Build Tauri app (AppImage + deb)
working-directory: Client
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run tauri build -- --bundles appimage,deb
# linuxdeploy bundles the runner's libwayland-* into the AppImage, which
# breaks Mesa EGL init on newer hosts (white window on Arch/Fedora —
# EGL_BAD_PARAMETER). Strip them and regenerate the updater artifact +
# signatures for the patched image.
- name: Strip host-incompatible libs from AppImage and re-sign
working-directory: Client
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
BUNDLE_DIR="src-tauri/target/release/bundle/appimage"
APPIMAGE=$(find "$BUNDLE_DIR" -name "*.AppImage" ! -name "*.sig" | head -1)
bash scripts/strip-appimage-bundled-libs.sh "$APPIMAGE"
TARBALL="$APPIMAGE.tar.gz"
rm -f "$TARBALL" "$APPIMAGE.sig" "$TARBALL.sig"
tar czf "$TARBALL" -C "$(dirname "$APPIMAGE")" "$(basename "$APPIMAGE")"
# Sign straight from the environment. TAURI_SIGNING_PRIVATE_KEY is
# the env form of --private-key, so ALSO passing -f/--private-key-path
# makes the CLI abort: "the argument '--private-key-path' cannot be
# used with '--private-key'". Keeping the key in the env instead of a
# temp file also keeps it off the runner's disk.
npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$APPIMAGE"
npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$TARBALL"
- name: Stage Linux release assets
shell: bash
run: |
mkdir -p linux-staging
BUNDLE_DIR="Client/src-tauri/target/release/bundle"
# AppImage
APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage" ! -name "*.sig" | head -1)
if [ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ]; then cp "$APPIMAGE" linux-staging/; fi
APPIMAGE_SIG=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.sig" | head -1)
if [ -n "$APPIMAGE_SIG" ] && [ -f "$APPIMAGE_SIG" ]; then cp "$APPIMAGE_SIG" linux-staging/; fi
APPIMAGE_TAR=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.tar.gz" ! -name "*.sig" | head -1)
if [ -n "$APPIMAGE_TAR" ] && [ -f "$APPIMAGE_TAR" ]; then cp "$APPIMAGE_TAR" linux-staging/; fi
APPIMAGE_TAR_SIG=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.tar.gz.sig" | head -1)
if [ -n "$APPIMAGE_TAR_SIG" ] && [ -f "$APPIMAGE_TAR_SIG" ]; then cp "$APPIMAGE_TAR_SIG" linux-staging/; fi
# .deb
DEB=$(find "$BUNDLE_DIR/deb" -name "*.deb" | head -1)
if [ -n "$DEB" ] && [ -f "$DEB" ]; then cp "$DEB" linux-staging/; fi
- name: Upload Linux release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: linux-release-assets
path: linux-staging/
release-server:
name: Build server (${{ matrix.os }})
needs: verify-versions
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact: server-windows
- os: ubuntu-latest
artifact: server-linux
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.26"
- name: Extract version from tag
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Build server (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: cd Server && go build -o chatserver.exe -ldflags "-s -w -X main.version=$VERSION" .
- name: Build server (Linux)
if: matrix.os == 'ubuntu-latest'
working-directory: Server
env:
CGO_ENABLED: "0"
run: go build -o chatserver -ldflags "-s -w -X main.version=$VERSION" .
# Boot-smoke the EXACT artifact that ships: this feed drives the signed
# self-update, so a binary that compiles but dies on boot would deploy
# itself to every auto-updating instance. CI's tests exercise the same
# commit but never this build (release ldflags, CGO_ENABLED=0) and never
# execute the produced binary. First run writes config.yaml, generates a
# self-signed cert, migrates a fresh SQLite DB — a real cold boot.
- name: Boot-smoke server binary
shell: bash
working-directory: Server
run: |
SMOKE_DIR="$RUNNER_TEMP/owncord-smoke"
mkdir -p "$SMOKE_DIR"
cd "$SMOKE_DIR"
BIN="$GITHUB_WORKSPACE/Server/chatserver"
[ -f "$GITHUB_WORKSPACE/Server/chatserver.exe" ] && BIN="$GITHUB_WORKSPACE/Server/chatserver.exe"
"$BIN" &
SERVER_PID=$!
ok=0
for _ in $(seq 1 30); do
sleep 1
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "::error::server process exited during boot smoke"
exit 1
fi
if "$BIN" healthcheck; then ok=1; break; fi
done
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
if [ "$ok" != "1" ]; then
echo "::error::server never reported healthy within 30s"
exit 1
fi
echo "boot smoke passed"
- name: Create tar.gz (Linux)
if: matrix.os == 'ubuntu-latest'
working-directory: Server
run: tar czf ../chatserver-linux-amd64.tar.gz chatserver
- name: Upload Windows binary
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifact }}
path: Server/chatserver.exe
- name: Upload Linux archive
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifact }}
path: chatserver-linux-amd64.tar.gz
release-client-linux-arm64:
name: Build Tauri (Linux ARM64)
needs: verify-versions
runs-on: ubuntu-22.04-arm
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
cache-dependency-path: Client/package-lock.json
- name: Install Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
libsecret-1-dev \
libdbus-1-dev \
libasound2-dev \
libssl-dev \
patchelf \
librsvg2-dev \
xdg-utils
- name: Install Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: Client/src-tauri
- name: Install npm dependencies
working-directory: Client
run: npm ci
- name: Build Tauri app (AppImage + deb)
working-directory: Client
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm run tauri build -- --bundles appimage,deb
# Same strip + re-sign as the x86_64 job — see the comment there.
- name: Strip host-incompatible libs from AppImage and re-sign
working-directory: Client
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
BUNDLE_DIR="src-tauri/target/release/bundle/appimage"
APPIMAGE=$(find "$BUNDLE_DIR" -name "*.AppImage" ! -name "*.sig" | head -1)
bash scripts/strip-appimage-bundled-libs.sh "$APPIMAGE"
TARBALL="$APPIMAGE.tar.gz"
rm -f "$TARBALL" "$APPIMAGE.sig" "$TARBALL.sig"
tar czf "$TARBALL" -C "$(dirname "$APPIMAGE")" "$(basename "$APPIMAGE")"
# Sign straight from the environment. TAURI_SIGNING_PRIVATE_KEY is
# the env form of --private-key, so ALSO passing -f/--private-key-path
# makes the CLI abort: "the argument '--private-key-path' cannot be
# used with '--private-key'". Keeping the key in the env instead of a
# temp file also keeps it off the runner's disk.
npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$APPIMAGE"
npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$TARBALL"
- name: Stage Linux ARM64 release assets
shell: bash
run: |
mkdir -p linux-arm64-staging
BUNDLE_DIR="Client/src-tauri/target/release/bundle"
# AppImage + updater artifact (.tar.gz) + signatures. Every filename
# must carry the arch: FindClientAssets matches on the
# _aarch64.AppImage.tar.gz suffix, and arch-less names would collide
# with the x86_64 assets when both artifact sets are downloaded into
# the same linux/ directory at publish time. Inserting _aarch64
# before ".AppImage" renames installer, tar.gz, and .sig
# consistently, so signatures keep pairing with their artifacts.
for f in "$BUNDLE_DIR"/appimage/*.AppImage "$BUNDLE_DIR"/appimage/*.AppImage.tar.gz "$BUNDLE_DIR"/appimage/*.sig; do
[ -f "$f" ] || continue
base="$(basename "$f")"
[[ "$base" == *aarch64* ]] || base="${base/.AppImage/_aarch64.AppImage}"
cp "$f" "linux-arm64-staging/$base"
done
# .deb
for f in "$BUNDLE_DIR"/deb/*.deb; do
[ -f "$f" ] && cp "$f" linux-arm64-staging/
done
- name: Upload Linux ARM64 release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: linux-arm64-release-assets
path: linux-arm64-staging/
release-server-docker:
name: Build & Push Server Docker Image
needs: verify-versions
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Extract version from tag
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ github.repository_owner }}/owncord-server
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
# Build locally first so the image can be boot-smoked BEFORE anything
# is pushed — a pushed :latest that dies on boot deploys itself to every
# `docker compose pull` upgrade.
- name: Build image (local, for smoke test)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: Server/
load: true
build-args: VERSION=${{ env.VERSION }}
tags: owncord-smoke:candidate
cache-from: type=gha
cache-to: type=gha,mode=max
# Shared with ci.yml's docker-build job so the smoke itself is exercised
# on every PR to main — the first alpha.3 release run died here on a
# smoke-harness bug (bare `docker run`, nowhere writable for the
# default config) that no pre-merge check had ever run.
- name: Boot-smoke Docker image
run: bash Server/scripts/docker-smoke.sh owncord-smoke:candidate
- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: Server/
push: true
build-args: VERSION=${{ env.VERSION }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
publish:
name: Publish GitHub Release
needs:
[
release-client-windows,
release-client-linux,
release-client-linux-arm64,
release-server,
release-server-docker,
]
runs-on: ubuntu-latest
# The `release` environment carries a required reviewer; naming it here
# is what makes that approval gate fire before anything is signed or
# published. Without this line the environment exists but never applies.
environment: release
permissions:
contents: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: npm
cache-dependency-path: Client/package-lock.json
- name: Download Windows client assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: windows-release-assets
path: windows
- name: Download Linux x86_64 client assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: linux-release-assets
path: linux
- name: Download Linux ARM64 client assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: linux-arm64-release-assets
path: linux
- name: Download Windows server binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: server-windows
path: windows
- name: Download Linux server archive
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: server-linux
path: linux
- name: Extract version from tag
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Create source snapshot (AGPL source availability)
shell: bash
run: |
git archive --format=tar.gz --prefix="OwnCord-${VERSION}/" \
-o "owncord-src-${{ github.ref_name }}.tar.gz" HEAD
# Checksum lines must use bare asset filenames: the v1.0.0 updater's
# ParseChecksumFile does an exact match on the last field, so a
# "windows/" prefix would strand every deployed server on 1.0.0.
- name: Generate SHA256 checksums
shell: bash
run: |
(cd windows && sha256sum -- *) > checksums.sha256
(cd linux && sha256sum -- *) >> checksums.sha256
sha256sum owncord-src-*.tar.gz >> checksums.sha256
# The legacy top-level asset/sha256 pair stays bound to the Windows
# binary so already-deployed servers (which only understand the
# single-asset schema) can still verify and update; the assets list
# binds every OS. Server-side schema: updater.releaseManifest.
- name: Generate server update manifest
shell: bash
run: |
WIN_HASH=$(sha256sum windows/chatserver.exe | awk '{print $1}')
LINUX_HASH=$(sha256sum linux/chatserver-linux-amd64.tar.gz | awk '{print $1}')
# protocol_epoch is read from the schema, never typed here, so the
# manifest cannot drift from the constants the binaries were built with.
# The server's client-update endpoint withholds any release whose epoch
# is newer than its own (Server/api/client_update.go).
EPOCH=$(jq -e '.protocol_epoch' protocol/schema.json)
printf '{"version":"v%s","asset":"chatserver.exe","sha256":"%s","assets":[{"asset":"chatserver.exe","sha256":"%s"},{"asset":"chatserver-linux-amd64.tar.gz","sha256":"%s"}],"protocol_epoch":%s}' \
"$VERSION" "$WIN_HASH" "$WIN_HASH" "$LINUX_HASH" "$EPOCH" > windows/server-update-manifest.json
- name: Sign server update assets
working-directory: Client
shell: bash
env:
SERVER_UPDATE_SIGNING_PRIVATE_KEY: ${{ secrets.SERVER_UPDATE_SIGNING_PRIVATE_KEY }}
SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
KEY_PATH=$(mktemp)
printf '%s' "$SERVER_UPDATE_SIGNING_PRIVATE_KEY" > "$KEY_PATH"
trap 'rm -f "$KEY_PATH"' EXIT
npm ci
npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../windows/chatserver.exe
npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../windows/server-update-manifest.json
# Fail closed before publishing: prove the freshly signed assets verify
# against the pinned public key that ships inside the server binary.
# Catches key/pubkey mismatch, signature format drift, and signer flag
# regressions — each of which has silently broken this pipeline before.
- name: Verify signed assets against pinned server update key
shell: bash
run: |
sudo apt-get update && sudo apt-get install -y minisign
base64 -d Server/updater/server_update_public_key.txt > "$RUNNER_TEMP/server_update.pub"
for f in windows/chatserver.exe windows/server-update-manifest.json; do
base64 -d "$f.sig" > "$RUNNER_TEMP/asset.minisig"
minisign -Vm "$f" -x "$RUNNER_TEMP/asset.minisig" -p "$RUNNER_TEMP/server_update.pub"
done
# The release body is the curated section for THIS tag, never the whole
# file. v1.2.0-alpha.4 published all 795 lines of CHANGELOG.md — every
# past release, plus the "How to write an entry" style guide aimed at
# contributors — because `--notes-file CHANGELOG.md` hands GitHub the
# entire file and nothing ever narrowed it.
#
# The `changelogen --output CHANGELOG.md` step that used to run here is
# gone. It ran after the tag existed, so its from-tag and to-tag were the
# same commit: it appended an empty `## <tag>...<tag>` heading whose
# compare link pointed at itself, and no step consumed the result.
# `npm run changelog` still exists for drafting an entry locally, which
# is the point in time where generating one is useful.
#
# Fail closed. Empty notes on a public download page are worse than a
# failed run: the run can be re-run once the entry is written, but a
# published release with no description has already been fetched.
- name: Extract this tag's release notes
shell: bash
env:
TAG: ${{ github.ref_name }}
DOC_BASE: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}
run: |
# Headings carry an optional title ("## v1.2.0-alpha.1 — Discord
# feature parity"), so match the tag as a whole word, not a prefix:
# a bare prefix match would let `v1.2.0` swallow `v1.2.0-alpha.4`.
awk -v tag="$TAG" '
$0 == "## " tag || index($0, "## " tag " ") == 1 { found = 1; next }
found && /^## / { exit }
found
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "::error::CHANGELOG.md has no '## $TAG' section. Write the entry, then re-run this release."
exit 1
fi
# Relative links resolve against the repository, not against a
# release page, so every one of them 404s for a release reader.
sed -i "s#](docs/#]($DOC_BASE/docs/#g" release-notes.md
echo "Release notes: $(wc -l < release-notes.md) lines, $(wc -c < release-notes.md) bytes"
# Sole publish target. This repo is public, so its own Releases page both
# satisfies AGPL source availability (via the owncord-src snapshot below)
# and serves as the publicly-readable feed that deployed servers and
# clients poll for updates. The former mirror step to a separate public
# releases repo existed only to work around this repo being private.
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mapfile -t assets < <(find windows linux -type f)
assets+=(checksums.sha256 owncord-src-*.tar.gz)
gh release create "${{ github.ref_name }}" \
--notes-file release-notes.md \
"${assets[@]}"