name: CI and tagged release on: push: branches: - main tags: - "v*.*.*" pull_request: workflow_dispatch: permissions: contents: read jobs: metadata: name: Resolve build metadata runs-on: ubuntu-latest timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} release: ${{ steps.version.outputs.release }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - id: version name: Resolve release version shell: bash env: RAW_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'v1.0.0' }} RELEASE_BUILD: ${{ startsWith(github.ref, 'refs/tags/') }} run: | if [ "$RELEASE_BUILD" = "true" ] && [[ ! "$RAW_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Tag must use the v-prefixed format v1.2.3" >&2 exit 1 fi version="${RAW_VERSION#v}" echo "version=$version" >> "$GITHUB_OUTPUT" echo "release=$RELEASE_BUILD" >> "$GITHUB_OUTPUT" security: name: Dependency security runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Audit web dependencies timeout-minutes: 5 run: bun audit - name: Install Rust dependency auditor timeout-minutes: 10 run: cargo install cargo-audit --version 0.22.2 --locked --no-default-features - name: Audit backend dependencies run: cargo audit --file backend/Cargo.lock - name: Audit Agent dependencies run: cargo audit --file agent/Cargo.lock quality: name: Quality checks needs: metadata runs-on: ubuntu-latest timeout-minutes: 40 env: NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} services: postgres: image: postgres:18-alpine env: POSTGRES_DB: nodeflare_smoke POSTGRES_USER: nodeflare POSTGRES_PASSWORD: SmokePostgresPassword123 ports: - 5432:5432 options: >- --health-cmd "pg_isready -U nodeflare -d nodeflare_smoke" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: components: clippy, rustfmt - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Install dependencies run: bun install --frozen-lockfile - name: Typecheck frontend run: bunx --bun tsc -b frontend - name: Test frontend run: bun test --cwd frontend - name: Build frontend run: bun run build:frontend - name: Check shell scripts run: | for script in install.sh agent/agent.sh agent/install-macos.sh agent/install-freebsd.sh scripts/*.sh start.sh dev.sh; do sh -n "$script" || exit "$?" done - name: Check PowerShell scripts shell: pwsh run: | $allErrors = @() foreach ($file in @("install.ps1", "agent/install.ps1")) { $tokens = $null $errors = $null [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $file), [ref]$tokens, [ref]$errors) > $null $allErrors += $errors } if ($allErrors.Count) { $allErrors | ForEach-Object { Write-Error $_ }; exit 1 } - name: Check backend formatting run: cargo fmt --manifest-path backend/Cargo.toml -- --check - name: Check Agent formatting run: cargo fmt --manifest-path agent/Cargo.toml -- --check - name: Check telemetry formatting run: cargo fmt --manifest-path shared/Cargo.toml -- --check - name: Test telemetry codec run: CARGO_TARGET_DIR=target/backend cargo test --locked --manifest-path shared/Cargo.toml - name: Lint backend run: CARGO_TARGET_DIR=target/backend cargo clippy --locked --manifest-path backend/Cargo.toml --all-targets -- -D warnings - name: Lint Agent run: CARGO_TARGET_DIR=target/agent cargo clippy --locked --manifest-path agent/Cargo.toml --all-targets -- -D warnings - name: Test backend run: CARGO_TARGET_DIR=target/backend cargo test --locked --manifest-path backend/Cargo.toml - name: Test PostgreSQL backup and notification persistence env: NODEFLARE_TEST_POSTGRES_URL: postgres://nodeflare:SmokePostgresPassword123@127.0.0.1:5432/nodeflare_smoke run: CARGO_TARGET_DIR=target/backend cargo test --locked --manifest-path backend/Cargo.toml postgres_restores_nullable_batches_and_pending_notifications -- --ignored - name: Test Agent run: CARGO_TARGET_DIR=target/agent cargo test --locked --manifest-path agent/Cargo.toml - name: Build Agent for smoke test run: CARGO_TARGET_DIR=target/agent cargo build --locked --manifest-path agent/Cargo.toml - name: Test Agent update and connection recovery run: MONITOR_AGENT_BINARY=target/agent/debug/nodeflare-agent node scripts/agent-runtime-test.mjs - name: Build backend run: CARGO_TARGET_DIR=target/backend cargo build --locked --manifest-path backend/Cargo.toml - name: Smoke test SQLite and PostgreSQL shell: bash env: POSTGRES_URL: postgres://nodeflare:SmokePostgresPassword123@127.0.0.1:5432/nodeflare_smoke run: | set -euo pipefail sqlite_pid="" postgres_pid="" cleanup() { if [ -n "$sqlite_pid" ]; then kill "$sqlite_pid" 2>/dev/null || true; fi if [ -n "$postgres_pid" ]; then kill "$postgres_pid" 2>/dev/null || true; fi } trap cleanup EXIT wait_ready() { local url=$1 local log_file=$2 local process_id=$3 for _ in $(seq 1 60); do if curl --fail --silent "$url/api/bootstrap" >/dev/null; then return 0; fi if ! kill -0 "$process_id" 2>/dev/null; then break; fi sleep 1 done cat "$log_file" return 1 } sqlite_dir="$RUNNER_TEMP/nodeflare-sqlite" mkdir -p "$sqlite_dir" sqlite_config="$sqlite_dir/config.toml" printf '%s\n' \ "database_url = \"sqlite://$sqlite_dir/nodeflare.db\"" \ 'bind_addr = "127.0.0.1:18080"' \ 'admin_username = "sqlite-smoke"' \ 'admin_password = "SmokePassword123"' \ 'turnstile_site_key = ""' \ 'turnstile_secret_key = ""' \ "frontend_dir = \"$GITHUB_WORKSPACE/frontend/dist\"" \ "admin_frontend_dir = \"$GITHUB_WORKSPACE/frontend/admin-dist\"" \ "theme_dir = \"$sqlite_dir/themes\"" > "$sqlite_config" target/backend/debug/nodeflare --config "$sqlite_config" > "$sqlite_dir/server.log" 2>&1 & sqlite_pid=$! wait_ready http://127.0.0.1:18080 "$sqlite_dir/server.log" "$sqlite_pid" MONITOR_BASE_URL=http://127.0.0.1:18080 \ MONITOR_ADMIN_USERNAME=sqlite-smoke \ MONITOR_ADMIN_PASSWORD=SmokePassword123 \ MONITOR_AGENT_BINARY="$GITHUB_WORKSPACE/target/agent/debug/nodeflare-agent" \ MONITOR_MIGRATION_URL="$POSTGRES_URL" \ MONITOR_MIGRATION_KIND=postgresql \ sh scripts/smoke-test.sh grep -F "database_url = \"$POSTGRES_URL\"" "$sqlite_config" kill "$sqlite_pid" wait "$sqlite_pid" || true sqlite_pid="" postgres_dir="$RUNNER_TEMP/nodeflare-postgres" mkdir -p "$postgres_dir" postgres_config="$postgres_dir/config.toml" printf '%s\n' \ "database_url = \"$POSTGRES_URL\"" \ 'bind_addr = "127.0.0.1:18081"' \ 'admin_username = "sqlite-smoke"' \ 'admin_password = "SmokePassword123"' \ 'turnstile_site_key = ""' \ 'turnstile_secret_key = ""' \ "frontend_dir = \"$GITHUB_WORKSPACE/frontend/dist\"" \ "admin_frontend_dir = \"$GITHUB_WORKSPACE/frontend/admin-dist\"" \ "theme_dir = \"$postgres_dir/themes\"" > "$postgres_config" target/backend/debug/nodeflare --config "$postgres_config" > "$postgres_dir/server.log" 2>&1 & postgres_pid=$! wait_ready http://127.0.0.1:18081 "$postgres_dir/server.log" "$postgres_pid" MONITOR_BASE_URL=http://127.0.0.1:18081 \ MONITOR_ADMIN_USERNAME=sqlite-smoke \ MONITOR_ADMIN_PASSWORD=SmokePassword123 \ MONITOR_AGENT_BINARY="$GITHUB_WORKSPACE/target/agent/debug/nodeflare-agent" \ MONITOR_MIGRATION_URL="sqlite://$postgres_dir/round-trip.db" \ MONITOR_MIGRATION_KIND=sqlite \ sh scripts/smoke-test.sh server: name: Server (${{ matrix.platform }}) needs: metadata if: needs.metadata.outputs.release == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: 45 permissions: attestations: write contents: read id-token: write env: CARGO_TARGET_DIR: target/server NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} strategy: fail-fast: false matrix: include: - platform: Linux x64 glibc 2.28+ runner: ubuntu-latest target: x86_64-unknown-linux-gnu asset: nodeflare-linux-x64-glibc extension: "" cross: false glibc: true glibc_image: quay.io/pypa/manylinux_2_28_x86_64@sha256:0536c364004fa2a3c5041120b6fe35d84fc5bfe31f04c6a6304f13eac4a67b63 native: true static_linux: false emulate: false freebsd_vm: false freebsd_arm: false - platform: Linux x64 musl runner: ubuntu-latest target: x86_64-unknown-linux-musl asset: nodeflare-linux-x64-musl extension: "" cross: true native: true static_linux: true emulate: false freebsd_vm: false freebsd_arm: false - platform: Linux ARM64 glibc 2.28+ runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu asset: nodeflare-linux-aarch64-glibc extension: "" cross: false glibc: true glibc_image: quay.io/pypa/manylinux_2_28_aarch64@sha256:1676a4f178dc6cf8a1d1e3b7e1d71fca891a466ebde72a33880ccda8af3383d8 native: true static_linux: false emulate: false freebsd_vm: false freebsd_arm: false - platform: Linux ARM64 musl runner: ubuntu-latest target: aarch64-unknown-linux-musl asset: nodeflare-linux-aarch64-musl extension: "" cross: true native: false static_linux: true emulate: true freebsd_vm: false freebsd_arm: false - platform: Windows x64 runner: windows-latest target: x86_64-pc-windows-msvc asset: nodeflare-windows-x64.exe extension: .exe cross: false native: true static_linux: false emulate: false freebsd_vm: false freebsd_arm: false rustflags: -C target-feature=+crt-static - platform: macOS ARM64 runner: macos-14 target: aarch64-apple-darwin asset: nodeflare-macos-aarch64 extension: "" cross: false native: true static_linux: false emulate: false freebsd_vm: false freebsd_arm: false rustflags: "" - platform: FreeBSD x64 runner: ubuntu-latest target: x86_64-unknown-freebsd asset: nodeflare-freebsd-x64 extension: "" cross: true native: false static_linux: false emulate: false freebsd_vm: true freebsd_arm: false - platform: FreeBSD ARM64 runner: ubuntu-latest target: aarch64-unknown-freebsd asset: nodeflare-freebsd-aarch64 extension: "" cross: true native: false static_linux: false emulate: false freebsd_vm: false freebsd_arm: true steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: targets: ${{ matrix.target }} - name: Install cross compiler if: matrix.cross run: cargo install cross --git https://github.com/cross-rs/cross --rev 8c1a8aa4b661711f4b7b6ac07c2e8929ce2f7d27 --locked - name: Build server for glibc 2.28 if: matrix.glibc == true shell: bash run: | docker run --rm \ --user "$(id -u):$(id -g)" \ --env CARGO_HOME=/opt/cargo \ --env RUSTUP_HOME=/opt/rustup \ --env RUSTUP_TOOLCHAIN=stable \ --env HOME=/tmp \ --env CARGO_TARGET_DIR \ --env NODEFLARE_VERSION \ --volume "$HOME/.cargo:/opt/cargo" \ --volume "$HOME/.rustup:/opt/rustup" \ --volume "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE" \ --workdir "$GITHUB_WORKSPACE" \ --entrypoint /opt/cargo/bin/cargo \ "${{ matrix.glibc_image }}" \ build --manifest-path backend/Cargo.toml --release --locked --target "${{ matrix.target }}" - name: Build server with cross if: matrix.cross run: CROSS_CONFIG=Cross.toml cross build --manifest-path backend/Cargo.toml --release --locked --target ${{ matrix.target }} - name: Build server with Cargo if: matrix.cross == false && matrix.glibc != true env: RUSTFLAGS: ${{ matrix.rustflags }} run: cargo build --manifest-path backend/Cargo.toml --release --locked --target ${{ matrix.target }} - name: Stage server binary shell: bash run: | mkdir -p release cp "target/server/${{ matrix.target }}/release/nodeflare${{ matrix.extension }}" "release/${{ matrix.asset }}" chmod +x "release/${{ matrix.asset }}" - name: Install ARM64 emulator if: matrix.emulate run: | sudo apt-get update sudo apt-get install --yes --no-install-recommends qemu-user-static libc6-arm64-cross libgcc-s1-arm64-cross - name: Verify server version natively if: matrix.native shell: bash run: | version_output=$("release/${{ matrix.asset }}" --version) actual_version="${version_output##* }" if [ "$actual_version" != "${{ needs.metadata.outputs.version }}" ]; then echo "Server version $actual_version does not match tag ${{ needs.metadata.outputs.version }}" >&2 exit 1 fi - name: Verify emulated server version if: matrix.emulate shell: bash run: | version_output=$(qemu-aarch64-static -L /usr/aarch64-linux-gnu "release/${{ matrix.asset }}" --version) actual_version="${version_output##* }" if [ "$actual_version" != "${{ needs.metadata.outputs.version }}" ]; then echo "Server version $actual_version does not match tag ${{ needs.metadata.outputs.version }}" >&2 exit 1 fi - name: Verify static server binary if: matrix.static_linux shell: bash run: | if readelf -l "release/${{ matrix.asset }}" | grep -q 'Requesting program interpreter'; then echo "Linux server must be statically linked" >&2 exit 1 fi - name: Verify glibc server binary if: endsWith(matrix.target, '-unknown-linux-gnu') shell: bash run: | readelf -l "release/${{ matrix.asset }}" | grep -q 'Requesting program interpreter' scripts/check-glibc-version.sh "release/${{ matrix.asset }}" 2.28 - name: Verify FreeBSD server if: matrix.freebsd_vm || matrix.freebsd_arm uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1.5.5 with: release: "15" arch: ${{ matrix.freebsd_arm && 'aarch64' || 'x86_64' }} envs: NODEFLARE_VERSION copyback: false run: | set -eu version_output=$(./release/${{ matrix.asset }} --version) actual_version=${version_output##* } if [ "$actual_version" != "$NODEFLARE_VERSION" ]; then echo "Server version $actual_version does not match tag $NODEFLARE_VERSION" >&2 exit 1 fi - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: subject-path: release/${{ matrix.asset }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.asset }} path: release/${{ matrix.asset }} if-no-files-found: error agent: name: Agent (${{ matrix.platform }}) needs: metadata if: needs.metadata.outputs.release == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: 45 permissions: attestations: write contents: read id-token: write env: CARGO_TARGET_DIR: target/agent NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} strategy: fail-fast: false matrix: include: - platform: Linux x64 glibc 2.28+ runner: ubuntu-latest target: x86_64-unknown-linux-gnu asset: agent-linux-x64-glibc extension: "" cross: false glibc: true glibc_image: quay.io/pypa/manylinux_2_28_x86_64@sha256:0536c364004fa2a3c5041120b6fe35d84fc5bfe31f04c6a6304f13eac4a67b63 native: true emulate: false static_linux: false - platform: Linux x64 musl runner: ubuntu-latest target: x86_64-unknown-linux-musl asset: agent-linux-x64-musl extension: "" cross: true native: true emulate: false static_linux: true - platform: Linux ARM64 glibc 2.28+ runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu asset: agent-linux-aarch64-glibc extension: "" cross: false glibc: true glibc_image: quay.io/pypa/manylinux_2_28_aarch64@sha256:1676a4f178dc6cf8a1d1e3b7e1d71fca891a466ebde72a33880ccda8af3383d8 native: true emulate: false static_linux: false - platform: Linux ARM64 musl runner: ubuntu-latest target: aarch64-unknown-linux-musl asset: agent-linux-aarch64-musl extension: "" cross: true native: false emulate: true static_linux: true - platform: Windows x64 runner: windows-latest target: x86_64-pc-windows-msvc asset: agent-windows-x64.exe extension: .exe cross: false native: true emulate: false static_linux: false rustflags: -C target-feature=+crt-static - platform: macOS ARM64 runner: macos-14 target: aarch64-apple-darwin asset: agent-macos-aarch64 extension: "" cross: false native: true emulate: false static_linux: false rustflags: "" - platform: FreeBSD x64 runner: ubuntu-latest target: x86_64-unknown-freebsd asset: agent-freebsd-x64 extension: "" cross: true native: false emulate: false static_linux: false - platform: FreeBSD ARM64 runner: ubuntu-latest target: aarch64-unknown-freebsd asset: agent-freebsd-aarch64 extension: "" cross: true native: false emulate: false static_linux: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: targets: ${{ matrix.target }} - name: Install cross compiler if: matrix.cross run: cargo install cross --git https://github.com/cross-rs/cross --rev 8c1a8aa4b661711f4b7b6ac07c2e8929ce2f7d27 --locked - name: Build Rust Agent for glibc 2.28 if: matrix.glibc == true shell: bash run: | docker run --rm \ --user "$(id -u):$(id -g)" \ --env CARGO_HOME=/opt/cargo \ --env RUSTUP_HOME=/opt/rustup \ --env RUSTUP_TOOLCHAIN=stable \ --env HOME=/tmp \ --env CARGO_TARGET_DIR \ --env NODEFLARE_VERSION \ --volume "$HOME/.cargo:/opt/cargo" \ --volume "$HOME/.rustup:/opt/rustup" \ --volume "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE" \ --workdir "$GITHUB_WORKSPACE" \ --entrypoint /opt/cargo/bin/cargo \ "${{ matrix.glibc_image }}" \ build --manifest-path agent/Cargo.toml --release --locked --target "${{ matrix.target }}" - name: Build Rust Agent with cross if: matrix.cross run: CROSS_CONFIG=Cross.toml cross build --manifest-path agent/Cargo.toml --release --locked --target ${{ matrix.target }} - name: Build Rust Agent with Cargo if: matrix.cross == false && matrix.glibc != true env: RUSTFLAGS: ${{ matrix.rustflags }} run: cargo build --manifest-path agent/Cargo.toml --release --locked --target ${{ matrix.target }} - name: Stage Agent binary shell: bash run: | mkdir -p release cp "target/agent/${{ matrix.target }}/release/nodeflare-agent${{ matrix.extension }}" "release/${{ matrix.asset }}" chmod +x "release/${{ matrix.asset }}" - name: Install ARM64 emulator if: matrix.emulate run: | sudo apt-get update sudo apt-get install --yes --no-install-recommends qemu-user-static libc6-arm64-cross libgcc-s1-arm64-cross - name: Verify Agent version natively if: matrix.native shell: bash run: | version_output=$("release/${{ matrix.asset }}" --version) actual_version="${version_output##* }" if [ "$actual_version" != "${{ needs.metadata.outputs.version }}" ]; then echo "Agent version $actual_version does not match tag ${{ needs.metadata.outputs.version }}" >&2 exit 1 fi - name: Verify emulated Agent version if: matrix.emulate shell: bash run: | version_output=$(qemu-aarch64-static -L /usr/aarch64-linux-gnu "release/${{ matrix.asset }}" --version) actual_version="${version_output##* }" if [ "$actual_version" != "${{ needs.metadata.outputs.version }}" ]; then echo "Agent version $actual_version does not match tag ${{ needs.metadata.outputs.version }}" >&2 exit 1 fi - name: Verify static Linux binary if: matrix.static_linux shell: bash run: | if readelf -l "release/${{ matrix.asset }}" | grep -q 'Requesting program interpreter'; then echo "Linux Agent must be statically linked" >&2 exit 1 fi - name: Verify glibc Agent binary if: endsWith(matrix.target, '-unknown-linux-gnu') shell: bash run: | readelf -l "release/${{ matrix.asset }}" | grep -q 'Requesting program interpreter' scripts/check-glibc-version.sh "release/${{ matrix.asset }}" 2.28 - name: Verify FreeBSD Agent in FreeBSD if: endsWith(matrix.target, '-unknown-freebsd') uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1.5.5 with: release: "15" arch: ${{ matrix.target == 'aarch64-unknown-freebsd' && 'aarch64' || 'x86_64' }} envs: NODEFLARE_VERSION copyback: false run: | set -eu version_output=$(./release/${{ matrix.asset }} --version) actual_version=${version_output##* } if [ "$actual_version" != "$NODEFLARE_VERSION" ]; then echo "Agent version $actual_version does not match tag $NODEFLARE_VERSION" >&2 exit 1 fi - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: subject-path: release/${{ matrix.asset }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.asset }} path: release/${{ matrix.asset }} if-no-files-found: error frontend: name: Frontend needs: metadata if: needs.metadata.outputs.release == 'true' runs-on: ubuntu-latest timeout-minutes: 15 permissions: attestations: write contents: read id-token: write env: NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} VITE_NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Install dependencies run: bun install --frozen-lockfile - name: Build public and admin frontends run: bun run build:frontend - name: Package frontend run: tar -czf "nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz" -C frontend dist admin-dist - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: subject-path: nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: frontend path: nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz if-no-files-found: error server-bundle: name: Server bundle (${{ matrix.platform }}) needs: [metadata, server, frontend] if: needs.metadata.outputs.release == 'true' runs-on: ubuntu-latest timeout-minutes: 10 permissions: attestations: write contents: read id-token: write strategy: fail-fast: false matrix: include: - platform: Linux x64 glibc 2.28+ binary: nodeflare-linux-x64-glibc package_binary: nodeflare asset: nodeflare-server-linux-x64-glibc.tar.gz format: tar - platform: Linux x64 musl binary: nodeflare-linux-x64-musl package_binary: nodeflare asset: nodeflare-server-linux-x64-musl.tar.gz format: tar - platform: Linux ARM64 glibc 2.28+ binary: nodeflare-linux-aarch64-glibc package_binary: nodeflare asset: nodeflare-server-linux-aarch64-glibc.tar.gz format: tar - platform: Linux ARM64 musl binary: nodeflare-linux-aarch64-musl package_binary: nodeflare asset: nodeflare-server-linux-aarch64-musl.tar.gz format: tar - platform: Windows x64 binary: nodeflare-windows-x64.exe package_binary: nodeflare.exe asset: nodeflare-server-windows-x64.zip format: zip - platform: macOS ARM64 binary: nodeflare-macos-aarch64 package_binary: nodeflare asset: nodeflare-server-macos-aarch64.tar.gz format: tar - platform: FreeBSD x64 binary: nodeflare-freebsd-x64 package_binary: nodeflare asset: nodeflare-server-freebsd-x64.tar.gz format: tar - platform: FreeBSD ARM64 binary: nodeflare-freebsd-aarch64 package_binary: nodeflare asset: nodeflare-server-freebsd-aarch64.tar.gz format: tar steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.binary }} path: inputs/server - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend path: inputs/frontend - name: Install ZIP tool if: matrix.format == 'zip' run: | sudo apt-get update sudo apt-get install --yes --no-install-recommends zip unzip - name: Package server shell: bash env: VERSION: ${{ needs.metadata.outputs.version }} BINARY: ${{ matrix.binary }} PACKAGE_BINARY: ${{ matrix.package_binary }} ASSET: ${{ matrix.asset }} FORMAT: ${{ matrix.format }} run: | set -euo pipefail mkdir -p package/share/frontend package/share/admin release inputs/frontend/unpacked cp "inputs/server/$BINARY" "package/$PACKAGE_BINARY" tar -xzf "inputs/frontend/nodeflare-frontend-$VERSION.tar.gz" -C inputs/frontend/unpacked cp -a inputs/frontend/unpacked/dist/. package/share/frontend/ cp -a inputs/frontend/unpacked/admin-dist/. package/share/admin/ cp docs/nodeflare.service package/nodeflare.service cp LICENSE package/LICENSE chmod 0755 "package/$PACKAGE_BINARY" chmod 0644 package/LICENSE package/nodeflare.service if [ "$FORMAT" = zip ]; then (cd package && zip -qr "../release/$ASSET" .) unzip -tq "release/$ASSET" else tar -czf "release/$ASSET" -C package . tar -tzf "release/$ASSET" >/dev/null fi - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: subject-path: release/${{ matrix.asset }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: server-bundle-${{ matrix.binary }} path: release/${{ matrix.asset }} if-no-files-found: error release: name: Publish GitHub Release if: startsWith(github.ref, 'refs/tags/v') needs: [metadata, security, quality, agent, server-bundle] runs-on: ubuntu-latest timeout-minutes: 10 permissions: actions: read contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Build changelog from commits shell: bash run: | previous_tag=$(git describe --tags --abbrev=0 "${GITHUB_SHA}^" 2>/dev/null || true) if [ -n "$previous_tag" ]; then commit_range="${previous_tag}..${GITHUB_SHA}" else commit_range="${GITHUB_SHA}" fi git log "$commit_range" --no-merges --pretty=format:'- %s' > CHANGELOG.md if [ ! -s CHANGELOG.md ]; then git log -1 --pretty=format:'- %s' "$GITHUB_SHA" > CHANGELOG.md fi printf '\n' >> CHANGELOG.md - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: agent-* path: release-assets merge-multiple: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: server-bundle-* path: release-assets merge-multiple: true - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.3 with: body_path: CHANGELOG.md files: release-assets/* fail_on_unmatched_files: true