name: CI and tagged release on: push: branches: - main tags: - "v*.*.*" pull_request: workflow_dispatch: inputs: version: description: Tag version with a v prefix, for example v0.0.1 required: true default: v0.0.1 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: ${{ github.event_name == 'workflow_dispatch' && inputs.version || startsWith(github.ref, 'refs/tags/') && github.ref_name || 'v0.0.0' }} RELEASE_BUILD: ${{ github.event_name == 'workflow_dispatch' || 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" quality: name: Quality checks needs: metadata runs-on: ubuntu-latest timeout-minutes: 30 env: NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: components: clippy, rustfmt targets: wasm32-unknown-unknown - 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 Rust formatting run: cargo fmt --all -- --check - name: Check Agent formatting run: cargo fmt --manifest-path agent/Cargo.toml -- --check - name: Lint Worker run: cargo clippy --locked --all-targets -- -D warnings - name: Lint Agent run: cargo clippy --locked --manifest-path agent/Cargo.toml --all-targets -- -D warnings - name: Test Worker run: cargo test --locked --lib - name: Test Agent run: cargo test --locked --manifest-path agent/Cargo.toml - name: Build Worker run: NODEFLARE_ADMIN_READY=1 sh scripts/build-worker.sh - name: Smoke test Worker and D1 shell: bash run: | set -euo pipefail state_dir="$RUNNER_TEMP/nodeflare-state" log_file="$RUNNER_TEMP/nodeflare-dev.log" export XDG_CONFIG_HOME="$RUNNER_TEMP/wrangler-config" mkdir -p "$XDG_CONFIG_HOME" bunx wrangler d1 migrations apply DB --local --persist-to "$state_dir" NODEFLARE_ADMIN_READY=1 NODEFLARE_WORKER_READY=1 \ bunx wrangler dev --local --persist-to "$state_dir" --port 8787 \ --var ADMIN_USERNAME:smoke --var ADMIN_PASSWORD:SmokePassword123 \ >"$log_file" 2>&1 & dev_pid=$! trap 'kill "$dev_pid" 2>/dev/null || true' EXIT ready=0 for _ in $(seq 1 30); do if curl --fail --silent http://127.0.0.1:8787/api/bootstrap >/dev/null; then ready=1 break fi sleep 1 done if [ "$ready" != 1 ]; then cat "$log_file" exit 1 fi if ! MONITOR_BASE_URL=http://127.0.0.1:8787 \ MONITOR_ADMIN_USERNAME=smoke MONITOR_ADMIN_PASSWORD=SmokePassword123 \ sh scripts/smoke-test.sh; then cat "$log_file" exit 1 fi 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 runner: ubuntu-latest target: x86_64-unknown-linux-musl asset: agent-linux-x64 extension: "" cross: true static_linux: true - platform: Linux ARM64 runner: ubuntu-latest target: aarch64-unknown-linux-musl asset: agent-linux-aarch64 extension: "" cross: 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 - platform: macOS ARM64 runner: macos-14 target: aarch64-apple-darwin asset: agent-macos-aarch64 extension: "" cross: false - platform: FreeBSD x64 runner: ubuntu-latest target: x86_64-unknown-freebsd asset: agent-freebsd-x64 extension: "" cross: 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 --version 0.2.5 --locked - 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 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.target == 'aarch64-unknown-linux-musl' run: | sudo apt-get update sudo apt-get install --yes --no-install-recommends qemu-user-static - name: Verify Linux Agent version if: matrix.cross && matrix.target == 'x86_64-unknown-linux-musl' 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 Linux ARM64 Agent version if: matrix.target == 'aarch64-unknown-linux-musl' shell: bash run: | version_output=$(qemu-aarch64-static "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 Agent version if: matrix.cross == false 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 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 FreeBSD Agent in FreeBSD if: matrix.target == 'x86_64-unknown-freebsd' uses: vmactions/freebsd-vm@83b151f58c6047089f4c80eb5ba2039d158ce093 # v1.5.3 with: release: "15" envs: NODEFLARE_VERSION copyback: false run: | set -eu version_output=$(./release/agent-freebsd-x64 --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 frontend run: bun run --cwd frontend build:public - name: Package frontend run: tar -czf "nodeflare-frontend-${{ needs.metadata.outputs.version }}.tar.gz" -C frontend/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 release: name: Publish GitHub Release if: startsWith(github.ref, 'refs/tags/v') needs: [metadata, quality, agent, frontend] 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: path: release-assets merge-multiple: true - uses: softprops/action-gh-release@fe965f7af51af5f2602596916f38a38df2e33de0 # v3.0.2 with: body_path: CHANGELOG.md files: release-assets/* fail_on_unmatched_files: true