mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fb3e5b96da
Bumps the github-actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [dorny/paths-filter](https://github.com/dorny/paths-filter) | `4.0.1` | `4.0.2` | | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.1.0` | `4.2.0` | | [docker/build-push-action](https://github.com/docker/build-push-action) | `7.2.0` | `7.3.0` | | [docker/login-action](https://github.com/docker/login-action) | `4.2.0` | `4.3.0` | | [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.1.0` | `4.2.0` | Updates `dorny/paths-filter` from 4.0.1 to 4.0.2 - [Release notes](https://github.com/dorny/paths-filter/releases) - [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md) - [Commits](https://github.com/dorny/paths-filter/compare/fbd0ab8f3e69293af611ebaee6363fc25e6d187d...7b450fff21473bca461d4b92ce414b9d0420d706) Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c) Updates `docker/build-push-action` from 7.2.0 to 7.3.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/f9f3042f7e2789586610d6e8b85c8f03e5195baf...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a) Updates `docker/login-action` from 4.2.0 to 4.3.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...c99871dec2022cc055c062a10cc1a1310835ceb4) Updates `docker/setup-qemu-action` from 4.1.0 to 4.2.0 - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/06116385d9baf250c9f4dcb4858b16962ea869c3...96fe6ef7f33517b61c61be40b68a1882f3264fb8) --- updated-dependencies: - dependency-name: dorny/paths-filter dependency-version: 4.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: docker/setup-buildx-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: docker/build-push-action dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: docker/login-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: docker/setup-qemu-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
738 lines
28 KiB
YAML
738 lines
28 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)) || (github.event_name == 'schedule' && 'CI - nightly') || 'CI - manual' }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
schedule:
|
|
- cron: "17 4 * * *"
|
|
pull_request:
|
|
branches: [main]
|
|
push:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24"
|
|
PNPM_VERSION: "10"
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
|
|
jobs:
|
|
dependency-review:
|
|
# Enable only when repository security analysis supports dependency review.
|
|
if: github.event_name == 'pull_request' && vars.ENABLE_DEPENDENCY_REVIEW == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
continue-on-error: true
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Dependency review
|
|
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
|
|
pr-conventions:
|
|
name: PR Conventions
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
issues: read
|
|
pull-requests: read
|
|
steps:
|
|
- name: Validate PR branch name
|
|
if: github.event.pull_request.user.login != 'dependabot[bot]'
|
|
shell: bash
|
|
env:
|
|
PR_BRANCH: ${{ github.head_ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
pattern='^BO-[0-9]+-[a-z0-9]+(-[a-z0-9]+)*$'
|
|
if [[ ! "$PR_BRANCH" =~ $pattern ]]; then
|
|
echo "::error::Branch name '$PR_BRANCH' is invalid."
|
|
echo "::error::Use BO-<issue-number>-<short-description> (example: BO-123-fix-reader-sync)."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Validate referenced issues exist
|
|
if: github.event.pull_request.user.login != 'dependabot[bot]'
|
|
shell: bash
|
|
env:
|
|
GH_REPO: ${{ github.repository }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_BRANCH: ${{ github.head_ref }}
|
|
PR_BODY: ${{ github.event.pull_request.body }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
validate_issue_exists() {
|
|
local issue_repo="$1"
|
|
local issue_number="$2"
|
|
local reference_context="$3"
|
|
local response_file
|
|
local http_code
|
|
local api_url
|
|
local state
|
|
local reference_label
|
|
|
|
response_file="$(mktemp)"
|
|
api_url="https://api.github.com/repos/${issue_repo}/issues/${issue_number}"
|
|
http_code="$(curl -sS -o "${response_file}" -w "%{http_code}" \
|
|
-H "Authorization: Bearer ${GH_TOKEN}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"${api_url}")"
|
|
|
|
if [[ "${issue_repo}" == "${GH_REPO}" ]]; then
|
|
reference_label="#${issue_number}"
|
|
else
|
|
reference_label="${issue_repo}#${issue_number}"
|
|
fi
|
|
|
|
if [[ "${http_code}" != "200" ]]; then
|
|
echo "::error::${reference_context} references ${reference_label}, but it does not exist or is not accessible."
|
|
rm -f "${response_file}"
|
|
return 1
|
|
fi
|
|
|
|
if jq -e '.pull_request != null' "${response_file}" > /dev/null; then
|
|
echo "::error::${reference_context} references ${reference_label}, but that number is a pull request, not an issue."
|
|
rm -f "${response_file}"
|
|
return 1
|
|
fi
|
|
|
|
state="$(jq -r '.state // "unknown"' "${response_file}")"
|
|
echo "${reference_context}: issue ${reference_label} exists (state=${state})."
|
|
rm -f "${response_file}"
|
|
}
|
|
|
|
if [[ "${PR_BRANCH}" =~ ^BO-([0-9]+)- ]]; then
|
|
branch_issue_number="${BASH_REMATCH[1]}"
|
|
else
|
|
echo "::error::Unable to extract issue number from branch '${PR_BRANCH}'."
|
|
exit 1
|
|
fi
|
|
|
|
validate_issue_exists "${GH_REPO}" "${branch_issue_number}" "Branch name"
|
|
|
|
closing_keyword_pattern='(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)'
|
|
issue_reference_pattern="(^|[^[:alnum:]_])${closing_keyword_pattern}([[:space:]]*:[[:space:]]*|[[:space:]]+)([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#[1-9][0-9]*"
|
|
issue_matches="$(printf '%s\n' "${PR_BODY}" | grep -Eio "${issue_reference_pattern}" || true)"
|
|
if [[ -z "${issue_matches}" ]]; then
|
|
echo "::error::PR description must include at least one GitHub closing-keyword issue reference (for example: 'Closes #123', 'Fixes: #123', or 'RESOLVED owner/repo#123')."
|
|
exit 1
|
|
fi
|
|
|
|
issue_refs="$(printf '%s\n' "${issue_matches}" \
|
|
| tr '[:upper:]' '[:lower:]' \
|
|
| sed -E 's/^[^a-z0-9_]*//' \
|
|
| sed -E "s/^${closing_keyword_pattern}([[:space:]]*:[[:space:]]*|[[:space:]]+)//" \
|
|
| sort -u)"
|
|
while IFS= read -r issue_ref; do
|
|
[[ -z "${issue_ref}" ]] && continue
|
|
|
|
if [[ "${issue_ref}" =~ ^([a-z0-9_.-]+/[a-z0-9_.-]+)#([1-9][0-9]*)$ ]]; then
|
|
issue_repo="${BASH_REMATCH[1]}"
|
|
issue_number="${BASH_REMATCH[2]}"
|
|
elif [[ "${issue_ref}" =~ ^#([1-9][0-9]*)$ ]]; then
|
|
issue_repo="${GH_REPO}"
|
|
issue_number="${BASH_REMATCH[1]}"
|
|
else
|
|
echo "::error::Unable to parse issue reference '${issue_ref}' in PR description."
|
|
exit 1
|
|
fi
|
|
|
|
validate_issue_exists "${issue_repo}" "${issue_number}" "PR description"
|
|
done <<< "${issue_refs}"
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Node.js and pnpm
|
|
uses: ./.github/actions/setup-node-pnpm
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
pnpm-version: ${{ env.PNPM_VERSION }}
|
|
|
|
- name: Lint PR title
|
|
shell: bash
|
|
env:
|
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlint --config commitlint.config.cjs
|
|
|
|
- name: Lint PR commit headers
|
|
shell: bash
|
|
env:
|
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
pnpm exec commitlint --config commitlint.config.cjs --from "$PR_BASE_SHA" --to "$PR_HEAD_SHA" --verbose
|
|
|
|
detect-changes:
|
|
name: Detect Changed Paths
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, pr-conventions]
|
|
timeout-minutes: 10
|
|
if: >
|
|
always() &&
|
|
(needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped') &&
|
|
(needs.pr-conventions.result == 'success' || needs.pr-conventions.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Manual and nightly runs include all scopes
|
|
id: manual
|
|
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
|
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' && github.event_name != 'schedule'
|
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # 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 }}"
|
|
|
|
lint:
|
|
name: Lint (${{ matrix.package_label }})
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, detect-changes]
|
|
timeout-minutes: 10
|
|
if: >
|
|
always() &&
|
|
(needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped') &&
|
|
needs.detect-changes.result == 'success'
|
|
strategy:
|
|
fail-fast: ${{ github.event_name == 'pull_request' }}
|
|
matrix:
|
|
package: [server, client]
|
|
include:
|
|
- package: server
|
|
package_label: Server
|
|
- package: client
|
|
package_label: Client
|
|
permissions:
|
|
contents: read
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- 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 }}
|
|
prefetch: "true"
|
|
|
|
- name: Run lint checks
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" ]]; then
|
|
pnpm --filter server lint:check
|
|
else
|
|
pnpm --filter client lint:check
|
|
fi
|
|
|
|
- name: Run format checks
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" ]]; then
|
|
pnpm --filter server format:check
|
|
else
|
|
pnpm --filter client format:check
|
|
fi
|
|
|
|
typecheck:
|
|
name: Type Check (${{ matrix.package_label }})
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, detect-changes]
|
|
timeout-minutes: 10
|
|
if: >
|
|
always() &&
|
|
(needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped') &&
|
|
needs.detect-changes.result == 'success'
|
|
strategy:
|
|
fail-fast: ${{ github.event_name == 'pull_request' }}
|
|
matrix:
|
|
package: [server, client]
|
|
include:
|
|
- package: server
|
|
package_label: Server
|
|
- package: client
|
|
package_label: Client
|
|
permissions:
|
|
contents: read
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- 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 }}
|
|
prefetch: "true"
|
|
|
|
- name: Run typecheck
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
shell: bash
|
|
env:
|
|
PACKAGE: ${{ matrix.package }}
|
|
run: |
|
|
if [[ "$PACKAGE" == "server" ]]; then
|
|
pnpm --filter server type-check
|
|
else
|
|
pnpm --filter @bookorbit/types build
|
|
pnpm --filter client type-check
|
|
fi
|
|
|
|
tests:
|
|
name: Tests (${{ matrix.package_label }})
|
|
runs-on: ubuntu-latest
|
|
needs: [dependency-review, detect-changes]
|
|
timeout-minutes: 20
|
|
if: >
|
|
always() &&
|
|
(needs.dependency-review.result == 'success' || needs.dependency-review.result == 'skipped') &&
|
|
needs.detect-changes.result == 'success'
|
|
strategy:
|
|
fail-fast: ${{ github.event_name == 'pull_request' }}
|
|
matrix:
|
|
package: [server, client]
|
|
include:
|
|
- package: server
|
|
package_label: Server
|
|
- package: client
|
|
package_label: Client
|
|
permissions:
|
|
contents: read
|
|
checks: write
|
|
env:
|
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- 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 }}
|
|
prefetch: "true"
|
|
|
|
- name: Prepare test report directory
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
shell: bash
|
|
run: mkdir -p "$GITHUB_WORKSPACE/test-results/${{ matrix.package }}"
|
|
|
|
- name: Run tests
|
|
if: steps.has-changes.outputs.run == 'true'
|
|
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 --coverage --reporter=default --reporter=github-actions --reporter=junit --outputFile.junit="$JUNIT_OUTPUT"
|
|
else
|
|
pnpm --filter client exec vitest --run --coverage --reporter=default --reporter=junit --outputFile="$JUNIT_OUTPUT"
|
|
fi
|
|
|
|
- name: Upload coverage reports to Codecov
|
|
if: steps.has-changes.outputs.run == 'true' && env.CODECOV_TOKEN != ''
|
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
with:
|
|
token: ${{ env.CODECOV_TOKEN }}
|
|
files: ${{ matrix.package == 'server' && 'server/coverage/lcov.info' || 'client/coverage/lcov.info' }}
|
|
flags: ${{ matrix.package }}
|
|
name: ${{ matrix.package }}-coverage
|
|
verbose: true
|
|
|
|
- name: Upload coverage reports to Codecov for fork PR
|
|
if: >
|
|
steps.has-changes.outputs.run == 'true' &&
|
|
env.CODECOV_TOKEN == '' &&
|
|
github.event_name == 'pull_request' &&
|
|
github.event.pull_request.head.repo.full_name != github.repository &&
|
|
github.event.repository.private == false
|
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
with:
|
|
files: ${{ matrix.package == 'server' && 'server/coverage/lcov.info' || 'client/coverage/lcov.info' }}
|
|
flags: ${{ matrix.package }}
|
|
name: ${{ matrix.package }}-coverage
|
|
verbose: true
|
|
|
|
- name: Publish test report annotations
|
|
if: >
|
|
always() &&
|
|
steps.has-changes.outputs.run == 'true' &&
|
|
hashFiles(format('test-results/{0}/junit.xml', matrix.package)) != '' &&
|
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # 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' &&
|
|
hashFiles(format('test-results/{0}/junit.xml', matrix.package)) != '' &&
|
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
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
|
|
- pr-conventions
|
|
- detect-changes
|
|
- lint
|
|
- typecheck
|
|
- tests
|
|
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 "pr-conventions=${{ needs.pr-conventions.result }}"
|
|
echo "detect-changes=${{ needs.detect-changes.result }}"
|
|
echo "lint=${{ needs.lint.result }}"
|
|
echo "typecheck=${{ needs.typecheck.result }}"
|
|
echo "tests=${{ needs.tests.result }}"
|
|
|
|
e2e-setup:
|
|
name: Detect E2E Suites
|
|
runs-on: ubuntu-latest
|
|
needs: [quality-gate]
|
|
if: always() && needs.quality-gate.result == 'success' && github.event_name == 'schedule'
|
|
timeout-minutes: 10
|
|
outputs:
|
|
matrix: ${{ steps.build-matrix.outputs.matrix }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Build suite matrix
|
|
id: build-matrix
|
|
shell: bash
|
|
env:
|
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
|
GITHUB_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
|
GITHUB_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
run: |
|
|
MATRIX=$(node scripts/e2e/select-matrix.mjs)
|
|
echo "matrix=$(echo "$MATRIX" | jq -c .)" >> "$GITHUB_OUTPUT"
|
|
echo "selected_suites=$(echo "$MATRIX" | jq -c .)"
|
|
|
|
e2e:
|
|
needs: [e2e-setup]
|
|
if: github.event_name == 'schedule' && 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 }}"
|
|
|
|
pr-image-scan:
|
|
name: PR Image Scan
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-gate]
|
|
timeout-minutes: 45
|
|
if: >
|
|
always() &&
|
|
github.event_name == 'pull_request' &&
|
|
needs.ci-gate.result == 'success'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Prepare PR image tag
|
|
id: pr-meta
|
|
shell: bash
|
|
env:
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
short_sha="${HEAD_SHA:0:12}"
|
|
scan_tag="pr-scan-${PR_NUMBER}-${short_sha}"
|
|
echo "scan_tag=${scan_tag}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Build image
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
push: false
|
|
load: true
|
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.pr-meta.outputs.scan_tag }}
|
|
platforms: linux/amd64
|
|
cache-from: type=gha,scope=ci-yml-pr-image
|
|
cache-to: type=gha,mode=max,scope=ci-yml-pr-image
|
|
|
|
- name: Scan image with Trivy
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
with:
|
|
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.pr-meta.outputs.scan_tag }}
|
|
format: table
|
|
vuln-type: os,library
|
|
severity: CRITICAL,HIGH
|
|
ignore-unfixed: true
|
|
exit-code: "1"
|
|
|
|
pr-image-publish:
|
|
name: PR Image Publish
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-gate, pr-image-scan]
|
|
timeout-minutes: 45
|
|
if: >
|
|
always() &&
|
|
github.event_name == 'pull_request' &&
|
|
needs.ci-gate.result == 'success' &&
|
|
needs.pr-image-scan.result == 'success' &&
|
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Prepare PR publish tag
|
|
id: pr-meta
|
|
shell: bash
|
|
env:
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
short_sha="${HEAD_SHA:0:12}"
|
|
publish_tag="pr-${PR_NUMBER}-${short_sha}"
|
|
echo "publish_tag=${publish_tag}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GHCR_TOKEN != '' && secrets.GHCR_TOKEN || github.token }}
|
|
|
|
- name: Build and push PR image
|
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
push: true
|
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.pr-meta.outputs.publish_tag }}
|
|
platforms: linux/amd64
|
|
cache-from: type=gha,scope=ci-yml-pr-image
|
|
cache-to: type=gha,mode=max,scope=ci-yml-pr-image
|
|
|
|
ci-performance:
|
|
name: CI Performance
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-gate]
|
|
if: always()
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- name: Compare duration with previous successful run
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
RUN_ID: ${{ github.run_id }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
CURRENT_JSON=$(gh api "repos/${GH_REPO}/actions/runs/${RUN_ID}")
|
|
CURRENT_STARTED=$(echo "$CURRENT_JSON" | jq -r '.run_started_at // .created_at')
|
|
CURRENT_UPDATED=$(echo "$CURRENT_JSON" | jq -r '.updated_at')
|
|
CURRENT_DURATION=$(node -e 'const [start, end] = process.argv.slice(1); console.log(Math.max(0, Math.round((Date.parse(end) - Date.parse(start)) / 1000)));' "$CURRENT_STARTED" "$CURRENT_UPDATED")
|
|
|
|
PREVIOUS_JSON=$(gh api "repos/${GH_REPO}/actions/workflows/ci.yml/runs?per_page=30&event=${EVENT_NAME}&status=completed")
|
|
PREVIOUS_STARTED=$(echo "$PREVIOUS_JSON" | jq -r --arg run_id "${RUN_ID}" '.workflow_runs[] | select((.id | tostring) != $run_id and .conclusion == "success") | (.run_started_at // .created_at)' | head -n1)
|
|
PREVIOUS_UPDATED=$(echo "$PREVIOUS_JSON" | jq -r --arg run_id "${RUN_ID}" '.workflow_runs[] | select((.id | tostring) != $run_id and .conclusion == "success") | .updated_at' | head -n1)
|
|
|
|
{
|
|
echo "## CI Runtime"
|
|
echo ""
|
|
echo "- Event: \`${EVENT_NAME}\`"
|
|
echo "- Current run duration: \`${CURRENT_DURATION}s\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
if [[ -z "$PREVIOUS_STARTED" || -z "$PREVIOUS_UPDATED" ]]; then
|
|
echo "- Baseline: unavailable (no previous successful run for this event)" >> "$GITHUB_STEP_SUMMARY"
|
|
exit 0
|
|
fi
|
|
|
|
PREVIOUS_DURATION=$(node -e 'const [start, end] = process.argv.slice(1); console.log(Math.max(0, Math.round((Date.parse(end) - Date.parse(start)) / 1000)));' "$PREVIOUS_STARTED" "$PREVIOUS_UPDATED")
|
|
RATIO=$(node -e 'const [current, previous] = process.argv.slice(1).map(Number); if (previous === 0) { console.log("N/A"); process.exit(0); } console.log((current / previous).toFixed(2));' "$CURRENT_DURATION" "$PREVIOUS_DURATION")
|
|
echo "- Previous successful run: \`${PREVIOUS_DURATION}s\`" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- Ratio (current/previous): \`${RATIO}\`" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
if [[ "$RATIO" != "N/A" ]] && node -e 'process.exit(Number(process.argv[1]) > 1.2 ? 0 : 1)' "$RATIO"; then
|
|
echo "::warning::CI duration regression detected: current=${CURRENT_DURATION}s previous=${PREVIOUS_DURATION}s ratio=${RATIO}"
|
|
fi
|