From 4e73e9e4823c4fdecd864b53932c1fdec71df503 Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 29 Sep 2026 19:39:56 +0200 Subject: [PATCH] Treat a pull request with no changed files as nothing to check Refs #109 --- .github/workflows/ci.yml | 29 ++++------------------- package.json | 2 +- scripts/classify-changes.sh | 34 +++++++++++++++++++++++++++ scripts/classify-changes.test.mjs | 38 +++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 25 deletions(-) create mode 100755 scripts/classify-changes.sh create mode 100644 scripts/classify-changes.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66719181..1873177f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: code: ${{ steps.classify.outputs.code }} drizzle: ${{ steps.classify.outputs.drizzle }} tutorial: ${{ steps.classify.outputs.tutorial }} + docs: ${{ steps.classify.outputs.docs }} steps: - name: Check out code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,38 +41,18 @@ jobs: BASE: ${{ github.event.pull_request.base.sha || github.event.before }} run: | if [[ "${{ github.event_name }}" == schedule ]]; then - { echo 'code=false'; echo 'drizzle=false'; echo 'tutorial=true'; } >> "$GITHUB_OUTPUT" + { echo 'code=false'; echo 'drizzle=false'; echo 'tutorial=true'; echo 'docs=false'; } >> "$GITHUB_OUTPUT" exit 0 fi if [[ "${{ github.event_name }}" == workflow_dispatch || "$BASE" =~ ^0+$ ]]; then - { echo 'code=true'; echo 'drizzle=true'; echo 'tutorial=true'; } >> "$GITHUB_OUTPUT" + { echo 'code=true'; echo 'drizzle=true'; echo 'tutorial=true'; echo 'docs=false'; } >> "$GITHUB_OUTPUT" exit 0 fi - files=$(git diff --name-only "$BASE" HEAD) - if [[ -z "$files" ]]; then - echo 'No changed files to classify' >&2 - exit 2 - fi - code=false - drizzle=false - tutorial=false - while IFS= read -r file; do - case "$file" in - Documentation/client-snippets/tutorial/*|Documentation/client-snippets/arc-without-event-sourcing/*|Documentation/tutorial-e2e/*|Documentation/validate-client-snippets.py|Source/MongoDB/*|Source/Core/*|Source/Tools/ProxyGenerator/*|.github/workflows/ci.yml|package.json|yarn.lock) tutorial=true ;; - esac - case "$file" in - Source/Drizzle/*|Documentation/sql/observing-postgresql.md|.github/workflows/ci.yml|package.json|yarn.lock) drizzle=true ;; - esac - case "$file" in - Documentation/*|*.md) ;; - *) code=true ;; - esac - done <<< "$files" - { echo "code=$code"; echo "drizzle=$drizzle"; echo "tutorial=$tutorial"; } >> "$GITHUB_OUTPUT" + git diff --name-only "$BASE" HEAD | bash scripts/classify-changes.sh >> "$GITHUB_OUTPUT" docs: needs: changes - if: needs.changes.outputs.code == 'false' && github.event_name != 'schedule' + if: needs.changes.outputs.docs == 'true' runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/package.json b/package.json index 36621f02..cf4ff7fd 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "set-version": "node scripts/set-version.mjs", "test": "vitest run", "specs": "yarn test", - "ci": "yarn tsc -b Source/Core Source/Testing Source/Chronicle Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn check:proxies && yarn build && yarn check:declarations && yarn check:declarations:self-test && yarn check:consumers && yarn check:chronicle-peer-floor:self-test && yarn check:chronicle-peer-floor && yarn check:fetch && yarn check:node-test && yarn check:observability-recipes && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && yarn docs:examples:self-test && yarn docs:examples && node --test scripts/for_release/*.test.mjs && yarn set-version --check" + "ci": "yarn tsc -b Source/Core Source/Testing Source/Chronicle Source/Tools/ProxyGenerator && yarn check:metadata && yarn workspace @cratis/arc.core.sample.tasks generate-proxies && yarn workspace @cratis/arc.sample.library generate-proxies && yarn check:proxies && yarn build && yarn check:declarations && yarn check:declarations:self-test && yarn check:consumers && yarn check:chronicle-peer-floor:self-test && yarn check:chronicle-peer-floor && yarn check:fetch && yarn check:node-test && yarn check:observability-recipes && yarn workspace @cratis/arc.sample.library.web build && yarn lint && yarn typecheck && yarn lint:tasks:arc && yarn test:client-generation:verify && yarn test && yarn workspace @cratis/arc.sample.library test:e2e && yarn test:legacy-decorators && yarn test:decorator-types && yarn docs:lint && yarn docs:snippets:self-test && yarn docs:snippets && yarn docs:examples:self-test && yarn docs:examples && node --test scripts/for_release/*.test.mjs && node --test scripts/classify-changes.test.mjs && yarn set-version --check" }, "devDependencies": { "@cratis/arc.react": "22.19.1", diff --git a/scripts/classify-changes.sh b/scripts/classify-changes.sh new file mode 100755 index 00000000..b4db1328 --- /dev/null +++ b/scripts/classify-changes.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# Reads changed file paths (one per line) from stdin and prints the CI job selection as +# key=value lines. An empty list (for example a pull request whose changes are already on +# the base branch) selects nothing, so every downstream job is skipped. +set -euo pipefail + +code=false +drizzle=false +tutorial=false +docs=false +seen=false + +while IFS= read -r file || [[ -n "$file" ]]; do + [[ -z "$file" ]] && continue + seen=true + case "$file" in + Documentation/client-snippets/tutorial/*|Documentation/client-snippets/arc-without-event-sourcing/*|Documentation/tutorial-e2e/*|Documentation/validate-client-snippets.py|Source/MongoDB/*|Source/Core/*|Source/Tools/ProxyGenerator/*|.github/workflows/ci.yml|package.json|yarn.lock) tutorial=true ;; + esac + case "$file" in + Source/Drizzle/*|Documentation/sql/observing-postgresql.md|.github/workflows/ci.yml|package.json|yarn.lock) drizzle=true ;; + esac + case "$file" in + Documentation/*|*.md) ;; + *) code=true ;; + esac +done + +if [[ "$seen" == true && "$code" == false ]]; then + docs=true +fi + +printf 'code=%s\ndrizzle=%s\ntutorial=%s\ndocs=%s\n' "$code" "$drizzle" "$tutorial" "$docs" diff --git a/scripts/classify-changes.test.mjs b/scripts/classify-changes.test.mjs new file mode 100644 index 00000000..22c841a2 --- /dev/null +++ b/scripts/classify-changes.test.mjs @@ -0,0 +1,38 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const script = join(dirname(fileURLToPath(import.meta.url)), 'classify-changes.sh'); + +function classify(files) { + const result = spawnSync('bash', [script], { input: files.join('\n'), encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + return Object.fromEntries(result.stdout.trim().split('\n').map(line => line.split('='))); +} + +test('when there are no changed files, nothing is selected', () => { + assert.deepEqual(classify([]), { code: 'false', drizzle: 'false', tutorial: 'false', docs: 'false' }); +}); + +test('when only blank lines are given, nothing is selected', () => { + assert.deepEqual(classify(['', '']), { code: 'false', drizzle: 'false', tutorial: 'false', docs: 'false' }); +}); + +test('when only documentation changed, only the documentation check is selected', () => { + assert.deepEqual(classify(['README.md', 'Documentation/index.md']), { code: 'false', drizzle: 'false', tutorial: 'false', docs: 'true' }); +}); + +test('when source changed, the complete gate is selected instead of the documentation check', () => { + assert.deepEqual(classify(['Source/Chronicle/index.ts']), { code: 'true', drizzle: 'false', tutorial: 'false', docs: 'false' }); +}); + +test('when core, drizzle and lock file changed, the matching integrations are selected', () => { + const result = classify(['Source/Core/a.ts', 'Source/Drizzle/b.ts']); + assert.equal(result.tutorial, 'true'); + assert.equal(result.drizzle, 'true'); + assert.equal(result.code, 'true'); +});