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 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}') printf '{"version":"v%s","asset":"chatserver.exe","sha256":"%s","assets":[{"asset":"chatserver.exe","sha256":"%s"},{"asset":"chatserver-linux-amd64.tar.gz","sha256":"%s"}]}' \ "$VERSION" "$WIN_HASH" "$WIN_HASH" "$LINUX_HASH" > 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 - name: Install root dependencies (changelogen) run: npm ci - name: Generate changelog shell: bash run: npx changelogen --output CHANGELOG.md # 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 CHANGELOG.md \ "${assets[@]}"