name: CI on: pull_request: branches: [main] push: branches: [main] workflow_dispatch: inputs: bump: description: Version bump type (only for workflow_dispatch) type: choice options: - patch - minor - major default: patch publish_npm: description: Publish to npm type: boolean default: true publish_crates: description: Publish to crates.io type: boolean default: true env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true permissions: contents: write jobs: changes: name: Changes runs-on: [self-hosted, netty] outputs: rust: ${{ steps.check.outputs.rust }} version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: dorny/paths-filter@v3 id: filter with: filters: | rust: - 'src/**' - 'tests/**' - 'Cargo.toml' - 'Cargo.lock' - 'npm/**' - 'flake.nix' - 'flake.lock' - 'docker/**' - '.github/workflows/**' - 'Makefile' - name: Set outputs id: check run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "rust=true" >> "$GITHUB_OUTPUT" else echo "rust=${{ steps.filter.outputs.rust }}" >> "$GITHUB_OUTPUT" fi - name: Calculate next version id: version if: github.event_name != 'pull_request' && steps.check.outputs.rust == 'true' run: | CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" BUMP="${{ inputs.bump || 'patch' }}" case "$BUMP" in major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; minor) MINOR=$((MINOR + 1)); PATCH=0 ;; patch) LATEST=$(git tag -l "v${MAJOR}.${MINOR}.*" | sort -V | tail -1) if [ -z "$LATEST" ]; then NEW_PATCH=$PATCH else LATEST_VER="${LATEST#v}" IFS='.' read -r _ _ LATEST_PATCH <<< "$LATEST_VER" NEW_PATCH=$((LATEST_PATCH + 1)) fi PATCH=$NEW_PATCH ;; esac NEW="${MAJOR}.${MINOR}.${PATCH}" echo "version=${NEW}" >> "$GITHUB_OUTPUT" echo "tag=v${NEW}" >> "$GITHUB_OUTPUT" echo "Computed version: ${NEW} (v${NEW})" validate: name: Validate needs: changes if: needs.changes.outputs.rust == 'true' runs-on: [self-hosted, netty] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - uses: pnpm/action-setup@v4 with: version: 10 run_install: false - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm cache-dependency-path: site/pnpm-lock.yaml - name: Install site dependencies run: pnpm --dir site install --frozen-lockfile - name: Format check run: make fmt-check - name: Clippy run: make lint - name: Unit tests run: make test-unit - name: Site format check run: make site-format-check integration: name: Integration (Xvfb) needs: changes if: needs.changes.outputs.rust == 'true' runs-on: [self-hosted, netty] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Xvfb integration tests run: make test-integration distribution: name: Distribution Validate needs: changes if: needs.changes.outputs.rust == 'true' runs-on: [self-hosted, netty] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: actions/setup-node@v4 with: node-version: 22 - name: Distribution validation run: make dist-validate # --- Release pipeline: update-manifests -> build -> release -> publish --- # These stay on ubuntu-latest for artifact upload/download and registry publishing. update-manifests: name: Update Manifests needs: [changes, validate, integration, distribution] if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable - uses: actions/setup-node@v4 with: node-version: 22 - name: Update versions run: | CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') NEW="${{ needs.changes.outputs.version }}" if [ "$CURRENT" != "$NEW" ]; then sed -i "0,/^version = \"${CURRENT}\"/s//version = \"${NEW}\"/" Cargo.toml cargo generate-lockfile fi node -e ' const fs = require("node:fs"); const p = "npm/deskctl/package.json"; const pkg = JSON.parse(fs.readFileSync(p, "utf8")); pkg.version = process.argv[1]; fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n"); ' "$NEW" - name: Commit, tag, and push run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Cargo.toml Cargo.lock npm/deskctl/package.json if ! git diff --cached --quiet; then git commit -m "release: ${{ needs.changes.outputs.tag }} [skip ci]" fi git tag "${{ needs.changes.outputs.tag }}" git push origin main --tags build: name: Build Release Asset needs: [changes, update-manifests] if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ needs.changes.outputs.tag }} - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev - name: Verify version run: | CARGO_VER=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') EXPECTED="${{ needs.changes.outputs.version }}" if [ "$CARGO_VER" != "$EXPECTED" ]; then echo "Version mismatch: Cargo.toml=$CARGO_VER expected=$EXPECTED" exit 1 fi echo "Building version $CARGO_VER" - name: Clippy run: cargo clippy -- -D warnings - name: Build run: cargo build --release --locked - uses: actions/upload-artifact@v4 with: name: deskctl-linux-x86_64 path: target/release/deskctl retention-days: 7 release: name: Release needs: [changes, build, update-manifests] if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: deskctl-linux-x86_64 path: artifacts/ - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chmod +x artifacts/deskctl mv artifacts/deskctl artifacts/deskctl-linux-x86_64 cd artifacts && sha256sum deskctl-linux-x86_64 > checksums.txt && cd .. if gh release view "${{ needs.changes.outputs.tag }}" >/dev/null 2>&1; then gh release upload "${{ needs.changes.outputs.tag }}" \ artifacts/deskctl-linux-x86_64 \ artifacts/checksums.txt \ --clobber else gh release create "${{ needs.changes.outputs.tag }}" \ --title "${{ needs.changes.outputs.tag }}" \ --generate-notes \ artifacts/deskctl-linux-x86_64 \ artifacts/checksums.txt fi publish-npm: name: Publish npm needs: [changes, update-manifests, release] if: >- github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' && (inputs.publish_npm == true || inputs.publish_npm == '') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ needs.changes.outputs.tag }} - uses: actions/setup-node@v4 with: node-version: 22 registry-url: https://registry.npmjs.org - name: Check if already published id: published run: | VERSION="${{ needs.changes.outputs.version }}" if npm view "deskctl@${VERSION}" version >/dev/null 2>&1; then echo "npm=true" >> "$GITHUB_OUTPUT" else echo "npm=false" >> "$GITHUB_OUTPUT" fi - name: Validate npm package if: steps.published.outputs.npm != 'true' run: node npm/deskctl/scripts/validate-package.js - name: Publish npm if: steps.published.outputs.npm != 'true' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: npm publish ./npm/deskctl --access public publish-crates: name: Publish crates.io needs: [changes, update-manifests, release] if: >- github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true' && (inputs.publish_crates == true || inputs.publish_crates == '') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ needs.changes.outputs.tag }} - uses: dtolnay/rust-toolchain@stable - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev - name: Check if already published id: published run: | VERSION="${{ needs.changes.outputs.version }}" if curl -fsSL "https://crates.io/api/v1/crates/deskctl/${VERSION}" >/dev/null 2>&1; then echo "crates=true" >> "$GITHUB_OUTPUT" else echo "crates=false" >> "$GITHUB_OUTPUT" fi - name: Publish crates.io if: steps.published.outputs.crates != 'true' env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish --locked