Skip to content

Angular series: migrate parts 7 to 15 to angular/AngularSeries (Angular 22 standalone) and add an npm CI lane #194

Angular series: migrate parts 7 to 15 to angular/AngularSeries (Angular 22 standalone) and add an npm CI lane

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

# 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)."