diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4a55f8f22..d59d4e43b 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -7,7 +7,7 @@ tone_instructions: > reviews: profile: assertive - request_changes_workflow: false + request_changes_workflow: true # No unsolicited summaries: CodeRabbit summarizes only on `@coderabbitai summary`. high_level_summary: false poem: false @@ -23,8 +23,6 @@ reviews: - ".*" ignore_usernames: - "dependabot[bot]" - ignore_title_keywords: - - WIP path_filters: - "!**/node_modules/**" diff --git a/.github/actions/changed-stacks/action.yml b/.github/actions/changed-stacks/action.yml new file mode 100644 index 000000000..725e50da9 --- /dev/null +++ b/.github/actions/changed-stacks/action.yml @@ -0,0 +1,53 @@ +name: Changed stacks +description: > + Report whether a pull request touches the C# or Vue stack, so workflows can + skip jobs for the stack it leaves alone. Non-PR events report both as changed. + Needs a checkout with fetch-depth 0. + +inputs: + own-paths: + description: The calling workflow's own files or folders, space-separated; a change to any runs both stacks. + required: true + +outputs: + csharp: + description: "'true' when C# code or its build/tooling changed" + value: ${{ steps.filter.outputs.csharp }} + vue: + description: "'true' when Vue code or its build/tooling changed" + value: ${{ steps.filter.outputs.vue }} + +runs: + using: composite + steps: + - id: filter + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + OWN_PATHS: ${{ inputs.own-paths }} + run: | + if [ -z "$BASE_REF" ]; then + echo "csharp=true" >> "$GITHUB_OUTPUT" + echo "vue=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + files=$(git diff --no-renames --name-only "origin/$BASE_REF...HEAD") + echo "Changed files:" + echo "$files" + + # Shared tooling runs both stacks; other workflow files run neither + shared="^(package(-lock)?\.json$|scripts/|\.editorconfig$|\.jscpd\.json$|\.npmrc$|\.github/actions/changed-stacks/)" + dot='\.' + for p in $OWN_PATHS; do shared="$shared|^${p//./$dot}"; done + csharp="$shared|^(web/|test/|Viper\.sln|Directory\.Build\.props$|global\.json$|\.globalconfig$|\.config/)" + vue="$shared|^(VueApp/|eslint\.config\.mjs$|\.oxlintrc\.json$|stylelint\.config\.mjs$|\.env)" + + for stack in csharp vue; do + if grep -qE "${!stack}" <<< "$files"; then + echo "$stack=true" >> "$GITHUB_OUTPUT" + else + echo "$stack=false" >> "$GITHUB_OUTPUT" + fi + done + cat "$GITHUB_OUTPUT" diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 0a8cbcb4f..5a0d0328e 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -2,7 +2,6 @@ name: Code Quality on: pull_request: - branches: [main] # Detects whether a PR *adds* new dead code or duplication compared to main. # - fallow covers VueApp (dead code, unused exports, complexity, duplication) @@ -13,8 +12,28 @@ on: # Passes if counts are equal to or lower than main. jobs: + changes: + name: Detect changed stacks + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + csharp: ${{ steps.stacks.outputs.csharp }} + vue: ${{ steps.stacks.outputs.vue }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - id: stacks + uses: ./.github/actions/changed-stacks + with: + own-paths: .github/workflows/code-quality.yml + fallow-regression: name: Fallow regression (VueApp) + needs: changes + if: needs.changes.outputs.vue == 'true' runs-on: ubuntu-latest steps: - name: Checkout PR head @@ -64,6 +83,8 @@ jobs: jscpd-regression-csharp: name: JSCPD regression (C#/Razor) + needs: changes + if: needs.changes.outputs.csharp == 'true' runs-on: ubuntu-latest steps: - name: Checkout PR head @@ -100,6 +121,8 @@ jobs: jscpd-regression-vue: name: JSCPD regression (Vue/TS) + needs: changes + if: needs.changes.outputs.vue == 'true' runs-on: ubuntu-latest steps: - name: Checkout PR head @@ -133,6 +156,8 @@ jobs: resharper-pr-gate: name: ReSharper PR-scoped gate (C#) + needs: changes + if: needs.changes.outputs.csharp == 'true' runs-on: ubuntu-latest steps: - name: Checkout PR head diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8874897f9..15120f790 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,9 +21,31 @@ on: - cron: '32 18 * * 6' jobs: + changes: + name: Detect changed stacks + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + csharp: ${{ steps.stacks.outputs.csharp }} + vue: ${{ steps.stacks.outputs.vue }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - id: stacks + uses: ./.github/actions/changed-stacks + with: + own-paths: .github/workflows/codeql.yml .github/codeql/ + analyze: name: Analyze + needs: changes runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + # Job-level `if` can't read the matrix; web/ holds plain JS, so a C# change runs both + env: + RUN_ANALYSIS: ${{ needs.changes.outputs.csharp == 'true' || (matrix.language == 'javascript' && needs.changes.outputs.vue == 'true') }} permissions: actions: read contents: read @@ -40,10 +62,12 @@ jobs: steps: - name: Checkout repository + if: env.RUN_ANALYSIS == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL + if: env.RUN_ANALYSIS == 'true' uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: ${{ matrix.language }} @@ -52,6 +76,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild + if: env.RUN_ANALYSIS == 'true' uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 # ℹ️ Command-line programs to run using the OS shell. @@ -65,6 +90,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis + if: env.RUN_ANALYSIS == 'true' uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coderabbit-gate.yml b/.github/workflows/coderabbit-gate.yml new file mode 100644 index 000000000..0992b7059 --- /dev/null +++ b/.github/workflows/coderabbit-gate.yml @@ -0,0 +1,90 @@ +name: CodeRabbit Gate + +# Passes when CodeRabbit's check run succeeded on the head commit and none of +# its threads are unresolved. A clean incremental review posts no PR review at +# all, so the job waits for CodeRabbit's check run instead of a review event. +# A declined finding passes once its thread is resolved and this check is re-run. + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + pull_request_review: + types: [submitted, dismissed] + # Catches a CodeRabbit review that finishes after the gate stopped polling. + # check_run only fires from the default branch's copy of this workflow. + check_run: + types: [completed] + +permissions: + pull-requests: read # read CodeRabbit's review threads + checks: read # read CodeRabbit's check run on the head commit + +concurrency: + # check_run fires for every check, so its runs must not cancel each other + group: coderabbit-gate-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + coderabbit-gate: + name: CodeRabbit approval + # CodeRabbit skips drafts and Dependabot PRs, so there is no verdict to wait for + if: ${{ github.event_name != 'check_run' && !github.event.pull_request.draft && github.event.pull_request.user.login != 'dependabot[bot]' }} + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Check CodeRabbit's review of the head commit + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -o pipefail + for _ in $(seq 40); do + run=$(gh api "repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/check-runs?check_name=CodeRabbit" \ + --jq '.check_runs | sort_by(.id) | last | select(.) | "\(.status) \(.conclusion)"') + [[ "$run" == completed* ]] && break + sleep 30 + done + if [ "$run" != "completed success" ]; then + echo "::error::CodeRabbit has not reviewed the latest commit (${run:-no run}). If it was rate limited or skipped, comment '@coderabbitai review'." + exit 1 + fi + + # Findings outside the diff have no thread, so only threaded findings block + unresolved=$(gh api graphql --paginate --slurp -F pr="$PR" -f owner="${GITHUB_REPOSITORY%/*}" -f repo="${GITHUB_REPOSITORY#*/}" -f query=' + query($owner:String!, $repo:String!, $pr:Int!, $endCursor:String) { + repository(owner:$owner, name:$repo) { pullRequest(number:$pr) { + reviewThreads(first:100, after:$endCursor) { + pageInfo { hasNextPage endCursor } + nodes { isResolved comments(first:1) { nodes { author { login } } } } + } + } } + }' | jq '[.[].data.repository.pullRequest.reviewThreads.nodes[] + | select(.isResolved == false and .comments.nodes[0].author.login == "coderabbitai")] | length') + if [ "$unresolved" -gt 0 ]; then + echo "::error::$unresolved CodeRabbit thread(s) unresolved. Address or resolve them, then re-run this check." + exit 1 + fi + echo "CodeRabbit's review of the latest commit is clean." + + rerun-after-coderabbit: + name: Re-run gate after CodeRabbit finishes + if: ${{ github.event_name == 'check_run' && github.event.check_run.name == 'CodeRabbit' }} + runs-on: ubuntu-latest + permissions: + actions: write # list and re-run this workflow's gate runs + steps: + - name: Re-run the commit's failed gate run + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.check_run.head_sha }} + # This run reports on the default branch, so re-run the PR's own gate run instead + run: | + run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow coderabbit-gate.yml --commit "$HEAD_SHA" -L 20 \ + --json databaseId,event,status,conclusion \ + --jq '[.[] | select(.event != "check_run")][0] | select(.status == "completed" and .conclusion == "failure") | .databaseId') + if [ -n "$run" ]; then + gh run rerun "$run" -R "$GITHUB_REPOSITORY" + else + echo "No failed gate run to refresh for $HEAD_SHA." + fi diff --git a/.github/workflows/development-merge-check.yml b/.github/workflows/development-merge-check.yml index 7a66c9f69..5ef4f4156 100644 --- a/.github/workflows/development-merge-check.yml +++ b/.github/workflows/development-merge-check.yml @@ -22,7 +22,7 @@ name: Development Merge Check on: pull_request: types: [opened, reopened, synchronize, ready_for_review] - branches: [main] + branches-ignore: [Development] push: branches: [Development] workflow_dispatch: @@ -95,7 +95,7 @@ jobs: // in Development without a new PR commit. Re-run each still-red PR's // check so its verdict refreshes against the updated Development. const prs = await github.paginate(github.rest.pulls.list, { - owner, repo, state: 'open', base: 'main', per_page: 100, + owner, repo, state: 'open', per_page: 100, }); const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { owner, repo, workflow_id: WORKFLOW, event: 'pull_request', per_page: 100, diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f68980f40..1e70e3de5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,11 +4,30 @@ on: push: branches: [main] pull_request: - branches: [main] jobs: + changes: + name: Detect changed stacks + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + csharp: ${{ steps.stacks.outputs.csharp }} + vue: ${{ steps.stacks.outputs.vue }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - id: stacks + uses: ./.github/actions/changed-stacks + with: + own-paths: .github/workflows/tests.yml + frontend: name: Frontend Tests + needs: changes + if: needs.changes.outputs.vue == 'true' runs-on: ubuntu-latest env: HAS_CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN != '' }} @@ -54,6 +73,8 @@ jobs: backend: name: Backend Tests + needs: changes + if: needs.changes.outputs.csharp == 'true' runs-on: ubuntu-latest env: HAS_CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN != '' }}