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
5 changes: 5 additions & 0 deletions .github/workflows/semantic-version-calculate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ jobs:
with:
node-version: "lts/*"

- name: Test the shared release scripts
# Every module's version and release notes are worked out through release/, so a
# broken filter there must fail here, before it can choose a version.
run: node --test release/*.test.js

- name: Set PROJECT_DIR environment variable
if: ${{ inputs.project_dir != '' }}
run: echo "PROJECT_DIR=${{ inputs.project_dir }}" >> "$GITHUB_ENV"
Expand Down
4 changes: 3 additions & 1 deletion api-detector-core/first-release-patch-analyzer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const commitAnalyzer = require(require.resolve('@semantic-release/commit-analyzer', { paths: [process.cwd()] }));
// Only the commits that change this module count for its version: see ../release/component-commits.js.
const { forComponent } = require('../release/component-commits');

// This module is seeded with a baseline tag (api-detector-core-v0.0.0) specifically so
// semantic-release finds a lastRelease and increments from it instead of falling back to its
Expand All @@ -15,6 +17,6 @@ module.exports = {
if (!context.lastRelease || !context.lastRelease.version || context.lastRelease.version === SEED_VERSION) {
return 'patch';
}
return commitAnalyzer.analyzeCommits(pluginConfig, context);
return commitAnalyzer.analyzeCommits(pluginConfig, forComponent(context));
}
};
2 changes: 1 addition & 1 deletion api-detector-core/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/
}
}],
["@semantic-release/release-notes-generator", {
["../release/component-release-notes", {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const commitAnalyzer = require(require.resolve('@semantic-release/commit-analyzer', { paths: [process.cwd()] }));
// Only the commits that change this module count for its version: see ../release/component-commits.js.
const { forComponent } = require('../release/component-commits');

// This module is seeded with a baseline tag (api-only-transcriberj-restdocs-v0.0.0) specifically so
// semantic-release finds a lastRelease and increments from it instead of falling back to its
Expand All @@ -15,6 +17,6 @@ module.exports = {
if (!context.lastRelease || !context.lastRelease.version || context.lastRelease.version === SEED_VERSION) {
return 'patch';
}
return commitAnalyzer.analyzeCommits(pluginConfig, context);
return commitAnalyzer.analyzeCommits(pluginConfig, forComponent(context));
}
};
2 changes: 1 addition & 1 deletion api-only-transcriberj-restdocs/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/
}
}],
["@semantic-release/release-notes-generator", {
["../release/component-release-notes", {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'],
Expand Down
4 changes: 3 additions & 1 deletion dsl-updater-core/first-release-patch-analyzer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const commitAnalyzer = require(require.resolve('@semantic-release/commit-analyzer', { paths: [process.cwd()] }));
// Only the commits that change this module count for its version: see ../release/component-commits.js.
const { forComponent } = require('../release/component-commits');

// This module is seeded with a baseline tag (api-detector-core-v0.0.0) specifically so
// semantic-release finds a lastRelease and increments from it instead of falling back to its
Expand All @@ -15,6 +17,6 @@ module.exports = {
if (!context.lastRelease || !context.lastRelease.version || context.lastRelease.version === SEED_VERSION) {
return 'patch';
}
return commitAnalyzer.analyzeCommits(pluginConfig, context);
return commitAnalyzer.analyzeCommits(pluginConfig, forComponent(context));
}
};
2 changes: 1 addition & 1 deletion dsl-updater-core/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/
}
}],
["@semantic-release/release-notes-generator", {
["../release/component-release-notes", {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'],
Expand Down
2 changes: 1 addition & 1 deletion geo-tracker-lens-pack/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
branches: ["main"],
tagFormat: "geo-tracker-lens-pack-v${version}",
plugins: [
["@semantic-release/commit-analyzer", {
["../release/component-commit-analyzer", {
preset: 'angular',
releaseRules: [
{ type: 'feat', release: 'minor' },
Expand Down
4 changes: 2 additions & 2 deletions hexagonal-spring-rules/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
// of restarting.
tagFormat: "hexagonal-spring-rules-v${version}",
plugins: [
["@semantic-release/commit-analyzer", {
["../release/component-commit-analyzer", {
preset: 'angular',
releaseRules: [
{ type: 'feat', release: 'minor' },
Expand All @@ -32,7 +32,7 @@ module.exports = {
["@semantic-release/npm", {
"npmPublish": false,
}],
["@semantic-release/release-notes-generator", {
["../release/component-release-notes", {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'],
Expand Down
14 changes: 14 additions & 0 deletions release/component-commit-analyzer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// The version of a module, from the commits that change it: see component-commits.js.
// Each module's release.config.js names this file where it named
// @semantic-release/commit-analyzer, with the same options; this hands those options, and a
// context holding only the module's own commits, to the real analyzer.
//
// Resolved from process.cwd() rather than from this file: semantic-release runs in the
// module's directory, and the dependency is installed in the module's own node_modules, not
// beside this file.
const commitAnalyzer = require(require.resolve('@semantic-release/commit-analyzer', { paths: [process.cwd()] }));
const { forComponent } = require('./component-commits');

module.exports = {
analyzeCommits: async (pluginConfig, context) => commitAnalyzer.analyzeCommits(pluginConfig, forComponent(context))
};
61 changes: 61 additions & 0 deletions release/component-commits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// This file lives at the repository root's release/ directory, shared across every module's
// own release.config.js: through the module's own first-release-patch-analyzer.js, or
// through component-commit-analyzer.js, and through component-release-notes.js.
//
// semantic-release reads every commit since a module's last tag, whichever module the commit
// changed, because it knows nothing of the other modules in this repository. So one module's
// feat raised another's version, and appeared in its changelog. The version and the release
// notes are therefore worked out from the commits that change something inside the module's
// own directory, and only those.
//
// It needs nothing but git, which semantic-release needs anyway, so it can be tested and
// required from anywhere.
const { execFileSync } = require('node:child_process');
const path = require('node:path');

/**
* The module a semantic-release run is for: the directory it runs in, relative to the
* repository's root, with forward slashes. Empty at the root itself.
*/
function componentDirectory(cwd) {
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd }).toString().trim();
return path.relative(fs(root), fs(cwd)).split(path.sep).join('/');
}

/** A path as the file system spells it, so a symlinked temporary directory compares equal. */
function fs(p) {
return require('node:fs').realpathSync(p);
}

/** The files a commit changes, relative to the repository's root. */
function changedFiles(hash, cwd) {
return execFileSync('git', ['diff-tree', '--no-commit-id', '--name-only', '-r', '--root', hash], { cwd })
.toString().split('\n').filter(Boolean);
}

/**
* The commits of a semantic-release context that change something inside the module's own
* directory. At the repository's root, every commit.
*/
function ownCommits(context) {
const cwd = context.cwd || process.cwd();
const directory = componentDirectory(cwd);
if (!directory) return context.commits;
const prefix = `${directory}/`;
return context.commits.filter((commit) => changedFiles(commit.hash, cwd).some((file) => file.startsWith(prefix)));
}

/**
* The context with only the module's own commits, for a plugin to work out a version or
* release notes from. The context itself is left as it is.
*/
function forComponent(context) {
const commits = ownCommits(context);
if (context.logger && commits !== context.commits) {
context.logger.log(`${commits.length} of ${context.commits.length} commits change `
+ `${componentDirectory(context.cwd || process.cwd())}/; only those count for its release`);
}
return { ...context, commits };
}

module.exports = { componentDirectory, ownCommits, forComponent };
81 changes: 81 additions & 0 deletions release/component-commits.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Run with: node --test release/*.test.js
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const { componentDirectory, ownCommits, forComponent } = require('./component-commits');

/** A repository with two components, and a commit for each kind of change. */
function repository() {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'component-commits-')));
const git = (...args) => execFileSync('git', args, { cwd: root }).toString().trim();
git('init', '-q');
git('config', 'user.email', 'test@example.com');
git('config', 'user.name', 'Test');
git('config', 'commit.gpgsign', 'false');
const commit = (message, ...files) => {
for (const file of files) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.appendFileSync(path.join(root, file), `${message}\n`);
}
git('add', '-A');
git('commit', '-q', '-m', message);
return { hash: git('rev-parse', 'HEAD'), message };
};
const commits = {
first: commit('feat: the first commit', 'publisher/a.js'),
publisher: commit('fix: the publisher', 'publisher/src/b.js'),
subscriber: commit('feat: the subscriber', 'subscriber/c.java'),
both: commit('fix: both', 'publisher/d.js', 'subscriber/d.java'),
outside: commit('ci: the workflows', '.github/workflows/e.yml'),
lookalike: commit('feat: a directory whose name starts the same', 'publisher-docs/f.md'),
};
return { root, commits };
}

test('a component is the directory semantic-release runs in, relative to the repository', () => {
const { root } = repository();

assert.equal(componentDirectory(path.join(root, 'publisher')), 'publisher');
assert.equal(componentDirectory(root), '');
});

test('a component keeps only the commits that change something inside its directory', () => {
const { root, commits } = repository();
const all = Object.values(commits);

const publisher = ownCommits({ cwd: path.join(root, 'publisher'), commits: all });
const subscriber = ownCommits({ cwd: path.join(root, 'subscriber'), commits: all });

assert.deepEqual(publisher.map((c) => c.message),
['feat: the first commit', 'fix: the publisher', 'fix: both']);
assert.deepEqual(subscriber.map((c) => c.message), ['feat: the subscriber', 'fix: both']);
});

test('run at the repository root, every commit counts, as before', () => {
const { root, commits } = repository();
const all = Object.values(commits);

assert.equal(ownCommits({ cwd: root, commits: all }).length, all.length);
});

test('the context is copied with the commits filtered, and says what it dropped', () => {
const { root, commits } = repository();
const logged = [];
const context = {
cwd: path.join(root, 'subscriber'),
commits: Object.values(commits),
logger: { log: (...args) => logged.push(args.join(' ')) },
lastRelease: { version: '1.0.0' },
};

const filtered = forComponent(context);

assert.equal(filtered.commits.length, 2);
assert.equal(filtered.lastRelease, context.lastRelease);
assert.equal(context.commits.length, 6, 'the original context is left alone');
assert.deepEqual(logged, ['2 of 6 commits change subscriber/; only those count for its release']);
});
14 changes: 14 additions & 0 deletions release/component-release-notes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// The release notes of a module, from the commits that change it: see component-commits.js.
// Each module's release.config.js names this file where it named
// @semantic-release/release-notes-generator, with the same options; this hands those options,
// and a context holding only the module's own commits, to the real generator.
//
// Resolved from process.cwd() rather than from this file: semantic-release runs in the
// module's directory, and the dependency is installed in the module's own node_modules, not
// beside this file.
const releaseNotesGenerator = require(require.resolve('@semantic-release/release-notes-generator', { paths: [process.cwd()] }));
const { forComponent } = require('./component-commits');

module.exports = {
generateNotes: async (pluginConfig, context) => releaseNotesGenerator.generateNotes(pluginConfig, forComponent(context))
};
4 changes: 2 additions & 2 deletions sedr-library/release.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
// old shared scheme, so numbering continues from there instead of restarting.
tagFormat: "sedr-library-v${version}",
plugins: [
["@semantic-release/commit-analyzer", {
["../release/component-commit-analyzer", {
preset: 'angular',
releaseRules: [
{ type: 'feat', release: 'minor' },
Expand All @@ -29,7 +29,7 @@ module.exports = {
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/
}
}],
["@semantic-release/release-notes-generator", {
["../release/component-release-notes", {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'],
Expand Down
Loading