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 # Cache node_modules keyed on the lockfile + Node version: an unchanged # lockfile skips `npm ci` (extraction + postinstall folds patch) and just # restores the tree. Save runs only on a miss AND only if install # succeeded (`success()`), so a failed `npm ci` can't poison the cache. # The act_runner cache server is reached over the dedicated `act-cache-net` # docker network (runner cache.host 172.30.0.2) — jobs previously landed on # isolated per-job networks and ETIMEDOUT on it (~5 min wasted per build). - name: Restore node_modules id: node-modules uses: actions/cache/restore@v4 with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json', '.node-version') }} - name: Install dependencies if: steps.node-modules.outputs.cache-hit != 'true' # 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 - name: Save node_modules if: steps.node-modules.outputs.cache-hit != 'true' && success() uses: actions/cache/save@v4 with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json', '.node-version') }} # ── Quality gates run BEFORE the slow build so a format/lint/type/test # error fails in seconds instead of after the ~minutes-long build. All are # hard gates — any failure fails the job and blocks the deploy. The tree is # held clean (prettier formatted, eslint 0 errors, typecheck 0), so these # gate real regressions. NOTE: the lotus-build.sh upstream-merge path can # deploy without CI; a later normal push surfaces any introduced issue here # — fix forward (or briefly re-soften a gate) rather than deploy broken. # eslint gates on errors only (existing no-explicit-any warnings stay # informational — check:eslint has no --max-warnings). - name: Prettier run: npm run check:prettier - name: ESLint run: npm run check:eslint - name: TypeScript run: npm run typecheck # 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 # ── Critical gate — if this fails, nothing deploys. Produces dist/. ── - name: Build run: npm run build env: NODE_OPTIONS: '--max_old_space_size=4096' VITE_APP_VERSION: ${{ github.sha }} # ── 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