diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d920b4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,47 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + time: "03:00" + timezone: Asia/Shanghai + open-pull-requests-limit: 10 + groups: + minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch + dev-dependencies: + dependency-type: development + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "03:00" + timezone: Asia/Shanghai + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: docker + directory: /docker + schedule: + interval: weekly + day: monday + time: "03:00" + timezone: Asia/Shanghai + open-pull-requests-limit: 5 + groups: + docker: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ea6bbe..ed621bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,54 @@ name: CI on: push: branches: [main, master] - tags: ['v*'] + paths-ignore: + - 'docs/**' + - 'README.md' + - 'README_EN.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - 'CODE_OF_CONDUCT.md' pull_request: +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: 22 + jobs: - test-and-build: + changes: + name: Detect Docs Changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + outputs: + docs: ${{ steps.filter.outputs.docs }} + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Detect docs-related changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + docs: + - 'docs/**' + - 'README.md' + - 'README_EN.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - 'CODE_OF_CONDUCT.md' + - '.github/workflows/docs-pages.yml' + + test-core: + name: Test Core + if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 20 @@ -18,26 +61,241 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v5 with: - node-version: 22 + node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies - run: npm ci + run: npm ci --prefer-offline --no-audit --no-fund - name: Run tests + env: + DB_PARITY_SKIP_LIVE_SCHEMA: 'true' run: npm test - - name: Build - run: npm run build + build-web: + name: Build Web + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Build web + run: npm run build:web + + build-server: + name: Build Server + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Build server + run: npm run build:server + + build-desktop: + name: Build Desktop + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Build desktop + run: npm run build:desktop + + schema-sqlite: + name: Schema Check (SQLite) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Run sqlite schema parity + run: npm run test:schema:parity + + - name: Run sqlite schema upgrade + run: npm run test:schema:upgrade + + schema-mysql: + name: Schema Check (MySQL) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + mysql: + image: mysql:8.4 + env: + MYSQL_DATABASE: metapi + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + env: + DB_PARITY_MYSQL_URL: mysql://root:root@127.0.0.1:3306/metapi + DB_PARITY_SQLITE: 'false' + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Run mysql schema parity + run: npm run test:schema:parity + + - name: Run mysql schema upgrade + run: npm run test:schema:upgrade + + schema-postgres: + name: Schema Check (Postgres) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: metapi + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres -d metapi" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + env: + DB_PARITY_POSTGRES_URL: postgres://postgres:postgres@127.0.0.1:5432/metapi + DB_PARITY_SQLITE: 'false' + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Run postgres schema parity + run: npm run test:schema:parity + + - name: Run postgres schema upgrade + run: npm run test:schema:upgrade + + docs-build: + name: Build Docs + if: github.event_name == 'pull_request' && needs.changes.outputs.docs == 'true' + needs: changes + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund + + - name: Build docs + run: npm run docs:build + + audit: + name: Audit Production Dependencies + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + continue-on-error: true + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --no-fund - name: Audit production dependencies run: npm audit --omit=dev --audit-level=high publish-docker-arch: name: Publish Docker Image (${{ matrix.arch }}) + if: github.event_name == 'push' runs-on: ${{ matrix.runner }} - needs: test-and-build - if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') timeout-minutes: 45 strategy: fail-fast: false @@ -78,7 +336,6 @@ jobs: tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch - type=ref,event=tag type=sha,format=short - name: Add architecture suffix to tags @@ -111,9 +368,9 @@ jobs: publish-docker: name: Publish Docker Manifest + if: github.event_name == 'push' runs-on: ubuntu-latest needs: publish-docker-arch - if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') timeout-minutes: 15 steps: @@ -144,7 +401,6 @@ jobs: tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch - type=ref,event=tag type=sha,format=short - name: Create multi-arch manifests diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml index f78a418..3c5ded5 100644 --- a/.github/workflows/docs-pages.yml +++ b/.github/workflows/docs-pages.yml @@ -3,6 +3,11 @@ name: Docs Pages on: push: branches: [main] + paths: + - 'docs/**' + - '.github/workflows/docs-pages.yml' + - 'package.json' + - 'package-lock.json' workflow_dispatch: permissions: @@ -11,7 +16,7 @@ permissions: id-token: write concurrency: - group: docs-pages + group: docs-pages-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/src/server/db/schemaParity.live.test.ts b/src/server/db/schemaParity.live.test.ts index c3b0a02..25c862a 100644 --- a/src/server/db/schemaParity.live.test.ts +++ b/src/server/db/schemaParity.live.test.ts @@ -9,11 +9,13 @@ const dbDir = dirname(fileURLToPath(import.meta.url)); const schemaContractPath = resolve(dbDir, 'generated/schemaContract.json'); const contract = JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract; +const skipLiveSchema = process.env.DB_PARITY_SKIP_LIVE_SCHEMA === 'true'; +const sqliteParity = !skipLiveSchema && process.env.DB_PARITY_SQLITE !== 'false' ? it : it.skip; const mysqlParity = process.env.DB_PARITY_MYSQL_URL ? it : it.skip; const postgresParity = process.env.DB_PARITY_POSTGRES_URL ? it : it.skip; describe('live schema parity', () => { - it('matches the contract for sqlite', async () => { + sqliteParity('matches the contract for sqlite', async () => { const sqliteUrl = await materializeFreshSchema('sqlite'); const live = await introspectLiveSchema({ dialect: 'sqlite', connectionString: sqliteUrl }); expect(live).toEqual(contract); diff --git a/src/server/db/schemaUpgrade.live.test.ts b/src/server/db/schemaUpgrade.live.test.ts index f58378a..fe1c8e9 100644 --- a/src/server/db/schemaUpgrade.live.test.ts +++ b/src/server/db/schemaUpgrade.live.test.ts @@ -3,11 +3,13 @@ import currentContract from './generated/schemaContract.json' with { type: 'json import { applyContractFixtureThenUpgrade, introspectLiveSchema } from './schemaIntrospection.js'; import { describe, expect, it } from 'vitest'; +const skipLiveSchema = process.env.DB_PARITY_SKIP_LIVE_SCHEMA === 'true'; +const sqliteUpgrade = !skipLiveSchema && process.env.DB_PARITY_SQLITE !== 'false' ? it : it.skip; const mysqlUpgrade = process.env.DB_PARITY_MYSQL_URL ? it : it.skip; const postgresUpgrade = process.env.DB_PARITY_POSTGRES_URL ? it : it.skip; describe('schema upgrade parity', () => { - it('upgrades sqlite to the current contract', async () => { + sqliteUpgrade('upgrades sqlite to the current contract', async () => { const sqliteUrl = await applyContractFixtureThenUpgrade('sqlite', baselineContract, currentContract); const live = await introspectLiveSchema({ dialect: 'sqlite', connectionString: sqliteUrl }); expect(live).toEqual(currentContract);