Skip to content

Commit 74bbaba

Browse files
Merge pull request #61 from testingbot/feat/maestro-cancel-previous
maestro --cancel-previous
2 parents ce5263f + 582f2eb commit 74bbaba

7 files changed

Lines changed: 129 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to `@testingbot/cli` are documented here. Releases are publi
66

77
### Added
88

9+
- `testingbot maestro --cancel-previous`: before starting, cancels your queued or running runs of the same repository and branch or pull request from an earlier commit (`cancelPrevious` on the run API). Runs on the same commit are left alone.
910
- `testingbot maestro` detects the repository URL, branch, commit and pull request in GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines and CircleCI and sends them as run metadata (`repoUrl` is new), so the dashboard can link to the commit and PR on any Git host. On a GitHub pull request the PR's head commit is used, not the merge commit. Explicit flags win.
1011
- `testingbot espresso` and `testingbot xcuitest` take the same CI/CD metadata as `maestro`: `--branch`, `--pr-url`, `-m/--metadata` and the CI environment detection, through one shared code path.
1112
- `--retry <count>`, `--download-artifacts [all|failed]` and `--artifacts-output-dir` for `testingbot espresso` and `testingbot xcuitest`. A retried run is started again on the uploaded app; the last attempt decides the exit code, the `--report` and the artifacts. Artifacts (video, logs, screenshots) are zipped with a folder per run, like `maestro`.

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ Values passed with `-e` are stored in plain text: they are returned in the run's
203203
| `--repo-owner <owner>` | Repository owner (e.g., GitHub organization or username) |
204204
| `-m, --metadata <KEY=VALUE>` | Free-form metadata attached to the run and shown in the dashboard (repeatable, e.g. `-m team=mobile -m env=staging`) |
205205
| `--check-name <name>` | Name the GitHub pull request check `TestingBot / <name>` so several runs on one commit (iOS, Android) post separate checks that branch protection can require individually |
206+
| `--cancel-previous` | Before starting, cancel your queued or running runs of the same repository and branch (or pull request) from an earlier commit. Runs on the same commit are left alone |
206207

207208
**Allure reports:** `--report allure` converts each run's results into Allure result files under `<report-output-dir>/allure-results/`, one JSON per flow with its steps, status and failure details. Render them with `allure serve <report-output-dir>/allure-results` (requires the [Allure CLI](https://allurereport.org/docs/install/)). Results from several runs or shards accumulate in the same directory.
208209

@@ -695,6 +696,8 @@ testingbot xcuitest app.ipa app-test.zip \
695696

696697
`maestro`, `espresso` and `xcuitest` all take `--branch`, `--commit-sha`, `--pull-request-id`, `--pr-url`, `--repo-owner`, `--repo-name` and `-m, --metadata <KEY=VALUE>` (repeatable). Inside GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines or CircleCI, the repository URL, branch, commit and pull request are read from the CI environment, so the dashboard links the run to them on whichever host the repository lives (GitHub, GitLab, Bitbucket, Azure Repos or self-hosted). Any flag overrides what was detected.
697698

699+
`testingbot maestro --cancel-previous` uses this context to cancel the runs of a previous push: your queued or running Maestro runs with the same repository and the same pull request (or branch) but a different commit. Runs on the same commit, such as the iOS and Android jobs of one pipeline, keep running.
700+
698701
### Real-time Progress
699702

700703
By default, the CLI shows real-time progress updates including:

‎src/cli.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,10 @@ program
851851
'Arbitrary metadata to attach to the run, shown in the dashboard (repeatable).',
852852
collectRepeatable,
853853
)
854+
.option(
855+
'--cancel-previous',
856+
'Cancel your queued or running runs of the same repository and branch (or pull request) from an earlier commit before starting this one.',
857+
)
854858
.option(
855859
'--check-name <name>',
856860
'Name the GitHub pull request check "TestingBot / <name>" so several runs on one commit (iOS, Android) post separate checks.',
@@ -1031,6 +1035,7 @@ program
10311035
ignoreChecksumCheck: args.ignoreChecksumCheck,
10321036
shardSplit: args.shardSplit,
10331037
retry: args.retry,
1038+
cancelPrevious: args.cancelPrevious,
10341039
debug: args.debug,
10351040
googlePlayStore: args.googlePlay,
10361041
disableAnimations: args.disableAnimations,

‎src/models/maestro_options.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export default class MaestroOptions {
136136
private _ignoreChecksumCheck: boolean;
137137
private _shardSplit?: number;
138138
private _retry: number;
139+
private _cancelPrevious: boolean;
139140
private _debug: boolean;
140141
private _configFile?: string;
141142
private _groups?: string[];
@@ -177,6 +178,7 @@ export default class MaestroOptions {
177178
ignoreChecksumCheck?: boolean;
178179
shardSplit?: number;
179180
retry?: number;
181+
cancelPrevious?: boolean;
180182
debug?: boolean;
181183
configFile?: string;
182184
groups?: string[];
@@ -244,6 +246,7 @@ export default class MaestroOptions {
244246
this._artifactsOutputDir = options?.artifactsOutputDir;
245247
this._ignoreChecksumCheck = options?.ignoreChecksumCheck ?? false;
246248
this._shardSplit = options?.shardSplit;
249+
this._cancelPrevious = options?.cancelPrevious ?? false;
247250
this._retry = options?.retry ?? 0;
248251
if (
249252
!Number.isInteger(this._retry) ||
@@ -410,6 +413,10 @@ export default class MaestroOptions {
410413
return this._retry;
411414
}
412415

416+
public get cancelPrevious(): boolean {
417+
return this._cancelPrevious;
418+
}
419+
413420
public get debug(): boolean {
414421
return this._debug;
415422
}

‎src/providers/maestro.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
491491
shardSplit: this.options.shardSplit,
492492
}),
493493
...(metadata && { metadata }),
494+
...(this.options.cancelPrevious && { cancelPrevious: true }),
494495
...(otherApps.length > 0 && {
495496
otherApps: previewOtherAppUrls,
496497
}),
@@ -2468,6 +2469,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
24682469
shardSplit: this.options.shardSplit,
24692470
}),
24702471
...(metadata && { metadata }),
2472+
...(this.options.cancelPrevious && { cancelPrevious: true }),
24712473
...(this.otherAppUrls.length > 0 && {
24722474
otherApps: this.otherAppUrls,
24732475
}),
@@ -2497,6 +2499,10 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
24972499
this.updateKey = result.update_key;
24982500
}
24992501

2502+
if (result.cancelled_previous) {
2503+
this.logCancelledPrevious(result.cancelled_previous);
2504+
}
2505+
25002506
// The server registers a GitHub check when the run carries repo + commit
25012507
// metadata and the TestingBot GitHub App is installed for that repo.
25022508
if (result.ci_check?.repo && !this.options.quiet) {
@@ -4259,6 +4265,26 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
42594265
}
42604266
}
42614267

4268+
/** What --cancel-previous did, as the run response reports it. */
4269+
private logCancelledPrevious(cancelled: {
4270+
run_ids?: number[];
4271+
skipped?: string;
4272+
}): void {
4273+
if (cancelled.skipped) {
4274+
logger.warn(
4275+
`--cancel-previous: nothing cancelled, ${cancelled.skipped}.`,
4276+
);
4277+
return;
4278+
}
4279+
const ids = cancelled.run_ids ?? [];
4280+
if (this.options.quiet) return;
4281+
logger.info(
4282+
ids.length > 0
4283+
? `Cancelled ${ids.length} run(s) of the previous push: ${ids.join(', ')}`
4284+
: 'No runs of a previous push were still running.',
4285+
);
4286+
}
4287+
42624288
private async createZipFromDirectory(
42634289
sourceDir: string,
42644290
zipPath: string,

‎tests/cli.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,21 @@ describe('TestingBotCTL CLI', () => {
20172017
expect(mockUploadOnly).toHaveBeenCalledTimes(1);
20182018
});
20192019

2020+
test('maestro --cancel-previous reaches the options', async () => {
2021+
await program.parseAsync([
2022+
'node',
2023+
'cli',
2024+
'maestro',
2025+
'app.apk',
2026+
'./flows',
2027+
'--cancel-previous',
2028+
]);
2029+
expect(
2030+
lastConstructorOptions<{ cancelPrevious?: boolean }>(Maestro)
2031+
.cancelPrevious,
2032+
).toBe(true);
2033+
});
2034+
20202035
test('detected CI repository metadata fills in what flags leave out', async () => {
20212036
(ciRepoMetadata as jest.Mock).mockReturnValueOnce({
20222037
repoUrl: 'https://dev.azure.com/acme/mobile/_git/app',

‎tests/providers/maestro.test.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,78 @@ describe('Maestro', () => {
921921
logSpy.mockRestore();
922922
});
923923

924+
describe('--cancel-previous', () => {
925+
const cancelling = () => {
926+
const provider = new Maestro(
927+
mockCredentials,
928+
new MaestroOptions('path/to/app.apk', 'path/to/flows', 'Pixel 6', {
929+
cancelPrevious: true,
930+
}),
931+
);
932+
provider['appId'] = 1234;
933+
return provider;
934+
};
935+
936+
it('asks the server to cancel the previous push and logs what it did', async () => {
937+
const provider = cancelling();
938+
axios.post = jest.fn().mockResolvedValueOnce({
939+
data: {
940+
success: true,
941+
runs: [],
942+
cancelled_previous: { run_ids: [7, 8] },
943+
},
944+
headers: {},
945+
});
946+
const logSpy = jest.spyOn(logger, 'info').mockImplementation(() => {});
947+
948+
await provider['runTests']();
949+
950+
expect((axios.post as jest.Mock).mock.calls[0][1].cancelPrevious).toBe(
951+
true,
952+
);
953+
expect(logSpy).toHaveBeenCalledWith(
954+
'Cancelled 2 run(s) of the previous push: 7, 8',
955+
);
956+
logSpy.mockRestore();
957+
});
958+
959+
it('warns when the server could not tell which runs to cancel', async () => {
960+
const provider = cancelling();
961+
axios.post = jest.fn().mockResolvedValueOnce({
962+
data: {
963+
success: true,
964+
runs: [],
965+
cancelled_previous: {
966+
run_ids: [],
967+
skipped: 'metadata has no commitSha',
968+
},
969+
},
970+
headers: {},
971+
});
972+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
973+
974+
await provider['runTests']();
975+
976+
expect(warnSpy).toHaveBeenCalledWith(
977+
'--cancel-previous: nothing cancelled, metadata has no commitSha.',
978+
);
979+
warnSpy.mockRestore();
980+
});
981+
982+
it('is not sent unless asked', async () => {
983+
maestro['appId'] = 1234;
984+
axios.post = jest
985+
.fn()
986+
.mockResolvedValueOnce({ data: { success: true }, headers: {} });
987+
988+
await maestro['runTests']();
989+
990+
expect((axios.post as jest.Mock).mock.calls[0][1]).not.toHaveProperty(
991+
'cancelPrevious',
992+
);
993+
});
994+
});
995+
924996
it('should not include maestroOptions when none are set', async () => {
925997
maestro['appId'] = 1234;
926998

0 commit comments

Comments
 (0)