Angular series: migrate parts 7 to 15 to angular/AngularSeries (Angular 22 standalone) and add an npm CI lane #194
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
| # Advisory only. This check is never marked as a required status check, and main | |
| # has no branch protection rule referencing it. It builds and tests exactly the | |
| # article folder(s) a PR touches -- nothing else. 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] | |
| # 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 . | |
| 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 | |
| - 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] | |
| 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 }}" | |
| echo "detect-changes: $detect" | |
| echo "build-and-test: $build" | |
| if [ "$detect" = "failure" ] || [ "$detect" = "cancelled" ] || [ "$build" = "failure" ] || [ "$build" = "cancelled" ]; then | |
| echo "A required job failed or was cancelled." | |
| exit 1 | |
| fi | |
| echo "All required jobs passed (or were skipped)." |