From 169036deef348b5955ca2c2a70169c876829eca3 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:28:40 +0100 Subject: [PATCH 1/9] feat(cloud): add --cancel-previous to supersede the previous CI run (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sends the opt-in flag with the submission; the API derives the CI context (repo + branch/PR + check name) from the run metadata and cancels the previous run's still-queued tests. The superseded run's own CLI, still polling, now exits 0 instead of failing the build: any result carrying `superseded_by:` settles the verdict as SUPERSEDED, even alongside a test that had already failed, because this run no longer speaks for the commit. The failure is still printed and still in tests[] under --json, where SUPERSEDED is a third value of an existing documented field. Sent as its own field rather than inside `config`, which is stamped onto every result row and shipped to the runner — this is a submission directive, not run configuration. Not exposed on the MCP tool: it has no CI metadata to derive a context from, so the flag could only ever be a no-op there. Co-authored-by: Claude Opus 5 (1M context) --- src/commands/cloud.ts | 2 + src/config/flags/execution.flags.ts | 6 ++ src/services/results-polling.service.ts | 83 +++++++++++++++++-- src/services/test-submission.service.ts | 14 ++++ test/unit/superseded-run.test.ts | 106 ++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 test/unit/superseded-run.test.ts diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index 3b8320d..a7173a3 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -194,6 +194,7 @@ export const cloudCommand = defineCommand({ collectRepeatedFlag(rawArgs, ['--exclude-tags']), ); let flows = args.flows as string | undefined; + const cancelPrevious = Boolean(args['cancel-previous']); const googlePlay = Boolean(args['google-play']); const ignoreShaCheck = Boolean(args['ignore-sha-check']); // Single opt-in for client-side envelope encryption of every sensitive @@ -819,6 +820,7 @@ export const cloudCommand = defineCommand({ androidNoSnapshot, apiUrl, appBinaryId: finalBinaryId, + cancelPrevious, cliVersion, commonRoot, continueOnFailure, diff --git a/src/config/flags/execution.flags.ts b/src/config/flags/execution.flags.ts index e02dabb..3e4988b 100644 --- a/src/config/flags/execution.flags.ts +++ b/src/config/flags/execution.flags.ts @@ -4,6 +4,12 @@ import type { ArgsDef } from 'citty'; * Test execution and flow management flags */ export const executionFlags = { + 'cancel-previous': { + type: 'boolean', + default: false, + description: + 'Cancel the still-queued tests of the previous run from the same CI context (repo + branch/PR + check name, read from your CI metadata). Tests already running are left to finish; cancelled tests are refunded at 75%. Does nothing outside CI.', + }, config: { type: 'string', description: diff --git a/src/services/results-polling.service.ts b/src/services/results-polling.service.ts index 774fc66..41b2cf7 100644 --- a/src/services/results-polling.service.ts +++ b/src/services/results-polling.service.ts @@ -81,9 +81,36 @@ export function deviceFromResultRow(r: { }; } +/** + * Was this row cancelled because a newer run from the same CI context + * replaced it (`dcd cloud --cancel-previous`)? + * + * Read through a structural cast for the same reason deviceFromResultRow + * does: the committed generated types are regenerated wholesale from dev's + * swagger and lag the API, and this must work against an API that already + * sends the field. + */ +export function isSupersededRow(r: unknown): boolean { + const reason = (r as { cancellation_reason?: string | null } | null) + ?.cancellation_reason; + return typeof reason === 'string' && reason.startsWith('superseded_by:'); +} + +/** The upload that superseded this run, for the console link. */ +export function supersedingUploadId(results: unknown[]): string | undefined { + for (const r of results) { + const reason = (r as { cancellation_reason?: string | null } | null) + ?.cancellation_reason; + if (typeof reason === 'string' && reason.startsWith('superseded_by:')) { + return reason.slice('superseded_by:'.length) || undefined; + } + } + return undefined; +} + export interface PollingResult { consoleUrl: string; - status: 'FAILED' | 'PASSED'; + status: 'FAILED' | 'PASSED' | 'SUPERSEDED'; tests: Array<{ /** Device this result ran on (present when the API reports it). */ device?: TestDevice; @@ -339,13 +366,24 @@ export class ResultsPollingService { ): PollingResult { const resultsWithoutEarlierTries = this.filterLatestResults(results); + // ANY superseded row settles the whole verdict, even alongside a test + // that had genuinely failed before the newer run replaced this one: this + // run no longer speaks for the commit, so failing the build on its behalf + // is wrong. Actions reaches the same conclusion — a cancelled run's + // conclusion is `cancelled`, whatever had already failed inside it. The + // failure is still printed and still in `tests[]`; only the exit code + // changes. + const superseded = resultsWithoutEarlierTries.some(isSupersededRow); + return { consoleUrl, // Anything other than an explicit pass (CANCELLED, ERROR, a status we // don't know about yet) must fail the run — this gates CI exit codes. - status: resultsWithoutEarlierTries.every((result) => result.status === 'PASSED') - ? 'PASSED' - : 'FAILED', + status: superseded + ? 'SUPERSEDED' + : resultsWithoutEarlierTries.every((result) => result.status === 'PASSED') + ? 'PASSED' + : 'FAILED', tests: resultsWithoutEarlierTries.map((r) => ({ // r carries config/simulator_name at runtime; the committed generated // types lag the API (regenerated wholesale from dev's swagger), so read @@ -386,8 +424,12 @@ export class ResultsPollingService { const pending = statusCounts.PENDING || 0; const queued = statusCounts.QUEUED || 0; const running = statusCounts.RUNNING || 0; + // CANCELLED is terminal, so it counts as completed. Without it a + // cancelled or superseded run's footer sticks at "8/12 completed" + // forever, having already stopped polling. + const cancelled = statusCounts.CANCELLED || 0; const total = results.length; - const completed = passed + failed; + const completed = passed + failed + cancelled; const summary = formatTestSummary({ completed, @@ -562,6 +604,37 @@ export class ResultsPollingService { testMetadata, ); + if (output.status === 'SUPERSEDED') { + // Exit 0: a newer run of the same CI context replaced this one, so + // failing the build here would fail it for work nobody is waiting on. + // Falls through to the success return below — RunFailedError, and with + // it the exit code 2 in `dcd cloud`, is never reached. + if (logger && !json) { + const newer = supersedingUploadId(updatedResults); + logger('\n'); + logger( + ui.warn( + 'Run superseded by a newer run from the same CI context — exiting 0', + ), + ); + if (newer) { + logger( + ui.branch( + ui.fields([ + [ + 'superseded by', + colors.url(consoleUrl.replace(uploadId, newer)), + ], + ]), + ), + ); + } + logger('\n'); + } + + return output; + } + if (output.status === 'FAILED') { if (debug && logger) { logger(`[DEBUG] Some tests failed, returning failed status`); diff --git a/src/services/test-submission.service.ts b/src/services/test-submission.service.ts index 61cda81..4a5fe78 100644 --- a/src/services/test-submission.service.ts +++ b/src/services/test-submission.service.ts @@ -18,6 +18,13 @@ export interface TestSubmissionConfig { androidNoSnapshot?: boolean; apiUrl?: string; appBinaryId: string; + /** + * Ask the API to cancel the still-queued tests of the previous run from + * the same CI context. Sent as its own field rather than inside `config`, + * which is stamped onto every result row and shipped to the runner — this + * is a one-off submission directive, not run configuration. + */ + cancelPrevious?: boolean; cliVersion: string; commonRoot: string; continueOnFailure?: boolean; @@ -86,6 +93,7 @@ export class TestSubmissionService { cliVersion, env = [], metadata = [], + cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, @@ -308,6 +316,12 @@ export class TestSubmissionService { ); } + // Only sent when asked for, so every other submission's wire shape is + // unchanged and the flag is simply ignored by an older API. + if (cancelPrevious) { + fields.cancelPrevious = 'true'; + } + this.setOptionalFields(fields, { androidApiLevel, androidDevice, diff --git a/test/unit/superseded-run.test.ts b/test/unit/superseded-run.test.ts new file mode 100644 index 0000000..2c729b4 --- /dev/null +++ b/test/unit/superseded-run.test.ts @@ -0,0 +1,106 @@ +import { expect } from 'chai'; + +import { + isSupersededRow, + ResultsPollingService, + supersedingUploadId, +} from '../../src/services/results-polling.service.js'; + +// A superseded run is one `dcd cloud --cancel-previous` replaced: a newer run +// of the same CI context cancelled its queued tests. The verdict below is what +// decides whether that older CI job exits 0 or fails the build, so it is worth +// pinning down separately from the polling loop around it. +const row = (overrides: Record = {}) => ({ + id: 1, + test_file_name: 'flow.yaml', + status: 'PASSED', + retry_of: null, + created_at: '2026-09-17T10:00:00.000Z', + duration_seconds: 1, + fail_reason: null, + simulator_name: 'pixel-7', + ...overrides, +}); + +const cancelledBySupersede = (id: number) => + row({ + id, + status: 'CANCELLED', + cancellation_reason: 'superseded_by:newer-upload', + }); + +// buildPollingResult is private; it is the whole point of this file, so reach +// it rather than re-implementing the verdict in the test. +const verdict = (results: unknown[]) => + ( + new ResultsPollingService() as unknown as { + buildPollingResult: ( + r: unknown[], + uploadId: string, + consoleUrl: string, + ) => { status: string }; + } + ).buildPollingResult(results, 'upload-1', 'https://console/upload-1').status; + +describe('superseded runs', () => { + describe('isSupersededRow', () => { + it('matches only the superseded token', () => { + expect(isSupersededRow(cancelledBySupersede(1))).to.equal(true); + expect(isSupersededRow(row({ status: 'CANCELLED' }))).to.equal(false); + expect( + isSupersededRow(row({ cancellation_reason: 'user' })), + ).to.equal(false); + expect(isSupersededRow(row())).to.equal(false); + expect(isSupersededRow(null)).to.equal(false); + }); + }); + + describe('supersedingUploadId', () => { + it('reads the newer upload id out of the reason', () => { + expect(supersedingUploadId([row(), cancelledBySupersede(2)])).to.equal( + 'newer-upload', + ); + }); + + it('is undefined when nothing was superseded', () => { + expect(supersedingUploadId([row()])).to.equal(undefined); + }); + }); + + describe('the run verdict', () => { + it('is PASSED when every test passed', () => { + expect(verdict([row(), row({ id: 2 })])).to.equal('PASSED'); + }); + + it('is FAILED for an ordinary cancel, as before', () => { + // No reason on the row: someone cancelled this run by hand, and the + // build should still go red. + expect(verdict([row(), row({ id: 2, status: 'CANCELLED' })])).to.equal( + 'FAILED', + ); + }); + + it('is SUPERSEDED when a newer run replaced this one', () => { + expect(verdict([row(), cancelledBySupersede(2)])).to.equal('SUPERSEDED'); + }); + + it('is SUPERSEDED even when a test had already genuinely failed', () => { + // This run no longer speaks for the commit — the newer one does — so it + // must not fail the build. The failure is still reported in tests[]. + const results = [ + row({ id: 1, status: 'FAILED', fail_reason: 'assertion failed' }), + cancelledBySupersede(2), + ]; + + expect(verdict(results)).to.equal('SUPERSEDED'); + }); + + it('is unchanged when every test finished before the newer run arrived', () => { + // Nothing was still queued, so nothing was cancelled and no marker was + // written: the verdict is whatever it would have been. + expect( + verdict([row(), row({ id: 2, status: 'FAILED' })]), + ).to.equal('FAILED'); + }); + }); +}); From 85065e10e7eda0c51c24ba181d3aa390047f3002 Mon Sep 17 00:00:00 2001 From: "dcd-cli-release-please[bot]" <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:30:34 +0100 Subject: [PATCH 2/9] chore(dev): release 5.5.0-beta.5 (#165) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> --- .release-please-manifest-beta.json | 2 +- CHANGELOG-beta.md | 7 +++++++ package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json index 52d1780..ce9a654 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.5.0-beta.4" + ".": "5.5.0-beta.5" } diff --git a/CHANGELOG-beta.md b/CHANGELOG-beta.md index 311eab8..c926b79 100644 --- a/CHANGELOG-beta.md +++ b/CHANGELOG-beta.md @@ -1,5 +1,12 @@ # Changelog +## [5.5.0-beta.5](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.4...v5.5.0-beta.5) (2026-09-17) + + +### Features + +* **cloud:** add --cancel-previous to supersede the previous CI run ([#164](https://github.com/devicecloud-dev/dcd-cli/issues/164)) ([169036d](https://github.com/devicecloud-dev/dcd-cli/commit/169036deef348b5955ca2c2a70169c876829eca3)) + ## [5.5.0-beta.4](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.3...v5.5.0-beta.4) (2026-09-14) diff --git a/package.json b/package.json index bc46aa1..275d999 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test:unit": "node scripts/test-runner.mjs --unit", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.5.0-beta.4", + "version": "5.5.0-beta.5", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" }, From a781e4c87e96100e83309b5b68785aa1a8ca4e15 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:15:04 +0100 Subject: [PATCH 3/9] fix(cloud): drop the stale result id from the superseded-by link (#166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link was built from this run's console URL with only the upload id swapped, so it kept `&result=` — a result of the run that was superseded, not of the one that replaced it. Opening it deep-linked the newer upload to a test it does not contain. Found running the feature end to end on dev. Co-authored-by: Claude Opus 5 (1M context) --- src/services/results-polling.service.ts | 23 ++++++++++++++++++++++- test/unit/superseded-run.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/services/results-polling.service.ts b/src/services/results-polling.service.ts index 41b2cf7..5b26839 100644 --- a/src/services/results-polling.service.ts +++ b/src/services/results-polling.service.ts @@ -108,6 +108,25 @@ export function supersedingUploadId(results: unknown[]): string | undefined { return undefined; } +/** + * The superseding run's console link, derived from this run's. + * + * The `result` parameter deep-links a single test, and that id belongs to + * THIS upload — carrying it over would point at a result the newer upload + * does not contain. Swap the upload id and drop it. + */ +export function supersedingConsoleUrl( + consoleUrl: string, + uploadId: string, + supersededBy: string, +): string { + return consoleUrl + .replace(uploadId, supersededBy) + .replace(/&result=[^&]*/, '') + .replace(/\?result=[^&]*&/, '?') + .replace(/\?result=[^&]*$/, ''); +} + export interface PollingResult { consoleUrl: string; status: 'FAILED' | 'PASSED' | 'SUPERSEDED'; @@ -623,7 +642,9 @@ export class ResultsPollingService { ui.fields([ [ 'superseded by', - colors.url(consoleUrl.replace(uploadId, newer)), + colors.url( + supersedingConsoleUrl(consoleUrl, uploadId, newer), + ), ], ]), ), diff --git a/test/unit/superseded-run.test.ts b/test/unit/superseded-run.test.ts index 2c729b4..c2b72e0 100644 --- a/test/unit/superseded-run.test.ts +++ b/test/unit/superseded-run.test.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { isSupersededRow, ResultsPollingService, + supersedingConsoleUrl, supersedingUploadId, } from '../../src/services/results-polling.service.js'; @@ -67,6 +68,30 @@ describe('superseded runs', () => { }); }); + describe('supersedingConsoleUrl', () => { + const base = 'https://dev.console.devicecloud.dev/results?upload=A&result=44376'; + + it('points at the newer upload and drops the old result id', () => { + // 44376 is a result of upload A; carried over it would deep-link B to a + // test that is not in it. + expect(supersedingConsoleUrl(base, 'A', 'B')).to.equal( + 'https://dev.console.devicecloud.dev/results?upload=B', + ); + }); + + it('handles a url with no result param', () => { + expect( + supersedingConsoleUrl('https://c/results?upload=A', 'A', 'B'), + ).to.equal('https://c/results?upload=B'); + }); + + it('handles result appearing first', () => { + expect( + supersedingConsoleUrl('https://c/results?result=1&upload=A', 'A', 'B'), + ).to.equal('https://c/results?upload=B'); + }); + }); + describe('the run verdict', () => { it('is PASSED when every test passed', () => { expect(verdict([row(), row({ id: 2 })])).to.equal('PASSED'); From 18fccc7aeff5c90ab931b90cae9f9c601d764e39 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:46:23 +0100 Subject: [PATCH 4/9] chore: ReleaseAs 5.6.0-beta.1 (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cloud): drop the stale result id from the superseded-by link The link was built from this run's console URL with only the upload id swapped, so it kept `&result=` — a result of the run that was superseded, not of the one that replaced it. Opening it deep-linked the newer upload to a test it does not contain. Found running the feature end to end on dev. Co-Authored-By: Claude Opus 5 (1M context) * chore(dev): release 5.5.0-beta.6 * chore: ReleaseAs 5.6.0-beta.1 * ReleaseAs 5.6.0-beta.1 * ReleaseAs 5.6.0-beta.1 --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> --- .release-please-manifest-beta.json | 2 +- CHANGELOG-beta.md | 7 +++++++ package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json index ce9a654..87cf6f8 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.5.0-beta.5" + ".": "5.5.0-beta.6" } diff --git a/CHANGELOG-beta.md b/CHANGELOG-beta.md index c926b79..ef45f55 100644 --- a/CHANGELOG-beta.md +++ b/CHANGELOG-beta.md @@ -1,5 +1,12 @@ # Changelog +## [5.5.0-beta.6](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.5...v5.5.0-beta.6) (2026-09-17) + + +### Bug Fixes + +* **cloud:** drop the stale result id from the superseded-by link ([#166](https://github.com/devicecloud-dev/dcd-cli/issues/166)) ([a781e4c](https://github.com/devicecloud-dev/dcd-cli/commit/a781e4c87e96100e83309b5b68785aa1a8ca4e15)) + ## [5.5.0-beta.5](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.4...v5.5.0-beta.5) (2026-09-17) diff --git a/package.json b/package.json index 275d999..0222942 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test:unit": "node scripts/test-runner.mjs --unit", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.5.0-beta.5", + "version": "5.5.0-beta.6", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" }, From a81237082fcada60086f63c8623a0bc7f5ce173f Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:12:53 +0100 Subject: [PATCH 5/9] chore: ReleaseAs: 5.6.0-beta.1 (#169) ReleaseAs: 5.6.0-beta.1 From d089531a50c0c239ce95c136f403b06892afe638 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:29:03 +0100 Subject: [PATCH 6/9] chore: re-anchor the beta line and pin the next beta to 5.6.0-beta.1 (#170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #168 merged release-please's own `chore(dev): release 5.5.0-beta.6` as an ordinary PR, so no v5.5.0-beta.6 tag or release was created. The manifest then pointed at an untagged version, release-please lost its anchor, rescanned the whole history and picked up the stale version pin from #137 — hence #167. Roll manifest, package.json and CHANGELOG-beta.md back to the last real release (v5.5.0-beta.5) and pin the next beta with a real footer. Release-As: 5.6.0-beta.1 --- .release-please-manifest-beta.json | 2 +- CHANGELOG-beta.md | 7 ------- package.json | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json index 87cf6f8..ce9a654 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.5.0-beta.6" + ".": "5.5.0-beta.5" } diff --git a/CHANGELOG-beta.md b/CHANGELOG-beta.md index ef45f55..c926b79 100644 --- a/CHANGELOG-beta.md +++ b/CHANGELOG-beta.md @@ -1,12 +1,5 @@ # Changelog -## [5.5.0-beta.6](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.5...v5.5.0-beta.6) (2026-09-17) - - -### Bug Fixes - -* **cloud:** drop the stale result id from the superseded-by link ([#166](https://github.com/devicecloud-dev/dcd-cli/issues/166)) ([a781e4c](https://github.com/devicecloud-dev/dcd-cli/commit/a781e4c87e96100e83309b5b68785aa1a8ca4e15)) - ## [5.5.0-beta.5](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.4...v5.5.0-beta.5) (2026-09-17) diff --git a/package.json b/package.json index 0222942..275d999 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test:unit": "node scripts/test-runner.mjs --unit", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.5.0-beta.6", + "version": "5.5.0-beta.5", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" }, From 2b05a1ec198108169635d5ed41ae33fe9b9d08a0 Mon Sep 17 00:00:00 2001 From: "dcd-cli-release-please[bot]" <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:35:22 +0100 Subject: [PATCH 7/9] chore(dev): release 5.6.0-beta.1 (#171) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> --- .release-please-manifest-beta.json | 2 +- CHANGELOG-beta.md | 12 ++++++++++++ package.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json index ce9a654..a52ae01 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.5.0-beta.5" + ".": "5.6.0-beta.1" } diff --git a/CHANGELOG-beta.md b/CHANGELOG-beta.md index c926b79..4161d22 100644 --- a/CHANGELOG-beta.md +++ b/CHANGELOG-beta.md @@ -1,5 +1,17 @@ # Changelog +## [5.6.0-beta.1](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.5...v5.6.0-beta.1) (2026-09-17) + + +### Bug Fixes + +* **cloud:** drop the stale result id from the superseded-by link ([#166](https://github.com/devicecloud-dev/dcd-cli/issues/166)) ([a781e4c](https://github.com/devicecloud-dev/dcd-cli/commit/a781e4c87e96100e83309b5b68785aa1a8ca4e15)) + + +### Miscellaneous + +* re-anchor the beta line and pin the next beta to 5.6.0-beta.1 ([#170](https://github.com/devicecloud-dev/dcd-cli/issues/170)) ([d089531](https://github.com/devicecloud-dev/dcd-cli/commit/d089531a50c0c239ce95c136f403b06892afe638)) + ## [5.5.0-beta.5](https://github.com/devicecloud-dev/dcd-cli/compare/v5.5.0-beta.4...v5.5.0-beta.5) (2026-09-17) diff --git a/package.json b/package.json index 275d999..a290de6 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test:unit": "node scripts/test-runner.mjs --unit", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.5.0-beta.5", + "version": "5.6.0-beta.1", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" }, From 8a8cc82fce2651d3b50ee417b086bf7949f6ac7c Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:13:24 +0100 Subject: [PATCH 8/9] ci: publish to npm via trusted publishing (#172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: publish to npm via trusted publishing npm already has this repo + npm-publish.yml registered as the trusted publisher, but nothing in CI asked for an OIDC claim, so publishes still used NPM_TOKEN — which expired 90 days after it was last set and failed 5.6.0-beta.1 with a 404 on PUT. Three things were missing, not one: - `id-token: write` on the publish job, and again on the two jobs that call it: a reusable workflow cannot hold a permission its caller lacks, and release-please.yml grants only contents/pull-requests. - npm >= 11.5.1 to do the exchange. Node 22 ships npm 10.9, so the publish job moves to Node 24 (npm 11.19). - `npm publish` rather than `pnpm publish`: pnpm only gained the exchange in v11 and this repo pins 10.17. pnpm still installs and builds. NPM_TOKEN is now unused and can be deleted once a release has gone out this way. Co-Authored-By: Claude Opus 5 (1M context) * fix: point repository.url at the source repo It was https://devicecloud.dev — the marketing site, not a git remote. Trusted publishing auto-enables provenance (npm's oidc.js sets it whenever the provenance config is at its default, the OIDC claim says the repo is public and the package is public — all true here), and the registry checks the generated provenance, which names GITHUB_SERVER_URL/GITHUB_REPOSITORY, against this field. A mismatch is a 422 at publish time; a dry run never sends provenance, so it would not have shown up until the real upload. homepage keeps pointing at devicecloud.dev. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/npm-publish.yml | 30 +++++++++++++++++++++------- .github/workflows/release-please.yml | 14 +++++++++++++ package.json | 2 +- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index feed479..8e1eef7 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -23,6 +23,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + # Trusted publishing: npm is configured with this repo + this workflow as + # the publisher for @devicecloud.dev/dcd, and mints a short-lived token + # from the OIDC claim instead of a long-lived NPM_TOKEN. Without this + # permission the runner cannot request the claim at all, and npm falls + # back to the token -- which is what expired on 2026-09-17 and failed the + # publish with a 404 on PUT. + id-token: write steps: - uses: actions/checkout@v7 # Setup .npmrc file to publish to npm @@ -31,9 +38,13 @@ jobs: with: run_install: false + # Node 24 for its bundled npm 11: trusted publishing needs npm >= 11.5.1, + # and Node 22 ships npm 10.9. This is the publish job only -- what the CLI + # itself supports at runtime is set by tsconfig, not by the Node that + # builds it. - uses: actions/setup-node@v7 with: - node-version: '22.x' + node-version: '24.x' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' cache-dependency-path: './pnpm-lock.yaml' @@ -74,14 +85,19 @@ jobs: fi echo "Version $VERSION is valid for beta release" + # `npm publish`, not `pnpm publish`: pnpm only learned the OIDC exchange + # in v11, and this repo pins pnpm 10.17 in packageManager. pnpm still does + # the install and the build above; only the upload differs. Safe here + # because this is a single package with no workspace: deps -- npm packs + # the same `files` list. + # + # No NODE_AUTH_TOKEN on either step: its presence would take precedence + # over the OIDC token and put us straight back on the expiring-secret + # path. - name: Publish Production Version if: ${{ inputs.release_type == 'prod' }} - run: pnpm publish --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish - name: Publish Beta Version if: ${{ inputs.release_type == 'beta' }} - run: pnpm publish --tag beta --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --tag beta diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 3913334..45f4d84 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -76,6 +76,13 @@ jobs: manifest-file: .release-please-manifest-beta.json publish-npm-prod: + # Must be granted here too: a reusable workflow can never hold a + # permission its caller does not, and this workflow's top-level + # block has none. Without it npm-publish's own id-token: write is + # silently dropped and trusted publishing falls back to a token. + permissions: + contents: read + id-token: write needs: release-please-prod if: needs.release-please-prod.outputs.release_created == 'true' uses: ./.github/workflows/npm-publish.yml @@ -92,6 +99,13 @@ jobs: secrets: inherit publish-npm-beta: + # Must be granted here too: a reusable workflow can never hold a + # permission its caller does not, and this workflow's top-level + # block has none. Without it npm-publish's own id-token: write is + # silently dropped and trusted publishing falls back to a token. + permissions: + contents: read + id-token: write needs: release-please-beta if: needs.release-please-beta.outputs.release_created == 'true' uses: ./.github/workflows/npm-publish.yml diff --git a/package.json b/package.json index a290de6..d4b42bf 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "repository": { "type": "git", - "url": "https://devicecloud.dev" + "url": "git+https://github.com/devicecloud-dev/dcd-cli.git" }, "scripts": { "dcd": "tsx src/index.ts", From 80eafc671fdbf87e5efb3416d48b300b16b71bb4 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:23:13 +0100 Subject: [PATCH 9/9] chore: merge production into dev (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: seed 5.0.0 production release History was squashed into the initial commit of this repo, so release-please has no user-facing commits to build a changelog from. This forces the first stable release of the 5.x line off the current dev tip and resets the prod manifest baseline to the prior npm `latest` (4.4.9) so Release-As produces a clean forward 5.0.0 instead of a no-op. Release-As: 5.0.0 * chore: promote v5 (#56) * fix(upgrade): compare prerelease versions per SemVer `isOutdated` stripped the prerelease suffix before comparing, so beta-to-beta bumps like 5.0.0-beta.0 -> 5.0.0-beta.1 both collapsed to [5,0,0], compared equal, and `dcd upgrade` reported "Already on the latest version". Same nudge in cloud.ts was affected. Replace the naive major.minor.patch compare with a SemVer 2.0.0 `compareSemver` helper that handles prerelease precedence (a prerelease ranks below its final release; identifiers compare dot-by-dot, numeric numerically and below alphanumeric). Add unit coverage for the regression and related cases. * fix: suppress refresh countdown in quiet mode When --quiet is passed (geared at CI), the live results footer no longer renders the "next refresh in Ns" / "refreshing…" countdown. The realtime connection indicator is still shown. * feat(cloud): warn on deprecated iOS 16 (removal 2026-08-23) * feat(cloud): drop legacy Maestro removed-versions block; soft-warn on deprecated 1.39.5/1.41.0 * fix(installer): make beta opt-in, add stable/beta channels The install scripts resolved the version from /latest.json, which (until a stable release exists) synthesized the newest prerelease — so the default `curl … | sh` was silently installing betas. Pair the proxy's new channel support (get.devicecloud.dev now serves stable on /latest.json and prereleases on ?channel=beta) with explicit opt-ins: - DCD_BETA — request the beta channel (latest prerelease). - DCD_VERSION — already pins an exact version; documented for rollback. - Default (no opt-in) installs the latest *stable* only. When no stable release exists yet, the installer errors with guidance pointing at DCD_BETA / DCD_VERSION instead of falling back to a beta. The manifest fetch is separated from parsing so a transient network/proxy failure (curl -f non-zero) is reported differently from a channel that has no release yet (HTTP 200 with "version": null). * chore: add open-source contribution governance Scaffolding to open dcd-cli to external contributors: - LICENSE (MIT), CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, CLA templates - CODEOWNERS, PR template, issue forms + config, dependabot, .editorconfig - pr-title-lint workflow: Conventional Commits on PR title (squash-merge model, types kept in sync with release-please changelog-sections) - cla workflow: CLA Assistant Lite - release-please: use a GitHub App token (falls back to GITHUB_TOKEN until the App secrets exist) so Release PRs trigger required checks under branch protection - cli-ci: also run on production so the dev->production promotion PR is gated * chore: drop CODEOWNERS Low value for a small maintainer team where anyone can review anything; the branch ruleset's approval requirement covers review without it. * fix(ci): keep dependabot and fork PRs green (#46) * fix(ci): keep dependabot and fork PRs green Dependabot/fork PRs run without repo secrets, so three jobs failed on them: - lint-and-test: HAS_PRIVATE_ACCESS was true for dependabot (same-repo head), so it tried to clone the private mock-api with an empty DCD_SSH_DEPLOY_KEY. Now excludes dependabot[bot], same as forks (skips mock-api + integration). - claude-code-review: skips dependabot/fork PRs (no CLAUDE_CODE_OAUTH_TOKEN). - cla: skips its action step until PERSONAL_ACCESS_TOKEN is configured so the check is green instead of 'Branch cla-signatures not found'; also fixes two invalid input names (custom-*-prompt -> custom-*-prcomment). * ci: group all github-actions bumps into one weekly PR Wildcard pattern so major action bumps join the group too, instead of one PR per action. * ci: power CLA via the shared automation GitHub App (#49) * ci: power CLA via the shared automation GitHub App Mint the CLA token from the same GitHub App release-please uses, instead of a personal PAT (no expiry, signature commits show as the bot). Rename the App secrets RELEASE_PLEASE_APP_* -> BOT_APP_* since one App now serves both workflows. CLA self-skips until BOT_APP_ID is set. Carries only the app-token delta — the dependabot/fork CI fixes and actions grouping already landed on dev via #46. * ci: allowlist internal maintainers (riglar, finalerock44) in CLA * docs: set legal entity to Moropo Ltd t/a DeviceCloud (#50) Fill the CLA party placeholder and the LICENSE/README copyright holder with the registered entity. CLA still pending legal review. * ci: bump the actions group across 1 directory with 6 updates (#47) Bumps the actions group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `2` | `3` | | [actions/checkout](https://github.com/actions/checkout) | `4` | `7` | | [pnpm/action-setup](https://github.com/pnpm/action-setup) | `4` | `6` | | [actions/setup-node](https://github.com/actions/setup-node) | `5` | `6` | | [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) | `5` | `6` | | [googleapis/release-please-action](https://github.com/googleapis/release-please-action) | `4` | `5` | Updates `actions/create-github-app-token` from 2 to 3 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3) Updates `actions/checkout` from 4 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) Updates `pnpm/action-setup` from 4 to 6 - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v4...v6) Updates `actions/setup-node` from 5 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) Updates `amannn/action-semantic-pull-request` from 5 to 6 - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v5...v6) Updates `googleapis/release-please-action` from 4 to 5 - [Release notes](https://github.com/googleapis/release-please-action/releases) - [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/release-please-action/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/create-github-app-token dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: amannn/action-semantic-pull-request dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: googleapis/release-please-action dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pnpm/action-setup dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: finalerock44 <77282157+finalerock44@users.noreply.github.com> * fix: v5 release blockers — installer, binary version, repeated flags,… (#51) fix: v5 release blockers — installer, binary version, repeated flags, upgrade, CI output - install.ps1: fix PS 5.1 parse error (`$asset:` -> `${asset}`) that made `irm | iex` a no-op on stock Windows; decode the octet-stream SHA256SUMS (Byte[] under -UseBasicParsing) to text before splitting. - build/version: stamp the version into the bun-compiled binary via `bun --define __DCD_CLI_VERSION__` (the compiled binary can't read package.json), so `dcd --version` no longer reports 0.0.0. npm/tsx path still falls back to reading package.json. Adds src/global.d.ts. - cloud: collect repeated `-e/--env`, `-m/--metadata`, `--include-tags`, `--exclude-tags`, `--exclude-flows` from rawArgs (citty/parseArgs kept only the last occurrence, silently dropping earlier values); echo the collected values too. - upgrade: query the beta channel for prerelease installs and distinguish "no newer release on this channel" from a real network failure, replacing the misleading "Could not reach the update manifest" error during the beta. - progress/polling: make the realtime status indicator TTY-aware — in non-interactive/CI output, print one line per state change instead of flooding logs with a per-frame spinner (not suppressed by --quiet/--json-file). - methods: downgrade primary-Backblaze-upload failure warnings to debug-only; the Supabase fallback recovers and validateUploadResults raises the only user-facing error (when every strategy fails). - list/status: build console links from the env the CLI targets (resolveFrontendUrl) instead of the API's hardcoded-prod consoleUrl. - cloud: validate a local --app-file exists during --dry-run. * chore(dev): release 5.0.0-beta.2 (#36) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * fix: stop CLA locking release PRs (breaks release pipeline) (#52) The CLA Assistant action defaults lock-pullrequest-aftermerge=true, so merging a release-please PR locked it; release-please then failed trying to comment on the locked PR, killing the Release job before npm publish + binary upload ran (seen on v5.0.0-beta.2). Set lock-pullrequest-aftermerge=false. Also skip release-please PRs in claude-code-review (version bumps — nothing to review, and it must never block a release). * feat(live): add a beta warning to `dcd live start` (#54) Prints a beta notice (billed at $0.03/min, contact support to enroll) before starting a session. The API's new enrollment gate returns a 403 whose "contact support" message the CLI already surfaces verbatim on a non-enrolled org. * chore(dev): release 5.0.0-beta.3 (#53) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Tom Riglar Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore(production): release 5.0.0 (#33) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.1.0 (#65) Promote the DB-driven notices feature (CLI render via ui, console banner, API block enforcement, CI identity forwarding) from dev to stable. Release-As: 5.1.0 * chore(production): release 5.1.0 (#68) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.1.1 (#73) Promote from dev to stable: * fix: recover cleanly when the stored session is dead (#72) — `dcd login` now mints a dedicated Supabase session for the CLI rather than reusing the browser's refresh-token family, and a dead stored session recovers instead of hard-failing. * deps: bump the minor-and-patch group (9 updates) (#71) and eslint-plugin-unicorn 68 -> 69 (#70). Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.1.1 * chore: release 5.2.0 (#83) Promote from dev to stable: * feat(cloud): upload-level device matrix (#1105) — one `dcd cloud` upload carries N device configs via repeatable `--ios-device-matrix :` / `--android-device-matrix :[:play]`, fanning out into one result row per (flow × config). Each flag names exactly one validated cell; there is no cross-product. Sequential flows form N independent depends_on chains, one per device. Adds a pre-submit cell-count + cost preview and a `device` object on each `--json` `tests[]` entry. * fix(cloud): refuse a device matrix on an API that cannot honour it — an older API silently strips the unknown field and runs one device, exiting 0; the CLI now fails loudly instead of under-testing in silence. * test: run the integration suite via execFile argv rather than a shell, clearing the whole js/shell-command-injection-from-environment class. REQUIRES the dcd API carrying #1105 to be on production first. Without it the matrix flags cannot be honoured (the CLI refuses, by design). Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.2.0 * chore(production): release 5.2.0 (#74) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.3.0 (#108) Promote from dev to stable: * feat: client-side envelope encryption of app binaries, flow zips and env vars (#94, #101) — opt-in via `--encrypt` / `DCD_ENCRYPT_BINARIES=1` and off by default, so uploads stay byte-identical unless asked for. Per-upload DEK, chunked AES-256-GCM container, X25519 sealed-box DEK wrap; encrypted binaries dedup on the plaintext hash so re-uploads still hit the cache. * feat(artifacts): prefer server-assembled bundle delivery for downloads (#93) — falls back to the inline endpoint on 501, so it degrades cleanly against an API that has not shipped bundles. * feat(device): add Android API level 37 (Android 17) (#107) — the flag enum accepts 37, but the device/API-level pair is validated against the compatibility matrix the *target* API serves, and production still tops out at 36, so 37 is refused client-side until the platform gate flips. * refactor(cloud): remove the enterprise-only --mitmHost / --mitmPath flags (#102). The submitted config payload for runs that never passed them is byte-identical. * fix(deps) / deps: clear every outstanding pnpm audit advisory (#89, #92, #95, #100, #106), bump chalk 5 -> 6, and regenerate the schema types from the current API swagger (#105). No platform prerequisite this time: the envelope decrypt half (dcd api + simulators) is already on production with both env KEK public keys pinned, bundle delivery has a 501 fallback, and API 37 is gated server-side. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.3.0 * chore(production): release 5.3.0 (#109) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.3.1 (#119) Promote from dev to stable: * fix(cloud): exclude config-shaped files from flow discovery (#114, closes dcd-cli#99) — a `config.yaml` sitting in a flows folder was picked up as a flow and blew up `processDependencies` with "Expected an array of steps". Detection is by shape, not filename, so several named configs can coexist in one folder. A flow merely *missing* its `---` separator still errors loudly rather than being silently dropped. * fix(cloud): reject malformed executionOrder instead of silently running in parallel (#117, closes dcd-cli#110) — the config was `yaml.load`ed and straight-cast, so an `executionOrder` in the wrong shape was ignored and every flow ran in parallel. A zod schema (`src/services/workspace-config.schema.ts`) is now the single source of truth, with `IWorkspaceConfig` inferred from it so the compile-time and runtime views cannot drift. * deps: bump the minor-and-patch group with 5 updates (#112), plus eslint-plugin-unicorn 72 -> 73 and pnpm/action-setup 6.0.9 -> 6.0.10. Behaviour changes users will notice: * A malformed `executionOrder` is now a hard error with a targeted message showing found-vs-expected. Anyone whose config was mis-shaped has been running flows in parallel without knowing; after this they get a clear failure instead. That is the point of the fix, but it is the one change that can turn a previously-green pipeline red. * Unrecognised top-level config keys emit a warning (with a did-you-mean for near-misses). Keys are preserved, not stripped — the config is forwarded to the API as `fields.workspaceConfig`, so stripping would silently alter the payload. * Config warnings go to stderr, so `--json` stdout stays parseable and the MCP server's JSON-RPC stdout channel stays clean. No platform prerequisite: both fixes are client-side (flow discovery and config validation). The submitted payload for an already-valid config is unchanged. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.3.1 * chore: promote the grouped deps bump into 5.3.1 (#123) * chore: promote the grouped deps bump into 5.3.1 Promote from dev to stable, picking up the one change that landed after the 5.3.1 promotion (#119) and so missed the pending release PR (#120): * deps: bump the minor-and-patch group with 6 updates (#121) — @supabase/supabase-js 2.112.2 -> 2.112.3, js-yaml 5.2.3 -> 5.3.0, @types/node 26.1.2 -> 26.2.0, eslint 10.8.0 -> 10.8.1, tsx 4.23.11 -> 4.23.12, typescript-eslint 8.66.0 -> 8.67.0. Carries only `pnpm-lock.yaml`. Dependabot left `package.json` untouched — every range already admitted the new versions — so this changes nothing for npm consumers, who resolve from those ranges. It matters only for the bun-compiled standalone binaries, which bake in the locked versions. `Release-As: 5.3.1` keeps the pending release PR (#120) on 5.3.1 rather than rolling it to 5.3.2. Release-please regenerates the 5.3.1 section from this commit's bullet alongside #119's three, so the changelog ends up carrying all four entries. Verified locally, because CI cannot check this branch: `pnpm install --frozen-lockfile` (the lockfile agrees with production's package.json), `pnpm lint` (0 errors, the same 32 pre-existing warnings — the eslint 10.8.1 and typescript-eslint 8.67.0 bumps add no findings), `pnpm typecheck`, `pnpm build`, and `pnpm audit --audit-level moderate` (no known vulnerabilities). The integration tests did NOT run: dcd#1036 deleted `mock-api/`, which is also what fails CI's `lint-and-test`, at a step that runs before the linter. Release-As: 5.3.1 * ci: stop reaching into the private dcd repo for the mock-api `lint-and-test` has failed on every same-repo PR since dcd#1036 deleted `mock-api/` from the private devicecloud-dev/dcd repo this morning. CI checked that directory out over an SSH deploy key and ran `pnpm install` in it; the sparse-checkout now matches nothing, so the job dies at that step — before the linter — and takes #120, #122 and #123 down with it. Rather than re-point at a mock, this removes the linkage. dcd-cli is PUBLIC and was holding `DCD_SSH_DEPLOY_KEY`, a credential granting read access to the private repo, and pulling the API's `swagger.json` onto the runner on every same-repo PR. Deleting the checkout drops both. * The `Checkout dcd (mock-api)` and `Install Mock API dependencies` steps are gone, along with the `HAS_PRIVATE_ACCESS` gate that existed only to keep them off fork and Dependabot PRs. Every PR now takes the same path, so forks stop being second-class. * CI runs `pnpm test:unit` — a new script that is the existing runner with `--unit`. `test/unit/*` is pure and needs no backend, so unit coverage is kept rather than dropped along with the integration suite. * `scripts/test-runner.mjs` no longer defaults `MOCK_API_DIR` to `../../dcd/mock-api`. With no mock available it degrades to the unit suite and says so, instead of the bare ENOENT it throws today. Set `MOCK_API_DIR` and the integration specs run exactly as before. `DCD_SSH_DEPLOY_KEY` can now be deleted from the repo's secrets — nothing reads it. That is a separate manual step, not something this commit can do. Two things are genuinely lost, both worth stating plainly rather than discovering later: * `test/integration/*` no longer runs anywhere automatically. * With it goes the CLI<->swagger contract-drift check. Drift used to surface as a Prism 422 — that is how the `googlePlay` multipart break and the `tempPath` missing-example break were both caught. Nothing replaces it yet. Verified locally: `pnpm test:unit` and a bare `pnpm test` both run the unit suite only and print the notice; 81 pass and the 7 `flow-paths` failures are Windows-only, asserting POSIX paths against win32 `path`. The same specs ran green on ubuntu in the last full CI run (job 94750122384, 2026-08-14), which is the platform CI uses. `pnpm lint`, `pnpm typecheck`, `pnpm build` and `pnpm audit --audit-level moderate` are all clean. * chore: promote the #124 docs into 5.3.1 (#125) Promote from dev to stable, carrying the documentation half of #124 that the 5.3.1 promotions dropped. `ci: stop reaching into the private dcd repo for the mock-api` (#124) touched six files on dev. The promote PR (#123) carried four of them — `cli-ci.yml`, `scripts/test-runner.mjs`, the `test:unit` script in `package.json`, and eight of the ten changed `CLAUDE.md` lines — and dropped `CONTRIBUTING.md` and `README.md` entirely. Production therefore ships the new CI shape while its docs still describe the old one: * `CONTRIBUTING.md` told contributors `pnpm test` boots a mock API, and that integration tests are "automatically skipped" on fork PRs only. Neither is true: CI runs no integration tests on any PR, and there is no default mock. It also omitted `pnpm test:unit` from the pre-push checklist, though it is now a required check. * `README.md`'s dev-scripts block still showed `pnpm test # build + boot mock API + integration/unit tests`. * `CLAUDE.md`'s Contributing section still described the `DCD_SSH_DEPLOY_KEY` mock-api checkout, which no longer exists. `README.md` is the one with reach beyond this repo: npm always includes it in the tarball regardless of the `files` field, so the stale snippet would render on the npmjs.com page for 5.3.1. Docs only — no source, workflow, script or lockfile change. The three files are now byte-identical to `dev`, leaving release plumbing (both manifests, `CHANGELOG.md`, `package.json` version) as the only remaining divergence, which is release-please's to own. `Release-As: 5.3.1` keeps the pending release PR (#120) on 5.3.1 rather than rolling it to 5.3.2. Release-As: 5.3.1 * chore(production): release 5.3.1 (#120) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.4.0 (#128) Promote from dev to stable: * feat: remove iOS 16 (#126) — the platform removed iOS 16 on 2026-08-24, so `--ios-version 16` now fails against the API regardless of CLI version. Dropping '16' from `EiOSVersions` moves that rejection client-side with a better message ("Expected one of: 18, 17, 26") instead of a round trip to a 400. `--help` follows automatically, since the option list is derived from the enum. Defaults were already iphone-14 / 17 and are unaffected — iPhone 14 keeps iOS 17 and 18. `src/types/generated/schema.types.ts` is regenerated against the post-removal API swagger; alongside narrowing `iOSVersion` it picks up the API-side drift that had accumulated since the artifact was last built on 2026-08-06. No platform prerequisite in the other direction: the API is already serving the reduced matrix, verified end-to-end on dev (a 5.3.1 CLI targeting iOS 16 is refused by the fetched compatibility data, and a per-flow override reaches the server-side gate). `Release-As: 5.4.0` because the promotion commit is a `chore:` and would otherwise cut no release at all. It is 5.4.0 rather than 6.0.0 deliberately: #126 carried a `!` in its PR title, which is what put the beta line on a 6.0.0-beta.2 release PR before #127 re-pinned it. Removing a version the platform no longer serves takes away nothing that still worked, so it is not a major. The `!` does not reach production anyway — promotions are squash-merged, so it lives only in dev's history. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.4.0 * chore(production): release 5.4.0 (#129) Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> * chore: release 5.4.1 (#145) Promote from dev to stable: iOS 27 in the `--ios-version` enum (#131/#134 — the iPhone 17 devices were already dropped), bplist-parser 0.5 named exports (#143), the CLA action fork (#135/#136) and the grouped dependency bumps. `Release-As: 5.4.1` because the promotion commit is a `chore:` and would otherwise cut no release at all. 5.4.1 rather than the 5.5.0 the iOS 27 `feat:` would roll, so the stable line matches the beta line already published as 5.4.1-beta.1. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.4.1 * chore: promote the notices targeting change into 5.4.1 Second promotion commit for 5.4.1: #145 carried dev as of this morning, and the notices change (#147) landed on dev after it. Source delta only; package.json version, CHANGELOG.md and the release-please manifests stay as production has them so release-please re-renders #146 with this entry added. * feat(notices): expose platform, device and Maestro version to notice targeting; include notices in --json output (#147) Release-As: 5.4.1 * chore(production): release 5.4.1 * chore: pin the 5.5.0 promotion package.json tracks production's stable line from here; release-please bumps it to 5.5.0 when the Release PR lands. Release-As: 5.5.0 * chore(production): release 5.5.0 --------- Signed-off-by: dependabot[bot] Co-authored-by: Tom Riglar Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: dcd-cli-release-please[bot] <296541543+dcd-cli-release-please[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 32dbe0e..9bf3852 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "5.0.0" + ".": "5.5.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index e01424f..c476b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [5.5.0](https://github.com/devicecloud-dev/dcd-cli/compare/v5.4.1...v5.5.0) (2026-09-15) + + +### Features + +* add Pixel 8, Pixel 10 family and Pixel 11 device slugs ([#151](https://github.com/devicecloud-dev/dcd-cli/issues/151)) ([dac5d1e](https://github.com/devicecloud-dev/dcd-cli/commit/dac5d1ec88c3488802e244338684cfc0f1e2d50f)) +* **cloud:** add --render-engine for the emulator software renderer ([#153](https://github.com/devicecloud-dev/dcd-cli/issues/153)) ([a2888fd](https://github.com/devicecloud-dev/dcd-cli/commit/a2888fd0154ce637a3a7fd02dc04431114df99e0)) + + +### Bug Fixes + +* register tsx's ESM loader only so mocha 12.0.1 can load find-up@8 ([#157](https://github.com/devicecloud-dev/dcd-cli/issues/157)) ([004b478](https://github.com/devicecloud-dev/dcd-cli/commit/004b47838983a6e1c6caca060e7c43f85a737f4a)) + + +### Dependencies + +* bump the minor-and-patch group with 7 updates ([#156](https://github.com/devicecloud-dev/dcd-cli/issues/156)) ([1e6e2c0](https://github.com/devicecloud-dev/dcd-cli/commit/1e6e2c087817218d9fc8da69a95089253b779d39)) + + +### Miscellaneous + +* pin the 5.5.0 promotion ([ea27543](https://github.com/devicecloud-dev/dcd-cli/commit/ea275430ad5e8369a83a07b51fd9c1ec0addd3d1)) + ## [5.4.1](https://github.com/devicecloud-dev/dcd-cli/compare/v5.4.0...v5.4.1) (2026-09-10)