# Quality gates (reusable). Callers: ci.yaml (PR), cd.yaml (push main). name: quality on: workflow_call: inputs: is_pull_request: type: boolean required: true base_sha: type: string required: false default: "" head_sha: type: string required: false default: "" before_sha: type: string required: false default: "" allow_registry_login: type: boolean required: false default: false secrets: ACTIONS_PAT: required: false permissions: contents: read jobs: changes: runs-on: ubuntu-latest outputs: web: ${{ steps.detect.outputs.web }} api: ${{ steps.detect.outputs.api }} docker: ${{ steps.detect.outputs.docker }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Push may contain several commits; github.event.before is then # more than one parent away. fetch-depth: 2 only has HEAD~1. fetch-depth: 0 - id: detect name: Detect changed paths per module env: IS_PR: ${{ inputs.is_pull_request }} BASE_SHA: ${{ inputs.base_sha }} HEAD_SHA: ${{ inputs.head_sha }} BEFORE_SHA: ${{ inputs.before_sha }} run: | set -euo pipefail web=false api=false docker=false set_all_flags_true() { web=true api=true docker=true } write_outputs() { for v in web api docker; do eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT" done } has_commit() { git cat-file -e "${1}^{commit}" 2>/dev/null } if [ "$IS_PR" = "true" ]; then FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" else after="${HEAD_SHA:-$(git rev-parse HEAD)}" before="$BEFORE_SHA" if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ] && has_commit "$before"; then FILES="$(git diff --name-only "$before" "$after")" elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then FILES="$(git diff --name-only HEAD~1 HEAD)" else set_all_flags_true write_outputs echo "No parent commit — full pipeline (all modules)" exit 0 fi fi if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then set_all_flags_true write_outputs echo "Empty diff — full pipeline fallback" exit 0 fi full_pipeline=false while IFS= read -r f || [ -n "${f:-}" ]; do [ -z "${f:-}" ] && continue case "$f" in .gitea/workflows/*|scripts/*) full_pipeline=true ;; .cursor/*|.claude/*|.codegraph/*|.agents/*) ;; *.md|AGENTS.md) ;; apps/web/README.md|apps/web/components.json|packages/ui/components.json) ;; apps/web/*|packages/ui/*) web=true ;; apps/api/*|packages/db/*) api=true ;; packages/shared/*) web=true api=true ;; deploy/compose/*|deploy/docker/*|.dockerignore|docker-compose.yml) docker=true ;; docs/*) ;; package.json|pnpm-lock.yaml|pnpm-workspace.yaml|turbo.json|.releaserc.json|commitlint.config.cjs) full_pipeline=true ;; *) ;; esac done <<< "$FILES" if $full_pipeline; then set_all_flags_true fi write_outputs echo "Changed files (first 30):" printf '%s\n' "$FILES" | head -n 30 echo "--- flags ---" echo "web=$web api=$api docker=$docker full_pipeline=$full_pipeline" web: needs: [changes] if: needs.changes.outputs.web == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules apps/api/node_modules packages/ui/node_modules packages/shared/node_modules packages/db/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: pnpm install, lint, test, build env: PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh pnpm --filter web lint pnpm exec turbo run test --filter=web pnpm exec turbo run build --filter=web api: needs: [changes] if: needs.changes.outputs.api == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules apps/api/node_modules packages/ui/node_modules packages/shared/node_modules packages/db/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: pnpm install, test, build env: PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh pnpm exec turbo run test --filter=@cdnmanager/api pnpm exec turbo run build --filter=@cdnmanager/api commitlint: if: inputs.is_pull_request runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Export cache paths run: sh scripts/ci/export-cache-env.sh - id: pnpm-hash run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT" - id: pnpm-cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ${{ env.PNPM_STORE_DIR }} ${{ env.COREPACK_HOME }} node_modules apps/web/node_modules apps/api/node_modules packages/ui/node_modules packages/shared/node_modules packages/db/node_modules key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }} restore-keys: | pnpm-${{ runner.os }}- - name: Lint commit messages env: BASE_SHA: ${{ inputs.base_sha }} HEAD_SHA: ${{ inputs.head_sha }} PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }} run: | set -euxo pipefail sh scripts/ci/pnpm-ci.sh pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" docker-check: needs: [changes] if: inputs.is_pull_request && needs.changes.outputs.docker == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 with: name: cdnmanager driver: docker-container cleanup: false - name: Log in to Gitea Registry if: inputs.allow_registry_login uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT }} - name: bake --print working-directory: deploy/docker env: BUILDX_BAKE_ENTITLEMENTS_FS: "0" BUILDX_BAKE_FILE_RELATIVE_PATHS: "1" run: docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl --print default - name: bake (no push) if: inputs.allow_registry_login working-directory: deploy/docker env: BUILDX_BAKE_ENTITLEMENTS_FS: "0" BUILDX_BAKE_FILE_RELATIVE_PATHS: "1" run: | set -euxo pipefail owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" export CACHE_REF_NODE="git.shx.one/${owner_lc}/cdnmanager-buildcache:node-buildcache" docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl default