mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Three latent bugs in the never-exercised publish path: - `tauri signer sign -k` loads the key from a *string*; we passed the mktemp path, so the CLI base64-decoded "/tmp/tmp.XXXX" and died with "Invalid symbol 46, offset 8" (the dot). Use `-f` (key from file). The stored secret was never read and never at fault. - checksums.sha256 lines carried "windows/"/"linux/" path prefixes; the v1.0.0 updater's ParseChecksumFile exact-matches the last field against "chatserver.exe", so every deployed 1.0.0 server would have failed the checksum lookup. Emit bare asset filenames (current updater accepts both). - No end-to-end proof the signed assets verify against the pinned public key that ships inside the server binary. Add a fail-closed minisign verify step before any release is created; it catches key/pubkey mismatch, signature format drift, and signer flag regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
458 lines
16 KiB
YAML
458 lines
16 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
|
|
jobs:
|
|
release-client-windows:
|
|
name: Build Tauri (Windows)
|
|
runs-on: windows-latest
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: Client/tauri-client/package-lock.json
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Rust cache
|
|
uses: swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: Client/tauri-client/src-tauri
|
|
|
|
- name: Install npm dependencies
|
|
working-directory: Client/tauri-client
|
|
run: npm ci
|
|
|
|
- name: Build Tauri app
|
|
working-directory: Client/tauri-client
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
VITE_KLIPY_API_KEY: ${{ secrets.VITE_KLIPY_API_KEY }}
|
|
run: npm run tauri build
|
|
|
|
- name: Stage Windows release assets
|
|
shell: bash
|
|
run: |
|
|
mkdir -p release-staging
|
|
NSIS_DIR="Client/tauri-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@v4
|
|
with:
|
|
name: windows-release-assets
|
|
path: release-staging/
|
|
|
|
release-client-linux:
|
|
name: Build Tauri (Linux)
|
|
runs-on: ubuntu-22.04
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: Client/tauri-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 \
|
|
libasound2-dev \
|
|
libssl-dev \
|
|
patchelf \
|
|
librsvg2-dev \
|
|
xdg-utils
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Rust cache
|
|
uses: swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: Client/tauri-client/src-tauri
|
|
|
|
- name: Install npm dependencies
|
|
working-directory: Client/tauri-client
|
|
run: npm ci
|
|
|
|
- name: Build Tauri app (AppImage + deb)
|
|
working-directory: Client/tauri-client
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
VITE_KLIPY_API_KEY: ${{ secrets.VITE_KLIPY_API_KEY }}
|
|
run: npm run tauri build -- --bundles appimage,deb
|
|
|
|
- name: Stage Linux release assets
|
|
shell: bash
|
|
run: |
|
|
mkdir -p linux-staging
|
|
BUNDLE_DIR="Client/tauri-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@v4
|
|
with:
|
|
name: linux-release-assets
|
|
path: linux-staging/
|
|
|
|
release-server:
|
|
name: Build server (${{ matrix.os }})
|
|
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@v4
|
|
|
|
- uses: actions/setup-go@v5
|
|
with:
|
|
go-version: "1.25"
|
|
|
|
- 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" .
|
|
|
|
- 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@v4
|
|
with:
|
|
name: ${{ matrix.artifact }}
|
|
path: Server/chatserver.exe
|
|
|
|
- name: Upload Linux archive
|
|
if: matrix.os == 'ubuntu-latest'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ matrix.artifact }}
|
|
path: chatserver-linux-amd64.tar.gz
|
|
|
|
release-client-linux-arm64:
|
|
name: Build Tauri (Linux ARM64)
|
|
runs-on: ubuntu-22.04-arm
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: Client/tauri-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 \
|
|
libasound2-dev \
|
|
libssl-dev \
|
|
patchelf \
|
|
librsvg2-dev \
|
|
xdg-utils
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Rust cache
|
|
uses: swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: Client/tauri-client/src-tauri
|
|
|
|
- name: Install npm dependencies
|
|
working-directory: Client/tauri-client
|
|
run: npm ci
|
|
|
|
- name: Build Tauri app (AppImage + deb)
|
|
working-directory: Client/tauri-client
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
VITE_KLIPY_API_KEY: ${{ secrets.VITE_KLIPY_API_KEY }}
|
|
run: npm run tauri build -- --bundles appimage,deb
|
|
|
|
- name: Stage Linux ARM64 release assets
|
|
shell: bash
|
|
run: |
|
|
mkdir -p linux-arm64-staging
|
|
BUNDLE_DIR="Client/tauri-client/src-tauri/target/release/bundle"
|
|
# AppImage (ensure arch is in filename)
|
|
for f in "$BUNDLE_DIR"/appimage/*.AppImage; do
|
|
[ -f "$f" ] || continue
|
|
[[ "$f" == *.sig ]] && continue
|
|
dest="linux-arm64-staging/$(basename "$f")"
|
|
# Append _aarch64 if bundler omits arch from filename
|
|
[[ "$(basename "$f")" == *aarch64* ]] || dest="${dest%.AppImage}_aarch64.AppImage"
|
|
cp "$f" "$dest"
|
|
done
|
|
for f in "$BUNDLE_DIR"/appimage/*.AppImage.tar.gz; do
|
|
[ -f "$f" ] && cp "$f" linux-arm64-staging/
|
|
done
|
|
for f in "$BUNDLE_DIR"/appimage/*.sig; do
|
|
[ -f "$f" ] && cp "$f" linux-arm64-staging/
|
|
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@v4
|
|
with:
|
|
name: linux-arm64-release-assets
|
|
path: linux-arm64-staging/
|
|
|
|
release-server-docker:
|
|
name: Build & Push Server Docker Image
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- 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@v3
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract Docker metadata
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ghcr.io/${{ github.repository_owner }}/owncord-server
|
|
tags: |
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=raw,value=latest
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v6
|
|
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
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: Client/tauri-client/package-lock.json
|
|
|
|
- name: Download Windows client assets
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: windows-release-assets
|
|
path: windows
|
|
|
|
- name: Download Linux x86_64 client assets
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: linux-release-assets
|
|
path: linux
|
|
|
|
- name: Download Linux ARM64 client assets
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: linux-arm64-release-assets
|
|
path: linux
|
|
|
|
- name: Download Windows server binary
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: server-windows
|
|
path: windows
|
|
|
|
- name: Download Linux server archive
|
|
uses: actions/download-artifact@v4
|
|
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
|
|
|
|
- name: Generate server update manifest
|
|
shell: bash
|
|
run: |
|
|
SERVER_HASH=$(sha256sum windows/chatserver.exe | awk '{print $1}')
|
|
printf '{"version":"v%s","asset":"chatserver.exe","sha256":"%s"}' "$VERSION" "$SERVER_HASH" > windows/server-update-manifest.json
|
|
|
|
- name: Sign server update assets
|
|
working-directory: Client/tauri-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
|
|
|
|
- name: Install root dependencies (changelogen)
|
|
run: npm ci
|
|
|
|
- name: Generate changelog
|
|
shell: bash
|
|
run: npx changelogen --output CHANGELOG.md
|
|
|
|
- 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 CHANGELOG.md \
|
|
"${assets[@]}"
|
|
|
|
# The public releases repo is what deployed servers and clients poll for
|
|
# updates, and it carries the AGPL source snapshot while the source repo
|
|
# is private. Publishing there must never be skipped silently once the
|
|
# source repo is private.
|
|
- name: Publish to public releases repo
|
|
shell: bash
|
|
env:
|
|
RELEASES_TOKEN: ${{ secrets.RELEASES_REPO_TOKEN }}
|
|
SOURCE_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
if [ -z "$RELEASES_TOKEN" ]; then
|
|
PRIVATE=$(GH_TOKEN="$SOURCE_REPO_TOKEN" gh api "repos/$GITHUB_REPOSITORY" --jq .private)
|
|
if [ "$PRIVATE" = "true" ]; then
|
|
echo "::error::Source repo is private and RELEASES_REPO_TOKEN is unset — binaries would ship with no public source or update feed (AGPL violation, broken updater)."
|
|
exit 1
|
|
fi
|
|
echo "::warning::RELEASES_REPO_TOKEN not set — skipping publish to J3vb/OwnCord-releases."
|
|
exit 0
|
|
fi
|
|
mapfile -t assets < <(find windows linux -type f)
|
|
assets+=(checksums.sha256 owncord-src-*.tar.gz)
|
|
GH_TOKEN="$RELEASES_TOKEN" gh release create "${{ github.ref_name }}" \
|
|
--repo J3vb/OwnCord-releases \
|
|
--notes-file CHANGELOG.md \
|
|
"${assets[@]}"
|