Update CaseInsensitiveSubstringSearch sample to .NET 10 #314
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # "CI result" (the last job) is a required status check on main. The workflow | |
| # builds and tests exactly the article folder(s) a PR touches -- nothing else -- | |
| # and checks that the PR carries no AI attribution (see no-ai-attribution). | |
| # A folder is classified once: | |
| # "dotnet" when it holds a .sln, "npm" when it holds a package.json at its root | |
| # or in an immediate subfolder (the layout an Angular series folder uses). | |
| # Folders in .github/ci-skip-folders.txt are filtered out before the matrix is | |
| # built, so they never show up as a run, passing or failing (edit that file, not | |
| # this one, to change what's excluded). | |
| name: PR Build | |
| on: | |
| pull_request: | |
| branches: [main] | |
| # "edited" re-runs the attribution check when the title or description changes. | |
| types: [opened, synchronize, reopened, edited] | |
| # Cancel superseded runs on the same PR when new commits land. | |
| concurrency: | |
| group: pr-build-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| detect-changes: | |
| name: Detect changed article folders | |
| runs-on: ubuntu-latest | |
| outputs: | |
| folders: ${{ steps.compute.outputs.folders }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Compute changed, buildable folders | |
| id: compute | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| SKIP_FILE=".github/ci-skip-folders.txt" | |
| # Skip list: strip comments and blank lines. | |
| mapfile -t skip_list < <( | |
| sed 's/\r$//' "$SKIP_FILE" | grep -vE '^\s*#' | grep -vE '^\s*$' | |
| ) | |
| is_skipped() { | |
| local f="$1" | |
| for s in "${skip_list[@]:-}"; do | |
| [ "$f" = "$s" ] && return 0 | |
| done | |
| return 1 | |
| } | |
| # Reduce every changed path to its "<category>/<article>" prefix. | |
| # Every article folder in this repo is exactly two levels deep. | |
| mapfile -t folders < <( | |
| git diff --name-only "$BASE_SHA" "$HEAD_SHA" \ | |
| | awk -F/ 'NF>=2 {print $1"/"$2}' \ | |
| | sort -u | |
| ) | |
| entries="[]" | |
| for f in "${folders[@]:-}"; do | |
| [ -z "$f" ] && continue | |
| if is_skipped "$f"; then | |
| echo "::notice::Skipping '$f' -- listed in $SKIP_FILE." | |
| continue | |
| fi | |
| # Classify the folder. A .sln makes it a .NET folder; otherwise a | |
| # package.json at the folder root or one level down makes it an npm | |
| # folder. Anything else has nothing to build. | |
| kind="" | |
| if compgen -G "$f"/*.sln > /dev/null 2>&1; then | |
| kind="dotnet" | |
| elif [ -f "$f/package.json" ] || compgen -G "$f"/*/package.json > /dev/null 2>&1; then | |
| kind="npm" | |
| fi | |
| # Folder may have been deleted in this PR -- skip if nothing buildable remains at HEAD. | |
| if [ -z "$kind" ]; then | |
| echo "::notice::Skipping '$f' -- no .sln at HEAD (deleted or restructured)." | |
| continue | |
| fi | |
| entries=$(jq -c --arg folder "$f" --arg kind "$kind" '. + [{folder: $folder, kind: $kind}]' <<<"$entries") | |
| done | |
| echo "folders=$entries" >> "$GITHUB_OUTPUT" | |
| echo "Changed, buildable folders:" | |
| echo "$entries" | jq . | |
| # Public repo: commits and PRs go out under the Code Maze name only. Fails when a | |
| # PR commit message, the PR title or the PR description carries an AI co-author | |
| # trailer or a "generated with" footer. Squash merges build their message from | |
| # these commits, so a clean PR also means a clean commit on main. | |
| no-ai-attribution: | |
| name: No AI attribution | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - name: Check commit messages, title and description | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| run: | | |
| set -euo pipefail | |
| pattern='co-authored-by:.*(claude|anthropic|copilot|chatgpt|openai)|generated (with|by) .{0,3}(claude|chatgpt|copilot)|noreply@anthropic\.com' | |
| found=0 | |
| check() { | |
| local where="$1" text="$2" | |
| local hits | |
| hits=$(printf '%s\n' "$text" | grep -Ein "$pattern" || true) | |
| if [ -n "$hits" ]; then | |
| echo "::error::AI attribution in $where:" | |
| printf '%s\n' "$hits" | |
| found=1 | |
| fi | |
| } | |
| check "PR title" "$PR_TITLE" | |
| check "PR description" "$PR_BODY" | |
| while IFS= read -r sha; do | |
| check "commit ${sha:0:7}" "$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha" --jq .commit.message)" | |
| done < <(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/commits" --jq '.[].sha') | |
| if [ "$found" -ne 0 ]; then | |
| echo "Remove the lines above (amend or reword the commits, edit the PR), then push again." | |
| exit 1 | |
| fi | |
| echo "No AI attribution found." | |
| build-and-test: | |
| name: Build & test (${{ matrix.folder }}) | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.folders != '[]' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.detect-changes.outputs.folders) }} | |
| steps: | |
| - name: Checkout (sparse) | |
| uses: actions/checkout@v4 | |
| with: | |
| sparse-checkout: | | |
| ${{ matrix.folder }} | |
| sparse-checkout-cone-mode: true | |
| - name: Setup .NET SDKs | |
| if: matrix.kind == 'dotnet' | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 6.0.x | |
| 7.0.x | |
| 8.0.x | |
| 9.0.x | |
| 10.0.x | |
| - name: Build and test | |
| if: matrix.kind == 'dotnet' | |
| working-directory: ${{ matrix.folder }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sln=$(find . -maxdepth 1 -iname "*.sln" | head -n1) | |
| if [ -z "$sln" ]; then | |
| echo "::notice::No .sln found in ${{ matrix.folder }} -- skipping." | |
| exit 0 | |
| fi | |
| echo "Building $sln" | |
| dotnet build "$sln" --configuration Release | |
| echo "Testing $sln (no-op for folders with no test project)" | |
| dotnet test "$sln" --configuration Release --no-build --filter "FullyQualifiedName!~Live" | |
| - name: Setup Node | |
| if: matrix.kind == 'npm' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: ${{ matrix.folder }}/**/package-lock.json | |
| - name: Build (npm) | |
| if: matrix.kind == 'npm' | |
| working-directory: ${{ matrix.folder }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Every directory at depth 0 or 1 that holds a package.json is its own | |
| # project: one folder may carry a single application or a set of them. | |
| mapfile -t projects < <( | |
| find . -maxdepth 2 -name package.json -not -path "*/node_modules/*" -printf '%h\n' \ | |
| | sort -u | |
| ) | |
| if [ ${#projects[@]} -eq 0 ]; then | |
| echo "::error::No package.json found in ${{ matrix.folder }}." | |
| exit 1 | |
| fi | |
| for p in "${projects[@]}"; do | |
| echo "::group::${{ matrix.folder }}/${p#./} -- npm ci && npm run build" | |
| ( | |
| cd "$p" | |
| npm ci | |
| npm run build | |
| ) | |
| echo "::endgroup::" | |
| done | |
| # Stable, folder-independent name for branch protection to require -- the | |
| # build-and-test job's displayed name varies with the matrix (one leg per | |
| # changed folder), so it can't be set as a required check directly. This | |
| # job always runs (even if earlier jobs are skipped) and only fails when a | |
| # needed job actually failed or was cancelled; an empty build matrix (a PR | |
| # touching no buildable folders) is a skip, not a failure, and passes here. | |
| ci-result: | |
| name: CI result | |
| needs: [detect-changes, build-and-test, no-ai-attribution] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check required jobs | |
| shell: bash | |
| run: | | |
| detect="${{ needs.detect-changes.result }}" | |
| build="${{ needs.build-and-test.result }}" | |
| attribution="${{ needs.no-ai-attribution.result }}" | |
| echo "detect-changes: $detect" | |
| echo "build-and-test: $build" | |
| echo "no-ai-attribution: $attribution" | |
| if [ "$detect" = "failure" ] || [ "$detect" = "cancelled" ] || [ "$build" = "failure" ] || [ "$build" = "cancelled" ]; then | |
| echo "A required job failed or was cancelled." | |
| exit 1 | |
| fi | |
| # The attribution check never legitimately skips, so anything but success fails. | |
| if [ "$attribution" != "success" ]; then | |
| echo "The AI attribution check did not pass." | |
| exit 1 | |
| fi | |
| echo "All required jobs passed (or were skipped)." |