Skip to content

Commit 81000c9

Browse files
authored
Merge pull request #4 from link-foundation/issue-3-e7a11fc0a4e7
fix(ci): Remove CI/CD check differences between PR and push events
2 parents ec35117 + 97750e5 commit 81000c9

File tree

3 files changed

+279
-12
lines changed

3 files changed

+279
-12
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'MyPackage': patch
3+
---
4+
5+
Fix CI/CD check differences between pull request and push events
6+
7+
Changes:
8+
9+
- Add `detect-changes` job with cross-platform `detect-code-changes.mjs` script
10+
- Make lint job independent of changeset-check (runs based on file changes only)
11+
- Allow docs-only PRs without changeset requirement
12+
- Handle changeset-check 'skipped' state in dependent jobs
13+
- Exclude `.changeset/`, `docs/`, `experiments/`, `examples/` folders and markdown files from code changes detection

.github/workflows/release.yml

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,45 @@ env:
3939
DOTNET_NOLOGO: true
4040

4141
jobs:
42-
# Changeset validation - only runs on PRs
42+
# === DETECT CHANGES - determines which jobs should run ===
43+
detect-changes:
44+
name: Detect Changes
45+
runs-on: ubuntu-latest
46+
if: github.event_name != 'workflow_dispatch'
47+
outputs:
48+
cs-changed: ${{ steps.changes.outputs.cs-changed }}
49+
csproj-changed: ${{ steps.changes.outputs.csproj-changed }}
50+
sln-changed: ${{ steps.changes.outputs.sln-changed }}
51+
props-changed: ${{ steps.changes.outputs.props-changed }}
52+
mjs-changed: ${{ steps.changes.outputs.mjs-changed }}
53+
docs-changed: ${{ steps.changes.outputs.docs-changed }}
54+
workflow-changed: ${{ steps.changes.outputs.workflow-changed }}
55+
any-code-changed: ${{ steps.changes.outputs.any-code-changed }}
56+
steps:
57+
- uses: actions/checkout@v4
58+
with:
59+
fetch-depth: 0
60+
61+
- name: Setup Bun
62+
uses: oven-sh/setup-bun@v2
63+
with:
64+
bun-version: latest
65+
66+
- name: Detect changes
67+
id: changes
68+
env:
69+
GITHUB_EVENT_NAME: ${{ github.event_name }}
70+
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
71+
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
72+
run: bun run scripts/detect-code-changes.mjs
73+
74+
# === CHANGESET CHECK - only runs on PRs with code changes ===
75+
# Docs-only PRs (./docs folder, markdown files) don't require changesets
4376
changeset-check:
4477
name: Changeset Validation
4578
runs-on: ubuntu-latest
46-
if: github.event_name == 'pull_request'
79+
needs: [detect-changes]
80+
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true'
4781
steps:
4882
- uses: actions/checkout@v4
4983
with:
@@ -69,12 +103,23 @@ jobs:
69103
# Run changeset validation script
70104
bun run scripts/validate-changeset.mjs
71105
72-
# Linting and formatting - runs after changeset check on PRs, immediately on main
106+
# === LINT AND FORMAT CHECK ===
107+
# Lint runs independently of changeset-check - it's a fast check that should always run
108+
# See: https://github.com/link-foundation/js-ai-driven-development-pipeline-template/pull/18 for why this dependency was removed
73109
lint:
74110
name: Lint and Format Check
75111
runs-on: ubuntu-latest
76-
needs: [changeset-check]
77-
if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success')
112+
needs: [detect-changes]
113+
if: |
114+
github.event_name == 'push' ||
115+
github.event_name == 'workflow_dispatch' ||
116+
needs.detect-changes.outputs.cs-changed == 'true' ||
117+
needs.detect-changes.outputs.csproj-changed == 'true' ||
118+
needs.detect-changes.outputs.sln-changed == 'true' ||
119+
needs.detect-changes.outputs.props-changed == 'true' ||
120+
needs.detect-changes.outputs.mjs-changed == 'true' ||
121+
needs.detect-changes.outputs.docs-changed == 'true' ||
122+
needs.detect-changes.outputs.workflow-changed == 'true'
78123
steps:
79124
- uses: actions/checkout@v4
80125

@@ -100,12 +145,13 @@ jobs:
100145
- name: Check file size limit
101146
run: bun run scripts/check-file-size.mjs
102147

103-
# Test on multiple OS
148+
# === TEST ON MULTIPLE OS ===
104149
test:
105150
name: Test (${{ matrix.os }})
106151
runs-on: ${{ matrix.os }}
107-
needs: [changeset-check]
108-
if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success')
152+
needs: [detect-changes, changeset-check]
153+
# Run if: push event, workflow_dispatch, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR)
154+
if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped')
109155
strategy:
110156
fail-fast: false
111157
matrix:
@@ -133,7 +179,8 @@ jobs:
133179
with:
134180
fail_ci_if_error: false
135181

136-
# Build package - only runs if lint and test pass
182+
# === BUILD PACKAGE ===
183+
# Only runs if lint and test pass
137184
build:
138185
name: Build Package
139186
runs-on: ubuntu-latest
@@ -162,7 +209,8 @@ jobs:
162209
name: nuget-package
163210
path: ./artifacts/*.nupkg
164211

165-
# Automatic release on push to main (using changesets)
212+
# === AUTOMATIC RELEASE ===
213+
# Runs on push to main using changesets
166214
release:
167215
name: Release
168216
needs: [lint, test, build]
@@ -233,7 +281,8 @@ jobs:
233281
--release-version "${{ steps.version.outputs.new_version }}" \
234282
--repository "${{ github.repository }}"
235283
236-
# Manual instant release via workflow_dispatch
284+
# === MANUAL INSTANT RELEASE ===
285+
# Triggered via workflow_dispatch with instant mode
237286
instant-release:
238287
name: Instant Release
239288
needs: [lint, test, build]
@@ -293,7 +342,8 @@ jobs:
293342
--release-version "${{ steps.version.outputs.new_version }}" \
294343
--repository "${{ github.repository }}"
295344
296-
# Manual changeset PR - creates a pull request with the changeset for review
345+
# === MANUAL CHANGESET PR ===
346+
# Creates a pull request with the changeset for review
297347
changeset-pr:
298348
name: Create Changeset PR
299349
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr'

scripts/detect-code-changes.mjs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Detect code changes for CI/CD pipeline
5+
*
6+
* This script detects what types of files have changed between two commits
7+
* and outputs the results for use in GitHub Actions workflow conditions.
8+
*
9+
* Key behavior:
10+
* - For PRs: compares PR head against base branch
11+
* - For pushes: compares HEAD against HEAD^
12+
* - Excludes certain folders and file types from "code changes" detection
13+
*
14+
* Excluded from code changes (don't require changesets):
15+
* - Markdown files (*.md) in any folder
16+
* - .changeset/ folder (changeset metadata)
17+
* - docs/ folder (documentation)
18+
* - experiments/ folder (experimental scripts)
19+
* - examples/ folder (example scripts)
20+
*
21+
* Usage:
22+
* bun run scripts/detect-code-changes.mjs
23+
*
24+
* Environment variables (set by GitHub Actions):
25+
* - GITHUB_EVENT_NAME: 'pull_request' or 'push'
26+
* - GITHUB_BASE_SHA: Base commit SHA for PR
27+
* - GITHUB_HEAD_SHA: Head commit SHA for PR
28+
*
29+
* Outputs (written to GITHUB_OUTPUT):
30+
* - cs-changed: 'true' if any .cs files changed
31+
* - csproj-changed: 'true' if any .csproj files changed
32+
* - sln-changed: 'true' if any .sln files changed
33+
* - props-changed: 'true' if any .props files changed (Directory.Build.props etc.)
34+
* - mjs-changed: 'true' if any .mjs files changed (scripts)
35+
* - docs-changed: 'true' if any .md files changed
36+
* - workflow-changed: 'true' if any .github/workflows/ files changed
37+
* - any-code-changed: 'true' if any code files changed (excludes docs, changesets, experiments, examples)
38+
*/
39+
40+
import { execSync } from 'child_process';
41+
import { appendFileSync } from 'fs';
42+
43+
/**
44+
* Execute a shell command and return trimmed output
45+
* @param {string} command - The command to execute
46+
* @returns {string} - The trimmed command output
47+
*/
48+
function exec(command) {
49+
try {
50+
return execSync(command, { encoding: 'utf-8' }).trim();
51+
} catch (error) {
52+
console.error(`Error executing command: ${command}`);
53+
console.error(error.message);
54+
return '';
55+
}
56+
}
57+
58+
/**
59+
* Write output to GitHub Actions output file
60+
* @param {string} name - Output name
61+
* @param {string} value - Output value
62+
*/
63+
function setOutput(name, value) {
64+
const outputFile = process.env.GITHUB_OUTPUT;
65+
if (outputFile) {
66+
appendFileSync(outputFile, `${name}=${value}\n`);
67+
}
68+
console.log(`${name}=${value}`);
69+
}
70+
71+
/**
72+
* Get the list of changed files between two commits
73+
* @returns {string[]} Array of changed file paths
74+
*/
75+
function getChangedFiles() {
76+
const eventName = process.env.GITHUB_EVENT_NAME || 'local';
77+
78+
if (eventName === 'pull_request') {
79+
const baseSha = process.env.GITHUB_BASE_SHA;
80+
const headSha = process.env.GITHUB_HEAD_SHA;
81+
82+
if (baseSha && headSha) {
83+
console.log(`Comparing PR: ${baseSha}...${headSha}`);
84+
try {
85+
// Ensure we have the base commit
86+
try {
87+
execSync(`git cat-file -e ${baseSha}`, { stdio: 'ignore' });
88+
} catch {
89+
console.log('Base commit not available locally, attempting fetch...');
90+
execSync(`git fetch origin ${baseSha}`, { stdio: 'inherit' });
91+
}
92+
const output = exec(`git diff --name-only ${baseSha} ${headSha}`);
93+
return output ? output.split('\n').filter(Boolean) : [];
94+
} catch (error) {
95+
console.error(`Git diff failed: ${error.message}`);
96+
}
97+
}
98+
}
99+
100+
// For push events or fallback
101+
console.log('Comparing HEAD^ to HEAD');
102+
try {
103+
const output = exec('git diff --name-only HEAD^ HEAD');
104+
return output ? output.split('\n').filter(Boolean) : [];
105+
} catch {
106+
// If HEAD^ doesn't exist (first commit), list all files in HEAD
107+
console.log('HEAD^ not available, listing all files in HEAD');
108+
const output = exec('git ls-tree --name-only -r HEAD');
109+
return output ? output.split('\n').filter(Boolean) : [];
110+
}
111+
}
112+
113+
/**
114+
* Check if a file should be excluded from code changes detection
115+
* @param {string} filePath - The file path to check
116+
* @returns {boolean} True if the file should be excluded
117+
*/
118+
function isExcludedFromCodeChanges(filePath) {
119+
// Exclude markdown files in any folder
120+
if (filePath.endsWith('.md')) {
121+
return true;
122+
}
123+
124+
// Exclude specific folders from code changes
125+
const excludedFolders = ['.changeset/', 'docs/', 'experiments/', 'examples/'];
126+
127+
for (const folder of excludedFolders) {
128+
if (filePath.startsWith(folder)) {
129+
return true;
130+
}
131+
}
132+
133+
return false;
134+
}
135+
136+
/**
137+
* Main function to detect changes
138+
*/
139+
function detectChanges() {
140+
console.log('Detecting file changes for CI/CD...\n');
141+
142+
const changedFiles = getChangedFiles();
143+
144+
console.log('Changed files:');
145+
if (changedFiles.length === 0) {
146+
console.log(' (none)');
147+
} else {
148+
changedFiles.forEach((file) => console.log(` ${file}`));
149+
}
150+
console.log('');
151+
152+
// Detect .cs file changes (C# source files)
153+
const csChanged = changedFiles.some((file) => file.endsWith('.cs'));
154+
setOutput('cs-changed', csChanged ? 'true' : 'false');
155+
156+
// Detect .csproj file changes (project files)
157+
const csprojChanged = changedFiles.some((file) => file.endsWith('.csproj'));
158+
setOutput('csproj-changed', csprojChanged ? 'true' : 'false');
159+
160+
// Detect .sln file changes (solution files)
161+
const slnChanged = changedFiles.some((file) => file.endsWith('.sln'));
162+
setOutput('sln-changed', slnChanged ? 'true' : 'false');
163+
164+
// Detect .props file changes (Directory.Build.props, etc.)
165+
const propsChanged = changedFiles.some((file) => file.endsWith('.props'));
166+
setOutput('props-changed', propsChanged ? 'true' : 'false');
167+
168+
// Detect .mjs file changes (scripts)
169+
const mjsChanged = changedFiles.some((file) => file.endsWith('.mjs'));
170+
setOutput('mjs-changed', mjsChanged ? 'true' : 'false');
171+
172+
// Detect documentation changes (any .md file)
173+
const docsChanged = changedFiles.some((file) => file.endsWith('.md'));
174+
setOutput('docs-changed', docsChanged ? 'true' : 'false');
175+
176+
// Detect workflow changes
177+
const workflowChanged = changedFiles.some((file) =>
178+
file.startsWith('.github/workflows/')
179+
);
180+
setOutput('workflow-changed', workflowChanged ? 'true' : 'false');
181+
182+
// Detect code changes (excluding docs, changesets, experiments, examples folders, and markdown files)
183+
const codeChangedFiles = changedFiles.filter(
184+
(file) => !isExcludedFromCodeChanges(file)
185+
);
186+
187+
console.log('\nFiles considered as code changes:');
188+
if (codeChangedFiles.length === 0) {
189+
console.log(' (none)');
190+
} else {
191+
codeChangedFiles.forEach((file) => console.log(` ${file}`));
192+
}
193+
console.log('');
194+
195+
// Check if any code files changed (.cs, .csproj, .sln, .props, .mjs, .json, .yml, .yaml, or workflow files)
196+
const codePattern = /\.(cs|csproj|sln|props|mjs|json|yml|yaml)$|\.github\/workflows\//;
197+
const codeChanged = codeChangedFiles.some((file) => codePattern.test(file));
198+
setOutput('any-code-changed', codeChanged ? 'true' : 'false');
199+
200+
console.log('\nChange detection completed.');
201+
}
202+
203+
// Run the detection
204+
detectChanges();

0 commit comments

Comments
 (0)