apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.21
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/.gitattributes +1 -0
- package/.github/actionlint.yaml +11 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
- package/.github/cli/apify-test-tools.mjs +135 -142
- package/.github/scripts/mask-config-secrets.sh +17 -0
- package/.github/scripts/run-with-apify-tokens.mjs +63 -103
- package/.github/workflows/_check_code.yaml +5 -21
- package/.github/workflows/_move_major_tag.yaml +2 -2
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
- package/.github/workflows/public_platform-tests.yaml +68 -30
- package/.github/workflows/public_pr-build-test.yaml +77 -38
- package/.github/workflows/public_push-build-latest.yaml +49 -11
- package/.github/workflows/public_review.yaml +5 -4
- package/.github/workflows/public_stale-issues.yaml +51 -0
- package/.husky/pre-commit +4 -4
- package/.prettierignore +2 -1
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +117 -17
- package/README.md +221 -595
- package/bin/actor-ids.ts +41 -0
- package/bin/affected-actors.ts +211 -0
- package/bin/build-from-local.ts +19 -1
- package/bin/build.ts +105 -39
- package/bin/diff-changes.ts +69 -36
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +73 -175
- package/bin/main.ts +142 -189
- package/bin/notify.ts +198 -0
- package/bin/run-tests.ts +182 -0
- package/bin/slack.ts +16 -76
- package/bin/stdin-input.ts +156 -0
- package/bin/test-files-glob.ts +126 -0
- package/bin/test-report.ts +141 -140
- package/bin/types.ts +4 -7
- package/bin/utils/config/load-config.ts +20 -2
- package/bin/utils/config/parser.ts +40 -8
- package/bin/utils/config/structures/grouped.ts +2 -2
- package/bin/utils/config/structures/legacy.ts +1 -1
- package/dist/bin/actor-ids.d.ts +15 -0
- package/dist/bin/actor-ids.d.ts.map +1 -0
- package/dist/bin/actor-ids.js +30 -0
- package/dist/bin/actor-ids.js.map +1 -0
- package/dist/bin/affected-actors.d.ts +55 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +142 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +2 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +6 -2
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +18 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +74 -24
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +21 -5
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +41 -28
- 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 +26 -34
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +60 -143
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +126 -140
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/notify.d.ts +35 -0
- package/dist/bin/notify.d.ts.map +1 -0
- package/dist/bin/notify.js +131 -0
- package/dist/bin/notify.js.map +1 -0
- package/dist/bin/run-tests.d.ts +21 -0
- package/dist/bin/run-tests.d.ts.map +1 -0
- package/dist/bin/run-tests.js +149 -0
- package/dist/bin/run-tests.js.map +1 -0
- package/dist/bin/slack.d.ts +2 -14
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +17 -46
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +79 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +124 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/test-files-glob.d.ts +18 -0
- package/dist/bin/test-files-glob.d.ts.map +1 -0
- package/dist/bin/test-files-glob.js +108 -0
- package/dist/bin/test-files-glob.js.map +1 -0
- package/dist/bin/test-report.d.ts +69 -8
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +76 -67
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +4 -7
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.d.ts +8 -0
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +14 -1
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +15 -2
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +29 -8
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/grouped.js +2 -2
- package/dist/bin/utils/config/structures/grouped.js.map +1 -1
- package/dist/bin/utils/config/structures/legacy.js +1 -1
- package/dist/bin/utils/config/structures/legacy.js.map +1 -1
- package/dist/lib/extend-expect.d.ts.map +1 -1
- package/dist/lib/extend-expect.js +37 -218
- package/dist/lib/extend-expect.js.map +1 -1
- package/dist/lib/lib.d.ts +1 -5
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +19 -31
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-checks.d.ts +14 -0
- package/dist/lib/run-checks.d.ts.map +1 -0
- package/dist/lib/run-checks.js +82 -0
- package/dist/lib/run-checks.js.map +1 -0
- package/dist/lib/run-test-result.d.ts +3 -6
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +41 -54
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/test/bundle/cli-bundle.test.js +76 -56
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
- package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
- package/dist/test/bundle/token-wrapper.test.js +80 -0
- package/dist/test/bundle/token-wrapper.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
- 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/actor-filtering.test.js +7 -0
- package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
- package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
- package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
- package/dist/test/unit/bin/actor-ids.test.js +40 -0
- package/dist/test/unit/bin/actor-ids.test.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 +224 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +153 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- 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 +49 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +12 -0
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +116 -6
- 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 +23 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +168 -114
- 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 +67 -288
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/notify.test.d.ts +2 -0
- package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
- package/dist/test/unit/bin/notify.test.js +222 -0
- package/dist/test/unit/bin/notify.test.js.map +1 -0
- 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 +92 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-cli.test.js +165 -0
- package/dist/test/unit/bin/test-cli.test.js.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.js +102 -0
- package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
- package/dist/test/unit/bin/test-report.test.d.ts +2 -0
- package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-report.test.js +100 -0
- package/dist/test/unit/bin/test-report.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
- package/dist/test/unit/run-checks.test.d.ts +2 -0
- package/dist/test/unit/run-checks.test.d.ts.map +1 -0
- package/dist/test/unit/run-checks.test.js +62 -0
- package/dist/test/unit/run-checks.test.js.map +1 -0
- package/dist/test/unit/run-test-result.test.d.ts +2 -0
- package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
- package/dist/test/unit/run-test-result.test.js +34 -0
- package/dist/test/unit/run-test-result.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +41 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
- package/docs/cli.md +473 -0
- package/docs/lib.md +276 -0
- package/docs/workflows.md +455 -0
- package/eslint.config.mjs +10 -1
- package/lib/extend-expect.ts +48 -258
- package/lib/lib.ts +25 -34
- package/lib/run-checks.ts +101 -0
- package/lib/run-test-result.ts +45 -55
- package/package.json +4 -2
- package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
- package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
- package/test/bundle/cli-bundle.test.ts +97 -67
- package/test/bundle/token-wrapper.test.ts +110 -0
- package/test/e2e/cli-bundle.e2e.test.ts +60 -93
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/actor-filtering.test.ts +8 -0
- package/test/unit/bin/actor-ids.test.ts +47 -0
- package/test/unit/bin/affected-actors-cli.test.ts +271 -0
- package/test/unit/bin/affected-actors.test.ts +221 -0
- package/test/unit/bin/build-cli.test.ts +61 -0
- package/test/unit/bin/build-from-local.test.ts +16 -0
- package/test/unit/bin/build.test.ts +149 -5
- package/test/unit/bin/compiled-cli.ts +23 -0
- package/test/unit/bin/diff-changes.test.ts +181 -114
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +76 -361
- package/test/unit/bin/notify.test.ts +299 -0
- package/test/unit/bin/stdin-input.test.ts +120 -0
- package/test/unit/bin/test-cli.test.ts +207 -0
- package/test/unit/bin/test-files-glob.test.ts +118 -0
- package/test/unit/bin/test-report.test.ts +118 -0
- package/test/unit/bin/utils/config/load-config.test.ts +26 -1
- package/test/unit/bin/utils/config/parser.test.ts +46 -5
- package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
- package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -0
- package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
- package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
- package/test/unit/run-checks.test.ts +76 -0
- package/test/unit/run-test-result.test.ts +44 -0
- package/test/unit/should-built-and-test.test.ts +46 -39
- package/tsconfig.json +1 -1
- package/vitest.config.ts +9 -0
- package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
- package/.github/scripts/check-major-tag-refs.mjs +0 -72
- package/dist/test/unit/bin/logging.test.d.ts +0 -2
- package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
- package/dist/test/unit/bin/logging.test.js +0 -43
- package/dist/test/unit/bin/logging.test.js.map +0 -1
- package/dist/test/unit/bin/slack.test.d.ts +0 -2
- package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
- package/dist/test/unit/bin/slack.test.js +0 -54
- package/dist/test/unit/bin/slack.test.js.map +0 -1
- package/test/unit/bin/logging.test.ts +0 -52
- package/test/unit/bin/slack.test.ts +0 -65
package/bin/main.ts
CHANGED
|
@@ -6,25 +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 {
|
|
9
|
+
import { getAffectedActors } from './affected-actors.js';
|
|
10
|
+
import { buildActors, DEFAULT_VERSION_KEYWORD, deleteOldBuilds } from './build.js';
|
|
10
11
|
import { runBuildsFromLocal } from './build-from-local.js';
|
|
11
|
-
import { getChangedActors } from './diff-changes.js';
|
|
12
|
-
import {
|
|
13
|
-
getBranchOnlyChangedFiles,
|
|
14
|
-
getChangedFiles,
|
|
15
|
-
getCommits,
|
|
16
|
-
getCurrentBranch,
|
|
17
|
-
getReleaseChanges,
|
|
18
|
-
getRepoName,
|
|
19
|
-
hasMergeFromTarget,
|
|
20
|
-
resolveReleaseBaseCommit,
|
|
21
|
-
resolveRepoUrl,
|
|
22
|
-
} from './git.js';
|
|
23
12
|
import { logger, logLevels } from './logger.js';
|
|
13
|
+
import { notify, notifyOnChoices } from './notify.js';
|
|
24
14
|
import { writeJson } from './output.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import type { Config } from './types.js';
|
|
15
|
+
import { runTests } from './run-tests.js';
|
|
16
|
+
import { parseBuildInput, parseNotifyInput, parseTestTargets, readStdin } from './stdin-input.js';
|
|
28
17
|
import { readConfigFile } from './utils/config/load-config.js';
|
|
29
18
|
|
|
30
19
|
/**
|
|
@@ -34,18 +23,11 @@ const middlewares = [(args: { logLevel: (typeof logLevels)[number] }) => logger.
|
|
|
34
23
|
|
|
35
24
|
/*
|
|
36
25
|
* Option groups shared across commands. Each is a plain object passed to `.options()`, so a command's
|
|
37
|
-
* builder composes them by chaining (`y.options(
|
|
26
|
+
* builder composes them by chaining (`y.options(affectedRangeOptions).options(actorSelectionOptions)`)
|
|
38
27
|
* instead of nesting wrapper functions. `as const` keeps literals like `demandOption: true` so yargs
|
|
39
28
|
* can infer non-optional types; `satisfies` validates the shape without widening it.
|
|
40
29
|
*/
|
|
41
30
|
|
|
42
|
-
/** Commit range to inspect: everything on `source-branch` since it diverged from `target-branch`. */
|
|
43
|
-
const gitRangeOptions = {
|
|
44
|
-
'target-branch': { type: 'string', demandOption: true },
|
|
45
|
-
'source-branch': { type: 'string', demandOption: true },
|
|
46
|
-
'base-commit': { type: 'string', demandOption: false },
|
|
47
|
-
} as const satisfies Record<string, Options>;
|
|
48
|
-
|
|
49
31
|
/**
|
|
50
32
|
* Where to build the Actors from. Defaults to the `origin` remote, which is then checked against each
|
|
51
33
|
* Actor's default version. Passing it explicitly skips that check (see assertRepoUrlMatchesDefaultVersion).
|
|
@@ -57,13 +39,25 @@ const repoUrlOptions = {
|
|
|
57
39
|
/**
|
|
58
40
|
* Actor-selection flags, applied to every command that reads the actor config so a caller can
|
|
59
41
|
* narrow the set it operates on (e.g. two-stage releases: `--ignore X`, then `--actors X`).
|
|
60
|
-
*
|
|
61
|
-
*/
|
|
42
|
+
* */
|
|
62
43
|
const actorSelectionOptions = {
|
|
63
44
|
actors: { type: 'string', array: true, default: [] as string[] },
|
|
64
45
|
ignore: { type: 'string', array: true, default: [] as string[] },
|
|
65
46
|
} as const satisfies Record<string, Options>;
|
|
66
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Commit range for get-affected-actors, which always ends at the checked-out HEAD. `base` and `since` combine:
|
|
50
|
+
* `base` alone is a branch's own commits (a PR), `since` alone is everything after a commit (a release), both is
|
|
51
|
+
* a branch after its last validated commit.
|
|
52
|
+
*/
|
|
53
|
+
const affectedRangeOptions = {
|
|
54
|
+
base: { type: 'string', describe: 'Only count commits on HEAD that are not on this branch' },
|
|
55
|
+
since: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
describe: 'Only count commits after this one. Ignored with --base if it is not on the branch',
|
|
58
|
+
},
|
|
59
|
+
} as const satisfies Record<string, Options>;
|
|
60
|
+
|
|
67
61
|
/**
|
|
68
62
|
* For commands that change something (builds, deletes, Slack posts). A dry run still does every read and check
|
|
69
63
|
* the real run does, credentials included, and only skips the writes. So it needs the same tokens, though
|
|
@@ -77,9 +71,84 @@ const dryRunOptions = {
|
|
|
77
71
|
},
|
|
78
72
|
} as const satisfies Record<string, Options>;
|
|
79
73
|
|
|
80
|
-
/** Flags
|
|
74
|
+
/** Flags of `build`, which builds exactly the Actors it is given. */
|
|
81
75
|
const buildOptions = {
|
|
82
|
-
'
|
|
76
|
+
'version-number': {
|
|
77
|
+
type: 'string',
|
|
78
|
+
demandOption: true,
|
|
79
|
+
describe: `Version to build, like 0.1, or "${DEFAULT_VERSION_KEYWORD}" for each Actor's default version`,
|
|
80
|
+
coerce: (value: string) => {
|
|
81
|
+
if (value !== DEFAULT_VERSION_KEYWORD && !/^\d+\.\d+$/.test(value)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`--version-number must look like 0.1 or be "${DEFAULT_VERSION_KEYWORD}", got "${value}"`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
} as const satisfies Record<string, Options>;
|
|
90
|
+
|
|
91
|
+
/** For the commands that build a version other than the default one, see assertEnvVarsMatchDefaultVersion. */
|
|
92
|
+
const envVarsSyncOptions = {
|
|
93
|
+
'require-env-vars-in-sync': {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
default: false,
|
|
96
|
+
describe: "Fail unless the built version's env var names match the default version's",
|
|
97
|
+
},
|
|
98
|
+
} as const satisfies Record<string, Options>;
|
|
99
|
+
|
|
100
|
+
/** Flags of `test`, which runs the repository's vitest on the Actors piped in, e.g. the builds from `build`. */
|
|
101
|
+
const testOptions = {
|
|
102
|
+
'test-files-glob': {
|
|
103
|
+
type: 'string',
|
|
104
|
+
default: 'test/platform/**',
|
|
105
|
+
describe: 'Glob (from the repo root) of the test files to run. Quote it, so the shell does not expand it',
|
|
106
|
+
},
|
|
107
|
+
'vitest-output-file': { type: 'string', describe: "Also keep vitest's own JSON report there, e.g. to upload it" },
|
|
108
|
+
reporter: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
describe:
|
|
111
|
+
'Vitest reporter (https://vitest.dev/guide/reporters) to add to `default` and `json`, which are always on, ' +
|
|
112
|
+
'e.g. github-actions. Repeat it for more',
|
|
113
|
+
// A plain string, not `array: true`: yargs would fill an array option with the arguments after it too. A
|
|
114
|
+
// repeated flag still comes as an array, a single one as a string, so both are normalized to an array.
|
|
115
|
+
coerce: (value: string | string[] | undefined) => (value === undefined ? [] : [value].flat()),
|
|
116
|
+
},
|
|
117
|
+
} as const satisfies Record<string, Options>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Flags of `notify`, one target per call: posting to more places is calling it again. Some flags only fit one kind
|
|
121
|
+
* of input; `notify` fails on the others rather than ignoring them.
|
|
122
|
+
*/
|
|
123
|
+
const notifyOptions = {
|
|
124
|
+
'slack-channel': {
|
|
125
|
+
type: 'string',
|
|
126
|
+
describe: 'Slack channel to post to',
|
|
127
|
+
// yargs collects a repeated flag into an array, which would reach Slack as one comma-joined channel
|
|
128
|
+
coerce: (value: string | string[]) => {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
throw new Error('--slack-channel takes one channel, call notify once for each channel');
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
on: {
|
|
136
|
+
choices: notifyOnChoices,
|
|
137
|
+
default: 'failure' as const,
|
|
138
|
+
describe: 'Tests only: notify only when tests fail, or always',
|
|
139
|
+
},
|
|
140
|
+
title: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
describe: 'Tests only: what the message calls the run, e.g. the CI workflow name',
|
|
143
|
+
},
|
|
144
|
+
'job-url': {
|
|
145
|
+
type: 'string',
|
|
146
|
+
describe: 'Tests only: link to the CI job, included in the message',
|
|
147
|
+
},
|
|
148
|
+
'repo-url': {
|
|
149
|
+
type: 'string',
|
|
150
|
+
describe: 'Releases only: the repository the release is named after. Defaults to the origin remote',
|
|
151
|
+
},
|
|
83
152
|
} as const satisfies Record<string, Options>;
|
|
84
153
|
|
|
85
154
|
/** Global flags, available to every command. */
|
|
@@ -92,82 +161,10 @@ const globalOptions = {
|
|
|
92
161
|
},
|
|
93
162
|
} as const satisfies Record<string, Options>;
|
|
94
163
|
|
|
95
|
-
const resolveChangedActors = async (config: Config, { isLatest }: { isLatest: boolean }) => {
|
|
96
|
-
const actorConfigs = await readConfigFile(config);
|
|
97
|
-
|
|
98
|
-
// This is an optimization for the common case where a branch only has cosmetic changes but had to smerge in
|
|
99
|
-
// functional changes from master (being up-to-date is a CI requirement). Master is already validated, and
|
|
100
|
-
// since the branch has no functional changes of its own, there is nothing new to validate.
|
|
101
|
-
// Exception: if the branch has any functional changes alongside the merge, we must re-test — even
|
|
102
|
-
// individually validated changes can have novel interactions when combined.
|
|
103
|
-
if (await hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
|
|
104
|
-
logger.info(
|
|
105
|
-
'[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',
|
|
106
|
-
);
|
|
107
|
-
const branchOnlyFiles = await getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
|
|
108
|
-
// Omit baseCommit to get full branch history. Validated functional commits can still interact with merged ones
|
|
109
|
-
const allBranchCommits = await getCommits({ ...config, baseCommit: undefined });
|
|
110
|
-
const branchOnlyActorsChanged = await getChangedActors({
|
|
111
|
-
filepathsChanged: branchOnlyFiles,
|
|
112
|
-
actorConfigs,
|
|
113
|
-
commits: allBranchCommits,
|
|
114
|
-
testFilesGlob: config.testFilesGlob,
|
|
115
|
-
});
|
|
116
|
-
if (branchOnlyActorsChanged.length === 0) {
|
|
117
|
-
logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
logger.info(
|
|
121
|
-
`[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${branchOnlyActorsChanged.length} functional changes, cannot optimize, we continue with full check`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 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)
|
|
126
|
-
const commits = await getCommits(config);
|
|
127
|
-
const changedFiles = await getChangedFiles(commits);
|
|
128
|
-
return getChangedActors({
|
|
129
|
-
filepathsChanged: changedFiles,
|
|
130
|
-
actorConfigs,
|
|
131
|
-
isLatest,
|
|
132
|
-
commits,
|
|
133
|
-
testFilesGlob: config.testFilesGlob,
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
|
|
137
164
|
await yargs()
|
|
138
165
|
.scriptName('public-actors-utils')
|
|
139
166
|
.options(globalOptions)
|
|
140
167
|
.middleware(middlewares)
|
|
141
|
-
.command(
|
|
142
|
-
'get-commits',
|
|
143
|
-
'',
|
|
144
|
-
(y) => y.options(gitRangeOptions),
|
|
145
|
-
async (args) => {
|
|
146
|
-
const commits = await getCommits(args);
|
|
147
|
-
writeJson(commits);
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
|
-
.command(
|
|
151
|
-
'get-latest-commit',
|
|
152
|
-
'',
|
|
153
|
-
(y) => y.options(gitRangeOptions),
|
|
154
|
-
async (args) => {
|
|
155
|
-
const commits = await getCommits(args);
|
|
156
|
-
if (commits.length > 0) {
|
|
157
|
-
writeJson(commits[commits.length - 1]);
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
)
|
|
161
|
-
.command(
|
|
162
|
-
'get-changed-files',
|
|
163
|
-
'',
|
|
164
|
-
(y) => y.options(gitRangeOptions),
|
|
165
|
-
async (args) => {
|
|
166
|
-
const commits = await getCommits(args);
|
|
167
|
-
const changedFiles = await getChangedFiles(commits);
|
|
168
|
-
writeJson(changedFiles);
|
|
169
|
-
},
|
|
170
|
-
)
|
|
171
168
|
.command(
|
|
172
169
|
'get-actor-configs',
|
|
173
170
|
'',
|
|
@@ -179,115 +176,71 @@ await yargs()
|
|
|
179
176
|
)
|
|
180
177
|
.command(
|
|
181
178
|
'get-affected-actors',
|
|
182
|
-
'',
|
|
183
|
-
(y) => y.options(
|
|
184
|
-
async (
|
|
185
|
-
|
|
186
|
-
writeJson(actorsChanged);
|
|
179
|
+
'Find the Actors whose context changed in a commit range ending at the checked-out HEAD',
|
|
180
|
+
(y) => y.options(affectedRangeOptions).options(actorSelectionOptions),
|
|
181
|
+
async (args) => {
|
|
182
|
+
writeJson(await getAffectedActors(args));
|
|
187
183
|
},
|
|
188
184
|
)
|
|
189
185
|
.command(
|
|
190
|
-
'
|
|
191
|
-
'',
|
|
192
|
-
(y) =>
|
|
193
|
-
y
|
|
194
|
-
.options(dryRunOptions)
|
|
195
|
-
.option('report-file', { type: 'string', demandOption: true })
|
|
196
|
-
.option('report-slack-channel', { type: 'string' })
|
|
197
|
-
.option('job-url', { type: 'string' })
|
|
198
|
-
.option('workflow-name', { type: 'string' }),
|
|
186
|
+
'test',
|
|
187
|
+
'Run the tests of the Actors listed on stdin (builds from build, or get-actor-configs for default builds), and print a report for notify',
|
|
188
|
+
(y) => y.options(testOptions),
|
|
199
189
|
async (args) => {
|
|
200
|
-
await
|
|
190
|
+
const report = await runTests({
|
|
191
|
+
testFilesGlob: args.testFilesGlob,
|
|
192
|
+
targets: parseTestTargets(await readStdin()),
|
|
193
|
+
vitestOutputFile: args.vitestOutputFile,
|
|
194
|
+
reporters: args.reporter,
|
|
195
|
+
});
|
|
196
|
+
writeJson(report);
|
|
197
|
+
// The report is printed either way, so `notify` later in the pipe still hears about the failure
|
|
198
|
+
if (!report.success) process.exitCode = 1;
|
|
201
199
|
},
|
|
202
200
|
)
|
|
203
201
|
.command(
|
|
204
|
-
'
|
|
205
|
-
'',
|
|
206
|
-
(y) =>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.
|
|
211
|
-
.
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const builds = await runBuilds({
|
|
217
|
-
...(await resolveRepoUrl(config.repoUrl)),
|
|
218
|
-
actorConfigs: actorsChanged,
|
|
219
|
-
branch: config.sourceBranch.replace('origin/', ''),
|
|
220
|
-
dryRun: config.dryRun,
|
|
221
|
-
useDockerCache: config.useDockerCache,
|
|
202
|
+
'notify',
|
|
203
|
+
'Tell the target about a test run (the output of test) or a release (of get-affected-actors) on stdin',
|
|
204
|
+
(y) => y.options(notifyOptions).options(dryRunOptions),
|
|
205
|
+
async (args) => {
|
|
206
|
+
await notify({
|
|
207
|
+
input: parseNotifyInput(await readStdin()),
|
|
208
|
+
slackChannel: args.slackChannel,
|
|
209
|
+
on: args.on,
|
|
210
|
+
title: args.title,
|
|
211
|
+
jobUrl: args.jobUrl,
|
|
212
|
+
repoUrl: args.repoUrl,
|
|
213
|
+
dryRun: args.dryRun,
|
|
222
214
|
});
|
|
223
|
-
writeJson(builds);
|
|
224
215
|
},
|
|
225
216
|
)
|
|
226
217
|
.command(
|
|
227
|
-
'
|
|
228
|
-
'',
|
|
229
|
-
(y) =>
|
|
230
|
-
y
|
|
231
|
-
.options(actorSelectionOptions)
|
|
232
|
-
.options(buildOptions)
|
|
233
|
-
.options(repoUrlOptions)
|
|
234
|
-
.options(dryRunOptions)
|
|
235
|
-
// The last commit that is already released, everything after it up to HEAD gets released.
|
|
236
|
-
// Required, see the README section "Releasing Actors" for how to set it for each merge strategy.
|
|
237
|
-
.option('base-commit', { type: 'string', demandOption: true })
|
|
238
|
-
.option('report-slack-channel', { type: 'string' })
|
|
239
|
-
.option('release-slack-channel', { type: 'string' }),
|
|
218
|
+
'build',
|
|
219
|
+
'Build the Actors listed on stdin, as {"actors": [{"actorFullName": ...}]}, from the checked-out branch',
|
|
220
|
+
(y) => y.options(buildOptions).options(repoUrlOptions).options(envVarsSyncOptions).options(dryRunOptions),
|
|
240
221
|
async (args) => {
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
const { commits, changedFiles, changelog } = changes;
|
|
249
|
-
const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
|
|
250
|
-
|
|
251
|
-
const isLatest = true;
|
|
252
|
-
const actorConfigs = await readConfigFile(args);
|
|
253
|
-
const actorsChanged = await getChangedActors({
|
|
254
|
-
filepathsChanged: changedFiles,
|
|
222
|
+
const input = parseBuildInput(await readStdin());
|
|
223
|
+
const names = input.actors.map(({ actorFullName }) => actorFullName);
|
|
224
|
+
// An empty --actors selects every Actor, but an empty list here means there is nothing to build. The configs
|
|
225
|
+
// are picked by name, so an Actor listed twice (e.g. in two lists of get-affected-actors) is built once
|
|
226
|
+
const actorConfigs = names.length > 0 ? await readConfigFile({ actors: names, ignore: [] }) : [];
|
|
227
|
+
const builds = await buildActors({
|
|
255
228
|
actorConfigs,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const builds = await runBuilds({
|
|
261
|
-
isLatest,
|
|
262
|
-
repoUrl,
|
|
263
|
-
shouldVerifyRepoUrl,
|
|
264
|
-
actorConfigs: actorsChanged,
|
|
265
|
-
branch,
|
|
266
|
-
dryRun,
|
|
267
|
-
useDockerCache: args.useDockerCache,
|
|
268
|
-
});
|
|
269
|
-
logger.info(JSON.stringify(builds));
|
|
270
|
-
|
|
271
|
-
await notifyToSlack({
|
|
272
|
-
changedFiles,
|
|
273
|
-
commits,
|
|
274
|
-
changelog,
|
|
275
|
-
repository: getRepoName(repoUrl),
|
|
276
|
-
dryRun,
|
|
277
|
-
// The newest commit, same as the pushed head commit
|
|
278
|
-
author: commits[commits.length - 1].author,
|
|
279
|
-
reportSlackChannel,
|
|
280
|
-
releaseSlackChannel,
|
|
229
|
+
versionNumber: args.versionNumber,
|
|
230
|
+
repoUrl: args.repoUrl,
|
|
231
|
+
dryRun: args.dryRun,
|
|
232
|
+
requireEnvVarsInSync: args.requireEnvVarsInSync,
|
|
281
233
|
});
|
|
234
|
+
writeJson(builds);
|
|
282
235
|
},
|
|
283
236
|
)
|
|
284
237
|
.command(
|
|
285
238
|
'build-from-local',
|
|
286
239
|
'',
|
|
287
|
-
(y) => y.options(actorSelectionOptions).options(dryRunOptions),
|
|
288
|
-
async ({ actors, ignore, dryRun }) => {
|
|
240
|
+
(y) => y.options(actorSelectionOptions).options(envVarsSyncOptions).options(dryRunOptions),
|
|
241
|
+
async ({ actors, ignore, dryRun, requireEnvVarsInSync }) => {
|
|
289
242
|
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
290
|
-
const builds = await runBuildsFromLocal({ actorConfigs, dryRun });
|
|
243
|
+
const builds = await runBuildsFromLocal({ actorConfigs, dryRun, requireEnvVarsInSync });
|
|
291
244
|
writeJson(builds);
|
|
292
245
|
},
|
|
293
246
|
)
|
package/bin/notify.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { getCommitUrl, getRepoName, resolveRepoUrl } from './git.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { getSlackClient, sendingLabel, sendSlackMessage } from './slack.js';
|
|
4
|
+
import type { NotifyInput, ReleaseChanges } from './stdin-input.js';
|
|
5
|
+
import type { TestCaseReport, TestReport } from './test-report.js';
|
|
6
|
+
|
|
7
|
+
export const notifyOnChoices = ['failure', 'always'] as const;
|
|
8
|
+
type NotifyOn = (typeof notifyOnChoices)[number];
|
|
9
|
+
|
|
10
|
+
interface Message {
|
|
11
|
+
text: string;
|
|
12
|
+
/** Posted in the message's thread, so a long list keeps the channel readable */
|
|
13
|
+
details: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A place a notification goes, one per `notify` call: a caller with more places calls `notify` once for each. A
|
|
18
|
+
* target checks its credentials when it is created, before we know whether there is anything to send, so a broken
|
|
19
|
+
* token shows up on a green run (and in dry runs) rather than on the day tests fail. Slack is the only one for now;
|
|
20
|
+
* another (e.g. email) is one more factory next to `createSlackTarget`.
|
|
21
|
+
*/
|
|
22
|
+
interface NotificationTarget {
|
|
23
|
+
send(message: Message): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SLACK_TOKEN_ENV_VAR = 'SLACK_BOT_TOKEN';
|
|
27
|
+
|
|
28
|
+
// Long enough for any sensible commit title, short enough that one runaway title doesn't swamp the message
|
|
29
|
+
const MAX_COMMIT_TITLE_LENGTH = 200;
|
|
30
|
+
|
|
31
|
+
interface NotifyOptions {
|
|
32
|
+
input: NotifyInput;
|
|
33
|
+
slackChannel?: string;
|
|
34
|
+
/** Tests only */
|
|
35
|
+
on: NotifyOn;
|
|
36
|
+
/** Tests only: what the message calls the run, e.g. the CI workflow name */
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Tests only: link to the CI job, included in the message */
|
|
39
|
+
jobUrl?: string;
|
|
40
|
+
/** Releases only, names the release. Defaults to the `origin` remote, see resolveRepoUrl */
|
|
41
|
+
repoUrl?: string;
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Tells the target about what is on stdin: a test run (the output of `test`) or a release (of `get-affected-actors`). */
|
|
46
|
+
export const notify = async (options: NotifyOptions) => {
|
|
47
|
+
assertFlagsFitInput(options);
|
|
48
|
+
const { input, slackChannel, dryRun } = options;
|
|
49
|
+
// A target is created only once we know a message may go out, so a release with no commits needs no token
|
|
50
|
+
const createTarget = async () => {
|
|
51
|
+
if (!slackChannel) {
|
|
52
|
+
logger.info('Skipping notification, no --slack-channel passed');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return createSlackTarget(SLACK_TOKEN_ENV_VAR, slackChannel, dryRun);
|
|
56
|
+
};
|
|
57
|
+
if (input.kind === 'tests') {
|
|
58
|
+
await notifyTestReport({ ...options, report: input.report, createTarget });
|
|
59
|
+
} else {
|
|
60
|
+
await notifyRelease({ ...options, changes: input.changes, createTarget });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A flag the input has no use for would be dropped silently, so it fails instead: the caller meant something by it. */
|
|
65
|
+
const assertFlagsFitInput = ({ input, on, title, jobUrl, repoUrl }: NotifyOptions) => {
|
|
66
|
+
const misfits =
|
|
67
|
+
input.kind === 'tests'
|
|
68
|
+
? [repoUrl && '--repo-url']
|
|
69
|
+
: [on !== 'failure' && '--on', title && '--title', jobUrl && '--job-url'];
|
|
70
|
+
const passed = misfits.filter(Boolean);
|
|
71
|
+
if (passed.length > 0) {
|
|
72
|
+
const what =
|
|
73
|
+
input.kind === 'tests'
|
|
74
|
+
? 'a test report (the output of test)'
|
|
75
|
+
: 'a release (the output of get-affected-actors)';
|
|
76
|
+
throw new Error(`${passed.join(', ')} can't be used with ${what} on stdin`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type CreateTarget = () => Promise<NotificationTarget | null>;
|
|
81
|
+
|
|
82
|
+
/** How a test run went: by default only when it failed, with `on: 'always'` always. */
|
|
83
|
+
const notifyTestReport = async ({
|
|
84
|
+
report,
|
|
85
|
+
on,
|
|
86
|
+
title,
|
|
87
|
+
jobUrl,
|
|
88
|
+
createTarget,
|
|
89
|
+
}: Pick<NotifyOptions, 'on' | 'title' | 'jobUrl'> & { report: TestReport; createTarget: CreateTarget }) => {
|
|
90
|
+
const target = await createTarget();
|
|
91
|
+
if (!target) return;
|
|
92
|
+
if (report.success && on === 'failure') {
|
|
93
|
+
logger.info('The tests passed, so there is nothing to notify about (pass --on always to notify anyway)');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await target.send(formatTestMessage({ report, title: title ?? '-', jobUrl }));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What a release changed: its changelog additions, then its commits, each linked to its commit page. Posts nothing
|
|
102
|
+
* when there are no commits, which means there is nothing new to release.
|
|
103
|
+
*/
|
|
104
|
+
const notifyRelease = async ({
|
|
105
|
+
changes: { commits, changelog },
|
|
106
|
+
repoUrl,
|
|
107
|
+
createTarget,
|
|
108
|
+
}: Pick<NotifyOptions, 'repoUrl'> & { changes: ReleaseChanges; createTarget: CreateTarget }) => {
|
|
109
|
+
if (commits.length === 0) {
|
|
110
|
+
logger.info('No new commits, so there is no release to announce');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!changelog) {
|
|
114
|
+
logger.warn('No new changelog entries found, did you forget to update it?');
|
|
115
|
+
}
|
|
116
|
+
const target = await createTarget();
|
|
117
|
+
if (!target) return;
|
|
118
|
+
|
|
119
|
+
const resolvedRepoUrl = (await resolveRepoUrl(repoUrl)).repoUrl;
|
|
120
|
+
await target.send({ text: formatReleaseMessage({ commits, changelog }, resolvedRepoUrl), details: [] });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The release, named after the repository and the author of its newest commit, the pushed head. */
|
|
124
|
+
export const formatReleaseMessage = ({ commits, changelog }: ReleaseChanges, repoUrl: string) => {
|
|
125
|
+
const { author } = commits[commits.length - 1];
|
|
126
|
+
let text = `*${getRepoName(repoUrl)}* – New release (by ${displayName(author)}):\n\n`;
|
|
127
|
+
if (changelog) text += `${changelog.trim()}\n\n`;
|
|
128
|
+
text += '*Commits*:\n';
|
|
129
|
+
text += commits
|
|
130
|
+
.map(({ sha, author: commitAuthor, message }, index) => {
|
|
131
|
+
const title =
|
|
132
|
+
message.length > MAX_COMMIT_TITLE_LENGTH
|
|
133
|
+
? `${message.slice(0, MAX_COMMIT_TITLE_LENGTH - 1)}…`
|
|
134
|
+
: message;
|
|
135
|
+
const url = getCommitUrl(repoUrl, sha);
|
|
136
|
+
const linked = url ? `<${url}|${escapeSlack(title)}>` : escapeSlack(title);
|
|
137
|
+
return `${index + 1}. ${linked} – ${displayName(commitAuthor)}`;
|
|
138
|
+
})
|
|
139
|
+
.join('\n');
|
|
140
|
+
return text;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Git authors come as "Name<email>", Slack only needs the name
|
|
144
|
+
const displayName = (author: string) => author.replace(/\s*<[^>]*>$/, '');
|
|
145
|
+
|
|
146
|
+
const createSlackTarget = async (
|
|
147
|
+
tokenEnvVar: string,
|
|
148
|
+
channel: string,
|
|
149
|
+
dryRun: boolean,
|
|
150
|
+
): Promise<NotificationTarget> => {
|
|
151
|
+
const slack = await getSlackClient(tokenEnvVar, dryRun);
|
|
152
|
+
return {
|
|
153
|
+
async send({ text, details }) {
|
|
154
|
+
logger.info(`${sendingLabel(dryRun)} slack message to channel ${channel}:\n${text}`);
|
|
155
|
+
if (details.length > 0) logger.info(`\tin its thread:\n\t${details.join('\n\t')}`);
|
|
156
|
+
if (!dryRun) await sendSlackMessage(slack, channel, text, details);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Slack reads these as markup, so they have to be escaped wherever text comes from the tests
|
|
162
|
+
const escapeSlack = (text: string) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
163
|
+
|
|
164
|
+
/** One line per failure message, linked to the run that failed, if the test started one. */
|
|
165
|
+
const formatFailures = ({ failureMessages, run }: TestCaseReport) => {
|
|
166
|
+
const link = run ? ` --- <${run.runLink}|${escapeSlack(run.actorId)}>` : '';
|
|
167
|
+
return failureMessages.map((message) => `${escapeSlack(message)}${link}`);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The message is the outcome and the first failure; the other failures go to its thread, so a channel with many
|
|
172
|
+
* failing runs stays readable.
|
|
173
|
+
*/
|
|
174
|
+
export const formatTestMessage = ({
|
|
175
|
+
report,
|
|
176
|
+
title,
|
|
177
|
+
jobUrl,
|
|
178
|
+
}: {
|
|
179
|
+
report: TestReport;
|
|
180
|
+
title: string;
|
|
181
|
+
jobUrl?: string;
|
|
182
|
+
}) => {
|
|
183
|
+
const { total, failed, passed } = report.summary;
|
|
184
|
+
const failedAssertions = report.tests.filter(({ status }) => status === 'failed').flatMap(formatFailures);
|
|
185
|
+
const failures = [...failedAssertions, ...report.errors.map((error) => `Error: ${escapeSlack(error)}`)];
|
|
186
|
+
const jobLink = jobUrl ? ` Check <${jobUrl}|the job>.` : '';
|
|
187
|
+
|
|
188
|
+
let text = `\`${escapeSlack(title)}\``;
|
|
189
|
+
if (report.success) {
|
|
190
|
+
text += `: all ${passed} tests passed.${jobLink}`;
|
|
191
|
+
return { text, details: [] };
|
|
192
|
+
}
|
|
193
|
+
text += `: has ${failedAssertions.length} failed assertions. Failing test suites: ${failed}/${total}.`;
|
|
194
|
+
if (report.errors.length > 0) text += ` Errors outside of tests: ${report.errors.length}.`;
|
|
195
|
+
text += jobLink;
|
|
196
|
+
if (failures.length > 0) text += `\n\n${failures[0]}`;
|
|
197
|
+
return { text, details: failures.slice(1).map((failure) => `• ${failure}`) };
|
|
198
|
+
};
|