apify-test-tools 0.10.2-beta.3 → 0.10.2-beta.5
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/cli/apify-test-tools.mjs +117 -118
- package/.github/scripts/run-with-apify-tokens.mjs +1 -1
- package/.github/workflows/public_pr-build-test.yaml +25 -25
- package/.github/workflows/public_push-build-latest.yaml +20 -4
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +4 -1
- package/README.md +104 -56
- package/bin/affected-actors.ts +135 -103
- package/bin/build.ts +53 -38
- package/bin/diff-changes.ts +7 -6
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +24 -69
- package/bin/main.ts +46 -80
- package/bin/slack.ts +15 -4
- package/bin/stdin-input.ts +65 -0
- package/bin/types.ts +6 -0
- package/dist/bin/affected-actors.d.ts +23 -27
- package/dist/bin/affected-actors.d.ts.map +1 -1
- package/dist/bin/affected-actors.js +111 -65
- package/dist/bin/affected-actors.js.map +1 -1
- package/dist/bin/build.d.ts +11 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +43 -23
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +4 -3
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +4 -4
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +2 -2
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +1 -4
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +7 -15
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +23 -61
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +39 -71
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +6 -3
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +13 -1
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +17 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +54 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/types.d.ts +5 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/test/bundle/cli-bundle.test.js +32 -11
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/e2e/cli-bundle.e2e.test.js +23 -57
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
- package/dist/test/e2e/helpers.d.ts +37 -0
- package/dist/test/e2e/helpers.d.ts.map +1 -0
- package/dist/test/e2e/helpers.js +57 -0
- package/dist/test/e2e/helpers.js.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js +161 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +68 -87
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +44 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build.test.js +61 -4
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
- package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
- package/dist/test/unit/bin/compiled-cli.js +16 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +55 -55
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +25 -28
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +28 -108
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.js +19 -2
- package/dist/test/unit/bin/slack.test.js.map +1 -1
- package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +50 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +18 -21
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/test/bundle/cli-bundle.test.ts +47 -11
- package/test/e2e/cli-bundle.e2e.test.ts +47 -77
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/affected-actors-cli.test.ts +193 -0
- package/test/unit/bin/affected-actors.test.ts +86 -94
- package/test/unit/bin/build-cli.test.ts +55 -0
- package/test/unit/bin/build.test.ts +71 -3
- package/test/unit/bin/compiled-cli.ts +17 -0
- package/test/unit/bin/diff-changes.test.ts +55 -55
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +35 -132
- package/test/unit/bin/slack.test.ts +21 -2
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/should-built-and-test.test.ts +19 -22
package/bin/git.ts
CHANGED
|
@@ -9,8 +9,8 @@ const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR
|
|
|
9
9
|
* Gets the list of changed files between the given commits (inclusive), so it diffs from the parent of the oldest.
|
|
10
10
|
*/
|
|
11
11
|
export const getChangedFiles = async (commits: Commit[]) => {
|
|
12
|
-
// getCommits never returns an empty list (
|
|
13
|
-
//
|
|
12
|
+
// getCommits never returns an empty list (it throws on an empty range, and the rerun check returns all
|
|
13
|
+
// commits when the base commit is the branch HEAD), so this signals a programmer error
|
|
14
14
|
if (commits.length === 0) {
|
|
15
15
|
throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
|
|
16
16
|
}
|
|
@@ -35,59 +35,22 @@ export const getChangedFilesBetween = async (fromRef: string, toRef: string) =>
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* The commit `git diff base...head` compares against: where `head` forked from `base`, or the last commit of `base`
|
|
39
|
+
* that `head` merged in. The diff from it holds only what `head` itself changed, like GitHub's "Files changed" tab.
|
|
40
|
+
* With criss-cross merges there are several such commits and git picks one.
|
|
41
41
|
*/
|
|
42
|
-
export const
|
|
43
|
-
|
|
44
|
-
await runGitCommand(['log', '--merges', '--pretty=format:%H', `${targetBranch}..${sourceBranch}`], {
|
|
45
|
-
operation: 'find merge commits on the source branch that are absent from the target branch',
|
|
46
|
-
})
|
|
47
|
-
)
|
|
48
|
-
.split('\n')
|
|
49
|
-
.filter(Boolean);
|
|
42
|
+
export const getMergeBase = async (base: string, head: string): Promise<string> =>
|
|
43
|
+
runGitCommand(['merge-base', base, head], { operation: `find where ${head} forked from ${base}` });
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let mergeBase: string;
|
|
61
|
-
try {
|
|
62
|
-
mergeBase = await runGitCommand(['merge-base', parent, targetBranch], {
|
|
63
|
-
operation: 'check whether a merge parent is reachable from the target branch',
|
|
64
|
-
});
|
|
65
|
-
} catch (error) {
|
|
66
|
-
// Exit code 1 on merge-base means the histories have no common ancestor.
|
|
67
|
-
if (error instanceof GitCommandError && error.exitCode === 1) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
if (mergeBase === parent) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Returns all files touched by non-merge commits on the branch (full history, ignoring baseCommit).
|
|
82
|
-
* Used to check whether the branch itself has any functional changes, independent of what master merged in.
|
|
83
|
-
*/
|
|
84
|
-
export const getBranchOnlyChangedFiles = async (sourceBranch: string, targetBranch: string): Promise<string[]> => {
|
|
85
|
-
const output = await runGitCommand(
|
|
86
|
-
['log', '--no-merges', '--name-only', '--pretty=format:', `${targetBranch}..${sourceBranch}`],
|
|
87
|
-
{ operation: 'list files changed by non-merge commits on the source branch' },
|
|
88
|
-
);
|
|
89
|
-
return output.split('\n').filter(Boolean);
|
|
90
|
-
};
|
|
45
|
+
/** Resolves a ref to a commit, or undefined when it isn't in the local history (e.g. it was force-pushed away). */
|
|
46
|
+
export const findCommitSha = async (ref: string): Promise<string | undefined> =>
|
|
47
|
+
runGitCommand(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
48
|
+
operation: `look up ${ref} in local Git history`,
|
|
49
|
+
}).catch((error: unknown) => {
|
|
50
|
+
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1
|
|
51
|
+
if (error instanceof GitCommandError && error.exitCode === 1) return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
91
54
|
|
|
92
55
|
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
93
56
|
|
|
@@ -120,8 +83,8 @@ export const getCommitsBetween = async (fromRef: string, toRef: string): Promise
|
|
|
120
83
|
const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
|
|
121
84
|
operation: 'list commits reachable from the source ref but absent from the target ref',
|
|
122
85
|
});
|
|
123
|
-
|
|
124
|
-
const commits =
|
|
86
|
+
// An empty range (e.g. `x..x`) prints nothing
|
|
87
|
+
const commits = output.split('\n').filter(Boolean).map(parseCommit);
|
|
125
88
|
commits.reverse();
|
|
126
89
|
return commits;
|
|
127
90
|
};
|
|
@@ -137,6 +100,9 @@ export const getCommits = async ({
|
|
|
137
100
|
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
|
|
138
101
|
const baseCommitSha = parseBaseCommit(baseCommit);
|
|
139
102
|
const commits = await getCommitsBetween(targetBranch, sourceBranch);
|
|
103
|
+
if (commits.length === 0) {
|
|
104
|
+
throw new Error(`${sourceBranch} has no commits that are not on ${targetBranch}.`);
|
|
105
|
+
}
|
|
140
106
|
|
|
141
107
|
// The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
|
|
142
108
|
// validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
|
|
@@ -181,16 +147,16 @@ export const parseCommit = (commitString: string): Commit => {
|
|
|
181
147
|
};
|
|
182
148
|
|
|
183
149
|
/**
|
|
184
|
-
* Returns the currently checked-out branch.
|
|
150
|
+
* Returns the currently checked-out branch. Builds point the Actor version at this branch,
|
|
185
151
|
* so a detached HEAD (no branch to point at) is an error rather than a guess.
|
|
186
152
|
*/
|
|
187
153
|
export const getCurrentBranch = async (): Promise<string> => {
|
|
188
154
|
const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
189
|
-
operation: 'determine the
|
|
155
|
+
operation: 'determine the checked-out branch',
|
|
190
156
|
});
|
|
191
157
|
if (branch === 'HEAD') {
|
|
192
158
|
throw new Error(
|
|
193
|
-
'Cannot determine the branch to
|
|
159
|
+
'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
|
|
194
160
|
);
|
|
195
161
|
}
|
|
196
162
|
return branch;
|
|
@@ -200,17 +166,6 @@ export const getCurrentBranch = async (): Promise<string> => {
|
|
|
200
166
|
export const resolveCommitSha = async (ref: string): Promise<string> =>
|
|
201
167
|
runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
|
|
202
168
|
|
|
203
|
-
/**
|
|
204
|
-
* Branch name a ref points to, without the remote: `origin/foo` and `foo` both give `foo`, and `HEAD`
|
|
205
|
-
* gives the checked-out branch. Null when there is no branch to point at (a SHA, a tag, a detached HEAD).
|
|
206
|
-
*/
|
|
207
|
-
export const resolveBranchName = async (ref: string): Promise<string | null> => {
|
|
208
|
-
const fullName = await runGitCommand(['rev-parse', '--symbolic-full-name', ref], {
|
|
209
|
-
operation: `resolve the branch name of ${ref}`,
|
|
210
|
-
});
|
|
211
|
-
return fullName.match(/^refs\/(?:heads|remotes\/[^/]+)\/(.+)$/)?.[1] ?? null;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
169
|
/**
|
|
215
170
|
* Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
|
|
216
171
|
* uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
|
package/bin/main.ts
CHANGED
|
@@ -6,13 +6,14 @@ import yargs, { type Options } from 'yargs';
|
|
|
6
6
|
// eslint-disable-next-line import/extensions --- With .js, it cannot find types
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
|
|
9
|
-
import { getAffectedActors
|
|
10
|
-
import {
|
|
9
|
+
import { getAffectedActors } from './affected-actors.js';
|
|
10
|
+
import { buildActors, DEFAULT_VERSION_KEYWORD, deleteOldBuilds } from './build.js';
|
|
11
11
|
import { runBuildsFromLocal } from './build-from-local.js';
|
|
12
|
-
import { getChangedFiles, getCommits
|
|
12
|
+
import { getChangedFiles, getCommits } from './git.js';
|
|
13
13
|
import { logger, logLevels } from './logger.js';
|
|
14
14
|
import { writeJson } from './output.js';
|
|
15
15
|
import { notifyToSlack } from './slack.js';
|
|
16
|
+
import { parseBuildInput, parseReleaseChanges, readStdin } from './stdin-input.js';
|
|
16
17
|
import { reportTestResults } from './test-report.js';
|
|
17
18
|
import { readConfigFile } from './utils/config/load-config.js';
|
|
18
19
|
|
|
@@ -83,9 +84,27 @@ const dryRunOptions = {
|
|
|
83
84
|
},
|
|
84
85
|
} as const satisfies Record<string, Options>;
|
|
85
86
|
|
|
86
|
-
/** Flags
|
|
87
|
+
/** Flags of `build`, which builds exactly the Actors it is given. */
|
|
87
88
|
const buildOptions = {
|
|
88
|
-
'
|
|
89
|
+
'version-number': {
|
|
90
|
+
type: 'string',
|
|
91
|
+
demandOption: true,
|
|
92
|
+
describe: `Version to build, like 0.1, or "${DEFAULT_VERSION_KEYWORD}" for each Actor's default version`,
|
|
93
|
+
coerce: (value: string) => {
|
|
94
|
+
if (value !== DEFAULT_VERSION_KEYWORD && !/^\d+\.\d+$/.test(value)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`--version-number must look like 0.1 or be "${DEFAULT_VERSION_KEYWORD}", got "${value}"`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
} as const satisfies Record<string, Options>;
|
|
103
|
+
|
|
104
|
+
/** Where `notify-release` posts the release. */
|
|
105
|
+
const releaseSlackOptions = {
|
|
106
|
+
'report-slack-channel': { type: 'string', describe: 'Channel for the full report: commits and changed files' },
|
|
107
|
+
'release-slack-channel': { type: 'string', describe: 'Channel for the changelog additions only' },
|
|
89
108
|
} as const satisfies Record<string, Options>;
|
|
90
109
|
|
|
91
110
|
/** Global flags, available to every command. */
|
|
@@ -146,7 +165,7 @@ await yargs()
|
|
|
146
165
|
'Find the Actors whose context changed in a commit range ending at the checked-out HEAD',
|
|
147
166
|
(y) => y.options(affectedRangeOptions).options(actorSelectionOptions),
|
|
148
167
|
async (args) => {
|
|
149
|
-
writeJson(
|
|
168
|
+
writeJson(await getAffectedActors(args));
|
|
150
169
|
},
|
|
151
170
|
)
|
|
152
171
|
.command(
|
|
@@ -165,89 +184,36 @@ await yargs()
|
|
|
165
184
|
)
|
|
166
185
|
.command(
|
|
167
186
|
'build',
|
|
168
|
-
'',
|
|
169
|
-
(y) =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
head: config.sourceBranch,
|
|
181
|
-
base: config.targetBranch,
|
|
182
|
-
since: config.baseCommit,
|
|
183
|
-
testFilesGlob: config.testFilesGlob,
|
|
184
|
-
actors: config.actors,
|
|
185
|
-
ignore: config.ignore,
|
|
186
|
-
});
|
|
187
|
-
const builds = await runBuilds({
|
|
188
|
-
...(await resolveRepoUrl(config.repoUrl)),
|
|
189
|
-
actorConfigs: actorsChanged,
|
|
190
|
-
branch: config.sourceBranch.replace('origin/', ''),
|
|
191
|
-
dryRun: config.dryRun,
|
|
192
|
-
useDockerCache: config.useDockerCache,
|
|
187
|
+
'Build the Actors listed on stdin, as {"actors": [{"actorFullName": ...}]}, from the checked-out branch',
|
|
188
|
+
(y) => y.options(buildOptions).options(repoUrlOptions).options(dryRunOptions),
|
|
189
|
+
async (args) => {
|
|
190
|
+
const input = parseBuildInput(await readStdin());
|
|
191
|
+
const names = input.actors.map(({ actorFullName }) => actorFullName);
|
|
192
|
+
// An empty --actors selects every Actor, but an empty list here means there is nothing to build
|
|
193
|
+
const actorConfigs = names.length > 0 ? await readConfigFile({ actors: names, ignore: [] }) : [];
|
|
194
|
+
const builds = await buildActors({
|
|
195
|
+
actorConfigs,
|
|
196
|
+
versionNumber: args.versionNumber,
|
|
197
|
+
repoUrl: args.repoUrl,
|
|
198
|
+
dryRun: args.dryRun,
|
|
193
199
|
});
|
|
194
200
|
writeJson(builds);
|
|
195
201
|
},
|
|
196
202
|
)
|
|
197
203
|
.command(
|
|
198
|
-
'release',
|
|
199
|
-
'',
|
|
200
|
-
(y) =>
|
|
201
|
-
y
|
|
202
|
-
.options(actorSelectionOptions)
|
|
203
|
-
.options(buildOptions)
|
|
204
|
-
.options(repoUrlOptions)
|
|
205
|
-
.options(dryRunOptions)
|
|
206
|
-
// The last commit that is already released, everything after it up to HEAD gets released.
|
|
207
|
-
// Required, see the README section "Releasing Actors" for how to set it for each merge strategy.
|
|
208
|
-
.option('base-commit', { type: 'string', demandOption: true })
|
|
209
|
-
.option('report-slack-channel', { type: 'string' })
|
|
210
|
-
.option('release-slack-channel', { type: 'string' }),
|
|
204
|
+
'notify-release',
|
|
205
|
+
'Announce a release on Slack, from the get-affected-actors output on stdin',
|
|
206
|
+
(y) => y.options(releaseSlackOptions).options(repoUrlOptions).options(dryRunOptions),
|
|
211
207
|
async (args) => {
|
|
212
|
-
const
|
|
213
|
-
const { commits, changedFiles, changelog, functionallyChangedActors, cosmeticallyChangedActors } =
|
|
214
|
-
await getAffectedActors({
|
|
215
|
-
head: 'HEAD',
|
|
216
|
-
since: args.baseCommit,
|
|
217
|
-
actors: args.actors,
|
|
218
|
-
ignore: args.ignore,
|
|
219
|
-
});
|
|
220
|
-
// A release ships cosmetic changes (README, input schema descriptions, ...) too
|
|
221
|
-
const actorsChanged = [...functionallyChangedActors, ...cosmeticallyChangedActors];
|
|
222
|
-
if (commits.length === 0) {
|
|
223
|
-
logger.info(`HEAD is the base commit ${args.baseCommit}, there is nothing new to release`);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
|
|
227
|
-
|
|
228
|
-
const isLatest = true;
|
|
229
|
-
const { dryRun, reportSlackChannel, releaseSlackChannel } = args;
|
|
230
|
-
const builds = await runBuilds({
|
|
231
|
-
isLatest,
|
|
232
|
-
repoUrl,
|
|
233
|
-
shouldVerifyRepoUrl,
|
|
234
|
-
actorConfigs: actorsChanged,
|
|
235
|
-
branch,
|
|
236
|
-
dryRun,
|
|
237
|
-
useDockerCache: args.useDockerCache,
|
|
238
|
-
});
|
|
239
|
-
logger.info(JSON.stringify(builds));
|
|
240
|
-
|
|
208
|
+
const { commits, changedFiles, changelog } = parseReleaseChanges(await readStdin());
|
|
241
209
|
await notifyToSlack({
|
|
242
|
-
changedFiles,
|
|
243
210
|
commits,
|
|
211
|
+
changedFiles,
|
|
244
212
|
changelog,
|
|
245
|
-
|
|
246
|
-
dryRun,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
reportSlackChannel,
|
|
250
|
-
releaseSlackChannel,
|
|
213
|
+
repoUrl: args.repoUrl,
|
|
214
|
+
dryRun: args.dryRun,
|
|
215
|
+
reportSlackChannel: args.reportSlackChannel,
|
|
216
|
+
releaseSlackChannel: args.releaseSlackChannel,
|
|
251
217
|
});
|
|
252
218
|
},
|
|
253
219
|
)
|
package/bin/slack.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
|
|
3
|
+
import { getRepoName, resolveRepoUrl } from './git.js';
|
|
3
4
|
import { logger } from './logger.js';
|
|
4
5
|
import type { Commit } from './types.js';
|
|
5
6
|
import { getEnvVar } from './utils.js';
|
|
6
7
|
|
|
7
8
|
type NotifyToSlackOptions = {
|
|
8
|
-
repository: string;
|
|
9
9
|
changedFiles: string[];
|
|
10
10
|
changelog: string | null;
|
|
11
11
|
commits: Commit[];
|
|
12
|
+
// Defaults to the `origin` remote, see resolveRepoUrl
|
|
13
|
+
repoUrl?: string;
|
|
12
14
|
dryRun: boolean;
|
|
13
|
-
author: string;
|
|
14
15
|
releaseSlackChannel?: string;
|
|
15
16
|
reportSlackChannel?: string;
|
|
16
17
|
};
|
|
@@ -37,16 +38,23 @@ export const sendingLabel = (dryRun: boolean) => (dryRun ? '[DRY RUN] Would send
|
|
|
37
38
|
// Git authors come as "Name<email>", Slack only needs the name
|
|
38
39
|
const displayName = (author: string) => author.replace(/\s*<[^>]*>$/, '');
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Posts what a release changed: its commits, changed files and changelog, as get-affected-actors reports them.
|
|
43
|
+
* Posts nothing when there are no commits, which means there is nothing new to release.
|
|
44
|
+
*/
|
|
40
45
|
export const notifyToSlack = async ({
|
|
41
46
|
changedFiles,
|
|
42
47
|
commits,
|
|
43
48
|
changelog,
|
|
44
|
-
|
|
49
|
+
repoUrl,
|
|
45
50
|
dryRun,
|
|
46
|
-
author,
|
|
47
51
|
releaseSlackChannel,
|
|
48
52
|
reportSlackChannel,
|
|
49
53
|
}: NotifyToSlackOptions) => {
|
|
54
|
+
if (commits.length === 0) {
|
|
55
|
+
logger.info('No new commits, so there is no release to announce');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
50
58
|
if (!changelog) {
|
|
51
59
|
logger.warn('No new changelog entries found, did you forget to update it?');
|
|
52
60
|
}
|
|
@@ -57,6 +65,9 @@ export const notifyToSlack = async ({
|
|
|
57
65
|
}
|
|
58
66
|
const slack = await getSlackClient('SLACK_TOKEN_RELEASES_BOT', dryRun);
|
|
59
67
|
|
|
68
|
+
const repository = getRepoName((await resolveRepoUrl(repoUrl)).repoUrl);
|
|
69
|
+
// The newest commit, same as the pushed head commit
|
|
70
|
+
const { author } = commits[commits.length - 1];
|
|
60
71
|
let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
|
|
61
72
|
|
|
62
73
|
// This one is just for broader public that only cares about public facing changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
import z, { prettifyError, type ZodType } from 'zod';
|
|
4
|
+
|
|
5
|
+
export const readStdin = async (): Promise<string> => {
|
|
6
|
+
// Nothing piped in: fail right away instead of waiting for someone to type
|
|
7
|
+
if (process.stdin.isTTY) {
|
|
8
|
+
throw new Error('Expected JSON on stdin, but nothing was piped in.');
|
|
9
|
+
}
|
|
10
|
+
let raw = '';
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
raw += chunk;
|
|
13
|
+
}
|
|
14
|
+
return raw;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseStdinJson = <T>(
|
|
18
|
+
raw: string,
|
|
19
|
+
schema: ZodType<T>,
|
|
20
|
+
{ expected, example }: { expected: string; example: string },
|
|
21
|
+
) => {
|
|
22
|
+
// Empty input is an error, not "nothing to do": it is what a failed command earlier in the pipe leaves behind
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
throw new Error(`Expected ${expected} as JSON on stdin, got nothing (${example}).`);
|
|
25
|
+
}
|
|
26
|
+
let json: unknown;
|
|
27
|
+
try {
|
|
28
|
+
json = JSON.parse(raw);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`The input on stdin is not valid JSON: ${(error as Error).message}`);
|
|
31
|
+
}
|
|
32
|
+
const result = schema.safeParse(json);
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
throw new Error(`The input on stdin is not ${expected} (${example}):\n${prettifyError(result.error)}`);
|
|
35
|
+
}
|
|
36
|
+
return result.data;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What `build` reads: the Actors to build. Only their names are read, the config file stays the source of truth
|
|
41
|
+
* for everything else about them. Other fields are ignored, so a get-affected-actors document only needs its Actor
|
|
42
|
+
* list picked, e.g. `jq '{ actors: .functionallyChangedActors }'`.
|
|
43
|
+
*/
|
|
44
|
+
const buildInputSchema = z.object({
|
|
45
|
+
actors: z.array(z.object({ actorFullName: z.string() })),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const parseBuildInput = (raw: string) =>
|
|
49
|
+
parseStdinJson(raw, buildInputSchema, {
|
|
50
|
+
expected: 'the Actors to build',
|
|
51
|
+
example: `e.g. echo '{"actors":[{"actorFullName":"owner/name"}]}' | apify-test-tools build --version-number 0.1`,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** What `notify-release` reads: the parts of a get-affected-actors document that describe what changed. */
|
|
55
|
+
const releaseChangesSchema = z.object({
|
|
56
|
+
commits: z.array(z.object({ sha: z.string(), author: z.string(), date: z.string(), message: z.string() })),
|
|
57
|
+
changedFiles: z.array(z.string()),
|
|
58
|
+
changelog: z.string().nullable(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const parseReleaseChanges = (raw: string) =>
|
|
62
|
+
parseStdinJson(raw, releaseChangesSchema, {
|
|
63
|
+
expected: 'the output of get-affected-actors',
|
|
64
|
+
example: 'e.g. apify-test-tools get-affected-actors --since <sha> | apify-test-tools notify-release',
|
|
65
|
+
});
|
package/bin/types.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface Config {
|
|
|
10
10
|
testFilesGlob?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** The changes from `fromRef` (excluded) to `toRef`, compared as trees like `git diff fromRef toRef`. */
|
|
14
|
+
export interface GitRange {
|
|
15
|
+
fromRef: string;
|
|
16
|
+
toRef: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export type Commit = {
|
|
14
20
|
sha: string;
|
|
15
21
|
author: string;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Commit } from './types.js';
|
|
2
2
|
interface AffectedActorsOptions {
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Base usually points to the master branch
|
|
5
|
+
* Only what HEAD changes compared to base counts, like `git diff base...HEAD` or GitHub's "Files changed" tab:
|
|
6
|
+
* the branch's own changes, not what it merged in from base.
|
|
7
|
+
*/
|
|
6
8
|
base?: string;
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
* ignored when it isn't
|
|
10
|
+
* With `base` it's the last validated commit of the branch: Actors that didn't change after it are left out.
|
|
11
|
+
* It's ignored when it is HEAD (a rerun) or isn't in the local history. Without `base` everything after it
|
|
12
|
+
* counts, and it must be an ancestor of HEAD (e.g. the last release).
|
|
10
13
|
*/
|
|
11
14
|
since?: string;
|
|
12
15
|
/** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
|
|
@@ -14,37 +17,30 @@ interface AffectedActorsOptions {
|
|
|
14
17
|
actors: string[];
|
|
15
18
|
ignore: string[];
|
|
16
19
|
}
|
|
17
|
-
interface AffectedActors {
|
|
18
|
-
headSha: string;
|
|
19
|
-
/** Branch head points to, which is what git builds are made from. Null for a SHA or a detached HEAD. */
|
|
20
|
-
ref: string | null;
|
|
21
|
-
commits: Commit[];
|
|
22
|
-
changedFiles: string[];
|
|
23
|
-
changelog: string | null;
|
|
24
|
-
/** Actors with at least one functional change. They need a new build and tests. */
|
|
25
|
-
functionallyChangedActors: ActorConfig[];
|
|
26
|
-
/** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
|
|
27
|
-
cosmeticallyChangedActors: ActorConfig[];
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Finds the Actors whose context changed in the given range. `base` selects the branch's own commits
|
|
31
|
-
* (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
|
|
32
|
-
*/
|
|
33
|
-
export declare const getAffectedActors: ({ head, base, since: sinceOption, testFilesGlob, actors, ignore, }: AffectedActorsOptions) => Promise<AffectedActors>;
|
|
34
20
|
/** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
|
|
35
|
-
|
|
36
|
-
version:
|
|
37
|
-
ref: string | null;
|
|
21
|
+
interface AffectedActors {
|
|
22
|
+
version: 1;
|
|
38
23
|
head: string;
|
|
24
|
+
/** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
|
|
39
25
|
commits: Commit[];
|
|
40
26
|
changedFiles: string[];
|
|
41
27
|
changelog: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Actors with at least one functional change. They need a new build and tests. Only the names: the config file
|
|
30
|
+
* stays the source of truth for the rest.
|
|
31
|
+
*/
|
|
42
32
|
functionallyChangedActors: {
|
|
43
33
|
actorFullName: string;
|
|
44
34
|
}[];
|
|
35
|
+
/** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
|
|
45
36
|
cosmeticallyChangedActors: {
|
|
46
37
|
actorFullName: string;
|
|
47
38
|
}[];
|
|
48
|
-
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
|
|
42
|
+
* (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
|
|
43
|
+
*/
|
|
44
|
+
export declare const getAffectedActors: ({ base, since: sinceOption, testFilesGlob, actors, ignore, }: AffectedActorsOptions) => Promise<AffectedActors>;
|
|
49
45
|
export {};
|
|
50
46
|
//# sourceMappingURL=affected-actors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"affected-actors.d.ts","sourceRoot":"","sources":["../../bin/affected-actors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"affected-actors.d.ts","sourceRoot":"","sources":["../../bin/affected-actors.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAe,MAAM,EAAY,MAAM,YAAY,CAAC;AAMhE,UAAU,qBAAqB;IAC3B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,gHAAgH;AAChH,UAAU,cAAc;IACpB,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0HAA0H;IAC1H,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvD,uHAAuH;IACvH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1D;AAyFD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,8DAMrC,qBAAqB,KAAG,OAAO,CAAC,cAAc,CA+ChD,CAAC"}
|