From 2bb38179f0ecf60bef29855406f4e9d2691da4e4 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:23:57 +0000 Subject: [PATCH 1/7] ci: optimize PR CI time and Netlify deploy preview filtering Co-Authored-By: Petr Plenkov --- .github/workflows/ci.yml | 29 ++++- .github/workflows/copilot-setup-steps.yml | 12 ++ netlify.toml | 8 +- website/scripts/netlify-ignore.cjs | 137 ++++++++++++++++++++++ 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 website/scripts/netlify-ignore.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 869de9832..7f7d76506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,26 @@ name: CI + on: push: branches: # Runs on merge to main only - main + paths-ignore: + - 'website/**' + - 'docs/**' + - '**/*.md' + - 'netlify.toml' pull_request: + paths-ignore: + - 'website/**' + - 'docs/**' + - '**/*.md' + - 'netlify.toml' + +# Cancel any in-progress runs for the same workflow/ref combination. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true # Needed for nx-set-shas when run on the main branch permissions: @@ -37,6 +53,13 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- # This line enables distribution # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci" @@ -50,8 +73,10 @@ jobs: - name: Print Environment Info run: npx nx report shell: bash - - run: npx nx format:check - - run: npx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 + - name: Check formatting for changed files + run: npx nx format:check --base=$NX_BASE --head=$NX_HEAD + - name: Run affected lint, test, build and e2e-ci + run: npx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 if: ${{ env.NX_CLOUD_ACCESS_TOKEN != '' }} - run: npx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud if: ${{ env.NX_CLOUD_ACCESS_TOKEN == '' }} diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 026f0a6b6..7bce9bf28 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -10,6 +10,11 @@ on: paths: - .github/workflows/copilot-setup-steps.yml +# Cancel any in-progress runs for the same workflow/ref combination. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: # Must be named exactly `copilot-setup-steps` for Copilot to pick it up. copilot-setup-steps: @@ -25,6 +30,13 @@ jobs: uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- - name: Install dependencies run: bun install diff --git a/netlify.toml b/netlify.toml index 2182976dd..e4dfa1b94 100644 --- a/netlify.toml +++ b/netlify.toml @@ -19,7 +19,13 @@ # which gets reported as a red `error` state + failing GitHub commit # status. Exit 0 from this command tells Netlify to skip the deploy # (reported as `skipped`, not `error`). Exit 1 means "proceed". - ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../netlify.toml" + # + # The script runs from the `website/` base directory. It checks the + # Netlify cached diff first, then falls back to the GitHub API for + # pull requests so deploy previews are skipped unless the PR actually + # touches the docs site or `netlify.toml`. For private repos, set the + # `GITHUB_TOKEN` environment variable in the Netlify UI. + ignore = "node ./scripts/netlify-ignore.cjs" [build.environment] NODE_VERSION = "20" diff --git a/website/scripts/netlify-ignore.cjs b/website/scripts/netlify-ignore.cjs new file mode 100644 index 000000000..533d1f0f1 --- /dev/null +++ b/website/scripts/netlify-ignore.cjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/** + * Netlify ignore script for the Docusaurus site under website/. + * + * Exit 0 -> skip the build (no deploy preview/production build needed). + * Exit 1 -> run the build (website/ or netlify.toml changed). + * + * The script is executed by Netlify from the `website/` base directory. + */ + +const { execSync } = require('node:child_process'); +const https = require('node:https'); + +const CONTEXT = process.env.CONTEXT || ''; +const IS_PULL_REQUEST = process.env.PULL_REQUEST === 'true'; +const REVIEW_ID = process.env.REVIEW_ID || ''; +const COMMIT_REF = process.env.COMMIT_REF || ''; +const CACHED_COMMIT_REF = process.env.CACHED_COMMIT_REF || ''; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; + +const REPO = 'abapify/adt-cli'; + +function hasWebsiteRelevantPath(filePath) { + if (!filePath) return false; + return filePath === 'netlify.toml' || filePath.startsWith('website/'); +} + +function gitDiffChangedFiles(refA, refB) { + try { + const out = execSync( + `git diff --name-only ${refA} ${refB} -- . ../netlify.toml`, + { + encoding: 'utf8', + cwd: process.cwd(), + stdio: ['pipe', 'pipe', 'ignore'], + }, + ); + return out.trim().split('\n').filter(Boolean); + } catch { + return null; + } +} + +function fetchJson(url) { + return new Promise((resolve, reject) => { + const options = { headers: { 'User-Agent': 'netlify-ignore-script' } }; + if (GITHUB_TOKEN) { + options.headers.Authorization = `token ${GITHUB_TOKEN}`; + } + https + .get(url, options, (res) => { + if (res.statusCode !== 200) { + res.resume(); + reject(new Error(`GitHub API returned ${res.statusCode}`)); + return; + } + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (err) { + reject(err); + } + }); + }) + .on('error', reject); + }); +} + +async function prTouchesWebsite() { + const pageSize = 100; + let page = 1; + while (true) { + const files = await fetchJson( + `https://api.github.com/repos/${REPO}/pulls/${REVIEW_ID}/files?per_page=${pageSize}&page=${page}`, + ); + if (!Array.isArray(files) || files.length === 0) return false; + for (const file of files) { + if (hasWebsiteRelevantPath(file.filename)) return true; + } + if (files.length < pageSize) return false; + page += 1; + } +} + +async function main() { + // Fast path: we have two distinct cached refs. If nothing in website/ or + // netlify.toml changed, skip the build. + if (COMMIT_REF && CACHED_COMMIT_REF && CACHED_COMMIT_REF !== COMMIT_REF) { + const files = gitDiffChangedFiles(CACHED_COMMIT_REF, COMMIT_REF); + if (files && files.length > 0) { + console.log( + `Building: ${files.length} change(s) in website/ or netlify.toml`, + ); + process.exit(1); + } + if (files && files.length === 0) { + console.log( + 'Skipping: no website/ or netlify.toml changes since the last build', + ); + process.exit(0); + } + // git diff failed; fall through to PR API or default build. + } + + // For pull/merge request previews, ask GitHub which files changed. + if ((IS_PULL_REQUEST || CONTEXT === 'deploy-preview') && REVIEW_ID) { + try { + const touches = await prTouchesWebsite(); + if (!touches) { + console.log( + 'Skipping deploy preview: PR does not touch website/ or netlify.toml', + ); + process.exit(0); + } + console.log( + 'Building deploy preview: PR touches website/ or netlify.toml', + ); + process.exit(1); + } catch (err) { + // If we cannot determine the changed files, build to be safe. + console.warn( + `Could not determine PR changed files (${err.message}); building to be safe`, + ); + process.exit(1); + } + } + + // Production / branch-deploy without useful cached refs or PR info: build. + console.log('Building: no cached diff or PR information available'); + process.exit(1); +} + +main(); From 61cbd2d2501c93bc06d8969dc1c4a90b058c263b Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:27:04 +0000 Subject: [PATCH 2/7] ci: use bunx for nx commands and prefer optional chaining Co-Authored-By: Petr Plenkov --- .github/workflows/ci.yml | 10 +++++----- website/scripts/netlify-ignore.cjs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f7d76506..c5ef71c05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,14 +71,14 @@ jobs: run: bash .github/hooks/scripts/test-hooks.sh shell: bash - name: Print Environment Info - run: npx nx report + run: bunx nx report shell: bash - name: Check formatting for changed files - run: npx nx format:check --base=$NX_BASE --head=$NX_HEAD + run: bunx nx format:check --base=$NX_BASE --head=$NX_HEAD - name: Run affected lint, test, build and e2e-ci - run: npx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 + run: bunx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 if: ${{ env.NX_CLOUD_ACCESS_TOKEN != '' }} - - run: npx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud + - run: bunx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud if: ${{ env.NX_CLOUD_ACCESS_TOKEN == '' }} - - run: npx nx-cloud fix-ci + - run: bunx nx-cloud fix-ci if: always() && env.NX_CLOUD_ACCESS_TOKEN != '' diff --git a/website/scripts/netlify-ignore.cjs b/website/scripts/netlify-ignore.cjs index 533d1f0f1..6ca738158 100644 --- a/website/scripts/netlify-ignore.cjs +++ b/website/scripts/netlify-ignore.cjs @@ -91,13 +91,13 @@ async function main() { // netlify.toml changed, skip the build. if (COMMIT_REF && CACHED_COMMIT_REF && CACHED_COMMIT_REF !== COMMIT_REF) { const files = gitDiffChangedFiles(CACHED_COMMIT_REF, COMMIT_REF); - if (files && files.length > 0) { + if (files?.length > 0) { console.log( `Building: ${files.length} change(s) in website/ or netlify.toml`, ); process.exit(1); } - if (files && files.length === 0) { + if (files?.length === 0) { console.log( 'Skipping: no website/ or netlify.toml changes since the last build', ); From f4cce21e7b6580152754dbb45274c5fb2e409d57 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:29:34 +0000 Subject: [PATCH 3/7] ci: call nx and nx-cloud from node_modules/.bin to lock versions Co-Authored-By: Petr Plenkov --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5ef71c05..2ba4b0e89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,14 +71,14 @@ jobs: run: bash .github/hooks/scripts/test-hooks.sh shell: bash - name: Print Environment Info - run: bunx nx report + run: ./node_modules/.bin/nx report shell: bash - name: Check formatting for changed files - run: bunx nx format:check --base=$NX_BASE --head=$NX_HEAD + run: ./node_modules/.bin/nx format:check --base=$NX_BASE --head=$NX_HEAD - name: Run affected lint, test, build and e2e-ci - run: bunx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 + run: ./node_modules/.bin/nx affected -t lint test build e2e-ci --verbose=false --parallel=3 if: ${{ env.NX_CLOUD_ACCESS_TOKEN != '' }} - - run: bunx nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud + - run: ./node_modules/.bin/nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud if: ${{ env.NX_CLOUD_ACCESS_TOKEN == '' }} - - run: bunx nx-cloud fix-ci + - run: ./node_modules/.bin/nx-cloud fix-ci if: always() && env.NX_CLOUD_ACCESS_TOKEN != '' From e47781a5787eb14905b50df53e40036ad7e9d3c4 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:49:43 +0000 Subject: [PATCH 4/7] ci: avoid paths-ignore blocking required checks and harden netlify ignore script Co-Authored-By: Petr Plenkov --- .github/workflows/ci.yml | 51 +++++++++++++++++++++--------- website/scripts/netlify-ignore.cjs | 26 +++++++-------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ba4b0e89..49f2bba6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,22 +5,12 @@ on: branches: # Runs on merge to main only - main - paths-ignore: - - 'website/**' - - 'docs/**' - - '**/*.md' - - 'netlify.toml' pull_request: - paths-ignore: - - 'website/**' - - 'docs/**' - - '**/*.md' - - 'netlify.toml' -# Cancel any in-progress runs for the same workflow/ref combination. +# Cancel in-progress PR runs; let main builds finish so the Nx Cloud cache is populated. concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Needed for nx-set-shas when run on the main branch permissions: @@ -47,13 +37,39 @@ jobs: # the PR is supposed to remove. nrwl/nx-set-shas still computes the correct # affected range from git history. Falls back to github.sha for push events. ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Check for code changes + id: changes + shell: bash + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.sha }}" + fi + + if [ "$BASE" = "0000000000000000000000000000000000000000" ]; then + echo "has-code-changes=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git diff --name-only "$BASE" "$HEAD" | grep -qvE '^(website/|docs/|.*\.md|netlify\.toml)$'; then + echo "has-code-changes=true" >> "$GITHUB_OUTPUT" + else + echo "has-code-changes=false" >> "$GITHUB_OUTPUT" + echo "Skipping heavy CI steps: only docs/website/markdown files changed." + fi - uses: actions/setup-node@v7 + if: steps.changes.outputs.has-code-changes == 'true' with: node-version: lts/* - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + if: steps.changes.outputs.has-code-changes == 'true' with: bun-version: 1.3.14 - name: Cache bun dependencies + if: steps.changes.outputs.has-code-changes == 'true' uses: actions/cache@v4 with: path: ~/.bun/install/cache @@ -64,21 +80,26 @@ jobs: # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci" - run: bun install + if: steps.changes.outputs.has-code-changes == 'true' - uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5 + if: steps.changes.outputs.has-code-changes == 'true' - name: Test tool-call hook scripts + if: steps.changes.outputs.has-code-changes == 'true' run: bash .github/hooks/scripts/test-hooks.sh shell: bash - name: Print Environment Info + if: steps.changes.outputs.has-code-changes == 'true' run: ./node_modules/.bin/nx report shell: bash - name: Check formatting for changed files + if: steps.changes.outputs.has-code-changes == 'true' run: ./node_modules/.bin/nx format:check --base=$NX_BASE --head=$NX_HEAD - name: Run affected lint, test, build and e2e-ci + if: ${{ env.NX_CLOUD_ACCESS_TOKEN != '' && steps.changes.outputs.has-code-changes == 'true' }} run: ./node_modules/.bin/nx affected -t lint test build e2e-ci --verbose=false --parallel=3 - if: ${{ env.NX_CLOUD_ACCESS_TOKEN != '' }} - run: ./node_modules/.bin/nx affected -t lint test build e2e-ci --verbose=false --parallel=3 --no-cloud - if: ${{ env.NX_CLOUD_ACCESS_TOKEN == '' }} + if: ${{ env.NX_CLOUD_ACCESS_TOKEN == '' && steps.changes.outputs.has-code-changes == 'true' }} - run: ./node_modules/.bin/nx-cloud fix-ci - if: always() && env.NX_CLOUD_ACCESS_TOKEN != '' + if: always() && env.NX_CLOUD_ACCESS_TOKEN != '' && steps.changes.outputs.has-code-changes == 'true' diff --git a/website/scripts/netlify-ignore.cjs b/website/scripts/netlify-ignore.cjs index 6ca738158..b62b25c6d 100644 --- a/website/scripts/netlify-ignore.cjs +++ b/website/scripts/netlify-ignore.cjs @@ -8,7 +8,7 @@ * The script is executed by Netlify from the `website/` base directory. */ -const { execSync } = require('node:child_process'); +const { spawnSync } = require('node:child_process'); const https = require('node:https'); const CONTEXT = process.env.CONTEXT || ''; @@ -26,19 +26,15 @@ function hasWebsiteRelevantPath(filePath) { } function gitDiffChangedFiles(refA, refB) { - try { - const out = execSync( - `git diff --name-only ${refA} ${refB} -- . ../netlify.toml`, - { - encoding: 'utf8', - cwd: process.cwd(), - stdio: ['pipe', 'pipe', 'ignore'], - }, - ); - return out.trim().split('\n').filter(Boolean); - } catch { + const result = spawnSync( + 'git', + ['diff', '--name-only', refA, refB, '--', '.', '../netlify.toml'], + { cwd: process.cwd(), encoding: 'utf8' }, + ); + if (result.error || result.status !== 0) { return null; } + return result.stdout.trim().split('\n').filter(Boolean); } function fetchJson(url) { @@ -71,11 +67,15 @@ function fetchJson(url) { } async function prTouchesWebsite() { + const reviewId = Number(REVIEW_ID); + if (!Number.isInteger(reviewId) || reviewId <= 0) { + throw new Error(`Invalid REVIEW_ID: ${REVIEW_ID}`); + } const pageSize = 100; let page = 1; while (true) { const files = await fetchJson( - `https://api.github.com/repos/${REPO}/pulls/${REVIEW_ID}/files?per_page=${pageSize}&page=${page}`, + `https://api.github.com/repos/${REPO}/pulls/${reviewId}/files?per_page=${pageSize}&page=${page}`, ); if (!Array.isArray(files) || files.length === 0) return false; for (const file of files) { From 47b94d040a93facfbae87176c77ef15a5b2b3ff6 Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:52:24 +0000 Subject: [PATCH 5/7] ci: restrict PATH for git spawn in netlify ignore script Co-Authored-By: Petr Plenkov --- website/scripts/netlify-ignore.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/scripts/netlify-ignore.cjs b/website/scripts/netlify-ignore.cjs index b62b25c6d..92d97564a 100644 --- a/website/scripts/netlify-ignore.cjs +++ b/website/scripts/netlify-ignore.cjs @@ -29,7 +29,12 @@ function gitDiffChangedFiles(refA, refB) { const result = spawnSync( 'git', ['diff', '--name-only', refA, refB, '--', '.', '../netlify.toml'], - { cwd: process.cwd(), encoding: 'utf8' }, + { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, PATH: '/usr/local/bin:/usr/bin:/bin' }, + shell: false, + }, ); if (result.error || result.status !== 0) { return null; From cfe93ffb6e5f904591ca8ef08d2e5480995e9a3c Mon Sep 17 00:00:00 2001 From: Devin Date: Fri, 24 Jul 2026 10:52:41 +0000 Subject: [PATCH 6/7] ci: use absolute git path in netlify ignore script Co-Authored-By: Petr Plenkov --- website/scripts/netlify-ignore.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/scripts/netlify-ignore.cjs b/website/scripts/netlify-ignore.cjs index 92d97564a..b90241fc1 100644 --- a/website/scripts/netlify-ignore.cjs +++ b/website/scripts/netlify-ignore.cjs @@ -27,7 +27,7 @@ function hasWebsiteRelevantPath(filePath) { function gitDiffChangedFiles(refA, refB) { const result = spawnSync( - 'git', + '/usr/bin/git', ['diff', '--name-only', refA, refB, '--', '.', '../netlify.toml'], { cwd: process.cwd(), From 7d6f9d3a76d423dd740fd1da35eacb0339234acf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:51:29 +0000 Subject: [PATCH 7/7] ci: fix docs-only path filter regex to match files inside website/ and docs/ Co-Authored-By: Petr Plenkov --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49f2bba6c..df1479960 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: exit 0 fi - if git diff --name-only "$BASE" "$HEAD" | grep -qvE '^(website/|docs/|.*\.md|netlify\.toml)$'; then + if git diff --name-only "$BASE" "$HEAD" | grep -qvE '^website/|^docs/|\.md$|^netlify\.toml$'; then echo "has-code-changes=true" >> "$GITHUB_OUTPUT" else echo "has-code-changes=false" >> "$GITHUB_OUTPUT"