mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
459 lines
18 KiB
YAML
459 lines
18 KiB
YAML
name: CI
|
|
run-name: >-
|
|
${{ (github.event_name == 'push' && format('CI - {0} ({1})', github.event.head_commit.message, github.ref_name)) || (github.event_name == 'pull_request' && format('CI - PR #{0}: {1}', github.event.pull_request.number, github.event.pull_request.title)) || 'CI - manual' }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
pull_request:
|
|
branches: [main]
|
|
push:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24"
|
|
PNPM_VERSION: "10"
|
|
|
|
jobs:
|
|
dependency-review:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
- name: Dependency review
|
|
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
|
|
|
detect-changes:
|
|
name: Detect Changed Paths
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review]
|
|
timeout-minutes: 10
|
|
if: always() && (needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped')
|
|
outputs:
|
|
server: ${{ steps.manual.outputs.server || steps.changes.outputs.server }}
|
|
client: ${{ steps.manual.outputs.client || steps.changes.outputs.client }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Manual dispatch runs all scopes
|
|
id: manual
|
|
if: github.event_name == 'workflow_dispatch'
|
|
shell: bash
|
|
run: |
|
|
echo "server=true" >> "$GITHUB_OUTPUT"
|
|
echo "client=true" >> "$GITHUB_OUTPUT"
|
|
|
|
# For push events dorny/paths-filter diffs github.event.before..github.sha.
|
|
# New-branch pushes (before = 0000...) are handled by treating all files as changed.
|
|
# Force-push edge cases (before SHA no longer reachable) may mark all files changed too.
|
|
- name: Compute changed scopes
|
|
id: changes
|
|
if: github.event_name != 'workflow_dispatch'
|
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
|
with:
|
|
filters: |
|
|
server:
|
|
- 'server/**'
|
|
- 'packages/**'
|
|
- 'package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'pnpm-workspace.yaml'
|
|
- '.github/actions/setup-node-pnpm/action.yml'
|
|
- '.github/workflows/ci.yml'
|
|
client:
|
|
- 'client/**'
|
|
- 'packages/**'
|
|
- 'package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'pnpm-workspace.yaml'
|
|
- '.github/actions/setup-node-pnpm/action.yml'
|
|
- '.github/workflows/ci.yml'
|
|
|
|
- name: Print change scope
|
|
shell: bash
|
|
run: |
|
|
echo "server=${{ steps.manual.outputs.server || steps.changes.outputs.server }}"
|
|
echo "client=${{ steps.manual.outputs.client || steps.changes.outputs.client }}"
|
|
|
|
checks:
|
|
name: ${{ matrix.check }} (${{ matrix.package }})
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, detect-changes]
|
|
timeout-minutes: 20
|
|
# matrix context is not available in job-level if; per-package change filtering
|
|
# is done via the has-changes gate step below.
|
|
if: >
|
|
always() &&
|
|
(needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped') &&
|
|
needs.detect-changes.result == 'success'
|
|
strategy:
|
|
# All matrix cells run independently so all failures are visible in one pass.
|
|
fail-fast: false
|
|
matrix:
|
|
package: [server, client]
|
|
check: [lint, typecheck, test]
|
|
permissions:
|
|
contents: read
|
|
# checks: write is only used by publish-unit-test-result-action in test matrix cells.
|
|
# Lint and typecheck cells receive this permission too due to the shared job definition.
|
|
checks: write
|
|
steps:
|
|
- name: Check for package changes
|
|
id: has-changes
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
SERVER_CHANGED: ${{ needs.detect-changes.outputs.server }}
|
|
CLIENT_CHANGED: ${{ needs.detect-changes.outputs.client }}
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" && "$SERVER_CHANGED" == "true" ]] || \
|
|
[[ "$PACKAGE" == "client" && "$CLIENT_CHANGED" == "true" ]]; then
|
|
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Checkout
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
- name: Set up Node.js and pnpm
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
uses: ./.github/actions/setup-node-pnpm
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
pnpm-version: ${{ env.PNPM_VERSION }}
|
|
|
|
- name: Run lint
|
|
if: steps.has-changes.outputs.run == 'true' && matrix.check == 'lint'
|
|
shell: bash
|
|
run: pnpm --filter "${{ matrix.package }}" lint:check
|
|
|
|
- name: Run typecheck
|
|
if: steps.has-changes.outputs.run == 'true' && matrix.check == 'typecheck'
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" ]]; then
|
|
pnpm --filter server type-check
|
|
else
|
|
pnpm --filter @projectx/types build
|
|
pnpm --filter client type-check
|
|
fi
|
|
|
|
- name: Prepare test report directory
|
|
if: steps.has-changes.outputs.run == 'true' && matrix.check == 'test'
|
|
shell: bash
|
|
run: mkdir -p "$GITHUB_WORKSPACE/test-results/${{ matrix.package }}"
|
|
|
|
- name: Run tests
|
|
if: steps.has-changes.outputs.run == 'true' && matrix.check == 'test'
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
JUNIT_OUTPUT: ${{ github.workspace }}/test-results/${{ matrix.package }}/junit.xml
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" ]]; then
|
|
pnpm --filter server exec vitest --run --reporter=default --reporter=github-actions --reporter=junit --outputFile.junit="$JUNIT_OUTPUT"
|
|
else
|
|
pnpm --filter client exec vitest --run --reporter=default --reporter=junit --outputFile="$JUNIT_OUTPUT"
|
|
fi
|
|
|
|
- name: Publish test report annotations
|
|
if: >
|
|
always() &&
|
|
steps.has-changes.outputs.run == 'true' &&
|
|
matrix.check == 'test' &&
|
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
uses: EnricoMi/publish-unit-test-result-action@c950f6fb443cb5af20a377fd0dfaa78838901040 # v2
|
|
with:
|
|
check_name: ${{ matrix.package == 'server' && 'Server Unit Tests' || 'Client Unit Tests' }}
|
|
files: ${{ format('test-results/{0}/junit.xml', matrix.package) }}
|
|
fail_on: nothing
|
|
comment_mode: off
|
|
|
|
- name: Upload test report artifact
|
|
if: >
|
|
always() &&
|
|
steps.has-changes.outputs.run == 'true' &&
|
|
matrix.check == 'test' &&
|
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
with:
|
|
name: test-report-${{ matrix.package }}
|
|
path: test-results/${{ matrix.package }}
|
|
retention-days: 7
|
|
|
|
quality-gate:
|
|
name: Quality Gate
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, detect-changes, checks]
|
|
timeout-minutes: 5
|
|
if: always()
|
|
steps:
|
|
- name: Fail if any quality job failed
|
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
|
run: |
|
|
echo "One or more quality jobs failed."
|
|
exit 1
|
|
|
|
- name: Quality checks complete
|
|
run: |
|
|
echo "dependency-review=${{ needs.dependency-review.result }}"
|
|
echo "detect-changes=${{ needs.detect-changes.result }}"
|
|
echo "checks=${{ needs.checks.result }}"
|
|
|
|
e2e-setup:
|
|
name: Detect E2E Suites
|
|
runs-on: ubuntu-latest
|
|
needs: [quality-gate]
|
|
if: needs.quality-gate.result == 'success'
|
|
timeout-minutes: 5
|
|
outputs:
|
|
matrix: ${{ steps.build-matrix.outputs.matrix }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Detect path changes per suite
|
|
id: changes
|
|
if: github.event_name != 'workflow_dispatch'
|
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
|
with:
|
|
filters: |
|
|
workflow-changed:
|
|
- '.github/workflows/ci.yml'
|
|
- '.github/workflows/e2e.yml'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
- 'Dockerfile'
|
|
- 'docker-compose*.yml'
|
|
- 'docker/**'
|
|
app-smoke:
|
|
- 'server/src/**'
|
|
- 'server/test/app-smoke.e2e-spec.ts'
|
|
- 'server/src/db/schema/**'
|
|
- 'packages/types/src/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
auth-session-security:
|
|
- 'server/src/modules/auth/**'
|
|
- 'server/src/modules/user/**'
|
|
- 'server/src/modules/role/**'
|
|
- 'server/src/common/guards/**'
|
|
- 'server/src/common/decorators/**'
|
|
- 'server/src/common/types/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'server/test/auth-session-security.e2e-spec.ts'
|
|
- 'server/test/e2e/app-harness.ts'
|
|
- 'packages/types/src/auth.ts'
|
|
- 'packages/types/src/permissions.ts'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
auth-recovery-oidc-logout:
|
|
- 'server/src/modules/auth/**'
|
|
- 'server/src/modules/user/**'
|
|
- 'server/src/modules/email/**'
|
|
- 'server/src/modules/metadata/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'server/test/auth-recovery-oidc-logout.e2e-spec.ts'
|
|
- 'server/test/e2e/app-harness.ts'
|
|
- 'packages/types/src/auth.ts'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
authorization-matrix:
|
|
- 'server/src/**'
|
|
- 'server/test/authorization-matrix.e2e-spec.ts'
|
|
- 'server/test/e2e/authorization-matrix/**'
|
|
- 'packages/types/src/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
email-lifecycle:
|
|
- 'server/src/modules/email/**'
|
|
- 'server/src/modules/book/**'
|
|
- 'server/src/modules/library/**'
|
|
- 'server/src/modules/auth/**'
|
|
- 'server/src/modules/cover/**'
|
|
- 'server/src/modules/scanner/**'
|
|
- 'server/src/modules/seed/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'packages/types/src/**'
|
|
- 'server/test/email-lifecycle.e2e-spec.ts'
|
|
- 'server/test/e2e/email/**'
|
|
- 'server/test/e2e/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- 'client/src/features/email/**'
|
|
- 'client/src/router/index.ts'
|
|
- 'client/src/features/settings/SettingsHeader.vue'
|
|
- 'client/src/views/HomeView.vue'
|
|
- 'client/src/views/CollectionView.vue'
|
|
- 'client/src/views/LensView.vue'
|
|
- 'client/src/features/book/components/BookCoverCard.vue'
|
|
- 'client/src/features/book/components/BookListRow.vue'
|
|
metadata-write:
|
|
- 'server/src/modules/book/**'
|
|
- 'server/src/modules/library/**'
|
|
- 'server/src/modules/file-write/**'
|
|
- 'server/src/modules/metadata/**'
|
|
- 'server/src/modules/app-settings/**'
|
|
- 'server/test/metadata-write.e2e-spec.ts'
|
|
- 'server/test/e2e/metadata-write/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'packages/types/src/file-write.ts'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
opds-auth-catalog:
|
|
- 'server/src/modules/opds/**'
|
|
- 'server/src/modules/book/**'
|
|
- 'server/src/modules/library/**'
|
|
- 'server/src/modules/collection/**'
|
|
- 'server/src/modules/lens/**'
|
|
- 'server/src/modules/user/**'
|
|
- 'server/src/modules/auth/**'
|
|
- 'server/src/modules/app-settings/**'
|
|
- 'server/test/opds-auth-catalog.e2e-spec.ts'
|
|
- 'server/test/e2e/opds/**'
|
|
- 'server/test/e2e/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'packages/types/src/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
reader-state-isolation:
|
|
- 'server/src/modules/bookmark/**'
|
|
- 'server/src/modules/annotation/**'
|
|
- 'server/src/modules/reader-preferences/**'
|
|
- 'server/src/modules/reading-session/**'
|
|
- 'server/src/modules/book/**'
|
|
- 'server/src/modules/library/**'
|
|
- 'server/src/modules/scanner/**'
|
|
- 'server/src/modules/user-book-status/**'
|
|
- 'server/src/scripts/db-prepare-e2e.ts'
|
|
- 'server/test/reader-state-isolation.e2e-spec.ts'
|
|
- 'server/test/e2e/reader-state-isolation/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'packages/types/src/reader-settings.ts'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
scanner-file-operations:
|
|
- 'server/src/modules/scanner/**'
|
|
- 'server/test/scanner-file-operations.e2e-spec.ts'
|
|
- 'server/test/e2e/scanner/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
scanner-scenarios:
|
|
- 'server/src/modules/scanner/**'
|
|
- 'server/test/scanner-scenarios.e2e-spec.ts'
|
|
- 'server/test/e2e/scanner/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
staging-ingest-finalize:
|
|
- 'server/src/modules/staging/**'
|
|
- 'server/src/modules/upload/**'
|
|
- 'server/src/modules/metadata/**'
|
|
- 'server/test/staging-ingest-finalize.e2e-spec.ts'
|
|
- 'server/test/e2e/staging/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
users-admin-lifecycle:
|
|
- 'server/src/modules/user/**'
|
|
- 'server/src/modules/auth/**'
|
|
- 'server/src/common/guards/**'
|
|
- 'server/src/common/decorators/**'
|
|
- 'server/src/common/types/**'
|
|
- 'server/src/db/schema/**'
|
|
- 'server/test/users-admin-lifecycle.e2e-spec.ts'
|
|
- 'server/test/e2e/users-admin-lifecycle/**'
|
|
- 'packages/types/src/auth.ts'
|
|
- 'packages/types/src/permissions.ts'
|
|
- 'scripts/test/**'
|
|
- 'scripts/db/**'
|
|
- '.github/workflows/e2e-runner.yml'
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Build suite matrix
|
|
id: build-matrix
|
|
shell: bash
|
|
run: |
|
|
SUITES_DEF=$(node scripts/test/list-suites-json.mjs)
|
|
|
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
MATRIX="$SUITES_DEF"
|
|
elif [ "${{ steps.changes.outputs.workflow-changed }}" == "true" ]; then
|
|
MATRIX="$SUITES_DEF"
|
|
else
|
|
CHANGED='${{ steps.changes.outputs.changes }}'
|
|
if [ -z "$CHANGED" ] || [ "$CHANGED" == "[]" ]; then
|
|
MATRIX="[]"
|
|
else
|
|
MATRIX=$(echo "$SUITES_DEF" | jq --argjson changed "$CHANGED" \
|
|
'[.[] | select(.id as $id | $changed | index($id) != null)]')
|
|
fi
|
|
fi
|
|
|
|
echo "matrix=$(echo "$MATRIX" | jq -c .)" >> "$GITHUB_OUTPUT"
|
|
|
|
e2e:
|
|
needs: [e2e-setup]
|
|
if: needs.e2e-setup.outputs.matrix != '[]'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
suite: ${{ fromJSON(needs.e2e-setup.outputs.matrix) }}
|
|
permissions:
|
|
contents: read
|
|
checks: write
|
|
uses: ./.github/workflows/e2e-runner.yml
|
|
with:
|
|
suite-id: ${{ matrix.suite.id }}
|
|
check-name: "E2E - ${{ matrix.suite.name }}"
|
|
artifact-name: "e2e-${{ matrix.suite.id }}-report"
|
|
job-timeout-minutes: ${{ matrix.suite.timeout }}
|
|
|
|
ci-gate:
|
|
name: CI Gate
|
|
runs-on: ubuntu-latest
|
|
needs: [quality-gate, e2e-setup, e2e]
|
|
timeout-minutes: 5
|
|
if: always()
|
|
steps:
|
|
- name: Fail if any job failed or was cancelled
|
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
|
run: |
|
|
echo "One or more CI jobs failed or were cancelled."
|
|
exit 1
|
|
|
|
- name: CI complete
|
|
run: |
|
|
echo "quality-gate=${{ needs.quality-gate.result }}"
|
|
echo "e2e-setup=${{ needs.e2e-setup.result }}"
|
|
echo "e2e=${{ needs.e2e.result }}"
|