Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/fix-ci-workflow-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'my-package': patch
---

Fix CI/CD check differences between pull request and push events

Changes:

- Add `detect-changes` job with cross-platform `detect-code-changes.mjs` script
- Make lint job independent of changeset-check (runs based on file changes only)
- Allow docs-only PRs without changeset requirement
- Handle changeset-check 'skipped' state in dependent jobs
- Exclude `.changeset/`, `docs/`, `experiments/`, `examples/` folders and markdown files from code changes detection
61 changes: 56 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,60 @@ concurrency:
cancel-in-progress: true

env:
JAVA_VERSION: '17'
JAVA_VERSION: '21'
JAVA_DISTRIBUTION: 'temurin'

jobs:
# =============================================================================
# Detect Changes - determines which jobs should run
# =============================================================================
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
if: github.event_name != 'workflow_dispatch'
outputs:
java-changed: ${{ steps.changes.outputs.java-changed }}
pom-changed: ${{ steps.changes.outputs.pom-changed }}
mjs-changed: ${{ steps.changes.outputs.mjs-changed }}
docs-changed: ${{ steps.changes.outputs.docs-changed }}
workflow-changed: ${{ steps.changes.outputs.workflow-changed }}
any-code-changed: ${{ steps.changes.outputs.any-code-changed }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Detect changes
id: changes
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bun scripts/detect-code-changes.mjs

# =============================================================================
# Lint and Format Check
# Runs independently of changeset-check - it's a fast check that should always run
# See: https://github.com/link-foundation/js-ai-driven-development-pipeline-template/pull/18
# =============================================================================
lint:
name: Lint & Format
runs-on: ubuntu-latest
needs: [detect-changes]
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' ||
needs.detect-changes.outputs.java-changed == 'true' ||
needs.detect-changes.outputs.pom-changed == 'true' ||
needs.detect-changes.outputs.mjs-changed == 'true' ||
needs.detect-changes.outputs.docs-changed == 'true' ||
needs.detect-changes.outputs.workflow-changed == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -76,11 +120,14 @@ jobs:
test:
name: Test (Java ${{ matrix.java }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: [detect-changes, changeset-check]
# Run if: push event, workflow_dispatch, OR changeset-check succeeded, OR changeset-check was skipped (docs-only PR)
if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changeset-check.result == 'success' || needs.changeset-check.result == 'skipped')
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
java: ['17', '21']
java: ['21']
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -96,7 +143,7 @@ jobs:
run: mvn test jacoco:report -B

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.java == '17'
if: matrix.os == 'ubuntu-latest' && matrix.java == '21'
uses: codecov/codecov-action@v4
with:
file: target/site/jacoco/jacoco.xml
Expand All @@ -112,6 +159,8 @@ jobs:
name: Build Package
runs-on: ubuntu-latest
needs: [lint, test]
# Run only if lint and test succeeded (handles skipped lint gracefully)
if: always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && needs.test.result == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -142,12 +191,14 @@ jobs:
retention-days: 7

# =============================================================================
# Changeset Validation (PRs only)
# Changeset Validation (PRs only, skipped for docs-only changes)
# Docs-only PRs (./docs folder, markdown files) don't require changesets
# =============================================================================
changeset-check:
name: Changeset Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [detect-changes]
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ This implementation:
- Supports three release modes: changeset, instant, and changeset-pr
- Eliminates merge conflicts on CHANGELOG.md



## [0.1.0] - 2024-12-27

### Added
Expand Down
195 changes: 195 additions & 0 deletions scripts/detect-code-changes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env node

/**
* Detect code changes for CI/CD pipeline
*
* This script detects what types of files have changed between two commits
* and outputs the results for use in GitHub Actions workflow conditions.
*
* Key behavior:
* - For PRs: compares PR head against base branch
* - For pushes: compares HEAD against HEAD^
* - Excludes certain folders and file types from "code changes" detection
*
* Excluded from code changes (don't require changesets):
* - Markdown files (*.md) in any folder
* - .changeset/ folder (changeset metadata)
* - docs/ folder (documentation)
* - experiments/ folder (experimental scripts)
* - examples/ folder (example scripts)
*
* Usage:
* node scripts/detect-code-changes.mjs
* bun scripts/detect-code-changes.mjs
*
* Environment variables (set by GitHub Actions):
* - GITHUB_EVENT_NAME: 'pull_request' or 'push'
* - GITHUB_BASE_SHA: Base commit SHA for PR
* - GITHUB_HEAD_SHA: Head commit SHA for PR
*
* Outputs (written to GITHUB_OUTPUT):
* - java-changed: 'true' if any .java files changed
* - pom-changed: 'true' if pom.xml changed
* - mjs-changed: 'true' if any .mjs files changed
* - docs-changed: 'true' if any .md files changed
* - workflow-changed: 'true' if any .github/workflows/ files changed
* - any-code-changed: 'true' if any code files changed (excludes docs, changesets, experiments, examples)
*/

import { execSync } from 'child_process';
import { appendFileSync } from 'fs';

/**
* Execute a shell command and return trimmed output
* @param {string} command - The command to execute
* @returns {string} - The trimmed command output
*/
function exec(command) {
try {
return execSync(command, { encoding: 'utf-8' }).trim();
} catch (error) {
console.error(`Error executing command: ${command}`);
console.error(error.message);
return '';
}
}

/**
* Write output to GitHub Actions output file
* @param {string} name - Output name
* @param {string} value - Output value
*/
function setOutput(name, value) {
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
appendFileSync(outputFile, `${name}=${value}\n`);
}
console.log(`${name}=${value}`);
}

/**
* Get the list of changed files between two commits
* @returns {string[]} Array of changed file paths
*/
function getChangedFiles() {
const eventName = process.env.GITHUB_EVENT_NAME || 'local';

if (eventName === 'pull_request') {
const baseSha = process.env.GITHUB_BASE_SHA;
const headSha = process.env.GITHUB_HEAD_SHA;

if (baseSha && headSha) {
console.log(`Comparing PR: ${baseSha}...${headSha}`);
try {
// Ensure we have the base commit
try {
execSync(`git cat-file -e ${baseSha}`, { stdio: 'ignore' });
} catch {
console.log('Base commit not available locally, attempting fetch...');
execSync(`git fetch origin ${baseSha}`, { stdio: 'inherit' });
}
const output = exec(`git diff --name-only ${baseSha} ${headSha}`);
return output ? output.split('\n').filter(Boolean) : [];
} catch (error) {
console.error(`Git diff failed: ${error.message}`);
}
}
}

// For push events or fallback
console.log('Comparing HEAD^ to HEAD');
try {
const output = exec('git diff --name-only HEAD^ HEAD');
return output ? output.split('\n').filter(Boolean) : [];
} catch {
// If HEAD^ doesn't exist (first commit), list all files in HEAD
console.log('HEAD^ not available, listing all files in HEAD');
const output = exec('git ls-tree --name-only -r HEAD');
return output ? output.split('\n').filter(Boolean) : [];
}
}

/**
* Check if a file should be excluded from code changes detection
* @param {string} filePath - The file path to check
* @returns {boolean} True if the file should be excluded
*/
function isExcludedFromCodeChanges(filePath) {
// Exclude markdown files in any folder
if (filePath.endsWith('.md')) {
return true;
}

// Exclude specific folders from code changes
const excludedFolders = ['.changeset/', 'docs/', 'experiments/', 'examples/'];

for (const folder of excludedFolders) {
if (filePath.startsWith(folder)) {
return true;
}
}

return false;
}

/**
* Main function to detect changes
*/
function detectChanges() {
console.log('Detecting file changes for CI/CD...\n');

const changedFiles = getChangedFiles();

console.log('Changed files:');
if (changedFiles.length === 0) {
console.log(' (none)');
} else {
changedFiles.forEach((file) => console.log(` ${file}`));
}
console.log('');

// Detect .java file changes
const javaChanged = changedFiles.some((file) => file.endsWith('.java'));
setOutput('java-changed', javaChanged ? 'true' : 'false');

// Detect pom.xml changes
const pomChanged = changedFiles.some((file) => file === 'pom.xml');
setOutput('pom-changed', pomChanged ? 'true' : 'false');

// Detect .mjs file changes (scripts)
const mjsChanged = changedFiles.some((file) => file.endsWith('.mjs'));
setOutput('mjs-changed', mjsChanged ? 'true' : 'false');

// Detect documentation changes (any .md file)
const docsChanged = changedFiles.some((file) => file.endsWith('.md'));
setOutput('docs-changed', docsChanged ? 'true' : 'false');

// Detect workflow changes
const workflowChanged = changedFiles.some((file) =>
file.startsWith('.github/workflows/')
);
setOutput('workflow-changed', workflowChanged ? 'true' : 'false');

// Detect code changes (excluding docs, changesets, experiments, examples folders, and markdown files)
const codeChangedFiles = changedFiles.filter(
(file) => !isExcludedFromCodeChanges(file)
);

console.log('\nFiles considered as code changes:');
if (codeChangedFiles.length === 0) {
console.log(' (none)');
} else {
codeChangedFiles.forEach((file) => console.log(` ${file}`));
}
console.log('');

// Check if any code files changed (.java, .mjs, .xml, .yml, .yaml, or workflow files)
const codePattern = /\.(java|mjs|xml|yml|yaml|properties)$|\.github\/workflows\//;
const codeChanged = codeChangedFiles.some((file) => codePattern.test(file));
setOutput('any-code-changed', codeChanged ? 'true' : 'false');

console.log('\nChange detection completed.');
}

// Run the detection
detectChanges();