docs: update and improve PortOS documentation #12122
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main, release] | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: '17 9 * * *' | |
| workflow_dispatch: | |
| workflow_call: | |
| inputs: | |
| full: | |
| description: Run the complete server, client, DB, lint, build, and smoke suite | |
| required: false | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| impact: | |
| name: Plan test impact | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]') | |
| outputs: | |
| full: ${{ steps.plan.outputs.full }} | |
| reason: ${{ steps.plan.outputs.reason }} | |
| server_mode: ${{ steps.plan.outputs.server_mode }} | |
| server_files: ${{ steps.plan.outputs.server_files }} | |
| client_mode: ${{ steps.plan.outputs.client_mode }} | |
| client_files: ${{ steps.plan.outputs.client_files }} | |
| db: ${{ steps.plan.outputs.db }} | |
| lint_mode: ${{ steps.plan.outputs.lint_mode }} | |
| lint_files: ${{ steps.plan.outputs.lint_files }} | |
| build: ${{ steps.plan.outputs.build }} | |
| smoke: ${{ steps.plan.outputs.smoke }} | |
| windows: ${{ steps.plan.outputs.windows }} | |
| windows_mode: ${{ steps.plan.outputs.windows_mode }} | |
| windows_files: ${{ steps.plan.outputs.windows_files }} | |
| server_native: ${{ steps.plan.outputs.server_native }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Select affected test surfaces | |
| id: plan | |
| env: | |
| CI_FORCE_FULL: ${{ github.event_name != 'pull_request' || inputs.full }} | |
| CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| CI_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: node scripts/ci-test-plan.js | |
| - name: Publish plan summary | |
| env: | |
| PLAN_REASON: ${{ steps.plan.outputs.reason }} | |
| SERVER_MODE: ${{ steps.plan.outputs.server_mode }} | |
| CLIENT_MODE: ${{ steps.plan.outputs.client_mode }} | |
| DB_MODE: ${{ steps.plan.outputs.db }} | |
| LINT_MODE: ${{ steps.plan.outputs.lint_mode }} | |
| BUILD_MODE: ${{ steps.plan.outputs.build }} | |
| SMOKE_MODE: ${{ steps.plan.outputs.smoke }} | |
| WINDOWS_MODE: ${{ steps.plan.outputs.windows }} | |
| WINDOWS_TEST_MODE: ${{ steps.plan.outputs.windows_mode }} | |
| SERVER_NATIVE: ${{ steps.plan.outputs.server_native }} | |
| run: | | |
| { | |
| echo "### CI impact plan" | |
| echo | |
| echo "- Reason: \`${PLAN_REASON}\`" | |
| echo "- Server tests: \`${SERVER_MODE}\`" | |
| echo "- Client tests: \`${CLIENT_MODE}\`" | |
| echo "- DB tests: \`${DB_MODE}\`" | |
| echo "- Client lint: \`${LINT_MODE}\`" | |
| echo "- Client build: \`${BUILD_MODE}\`" | |
| echo "- Server smoke: \`${SMOKE_MODE}\`" | |
| echo "- Windows server tests: \`${WINDOWS_MODE}\` (\`${WINDOWS_TEST_MODE}\`)" | |
| echo "- Server native rebuild: \`${SERVER_NATIVE}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| server: | |
| # Preserve the historical required-check context while branch protection | |
| # transitions to the stable aggregate "CI Gate" check. | |
| name: test (24.x) | |
| needs: impact | |
| if: needs.impact.outputs.server_mode != 'skip' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Use Node.js 24.x | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| cache-dependency-path: server/package-lock.json | |
| - name: Install server dependencies | |
| run: npm ci --prefix server | |
| # After npm ci: it wipes node_modules, so a pre-install restore is lost. | |
| # Vite/Vitest write transform artifacts under node_modules/.vite (and | |
| # historically .vitest). Reusing them skips re-transforming unchanged files. | |
| - name: Cache Vitest transform artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| server/node_modules/.vite | |
| server/node_modules/.vitest | |
| key: vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json', 'server/vitest.config.js', 'scripts/vitestCiPool.js') }} | |
| restore-keys: | | |
| vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json') }}- | |
| vitest-server-${{ runner.os }}- | |
| # server/.npmrc pins ignore-scripts=true, so the native deps npm skipped | |
| # need the explicit allowlist rebuild to be usable (node-pty et al). | |
| # Always-run-only plans (prompt-upgrade + changelog fragment guards) | |
| # import no native addons, so skip the rebuild there. | |
| - name: Rebuild trusted native dependencies | |
| if: needs.impact.outputs.server_native == 'true' | |
| run: node scripts/trusted-rebuilds.js server | |
| - name: Check server entry-point syntax | |
| run: node --check server/index.js | |
| - name: Run server tests | |
| if: needs.impact.outputs.server_mode != 'skip' | |
| env: | |
| CI_TEST_MODE: ${{ needs.impact.outputs.server_mode }} | |
| CI_TEST_FILES: ${{ needs.impact.outputs.server_files }} | |
| CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: node scripts/run-ci-tests.js server | |
| # Smoke-boot does not need Postgres: NODE_ENV=test selects the file | |
| # backend. It does need the native rebuild (server boot loads node-pty), | |
| # which is why it lives on this job rather than a third installer. | |
| - name: Smoke-boot server | |
| if: needs.impact.outputs.smoke == 'true' | |
| run: npm run smoke | |
| client: | |
| name: Client tests and build | |
| needs: impact | |
| if: needs.impact.outputs.client_mode != 'skip' || needs.impact.outputs.build == 'true' || needs.impact.outputs.lint_mode != 'skip' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Use Node.js 24.x | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| cache-dependency-path: client/package-lock.json | |
| - name: Install client dependencies | |
| run: npm ci --prefix client | |
| - name: Cache Vitest transform artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| client/node_modules/.vite | |
| client/node_modules/.vitest | |
| key: vitest-client-${{ runner.os }}-${{ hashFiles('client/package-lock.json', 'client/vitest.config.js', 'scripts/vitestCiPool.js') }} | |
| restore-keys: | | |
| vitest-client-${{ runner.os }}-${{ hashFiles('client/package-lock.json') }}- | |
| vitest-client-${{ runner.os }}- | |
| - name: Lint client | |
| if: needs.impact.outputs.lint_mode != 'skip' | |
| env: | |
| CI_LINT_MODE: ${{ needs.impact.outputs.lint_mode }} | |
| CI_LINT_FILES: ${{ needs.impact.outputs.lint_files }} | |
| run: node scripts/run-ci-lint.js | |
| - name: Run client tests | |
| if: needs.impact.outputs.client_mode != 'skip' | |
| env: | |
| CI_TEST_MODE: ${{ needs.impact.outputs.client_mode }} | |
| CI_TEST_FILES: ${{ needs.impact.outputs.client_files }} | |
| CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: node scripts/run-ci-tests.js client | |
| - name: Build client | |
| if: needs.impact.outputs.build == 'true' | |
| run: npm run build --prefix client | |
| database: | |
| name: DB tests | |
| needs: impact | |
| if: needs.impact.outputs.db == 'true' | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg17 | |
| env: | |
| POSTGRES_USER: portos | |
| POSTGRES_PASSWORD: portos | |
| POSTGRES_DB: portos | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U portos" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| PGHOST: localhost | |
| PGPORT: 5432 | |
| PGUSER: portos | |
| PGPASSWORD: portos | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Use Node.js 24.x | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| cache-dependency-path: server/package-lock.json | |
| - name: Install server dependencies | |
| run: npm ci --prefix server | |
| - name: Cache Vitest transform artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| server/node_modules/.vite | |
| server/node_modules/.vitest | |
| key: vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json', 'server/vitest.config.js', 'server/vitest.config.db.js', 'scripts/vitestCiPool.js') }} | |
| restore-keys: | | |
| vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json') }}- | |
| vitest-server-${{ runner.os }}- | |
| # server/.npmrc pins ignore-scripts=true — DB suites that touch sharp / | |
| # node-pty still need the explicit allowlist rebuild. | |
| - name: Rebuild trusted native dependencies | |
| run: node scripts/trusted-rebuilds.js server | |
| - name: Provision test database (portos_test) | |
| run: npm run setup:db:test --prefix server | |
| - name: Run DB-backed tests (portos_test) | |
| run: | | |
| set +e | |
| start=$(date +%s) | |
| npm run test:db:ci --prefix server | |
| status=$? | |
| elapsed=$(( $(date +%s) - start )) | |
| echo "⏱ server db suite: ${elapsed}s" | tee -a "$GITHUB_STEP_SUMMARY" | |
| exit $status | |
| env: | |
| PGDATABASE: portos_test | |
| lint: | |
| # Historical required-check name. Lint itself now runs inside the client | |
| # job so we do not pay a second `npm ci --prefix client`. This job only | |
| # mirrors that result so branch protection can keep requiring "lint". | |
| name: lint | |
| needs: [impact, client] | |
| if: always() && needs.impact.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require client-job lint when it was selected | |
| if: needs.impact.outputs.lint_mode != 'skip' | |
| env: | |
| CLIENT_RESULT: ${{ needs.client.result }} | |
| run: | | |
| if [ "$CLIENT_RESULT" != "success" ]; then | |
| echo "Lint ran inside the client job, which did not succeed ($CLIENT_RESULT)." | |
| exit 1 | |
| fi | |
| - name: Lint not selected | |
| if: needs.impact.outputs.lint_mode == 'skip' | |
| run: echo "Lint skipped by impact plan" | |
| windows-server: | |
| name: Windows server unit tests | |
| needs: impact | |
| if: needs.impact.outputs.windows == 'true' | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Use Node.js 24.x | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| cache-dependency-path: server/package-lock.json | |
| - name: Install server dependencies | |
| run: npm ci --prefix server | |
| - name: Cache Vitest transform artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| server/node_modules/.vite | |
| server/node_modules/.vitest | |
| key: vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json', 'server/vitest.config.js', 'scripts/vitestCiPool.js') }} | |
| restore-keys: | | |
| vitest-server-${{ runner.os }}-${{ hashFiles('server/package-lock.json') }}- | |
| vitest-server-${{ runner.os }}- | |
| - name: Rebuild trusted native dependencies | |
| run: node scripts/trusted-rebuilds.js server | |
| - name: Check server entry-point syntax | |
| run: node --check server/index.js | |
| - name: Run server tests on Windows | |
| env: | |
| CI_TEST_MODE: ${{ needs.impact.outputs.windows_mode }} | |
| CI_TEST_FILES: ${{ needs.impact.outputs.windows_files }} | |
| CI_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: node scripts/run-ci-tests.js server | |
| gate: | |
| name: CI Gate | |
| if: always() | |
| needs: [impact, server, client, database, lint, windows-server] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require every selected job to pass | |
| env: | |
| IMPACT_RESULT: ${{ needs.impact.result }} | |
| SERVER_RESULT: ${{ needs.server.result }} | |
| CLIENT_RESULT: ${{ needs.client.result }} | |
| DATABASE_RESULT: ${{ needs.database.result }} | |
| LINT_RESULT: ${{ needs.lint.result }} | |
| WINDOWS_SERVER_RESULT: ${{ needs.windows-server.result }} | |
| run: | | |
| node -e ' | |
| const results = { | |
| impact: process.env.IMPACT_RESULT, | |
| server: process.env.SERVER_RESULT, | |
| client: process.env.CLIENT_RESULT, | |
| database: process.env.DATABASE_RESULT, | |
| lint: process.env.LINT_RESULT, | |
| windows_server: process.env.WINDOWS_SERVER_RESULT, | |
| }; | |
| const failed = Object.entries(results) | |
| .filter(([, result]) => !["success", "skipped"].includes(result)); | |
| if (failed.length) { | |
| console.error("Selected CI jobs did not pass:", failed); | |
| process.exit(1); | |
| } | |
| console.log("CI gate passed:", results); | |
| ' |