apify-test-tools 0.10.1-beta.2 → 0.10.1-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/checkout-restore-dependencies/action.yaml +4 -3
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +7 -2
- package/.github/workflows/public_platform-tests.yaml +0 -1
- package/.github/workflows-package-version +1 -1
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +13 -3
- package/README.md +2 -2
- package/bin/build-from-local.ts +8 -7
- package/bin/build.ts +22 -21
- package/bin/diff-changes.ts +11 -10
- package/bin/diff-json-schema.ts +14 -12
- package/bin/git.ts +89 -47
- package/bin/logger.ts +23 -0
- package/bin/main.ts +44 -38
- package/bin/output.ts +6 -0
- package/bin/slack.ts +8 -7
- package/bin/test-report.ts +22 -21
- package/bin/types.ts +0 -1
- package/bin/utils/config/actor-json.ts +84 -0
- package/bin/utils/config/load-config.ts +46 -46
- package/bin/utils/config/parser.ts +33 -28
- package/bin/utils/config/structures/base.ts +7 -0
- package/bin/utils/json-file.ts +11 -14
- package/bin/utils/path/repo-relative.ts +156 -0
- package/bin/utils/path/schema.ts +18 -0
- package/bin/utils.ts +63 -44
- package/dist/bin/build-from-local.d.ts +1 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +8 -7
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +22 -21
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +1 -1
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +9 -8
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +1 -1
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +13 -12
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +10 -10
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +78 -37
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/logger.d.ts +11 -0
- package/dist/bin/logger.d.ts.map +1 -0
- package/dist/bin/logger.js +20 -0
- package/dist/bin/logger.js.map +1 -0
- package/dist/bin/main.d.ts +1 -1
- package/dist/bin/main.js +44 -38
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/output.d.ts +3 -0
- package/dist/bin/output.d.ts.map +1 -0
- package/dist/bin/output.js +6 -0
- package/dist/bin/output.js.map +1 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +8 -7
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +22 -21
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +0 -1
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/actor-json.d.ts +14 -0
- package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
- package/dist/bin/utils/config/actor-json.js +60 -0
- package/dist/bin/utils/config/actor-json.js.map +1 -0
- package/dist/bin/utils/config/load-config.d.ts +12 -2
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +32 -31
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +4 -12
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +24 -23
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/base.d.ts +5 -0
- package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
- package/dist/bin/utils/config/structures/base.js.map +1 -1
- package/dist/bin/utils/json-file.d.ts +3 -2
- package/dist/bin/utils/json-file.d.ts.map +1 -1
- package/dist/bin/utils/json-file.js +8 -13
- package/dist/bin/utils/json-file.js.map +1 -1
- package/dist/bin/utils/path/repo-relative.d.ts +35 -0
- package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
- package/dist/bin/utils/path/repo-relative.js +137 -0
- package/dist/bin/utils/path/repo-relative.js.map +1 -0
- package/dist/bin/utils/path/schema.d.ts +7 -0
- package/dist/bin/utils/path/schema.d.ts.map +1 -0
- package/dist/bin/utils/path/schema.js +16 -0
- package/dist/bin/utils/path/schema.js.map +1 -0
- package/dist/bin/utils.d.ts +14 -6
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +52 -36
- package/dist/bin/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +4 -40
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +97 -96
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +52 -48
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git-command.test.d.ts +2 -0
- package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
- package/dist/test/unit/bin/git-command.test.js +54 -0
- package/dist/test/unit/bin/git-command.test.js.map +1 -0
- package/dist/test/unit/bin/git.test.js +211 -94
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/logger.test.d.ts +2 -0
- package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logger.test.js +44 -0
- package/dist/test/unit/bin/logger.test.js.map +1 -0
- package/dist/test/unit/bin/logging.test.d.ts +2 -0
- package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logging.test.js +43 -0
- package/dist/test/unit/bin/logging.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/files.test.js +16 -27
- package/dist/test/unit/bin/utils/files.test.js.map +1 -1
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
- package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +40 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eslint.config.mjs +9 -1
- package/package.json +5 -5
- package/test/unit/bin/build-from-local.test.ts +4 -54
- package/test/unit/bin/diff-changes.test.ts +97 -96
- package/test/unit/bin/diff.test.ts +53 -48
- package/test/unit/bin/git-command.test.ts +71 -0
- package/test/unit/bin/git.test.ts +246 -91
- package/test/unit/bin/logger.test.ts +55 -0
- package/test/unit/bin/logging.test.ts +52 -0
- package/test/unit/bin/utils/config/load-config.test.ts +63 -26
- package/test/unit/bin/utils/config/parser.test.ts +81 -27
- package/test/unit/bin/utils/files.test.ts +19 -33
- package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
- package/test/unit/bin/utils/path/schema.test.ts +65 -0
- package/test/unit/should-built-and-test.test.ts +40 -38
package/bin/git.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import type { Commit, Config } from './types.js';
|
|
2
|
-
import {
|
|
3
|
+
import { GitCommandError, runGitCommand } from './utils.js';
|
|
3
4
|
|
|
4
5
|
export const GIT_FORMAT_SEPARATOR = '»¦«';
|
|
5
6
|
const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR);
|
|
@@ -7,19 +8,20 @@ const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR
|
|
|
7
8
|
/**
|
|
8
9
|
* Gets the list of changed files between the given commits (inclusive).
|
|
9
10
|
*/
|
|
10
|
-
export const getChangedFiles = (commits: Commit[]) => {
|
|
11
|
+
export const getChangedFiles = async (commits: Commit[]) => {
|
|
11
12
|
// getCommits never returns an empty list (the rerun check returns all commits when the base commit
|
|
12
13
|
// is the branch HEAD, and an empty git range throws when parsing), so this signals a programmer error
|
|
13
14
|
if (commits.length === 0) {
|
|
14
15
|
throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const changedFilesString =
|
|
18
|
-
|
|
18
|
+
const changedFilesString = await runGitCommand(
|
|
19
|
+
['diff', '--name-only', `${commits[0].sha}~..${commits[commits.length - 1].sha}`],
|
|
20
|
+
{ operation: 'get the list of changed files between commits' },
|
|
19
21
|
);
|
|
20
22
|
|
|
21
23
|
const changedFiles = changedFilesString.split('\n').filter(Boolean);
|
|
22
|
-
|
|
24
|
+
logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
|
|
23
25
|
return changedFiles;
|
|
24
26
|
};
|
|
25
27
|
|
|
@@ -28,17 +30,36 @@ export const getChangedFiles = (commits: Commit[]) => {
|
|
|
28
30
|
* (i.e. a genuine "merge from target" commit, not a merge of some unrelated branch).
|
|
29
31
|
* Uses the full targetBranch..sourceBranch range, ignoring baseCommit.
|
|
30
32
|
*/
|
|
31
|
-
export const hasMergeFromTarget = (sourceBranch: string, targetBranch: string): boolean => {
|
|
32
|
-
const mergeShas =
|
|
33
|
+
export const hasMergeFromTarget = async (sourceBranch: string, targetBranch: string): Promise<boolean> => {
|
|
34
|
+
const mergeShas = (
|
|
35
|
+
await runGitCommand(['log', '--merges', '--pretty=format:%H', `${targetBranch}..${sourceBranch}`], {
|
|
36
|
+
operation: 'find merge commits on the source branch that are absent from the target branch',
|
|
37
|
+
})
|
|
38
|
+
)
|
|
33
39
|
.split('\n')
|
|
34
40
|
.filter(Boolean);
|
|
35
41
|
|
|
36
42
|
for (const sha of mergeShas) {
|
|
37
|
-
const parents =
|
|
43
|
+
const parents = (
|
|
44
|
+
await runGitCommand(['log', '-1', '--pretty=format:%P', sha], {
|
|
45
|
+
operation: 'get parents of a merge commit',
|
|
46
|
+
})
|
|
47
|
+
).split(' ');
|
|
38
48
|
for (const parent of parents) {
|
|
39
49
|
// git merge-base A B outputs the common ancestor.
|
|
40
50
|
// If that equals A, then A is an ancestor of B (i.e. parent is reachable from targetBranch).
|
|
41
|
-
|
|
51
|
+
let mergeBase: string;
|
|
52
|
+
try {
|
|
53
|
+
mergeBase = await runGitCommand(['merge-base', parent, targetBranch], {
|
|
54
|
+
operation: 'check whether a merge parent is reachable from the target branch',
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Exit code 1 on merge-base means the histories have no common ancestor.
|
|
58
|
+
if (error instanceof GitCommandError && error.exitCode === 1) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
42
63
|
if (mergeBase === parent) {
|
|
43
64
|
return true;
|
|
44
65
|
}
|
|
@@ -51,9 +72,10 @@ export const hasMergeFromTarget = (sourceBranch: string, targetBranch: string):
|
|
|
51
72
|
* Returns all files touched by non-merge commits on the branch (full history, ignoring baseCommit).
|
|
52
73
|
* Used to check whether the branch itself has any functional changes, independent of what master merged in.
|
|
53
74
|
*/
|
|
54
|
-
export const getBranchOnlyChangedFiles = (sourceBranch: string, targetBranch: string): string[] => {
|
|
55
|
-
const output =
|
|
56
|
-
|
|
75
|
+
export const getBranchOnlyChangedFiles = async (sourceBranch: string, targetBranch: string): Promise<string[]> => {
|
|
76
|
+
const output = await runGitCommand(
|
|
77
|
+
['log', '--no-merges', '--name-only', '--pretty=format:', `${targetBranch}..${sourceBranch}`],
|
|
78
|
+
{ operation: 'list files changed by non-merge commits on the source branch' },
|
|
57
79
|
);
|
|
58
80
|
return output.split('\n').filter(Boolean);
|
|
59
81
|
};
|
|
@@ -81,10 +103,12 @@ export const parseBaseCommit = (shaOrCommit: string | undefined): string | undef
|
|
|
81
103
|
return sha;
|
|
82
104
|
};
|
|
83
105
|
|
|
84
|
-
const fetchAllBranchCommits = (sourceBranch: string, targetBranch: string): Commit[] => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
const fetchAllBranchCommits = async (sourceBranch: string, targetBranch: string): Promise<Commit[]> => {
|
|
107
|
+
const output = await runGitCommand(
|
|
108
|
+
['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${targetBranch}..${sourceBranch}`],
|
|
109
|
+
{ operation: 'list commits reachable from the source ref but absent from the target ref' },
|
|
110
|
+
);
|
|
111
|
+
const commitsStrings = output.split('\n');
|
|
88
112
|
const commits = commitsStrings.map((commitString) => parseCommit(commitString));
|
|
89
113
|
commits.reverse();
|
|
90
114
|
return commits;
|
|
@@ -94,22 +118,22 @@ const fetchAllBranchCommits = (sourceBranch: string, targetBranch: string): Comm
|
|
|
94
118
|
* Gets the commits between sourceBranch and targetBranch (exclusive).
|
|
95
119
|
* - If baseCommit is provided, only returns commits after the baseCommit.
|
|
96
120
|
*/
|
|
97
|
-
export const getCommits = ({
|
|
121
|
+
export const getCommits = async ({
|
|
98
122
|
sourceBranch,
|
|
99
123
|
targetBranch,
|
|
100
124
|
baseCommit,
|
|
101
|
-
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Commit[] => {
|
|
125
|
+
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
|
|
102
126
|
const baseCommitSha = parseBaseCommit(baseCommit);
|
|
103
|
-
const commits = fetchAllBranchCommits(sourceBranch, targetBranch);
|
|
127
|
+
const commits = await fetchAllBranchCommits(sourceBranch, targetBranch);
|
|
104
128
|
|
|
105
129
|
// The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
|
|
106
130
|
// validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
|
|
107
131
|
const headSha = commits[commits.length - 1]?.sha;
|
|
108
132
|
if (baseCommitSha !== undefined && baseCommitSha === headSha) {
|
|
109
|
-
|
|
133
|
+
logger.info(
|
|
110
134
|
`Detected rerun with the same commit that we already validated. This usually means the user wants to rerun the Action from scratch, ignoring last validated commit ${baseCommitSha} and returning all commits`,
|
|
111
135
|
);
|
|
112
|
-
|
|
136
|
+
logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
|
|
113
137
|
return commits;
|
|
114
138
|
}
|
|
115
139
|
|
|
@@ -118,17 +142,15 @@ export const getCommits = ({
|
|
|
118
142
|
const hasBaseCommit = baseCommitIndex !== -1;
|
|
119
143
|
if (hasBaseCommit) {
|
|
120
144
|
const commitsUpToBaseCommit = commits.slice(baseCommitIndex + 1);
|
|
121
|
-
|
|
145
|
+
logger.info(
|
|
122
146
|
`Found base commit ${baseCommitSha} at index ${baseCommitIndex}, returning ${commitsUpToBaseCommit.length} commits after it`,
|
|
123
147
|
);
|
|
124
|
-
|
|
148
|
+
logger.info(`Commits being returned: ${commitsUpToBaseCommit.map((c) => c.sha).join(', ')}`);
|
|
125
149
|
return commitsUpToBaseCommit;
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
131
|
-
console.error(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
|
|
152
|
+
logger.info(`Base commit ${baseCommitSha} not found in the commit range, returning all ${commits.length} commits`);
|
|
153
|
+
logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
|
|
132
154
|
return commits;
|
|
133
155
|
};
|
|
134
156
|
|
|
@@ -150,8 +172,10 @@ export const parseCommit = (commitString: string): Commit => {
|
|
|
150
172
|
* Returns the currently checked-out branch. Release builds point the Actor version at this branch,
|
|
151
173
|
* so a detached HEAD (no branch to point at) is an error rather than a guess.
|
|
152
174
|
*/
|
|
153
|
-
export const getCurrentBranch = (): string => {
|
|
154
|
-
const branch =
|
|
175
|
+
export const getCurrentBranch = async (): Promise<string> => {
|
|
176
|
+
const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
177
|
+
operation: 'determine the current branch for release',
|
|
178
|
+
});
|
|
155
179
|
if (branch === 'HEAD') {
|
|
156
180
|
throw new Error(
|
|
157
181
|
'Cannot determine the branch to release: HEAD is detached. Check out the branch you want to release.',
|
|
@@ -164,11 +188,11 @@ export const getCurrentBranch = (): string => {
|
|
|
164
188
|
* Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
|
|
165
189
|
* uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
|
|
166
190
|
*/
|
|
167
|
-
const getOriginRepoUrl = (): string => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
191
|
+
const getOriginRepoUrl = async (): Promise<string> => {
|
|
192
|
+
const rawUrl = await runGitCommand(['remote', 'get-url', 'origin'], {
|
|
193
|
+
operation: 'read the origin repository URL',
|
|
194
|
+
});
|
|
195
|
+
return rawUrl.replace(/^https:\/\/github\.com\//, 'git@github.com:');
|
|
172
196
|
};
|
|
173
197
|
|
|
174
198
|
/**
|
|
@@ -176,8 +200,8 @@ const getOriginRepoUrl = (): string => {
|
|
|
176
200
|
* each Actor's default version, so a fork or mirror remote can't repoint a published Actor. An explicit
|
|
177
201
|
* --repo-url skips that check: passing it is how you move an Actor to another repository on purpose.
|
|
178
202
|
*/
|
|
179
|
-
export const resolveRepoUrl = (explicitRepoUrl: string | undefined) => ({
|
|
180
|
-
repoUrl: explicitRepoUrl ?? getOriginRepoUrl(),
|
|
203
|
+
export const resolveRepoUrl = async (explicitRepoUrl: string | undefined) => ({
|
|
204
|
+
repoUrl: explicitRepoUrl ?? (await getOriginRepoUrl()),
|
|
181
205
|
shouldVerifyRepoUrl: explicitRepoUrl === undefined,
|
|
182
206
|
});
|
|
183
207
|
|
|
@@ -212,7 +236,7 @@ const ZERO_SHA_REGEX = /^0{40}$/;
|
|
|
212
236
|
* Unlike the PR path (getCommits), there is no lenient fallback. Falling back to "everything"
|
|
213
237
|
* would rebuild the latest build of every Actor and post it to Slack, so every problem is an error.
|
|
214
238
|
*/
|
|
215
|
-
export const resolveReleaseBaseCommit = (baseCommit: string): string => {
|
|
239
|
+
export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<string> => {
|
|
216
240
|
const sha = parseBaseCommit(baseCommit);
|
|
217
241
|
if (!sha) {
|
|
218
242
|
throw new Error('--base-commit is required for release. See the README section "Releasing Actors".');
|
|
@@ -223,15 +247,27 @@ export const resolveReleaseBaseCommit = (baseCommit: string): string => {
|
|
|
223
247
|
`Rerun with --base-commit set to the last commit before the changes you want to release.`,
|
|
224
248
|
);
|
|
225
249
|
}
|
|
226
|
-
// --quiet
|
|
227
|
-
|
|
250
|
+
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1.
|
|
251
|
+
const verifiedSha = await runGitCommand(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], {
|
|
252
|
+
operation: 'verify the release base commit in local Git history',
|
|
253
|
+
}).catch((error: unknown) => {
|
|
254
|
+
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
255
|
+
throw error;
|
|
256
|
+
});
|
|
257
|
+
if (!verifiedSha) {
|
|
228
258
|
throw new Error(
|
|
229
259
|
`Base commit ${sha} is not in the local git history. Either the checkout is shallow ` +
|
|
230
260
|
`(fetch the full history, e.g. fetch-depth: 0 in actions/checkout) or the branch was force-pushed.`,
|
|
231
261
|
);
|
|
232
262
|
}
|
|
233
263
|
// git merge-base A B outputs the common ancestor. If that equals A, then A is an ancestor of B.
|
|
234
|
-
|
|
264
|
+
const mergeBase = await runGitCommand(['merge-base', verifiedSha, 'HEAD'], {
|
|
265
|
+
operation: 'check whether the release base commit is an ancestor of HEAD',
|
|
266
|
+
}).catch((error: unknown) => {
|
|
267
|
+
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
268
|
+
throw error;
|
|
269
|
+
});
|
|
270
|
+
if (mergeBase !== sha) {
|
|
235
271
|
throw new Error(
|
|
236
272
|
`Base commit ${sha} is not an ancestor of HEAD, most likely because the branch was force-pushed. ` +
|
|
237
273
|
`The changed files cannot be determined reliably. Rerun with --base-commit set to an ancestor of HEAD ` +
|
|
@@ -244,11 +280,13 @@ export const resolveReleaseBaseCommit = (baseCommit: string): string => {
|
|
|
244
280
|
const CHANGELOG_PATH = 'CHANGELOG.md';
|
|
245
281
|
|
|
246
282
|
/** Returns the lines added to the root CHANGELOG.md between baseSha and HEAD, or null if it didn't change. */
|
|
247
|
-
const getChangelogAdditions = (baseSha: string, changedFiles: string[]): string | null => {
|
|
283
|
+
const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): Promise<string | null> => {
|
|
248
284
|
if (!changedFiles.includes(CHANGELOG_PATH)) {
|
|
249
285
|
return null;
|
|
250
286
|
}
|
|
251
|
-
const diff =
|
|
287
|
+
const diff = await runGitCommand(['diff', baseSha, 'HEAD', '--', CHANGELOG_PATH], {
|
|
288
|
+
operation: 'read changelog changes since the last release',
|
|
289
|
+
});
|
|
252
290
|
|
|
253
291
|
const added: string[] = [];
|
|
254
292
|
let startedChangelog = false;
|
|
@@ -276,12 +314,16 @@ const getChangelogAdditions = (baseSha: string, changedFiles: string[]): string
|
|
|
276
314
|
* list: with a merge commit, the oldest commit of the merged branch can be older than baseSha, and
|
|
277
315
|
* diffing from its parent would pull in already-released changes.
|
|
278
316
|
*/
|
|
279
|
-
export const getReleaseChanges = (baseSha: string) => {
|
|
280
|
-
if (
|
|
317
|
+
export const getReleaseChanges = async (baseSha: string) => {
|
|
318
|
+
if (
|
|
319
|
+
(await runGitCommand(['rev-parse', 'HEAD'], { operation: 'identify the current release commit' })) === baseSha
|
|
320
|
+
) {
|
|
281
321
|
return null;
|
|
282
322
|
}
|
|
283
|
-
const commits = fetchAllBranchCommits('HEAD', baseSha);
|
|
284
|
-
const changedFiles =
|
|
285
|
-
|
|
323
|
+
const commits = await fetchAllBranchCommits('HEAD', baseSha);
|
|
324
|
+
const changedFiles = await runGitCommand(['diff', '--name-only', baseSha, 'HEAD'], {
|
|
325
|
+
operation: 'list files changed since the last release',
|
|
326
|
+
}).then((output) => output.split('\n').filter(Boolean));
|
|
327
|
+
const changelog = await getChangelogAdditions(baseSha, changedFiles);
|
|
286
328
|
return { commits, changedFiles, changelog };
|
|
287
329
|
};
|
package/bin/logger.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { formatWithOptions } from 'node:util';
|
|
3
|
+
|
|
4
|
+
export const logLevels = ['debug', 'info', 'warn', 'error', 'silent'] as const;
|
|
5
|
+
export type LogLevel = (typeof logLevels)[number];
|
|
6
|
+
|
|
7
|
+
let currentLevel: LogLevel = 'info';
|
|
8
|
+
|
|
9
|
+
const write = (level: Exclude<LogLevel, 'silent'>, args: unknown[]) => {
|
|
10
|
+
if (logLevels.indexOf(level) < logLevels.indexOf(currentLevel)) return;
|
|
11
|
+
process.stderr.write(`${formatWithOptions({ colors: false }, ...args)}\n`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** All diagnostics go to stderr, leaving stdout available for machine-readable results. */
|
|
15
|
+
export const logger = {
|
|
16
|
+
setLevel(level: LogLevel): void {
|
|
17
|
+
currentLevel = level;
|
|
18
|
+
},
|
|
19
|
+
debug: (...args: unknown[]): void => write('debug', args),
|
|
20
|
+
info: (...args: unknown[]): void => write('info', args),
|
|
21
|
+
warn: (...args: unknown[]): void => write('warn', args),
|
|
22
|
+
error: (...args: unknown[]): void => write('error', args),
|
|
23
|
+
};
|
package/bin/main.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
|
|
@@ -20,16 +20,17 @@ import {
|
|
|
20
20
|
resolveReleaseBaseCommit,
|
|
21
21
|
resolveRepoUrl,
|
|
22
22
|
} from './git.js';
|
|
23
|
+
import { logger, logLevels } from './logger.js';
|
|
24
|
+
import { writeJson } from './output.js';
|
|
23
25
|
import { notifyToSlack } from './slack.js';
|
|
24
26
|
import { reportTestResults } from './test-report.js';
|
|
25
27
|
import type { Config } from './types.js';
|
|
26
|
-
import { setCwd } from './utils.js';
|
|
27
28
|
import { readConfigFile } from './utils/config/load-config.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Middlewares to be run before every command execution
|
|
31
32
|
*/
|
|
32
|
-
const middlewares = [
|
|
33
|
+
const middlewares = [(args: { logLevel: (typeof logLevels)[number] }) => logger.setLevel(args.logLevel)];
|
|
33
34
|
|
|
34
35
|
/*
|
|
35
36
|
* Option groups shared across commands. Each is a plain object passed to `.options()`, so a command's
|
|
@@ -70,8 +71,13 @@ const buildOptions = {
|
|
|
70
71
|
|
|
71
72
|
/** Global flags, available to every command. */
|
|
72
73
|
const globalOptions = {
|
|
74
|
+
'log-level': {
|
|
75
|
+
type: 'string',
|
|
76
|
+
choices: logLevels,
|
|
77
|
+
default: 'info',
|
|
78
|
+
describe: 'Minimum diagnostic level written to stderr; silent disables logging',
|
|
79
|
+
},
|
|
73
80
|
'dry-run': { type: 'boolean', default: false },
|
|
74
|
-
workspace: { type: 'string' },
|
|
75
81
|
} as const satisfies Record<string, Options>;
|
|
76
82
|
|
|
77
83
|
const resolveChangedActors = async (config: Config, { isLatest }: { isLatest: boolean }) => {
|
|
@@ -82,30 +88,30 @@ const resolveChangedActors = async (config: Config, { isLatest }: { isLatest: bo
|
|
|
82
88
|
// since the branch has no functional changes of its own, there is nothing new to validate.
|
|
83
89
|
// Exception: if the branch has any functional changes alongside the merge, we must re-test — even
|
|
84
90
|
// individually validated changes can have novel interactions when combined.
|
|
85
|
-
if (hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
|
|
86
|
-
|
|
91
|
+
if (await hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
|
|
92
|
+
logger.info(
|
|
87
93
|
'[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
|
|
88
94
|
);
|
|
89
|
-
const branchOnlyFiles = getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
|
|
95
|
+
const branchOnlyFiles = await getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
|
|
90
96
|
// Omit baseCommit to get full branch history. Validated functional commits can still interact with merged ones
|
|
91
|
-
const allBranchCommits = getCommits({ ...config, baseCommit: undefined });
|
|
92
|
-
const branchOnlyActorsChanged = getChangedActors({
|
|
97
|
+
const allBranchCommits = await getCommits({ ...config, baseCommit: undefined });
|
|
98
|
+
const branchOnlyActorsChanged = await getChangedActors({
|
|
93
99
|
filepathsChanged: branchOnlyFiles,
|
|
94
100
|
actorConfigs,
|
|
95
101
|
commits: allBranchCommits,
|
|
96
102
|
});
|
|
97
103
|
if (branchOnlyActorsChanged.length === 0) {
|
|
98
|
-
|
|
104
|
+
logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
|
|
99
105
|
return [];
|
|
100
106
|
}
|
|
101
|
-
|
|
107
|
+
logger.info(
|
|
102
108
|
`[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${branchOnlyActorsChanged.length} functional changes, cannot optimize, we continue with full check`,
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
// If the optimization doesn't apply, we check all branch commits including merges for full coverage. We don't reuse the merge optimization results because here we can apply baseCommit and check merge commits (they might be functional or just cosmetic)
|
|
107
|
-
const commits = getCommits(config);
|
|
108
|
-
const changedFiles = getChangedFiles(commits);
|
|
113
|
+
const commits = await getCommits(config);
|
|
114
|
+
const changedFiles = await getChangedFiles(commits);
|
|
109
115
|
return getChangedActors({ filepathsChanged: changedFiles, actorConfigs, isLatest, commits });
|
|
110
116
|
};
|
|
111
117
|
|
|
@@ -117,19 +123,19 @@ await yargs()
|
|
|
117
123
|
'get-commits',
|
|
118
124
|
'',
|
|
119
125
|
(y) => y.options(gitRangeOptions),
|
|
120
|
-
(args) => {
|
|
121
|
-
const commits = getCommits(args);
|
|
122
|
-
|
|
126
|
+
async (args) => {
|
|
127
|
+
const commits = await getCommits(args);
|
|
128
|
+
writeJson(commits);
|
|
123
129
|
},
|
|
124
130
|
)
|
|
125
131
|
.command(
|
|
126
132
|
'get-latest-commit',
|
|
127
133
|
'',
|
|
128
134
|
(y) => y.options(gitRangeOptions),
|
|
129
|
-
(args) => {
|
|
130
|
-
const commits = getCommits(args);
|
|
135
|
+
async (args) => {
|
|
136
|
+
const commits = await getCommits(args);
|
|
131
137
|
if (commits.length > 0) {
|
|
132
|
-
|
|
138
|
+
writeJson(commits[commits.length - 1]);
|
|
133
139
|
}
|
|
134
140
|
},
|
|
135
141
|
)
|
|
@@ -137,10 +143,10 @@ await yargs()
|
|
|
137
143
|
'get-changed-files',
|
|
138
144
|
'',
|
|
139
145
|
(y) => y.options(gitRangeOptions),
|
|
140
|
-
(args) => {
|
|
141
|
-
const commits = getCommits(args);
|
|
142
|
-
const changedFiles = getChangedFiles(commits);
|
|
143
|
-
|
|
146
|
+
async (args) => {
|
|
147
|
+
const commits = await getCommits(args);
|
|
148
|
+
const changedFiles = await getChangedFiles(commits);
|
|
149
|
+
writeJson(changedFiles);
|
|
144
150
|
},
|
|
145
151
|
)
|
|
146
152
|
.command(
|
|
@@ -149,7 +155,7 @@ await yargs()
|
|
|
149
155
|
(y) => y.options(actorSelectionOptions),
|
|
150
156
|
async ({ actors, ignore }) => {
|
|
151
157
|
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
152
|
-
|
|
158
|
+
writeJson(actorConfigs);
|
|
153
159
|
},
|
|
154
160
|
)
|
|
155
161
|
.command(
|
|
@@ -158,7 +164,7 @@ await yargs()
|
|
|
158
164
|
(y) => y.options(gitRangeOptions).options(actorSelectionOptions),
|
|
159
165
|
async (config) => {
|
|
160
166
|
const actorsChanged = await resolveChangedActors(config, { isLatest: false });
|
|
161
|
-
|
|
167
|
+
writeJson(actorsChanged);
|
|
162
168
|
},
|
|
163
169
|
)
|
|
164
170
|
.command(
|
|
@@ -181,13 +187,13 @@ await yargs()
|
|
|
181
187
|
async (config) => {
|
|
182
188
|
const actorsChanged = await resolveChangedActors(config, { isLatest: false });
|
|
183
189
|
const builds = await runBuilds({
|
|
184
|
-
...resolveRepoUrl(config.repoUrl),
|
|
190
|
+
...(await resolveRepoUrl(config.repoUrl)),
|
|
185
191
|
actorConfigs: actorsChanged,
|
|
186
192
|
branch: config.sourceBranch.replace('origin/', ''),
|
|
187
193
|
dryRun: config.dryRun,
|
|
188
194
|
useDockerCache: config.useDockerCache,
|
|
189
195
|
});
|
|
190
|
-
|
|
196
|
+
writeJson(builds);
|
|
191
197
|
},
|
|
192
198
|
)
|
|
193
199
|
.command(
|
|
@@ -204,19 +210,19 @@ await yargs()
|
|
|
204
210
|
.option('report-slack-channel', { type: 'string' })
|
|
205
211
|
.option('release-slack-channel', { type: 'string' }),
|
|
206
212
|
async (args) => {
|
|
207
|
-
const baseSha = resolveReleaseBaseCommit(args.baseCommit);
|
|
208
|
-
const branch = getCurrentBranch();
|
|
209
|
-
const changes = getReleaseChanges(baseSha);
|
|
213
|
+
const baseSha = await resolveReleaseBaseCommit(args.baseCommit);
|
|
214
|
+
const branch = await getCurrentBranch();
|
|
215
|
+
const changes = await getReleaseChanges(baseSha);
|
|
210
216
|
if (!changes) {
|
|
211
|
-
|
|
217
|
+
logger.info(`HEAD is the base commit ${baseSha}, there is nothing new to release`);
|
|
212
218
|
return;
|
|
213
219
|
}
|
|
214
220
|
const { commits, changedFiles, changelog } = changes;
|
|
215
|
-
const { repoUrl, shouldVerifyRepoUrl } = resolveRepoUrl(args.repoUrl);
|
|
221
|
+
const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
|
|
216
222
|
|
|
217
223
|
const isLatest = true;
|
|
218
224
|
const actorConfigs = await readConfigFile(args);
|
|
219
|
-
const actorsChanged = getChangedActors({
|
|
225
|
+
const actorsChanged = await getChangedActors({
|
|
220
226
|
filepathsChanged: changedFiles,
|
|
221
227
|
actorConfigs,
|
|
222
228
|
isLatest,
|
|
@@ -232,7 +238,7 @@ await yargs()
|
|
|
232
238
|
dryRun,
|
|
233
239
|
useDockerCache: args.useDockerCache,
|
|
234
240
|
});
|
|
235
|
-
|
|
241
|
+
logger.info(JSON.stringify(builds));
|
|
236
242
|
|
|
237
243
|
await notifyToSlack({
|
|
238
244
|
changedFiles,
|
|
@@ -254,7 +260,7 @@ await yargs()
|
|
|
254
260
|
async ({ actors, ignore, dryRun }) => {
|
|
255
261
|
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
256
262
|
const builds = await runBuildsFromLocal({ actorConfigs, dryRun });
|
|
257
|
-
|
|
263
|
+
writeJson(builds);
|
|
258
264
|
},
|
|
259
265
|
)
|
|
260
266
|
.command(
|
|
@@ -273,11 +279,11 @@ await yargs()
|
|
|
273
279
|
// or a missing config file) arrive here as `err`. A malformed selection must fail loudly
|
|
274
280
|
// rather than silently operate on the wrong set of actors — print the message, no stack.
|
|
275
281
|
if (err) {
|
|
276
|
-
|
|
282
|
+
logger.error(`[ERROR]: ${err.message}`);
|
|
277
283
|
} else {
|
|
278
284
|
// Argument-parsing/validation failure — keep yargs' usage output.
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
logger.error(yargsInstance.help());
|
|
286
|
+
logger.error(`\n${msg}`);
|
|
281
287
|
}
|
|
282
288
|
process.exit(1);
|
|
283
289
|
})
|
package/bin/output.ts
ADDED
package/bin/slack.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
|
|
3
|
+
import { logger } from './logger.js';
|
|
3
4
|
import type { Commit } from './types.js';
|
|
4
5
|
import { getEnvVar } from './utils.js';
|
|
5
6
|
|
|
@@ -30,7 +31,7 @@ export const notifyToSlack = async ({
|
|
|
30
31
|
const slack = new WebClient(getEnvVar('SLACK_TOKEN_RELEASES_BOT'));
|
|
31
32
|
|
|
32
33
|
if (!changelog) {
|
|
33
|
-
|
|
34
|
+
logger.warn('No new changelog entries found, did you forget to update it?');
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
|
|
@@ -38,9 +39,9 @@ export const notifyToSlack = async ({
|
|
|
38
39
|
// This one is just for broader public that only cares about public facing changes
|
|
39
40
|
if (changelog && releaseSlackChannel) {
|
|
40
41
|
shortMessage += `*Additions to the changelog*:\n\n${changelog}\n`;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
logger.info(`=========================================`);
|
|
43
|
+
logger.info(`**Sending slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
|
|
44
|
+
logger.info(`=========================================`);
|
|
44
45
|
if (!dryRun) {
|
|
45
46
|
await slack.chat.postMessage({
|
|
46
47
|
channel: releaseSlackChannel,
|
|
@@ -60,9 +61,9 @@ export const notifyToSlack = async ({
|
|
|
60
61
|
|
|
61
62
|
// This one is for devs and project managers that need to know more details
|
|
62
63
|
if (reportSlackChannel) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
logger.info(`=========================================`);
|
|
65
|
+
logger.info(`Sending slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
|
|
66
|
+
logger.info(`=========================================`);
|
|
66
67
|
if (!dryRun) {
|
|
67
68
|
await slack.chat.postMessage({
|
|
68
69
|
text: longMessage,
|
package/bin/test-report.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
+
import { logger } from './logger.js';
|
|
3
4
|
import { sendSlackMessage } from './slack.js';
|
|
4
5
|
import { getEnvVar } from './utils.js';
|
|
5
6
|
|
|
@@ -38,23 +39,23 @@ export const reportTestResults = async ({
|
|
|
38
39
|
|
|
39
40
|
const failedAssertions: { message: string; runLink: string; actorId: string }[] = [];
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
logger.info();
|
|
43
|
+
logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
|
|
44
|
+
logger.info();
|
|
45
|
+
logger.info('**************************************************');
|
|
46
|
+
logger.info('* Successes *');
|
|
47
|
+
logger.info('**************************************************');
|
|
48
|
+
logger.info();
|
|
48
49
|
for (const [i, aResult] of passed.entries()) {
|
|
49
50
|
const { fullName } = aResult;
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
logger.info(`${i + 1}) ${fullName} ... ${aResult.meta.runLink}`);
|
|
52
|
+
logger.info();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
logger.info('**************************************************');
|
|
56
|
+
logger.info('* Failures *');
|
|
57
|
+
logger.info('**************************************************');
|
|
58
|
+
logger.info();
|
|
58
59
|
for (const [i, aResult] of failed.entries()) {
|
|
59
60
|
const { failureMessages, fullName, meta } = aResult;
|
|
60
61
|
if (failureMessages) {
|
|
@@ -66,15 +67,15 @@ export const reportTestResults = async ({
|
|
|
66
67
|
})),
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
logger.info(`${i + 1}) ${fullName} ... ${meta.runLink}`);
|
|
71
|
+
logger.info();
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
logger.info();
|
|
74
|
+
logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
|
|
75
|
+
logger.info();
|
|
75
76
|
|
|
76
77
|
if (!reportSlackChannel) {
|
|
77
|
-
|
|
78
|
+
logger.info(
|
|
78
79
|
`Skipping slack notification. If you want to enable it, add --report-slack-channel flag and make sure SLACK_TOKEN_TESTS_BOT env variable is set.`,
|
|
79
80
|
);
|
|
80
81
|
return;
|
|
@@ -94,8 +95,8 @@ export const reportTestResults = async ({
|
|
|
94
95
|
.slice(1)
|
|
95
96
|
.map(({ message, runLink, actorId }) => `• ${message} --- <${runLink}|${actorId}>`);
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
logger.info('SLACK:', slackMessage);
|
|
99
|
+
logger.info('\tblocks:', blocks.join('\n\t\t'));
|
|
99
100
|
|
|
100
101
|
if (!reportSlackChannel) {
|
|
101
102
|
return;
|