Skip to content

Commit 00b7396

Browse files
committed
Update release spec to only list changed packages
This PR adds a new property to the Package object, `hasChangesSinceLatestRelease`. This property is computed when a package is read from a project by running a diff between the last-created Git tag associated with that package and whatever the HEAD commit happens to be, then checking to see whether any of the files changed belong to that package. If they do, then `hasChangesSinceLatestRelease` is true, otherwise it's false. This property is then used to filter the list of packages that are placed within the release spec template when it is generated. There are a couple of things to consider here: * "the last-created Git tag associated with that package" — how do we know this? How do we map a package's version to a tag? We have to account for tags that were created by `action-create-release-pr` in the past as well as the tags that this tool will create in the future. We also have to know what kind of package this is — whether it's the root package of a monorepo or a workspace package — because the set of possible tags will differ. These differences are documented in `readMonorepoRootPackage` and `readMonorepoWorkspacePackage`. * What happens if a repo has no tags? Then all of the packages in that repo are considered to have changed, as they have yet to receive their initial release, so they will all be included in the release spec template.
1 parent 69c81e8 commit 00b7396

11 files changed

+1064
-94
lines changed

src/misc-utils.test.ts

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
isErrorWithStack,
77
wrapError,
88
resolveExecutable,
9-
getStdoutFromCommand,
109
runCommand,
10+
getStdoutFromCommand,
11+
getLinesFromCommand,
12+
placeInSpecificOrder,
1113
} from './misc-utils';
1214

1315
jest.mock('which');
@@ -135,6 +137,24 @@ describe('misc-utils', () => {
135137
});
136138
});
137139

140+
describe('runCommand', () => {
141+
it('runs the command, discarding its output', async () => {
142+
const execaSpy = jest
143+
.spyOn(execaModule, 'default')
144+
// Typecast: It's difficult to provide a full return value for execa
145+
.mockResolvedValue({ stdout: ' some output ' } as any);
146+
147+
const result = await runCommand('some command', ['arg1', 'arg2'], {
148+
all: true,
149+
});
150+
151+
expect(execaSpy).toHaveBeenCalledWith('some command', ['arg1', 'arg2'], {
152+
all: true,
153+
});
154+
expect(result).toBeUndefined();
155+
});
156+
});
157+
138158
describe('getStdoutFromCommand', () => {
139159
it('executes the given command and returns a version of the standard out from the command with whitespace trimmed', async () => {
140160
const execaSpy = jest
@@ -155,21 +175,75 @@ describe('misc-utils', () => {
155175
});
156176
});
157177

158-
describe('runCommand', () => {
159-
it('runs the command, discarding its output', async () => {
178+
describe('getLinesFromCommand', () => {
179+
it('executes the given command and returns the standard out from the command split into lines', async () => {
160180
const execaSpy = jest
161181
.spyOn(execaModule, 'default')
162182
// Typecast: It's difficult to provide a full return value for execa
163-
.mockResolvedValue({ stdout: ' some output ' } as any);
183+
.mockResolvedValue({ stdout: 'line 1\nline 2\nline 3' } as any);
164184

165-
const result = await runCommand('some command', ['arg1', 'arg2'], {
185+
const lines = await getLinesFromCommand(
186+
'some command',
187+
['arg1', 'arg2'],
188+
{ all: true },
189+
);
190+
191+
expect(execaSpy).toHaveBeenCalledWith('some command', ['arg1', 'arg2'], {
166192
all: true,
167193
});
194+
expect(lines).toStrictEqual(['line 1', 'line 2', 'line 3']);
195+
});
196+
197+
it('does not strip leading and trailing whitespace from the output, but does remove empty lines', async () => {
198+
const execaSpy = jest
199+
.spyOn(execaModule, 'default')
200+
// Typecast: It's difficult to provide a full return value for execa
201+
.mockResolvedValue({
202+
stdout: ' line 1\nline 2\n\n line 3 \n',
203+
} as any);
204+
205+
const lines = await getLinesFromCommand(
206+
'some command',
207+
['arg1', 'arg2'],
208+
{ all: true },
209+
);
168210

169211
expect(execaSpy).toHaveBeenCalledWith('some command', ['arg1', 'arg2'], {
170212
all: true,
171213
});
172-
expect(result).toBeUndefined();
214+
expect(lines).toStrictEqual([' line 1', 'line 2', ' line 3 ']);
215+
});
216+
});
217+
218+
describe('placeInSpecificOrder', () => {
219+
it('returns the first set of strings if the second set of strings is the same', () => {
220+
expect(
221+
placeInSpecificOrder(['foo', 'bar', 'baz'], ['foo', 'bar', 'baz']),
222+
).toStrictEqual(['foo', 'bar', 'baz']);
223+
});
224+
225+
it('returns the first set of strings if the second set of strings is completely different', () => {
226+
expect(
227+
placeInSpecificOrder(['foo', 'bar', 'baz'], ['qux', 'blargh']),
228+
).toStrictEqual(['foo', 'bar', 'baz']);
229+
});
230+
231+
it('returns the second set of strings if both sets of strings exactly have the same elements (just in a different order)', () => {
232+
expect(
233+
placeInSpecificOrder(
234+
['foo', 'qux', 'bar', 'baz'],
235+
['baz', 'foo', 'bar', 'qux'],
236+
),
237+
).toStrictEqual(['baz', 'foo', 'bar', 'qux']);
238+
});
239+
240+
it('returns the first set of strings with the items common to both sets rearranged according to the second set, placing those unique to the first set last', () => {
241+
expect(
242+
placeInSpecificOrder(
243+
['foo', 'zing', 'qux', 'bar', 'baz', 'zam'],
244+
['baz', 'foo', 'bar', 'bam', 'qux', 'zox'],
245+
),
246+
).toStrictEqual(['baz', 'foo', 'bar', 'qux', 'zing', 'zam']);
173247
});
174248
});
175249
});

src/misc-utils.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ export async function resolveExecutable(
118118
}
119119
}
120120

121+
/**
122+
* Runs a command, discarding its output.
123+
*
124+
* @param command - The command to execute.
125+
* @param args - The positional arguments to the command.
126+
* @param options - The options to `execa`.
127+
* @throws An `execa` error object if the command fails in some way.
128+
* @see `execa`.
129+
*/
130+
export async function runCommand(
131+
command: string,
132+
args?: readonly string[] | undefined,
133+
options?: execa.Options<string> | undefined,
134+
): Promise<void> {
135+
await execa(command, args, options);
136+
}
137+
121138
/**
122139
* Runs a command, retrieving the standard output with leading and trailing
123140
* whitespace removed.
@@ -138,18 +155,46 @@ export async function getStdoutFromCommand(
138155
}
139156

140157
/**
141-
* Runs a command, discarding its output.
158+
* Runs a Git command, splitting up the immediate output into lines.
142159
*
143160
* @param command - The command to execute.
144161
* @param args - The positional arguments to the command.
145162
* @param options - The options to `execa`.
163+
* @returns The standard output of the command.
146164
* @throws An `execa` error object if the command fails in some way.
147165
* @see `execa`.
148166
*/
149-
export async function runCommand(
167+
export async function getLinesFromCommand(
150168
command: string,
151169
args?: readonly string[] | undefined,
152170
options?: execa.Options<string> | undefined,
153-
): Promise<void> {
154-
await execa(command, args, options);
171+
): Promise<string[]> {
172+
const { stdout } = await execa(command, args, options);
173+
return stdout.split('\n').filter((value) => value !== '');
174+
}
175+
176+
/**
177+
* Reorders the given set of strings according to the sort order.
178+
*
179+
* @param unsortedStrings - A set of strings that need to be sorted.
180+
* @param sortedStrings - A set of strings that designate the order in which
181+
* the first set of strings should be placed.
182+
* @returns A sorted version of `unsortedStrings`.
183+
*/
184+
export function placeInSpecificOrder(
185+
unsortedStrings: string[],
186+
sortedStrings: string[],
187+
): string[] {
188+
const unsortedStringsCopy = unsortedStrings.slice();
189+
const newSortedStrings: string[] = [];
190+
sortedStrings.forEach((string) => {
191+
const index = unsortedStringsCopy.indexOf(string);
192+
193+
if (index !== -1) {
194+
unsortedStringsCopy.splice(index, 1);
195+
newSortedStrings.push(string);
196+
}
197+
});
198+
newSortedStrings.push(...unsortedStringsCopy);
199+
return newSortedStrings;
155200
}

0 commit comments

Comments
 (0)