name: CI on: push: branches: [lotus] pull_request: branches: [lotus] # Only the newest commit per ref needs to build: a superseded push cancels its # in-flight run. This keeps the shared act_runner free (web CI otherwise queues # behind long Tauri desktop builds) and — since `trigger-desktop` is `needs: # build` — means only the latest lotus commit ever kicks a desktop build, # instead of one per rapid push. Cancelling a superseded run is deploy-safe # ONLY because lotus_deploy.sh re-resolves origin/lotus each poll iteration and # retargets its CI gate to HEAD — otherwise a run cancelled mid-poll would # strand the newest commit undeployed. Keep those two in sync. concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: name: Build & Quality Checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: npm - name: Install dependencies # Harden against transient registry network failures (ECONNRESET etc.): # raise npm's built-in fetch retries/timeouts and retry `npm ci` up to # 3 times with backoff before failing the build. run: | npm config set fetch-retries 5 npm config set fetch-retry-mintimeout 20000 npm config set fetch-retry-maxtimeout 120000 npm config set fetch-timeout 600000 for attempt in 1 2 3; do echo "npm ci attempt $attempt…" npm ci && break if [ "$attempt" = "3" ]; then echo "npm ci failed after 3 attempts" >&2 exit 1 fi echo "npm ci failed; retrying in $((attempt * 15))s…" >&2 sleep $((attempt * 15)) done # ── Critical gate — if this fails, nothing deploys ────────────────── - name: Build run: npm run build env: NODE_OPTIONS: '--max_old_space_size=4096' VITE_APP_VERSION: ${{ github.sha }} # Unit tests are a hard gate too — deterministic pure-logic tests on Node's # built-in runner via tsx (no vitest — Vite 8 is ahead of vitest's range). # A failure blocks the deploy. - name: Unit tests run: npm test # ── Quality gates (hard — a failure fails the job and blocks deploy) ── # The tree is held clean (typecheck 0, eslint 0 errors, prettier # formatted), so these gate real regressions instead of relying on local # runs. NOTE: an upstream-stable merge (the lotus-build.sh path) could # introduce upstream type/lint/format issues; that path deploys without # CI, but a subsequent normal push would surface the failure here — fix # forward (or briefly re-soften a gate) rather than let it deploy broken. # eslint gates on errors only (existing `no-explicit-any` warnings stay # informational — `check:eslint` has no --max-warnings). - name: TypeScript run: npm run typecheck - name: ESLint run: npm run check:eslint - name: Prettier run: npm run check:prettier # ── Security (informational — findings shouldn't block a deploy) ───── - name: Audit (high/critical) run: npm audit --audit-level=high --omit=dev continue-on-error: true # ── Bundle size report (informational — never blocks a deploy) ─────── - name: Report bundle sizes continue-on-error: true run: | echo "### Bundle sizes" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| File | Size | Gzip |" >> $GITHUB_STEP_SUMMARY echo "|------|------|------|" >> $GITHUB_STEP_SUMMARY find dist/assets -name "*.js" -not -name "*.map" | sort | while read f; do name=$(basename "$f") size=$(du -sh "$f" | cut -f1) gzip_size=$(gzip -c "$f" | wc -c | awk '{printf "%.1f kB", $1/1024}') echo "| $name | $size | $gzip_size |" >> $GITHUB_STEP_SUMMARY done # ── Desktop build trigger ────────────────────────────────────────────── # Gated on `build` succeeding so a broken push (e.g. failing `npm ci` or # `npm run build`) never bumps the cinny-desktop submodule and kicks off the # slow Tauri release builds, which would only error out downstream. Only # runs on a real push to lotus — not on pull_request CI runs. trigger-desktop: name: Trigger Desktop Build needs: build if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/lotus' }} runs-on: ubuntu-latest steps: - name: Bump cinny submodule env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | CINNY_SHA="${{ github.sha }}" git clone "https://x-access-token:$TOKEN@code.lotusguild.org/LotusGuild/cinny-desktop.git" desktop cd desktop git config user.email "ci@lotusguild.org" git config user.name "Lotus CI" git submodule update --init cinny git -C cinny fetch origin git -C cinny checkout "$CINNY_SHA" git add cinny if git diff --cached --quiet; then echo "Submodule already at $CINNY_SHA, nothing to do" else git commit -m "chore: bump cinny submodule to ${CINNY_SHA:0:8}" git push origin main echo "Pushed — cinny-desktop release.yml will start via on:push trigger" fi