From 6c533e7ae9e55b64e5ffe63a9c9a6934e9250938 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Wed, 24 Jun 2026 09:04:09 +0100 Subject: [PATCH 01/16] 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. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/services/version.service.ts | 79 +++++++++++++++++++++++-------- test/unit/version.service.test.ts | 35 ++++++++++++++ 2 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 test/unit/version.service.test.ts diff --git a/src/services/version.service.ts b/src/services/version.service.ts index ceda057..953a4b9 100644 --- a/src/services/version.service.ts +++ b/src/services/version.service.ts @@ -3,6 +3,58 @@ import { CompatibilityData } from '../utils/compatibility.js'; const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json'; const MANIFEST_TIMEOUT_MS = 3000; +/** + * Compare two semantic versions per SemVer 2.0.0 precedence rules. + * Returns a negative number if `a < b`, positive if `a > b`, and 0 if equal. + * + * Implements the prerelease rules that the previous naive comparator dropped: + * - A version WITH a prerelease has lower precedence than the same version + * without one ("1.0.0-beta" < "1.0.0"). + * - Prerelease identifiers are compared dot-separated, left to right: + * numeric identifiers compare numerically, alphanumeric ones compare + * lexically (ASCII), and numeric always sorts below alphanumeric. A longer + * set of identifiers wins when all preceding ones are equal. + */ +function compareSemver(a: string, b: string): number { + const split = (v: string): { release: number[]; pre: string[] } => { + const [core, ...preParts] = v.trim().replace(/^v/, '').split('-'); + const nums = core.split('.').map((n) => Number(n) || 0); + const pre = preParts.join('-'); + return { + release: [nums[0] || 0, nums[1] || 0, nums[2] || 0], + pre: pre ? pre.split('.') : [], + }; + }; + + const left = split(a); + const right = split(b); + + for (let i = 0; i < 3; i++) { + if (left.release[i] !== right.release[i]) { + return left.release[i] - right.release[i]; + } + } + + // Equal release: a version with no prerelease outranks one that has it. + if (left.pre.length === 0 && right.pre.length === 0) return 0; + if (left.pre.length === 0) return 1; + if (right.pre.length === 0) return -1; + + const len = Math.min(left.pre.length, right.pre.length); + for (let i = 0; i < len; i++) { + const lp = left.pre[i]; + const rp = right.pre[i]; + if (lp === rp) continue; + const ln = /^\d+$/.test(lp); + const rn = /^\d+$/.test(rp); + if (ln && rn) return Number(lp) - Number(rp); + if (ln) return -1; // numeric identifiers sort below alphanumeric + if (rn) return 1; + return lp < rp ? -1 : 1; + } + return left.pre.length - right.pre.length; +} + /** * Service for handling version validation and checking */ @@ -29,28 +81,15 @@ export class VersionService { } /** - * Compare two semantic version strings - * @param current - Current version - * @param latest - Latest version - * @returns true if current is older than latest + * Compare two semantic version strings (SemVer 2.0.0 precedence, including + * prerelease tags). Returns true if `current` is strictly older than `latest`. + * + * Prerelease handling matters here: a beta-to-beta bump such as + * "5.0.0-beta.0" -> "5.0.0-beta.1" shares the same major.minor.patch, so we + * must compare the prerelease identifiers to detect that an upgrade exists. */ isOutdated(current: string, latest: string): boolean { - // Strip any prerelease suffix ("1.2.3-beta.1" -> "1.2.3") and default - // missing segments to 0 so short/prerelease versions still compare. - const parts = (version: string): number[] => { - const nums = version.split('-')[0].split('.').map(Number); - return [nums[0] || 0, nums[1] || 0, nums[2] || 0]; - }; - - const currentParts = parts(current); - const latestParts = parts(latest); - - for (let i = 0; i < 3; i++) { - if (currentParts[i] < latestParts[i]) return true; - if (currentParts[i] > latestParts[i]) return false; - } - - return false; + return compareSemver(current, latest) < 0; } /** diff --git a/test/unit/version.service.test.ts b/test/unit/version.service.test.ts new file mode 100644 index 0000000..4ffe6ff --- /dev/null +++ b/test/unit/version.service.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; + +import { VersionService } from '../../src/services/version.service.js'; + +describe('VersionService.isOutdated', () => { + const svc = new VersionService(); + + it('detects a release-level upgrade', () => { + expect(svc.isOutdated('5.0.0', '5.0.1')).to.equal(true); + expect(svc.isOutdated('4.9.0', '5.0.0')).to.equal(true); + expect(svc.isOutdated('5.1.0', '5.0.9')).to.equal(false); + }); + + it('detects a beta-to-beta prerelease upgrade', () => { + // Regression: both reduce to 5.0.0 under a naive major.minor.patch compare. + expect(svc.isOutdated('5.0.0-beta.0', '5.0.0-beta.1')).to.equal(true); + expect(svc.isOutdated('5.0.0-beta.1', '5.0.0-beta.0')).to.equal(false); + expect(svc.isOutdated('5.0.0-beta.10', '5.0.0-beta.2')).to.equal(false); + }); + + it('ranks a prerelease below its final release', () => { + expect(svc.isOutdated('5.0.0-beta.1', '5.0.0')).to.equal(true); + expect(svc.isOutdated('5.0.0', '5.0.0-beta.1')).to.equal(false); + }); + + it('returns false when versions are equal', () => { + expect(svc.isOutdated('5.0.0', '5.0.0')).to.equal(false); + expect(svc.isOutdated('5.0.0-beta.1', '5.0.0-beta.1')).to.equal(false); + }); + + it('tolerates a leading v and short versions', () => { + expect(svc.isOutdated('v5.0.0', 'v5.0.1')).to.equal(true); + expect(svc.isOutdated('5.0', '5.0.1')).to.equal(true); + }); +}); From d543981b0e2dc274d664bf378da4154a77a6d2e8 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Wed, 24 Jun 2026 09:21:43 +0100 Subject: [PATCH 02/16] fix: suppress refresh countdown in quiet mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/services/results-polling.service.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/services/results-polling.service.ts b/src/services/results-polling.service.ts index 9146f69..881af60 100644 --- a/src/services/results-polling.service.ts +++ b/src/services/results-polling.service.ts @@ -166,6 +166,7 @@ export class ResultsPollingService { realtimeEnabled, subscription?.isConnected() ?? false, nextPollAt, + quiet, ); ux.action.status = footer ? `${statusBody}\n${footer}` : statusBody; }; @@ -644,12 +645,13 @@ export class ResultsPollingService { * Build the live footer shown under the status display: whether realtime * updates are connected (for logged-in users) and how long until the next * backstop poll. While a fetch is in flight (`nextPollAt` is null) the - * countdown reads "refreshing…". + * countdown reads "refreshing…". In quiet mode the countdown is omitted. */ private buildStatusFooter( realtimeEnabled: boolean, realtimeConnected: boolean, nextPollAt: null | number, + quiet: boolean, ): string { const parts: string[] = []; @@ -661,11 +663,15 @@ export class ResultsPollingService { ); } - if (nextPollAt === null) { - parts.push(colors.dim('refreshing…')); - } else { - const secondsLeft = Math.max(0, Math.ceil((nextPollAt - Date.now()) / 1000)); - parts.push(colors.dim(`next refresh in ${secondsLeft}s`)); + // The countdown to the next backstop poll is noise in quiet mode (geared at + // CI), so suppress it there while keeping the realtime indicator. + if (!quiet) { + if (nextPollAt === null) { + parts.push(colors.dim('refreshing…')); + } else { + const secondsLeft = Math.max(0, Math.ceil((nextPollAt - Date.now()) / 1000)); + parts.push(colors.dim(`next refresh in ${secondsLeft}s`)); + } } return parts.join(colors.dim(' · ')); From ea62f724653b3e1173036c4abe66aa4e110c0a0e Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:14:47 +0100 Subject: [PATCH 03/16] feat(cloud): warn on deprecated iOS 16 (removal 2026-08-23) --- src/commands/cloud.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index 3476c0d..a8fa847 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -461,6 +461,21 @@ export const cloudCommand = defineCommand({ logger: (m: string) => out(m), }); + // iOS 16 deprecation notice (soft warning during the grace period; + // removed on 23 August 2026). Only fires on an explicit --ios-version 16 — + // when omitted the API defaults to iOS 17, so no false warning. + const DEPRECATED_IOS_VERSIONS = ['16']; + if (iOSVersion && DEPRECATED_IOS_VERSIONS.includes(iOSVersion)) { + warnOut(ui.warn(colors.bold('iOS 16 is deprecated'))); + warnOut( + ui.branch([ + 'iOS 16 will be removed on 23 August 2026; after that, tests targeting it will fail.', + 'Switch to iOS 17 or newer — iPhone 14 also supports 17 and 18.', + `${colors.dim('See:')} ${colors.url('https://docs.devicecloud.dev/getting-started/devices-configuration')}`, + ]), + ); + } + deviceValidationService.validateAndroidDevice( androidApiLevel, androidDevice, From 62c767295cb99339cbc3326c6bf319caf637d649 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:13:25 +0100 Subject: [PATCH 04/16] feat(cloud): drop legacy Maestro removed-versions block; soft-warn on deprecated 1.39.5/1.41.0 --- src/commands/cloud.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index a8fa847..c7bcc00 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -367,11 +367,17 @@ export const cloudCommand = defineCommand({ }, ); - const REMOVED_MAESTRO_VERSIONS = ['1.39.1', '1.39.2', '1.39.7', '2.0.3', '2.4.0']; - if (REMOVED_MAESTRO_VERSIONS.includes(resolvedMaestroVersion)) { - throw new CliError( - `Maestro version ${resolvedMaestroVersion} is no longer supported. ` + - `Please upgrade to a newer version. See: https://docs.devicecloud.dev/configuration/maestro-versions`, + // Soft deprecation notice for Maestro versions slated for removal on + // 26 June 2026. Non-fatal — these still run during the grace period. + const DEPRECATED_MAESTRO_VERSIONS = ['1.39.5', '1.41.0']; + if (DEPRECATED_MAESTRO_VERSIONS.includes(resolvedMaestroVersion)) { + warnOut(ui.warn(colors.bold(`Maestro ${resolvedMaestroVersion} is deprecated`))); + warnOut( + ui.branch([ + `Maestro ${resolvedMaestroVersion} will be removed on 26 June 2026; after that, tests pinned to it will fail.`, + 'Upgrade to Maestro 2.6.0 or above.', + `${colors.dim('See:')} ${colors.url('https://docs.devicecloud.dev/configuration/maestro-versions')}`, + ]), ); } From ec16bccd044f892f7fd1997aac977c77aa14376d Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Wed, 24 Jun 2026 12:52:40 +0100 Subject: [PATCH 05/16] fix(installer): make beta opt-in, add stable/beta channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). Co-Authored-By: Claude Opus 4.8 (1M context) --- install.ps1 | 35 +++++++++++++++++++++++++++++++---- install.sh | 45 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/install.ps1 b/install.ps1 index f96cbb4..07a70b7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -4,7 +4,8 @@ # irm https://get.devicecloud.dev/install.ps1 | iex # # Env vars: -# DCD_VERSION Pin a specific version (default: latest) +# DCD_VERSION Pin a specific version, e.g. for rollback (default: latest stable) +# DCD_BETA Set to any value to install the latest beta/prerelease (opt-in) # DCD_INSTALL_DIR Override install location (default: $env:USERPROFILE\.dcd\bin) # DCD_DOWNLOAD_BASE Override the download host (default: https://get.devicecloud.dev) @@ -25,13 +26,39 @@ if ([Environment]::Is64BitOperatingSystem -ne $true) { $asset = 'dcd-windows-x64.exe' # --- resolve version --- +# Precedence: explicit DCD_VERSION pin > DCD_BETA opt-in > latest stable. if ($env:DCD_VERSION) { $version = $env:DCD_VERSION } else { - Write-Host 'Resolving latest version...' - $manifest = Invoke-RestMethod -Uri "$DownloadBase/latest.json" + if ($env:DCD_BETA) { + Write-Host 'Resolving latest beta version...' + $manifestUrl = "$DownloadBase/latest.json?channel=beta" + $channel = 'beta' + } else { + Write-Host 'Resolving latest version...' + $manifestUrl = "$DownloadBase/latest.json" + $channel = 'stable' + } + try { + $manifest = Invoke-RestMethod -Uri $manifestUrl + } catch { + throw "Could not reach $manifestUrl" + } + # A null version means the channel has no release yet (HTTP 200), as opposed + # to a transient failure (which throws above). Stable is the default and beta + # is strictly opt-in, so refuse to silently fall back to a prerelease. $version = $manifest.version - if (-not $version) { throw "Could not resolve latest version from $DownloadBase/latest.json" } + if (-not $version) { + if ($channel -eq 'stable') { + throw @" +No stable dcd release is available yet. + Install the latest beta: `$env:DCD_BETA=1; irm '$DownloadBase/install.ps1' | iex + Or pin a version: `$env:DCD_VERSION='5.0.0-beta.1'; irm '$DownloadBase/install.ps1' | iex +"@ + } else { + throw "No beta release is available yet from $manifestUrl" + } + } } $url = "$DownloadBase/download/$version/$asset" diff --git a/install.sh b/install.sh index ac94e43..4d25dc8 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,8 @@ # curl -fsSL https://get.devicecloud.dev/install.sh | sh # # Env vars: -# DCD_VERSION Pin a specific version (default: latest) +# DCD_VERSION Pin a specific version, e.g. for rollback (default: latest stable) +# DCD_BETA Set to any value to install the latest beta/prerelease (opt-in) # DCD_INSTALL_DIR Override install location (default: $HOME/.dcd/bin) # DCD_DOWNLOAD_BASE Override the download host (default: https://get.devicecloud.dev) # @@ -23,6 +24,17 @@ info() { printf '%s\n' "$1" } +# Stable is the default channel and beta is strictly opt-in, so when no stable +# release exists yet (only prereleases published) we refuse to silently install a +# beta and instead point the user at the two explicit opt-ins. $DOWNLOAD_BASE is +# echoed so a custom host shows the right command. +no_stable_release_err() { + printf 'error: No stable dcd release is available yet.\n' >&2 + printf ' Install the latest beta: curl -fsSL %s/install.sh | DCD_BETA=1 sh\n' "$DOWNLOAD_BASE" >&2 + printf ' Or pin a version: curl -fsSL %s/install.sh | DCD_VERSION=5.0.0-beta.1 sh\n' "$DOWNLOAD_BASE" >&2 + exit 1 +} + # Find a dcd on PATH other than the one we just installed — usually a leftover # `npm install -g @devicecloud.dev/dcd` that can shadow this binary. Runs in a # subshell so the temporary IFS change never leaks back to the caller. @@ -121,17 +133,40 @@ main() { asset="dcd-${os_id}-${arch_id}" # --- resolve version --- + # Precedence: explicit DCD_VERSION pin > DCD_BETA opt-in > latest stable. if [ -n "${DCD_VERSION:-}" ]; then version="$DCD_VERSION" else - info "Resolving latest version..." - # /latest.json returns { "version": "5.1.0", ... } + if [ -n "${DCD_BETA:-}" ]; then + channel=beta + manifest_url="$DOWNLOAD_BASE/latest.json?channel=beta" + info "Resolving latest beta version..." + else + channel=stable + manifest_url="$DOWNLOAD_BASE/latest.json" + info "Resolving latest version..." + fi + + # Fetch the manifest separately from parsing so we can tell a transient + # network/proxy failure (curl -f returns non-zero → empty $manifest) apart + # from a channel that simply has no release yet (HTTP 200 with + # "version": null → $manifest non-empty but $version empty). + manifest=$(curl -fsSL "$manifest_url") || manifest="" + [ -z "$manifest" ] && err "Could not reach $manifest_url" + # /latest.json returns { "version": "5.1.0", ... }; a null version is unquoted + # and so won't match this quoted-string pattern. version=$( - curl -fsSL "$DOWNLOAD_BASE/latest.json" \ + printf '%s' "$manifest" \ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -n1 ) - [ -z "$version" ] && err "Could not resolve latest version from $DOWNLOAD_BASE/latest.json" + if [ -z "$version" ]; then + if [ "$channel" = stable ]; then + no_stable_release_err + else + err "No beta release is available yet from $manifest_url" + fi + fi fi url="$DOWNLOAD_BASE/download/${version}/${asset}" From 77cf138c80e1d39441565fec4c9d6e83dae5fd6c Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:53:55 +0100 Subject: [PATCH 06/16] 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 --- .editorconfig | 15 +++ .github/CODEOWNERS | 16 +++ .github/ISSUE_TEMPLATE/bug_report.yml | 63 +++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 35 +++++ .github/PULL_REQUEST_TEMPLATE.md | 36 +++++ .github/dependabot.yml | 34 +++++ .github/workflows/cla.yml | 49 +++++++ .github/workflows/cli-ci.yml | 6 +- .github/workflows/pr-title-lint.yml | 43 ++++++ .github/workflows/release-please.yml | 27 +++- CLA.md | 140 ++++++++++++++++++++ CODE_OF_CONDUCT.md | 132 ++++++++++++++++++ CONTRIBUTING.md | 147 +++++++++++++++++++++ LICENSE | 21 +++ README.md | 17 +++ SECURITY.md | 55 ++++++++ 17 files changed, 843 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/cla.yml create mode 100644 .github/workflows/pr-title-lint.yml create mode 100644 CLA.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..16cdc57 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig — https://editorconfig.org +# Keep editors aligned with the Prettier config (.prettierrc). +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# Markdown uses two trailing spaces for hard line breaks — don't strip them. +[*.md] +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1867eb6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,16 @@ +# Code owners for dcd-cli. +# Listed owners are requested for review automatically and — when the branch +# ruleset has "Require review from Code Owners" enabled — must approve before a +# PR can merge. +# +# NOTE: replace @devicecloud-dev/cli-maintainers with the real maintainer team +# slug (or individual @handles) before enabling Code Owner review in the ruleset. + +* @devicecloud-dev/cli-maintainers + +# Release pipeline and CI are sensitive — keep them owned by maintainers. +/.github/ @devicecloud-dev/cli-maintainers +/release-please-config.json @devicecloud-dev/cli-maintainers +/release-please-config-beta.json @devicecloud-dev/cli-maintainers +/.release-please-manifest.json @devicecloud-dev/cli-maintainers +/.release-please-manifest-beta.json @devicecloud-dev/cli-maintainers diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4a865eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +name: Bug report +description: Report a problem with the dcd CLI or dcd-mcp server +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug! Please fill in the details below. + + ⚠️ **Do not report security vulnerabilities here** — see our + [Security Policy](https://github.com/devicecloud-dev/dcd-cli/blob/dev/SECURITY.md). + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug, including what you expected to happen instead. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: The exact `dcd` command(s) you ran and what followed. Redact any API keys. + placeholder: | + 1. Run `dcd cloud --apiKey *** app.apk flows/` + 2. ... + 3. See error + validations: + required: true + - type: input + id: version + attributes: + label: CLI version + description: Output of `dcd --version`. + placeholder: "e.g. 5.0.0" + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - macOS + - Linux + - Windows + - Other (note in description) + validations: + required: true + - type: input + id: install + attributes: + label: How did you install dcd? + placeholder: "binary (curl/irm), npm global, npx, …" + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs / output + description: Relevant output. Re-run with more detail if you can. This is automatically formatted as code. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8769523 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Questions & help + url: https://discord.gg/gm3mJwcNw8 + about: For usage questions and general help, ask in our Discord rather than opening an issue. + - name: Documentation + url: https://docs.devicecloud.dev + about: Check the docs for installation, usage, and command reference. + - name: Report a security vulnerability + url: https://github.com/devicecloud-dev/dcd-cli/blob/dev/SECURITY.md + about: Do not file security issues publicly — email security@devicecloud.dev (see our Security Policy). diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..ece12e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: Feature request +description: Suggest an idea or improvement for the dcd CLI or dcd-mcp server +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: Thanks for the suggestion! Please describe the problem before the solution. + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: What are you trying to do, and where does the CLI get in the way today? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: What would you like to happen? A concrete command/flag/output sketch helps. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches or workarounds you've thought about. + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context + description: Anything else — links, screenshots, related issues. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..288cc94 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + + +## What & why + + + +## Type of change + + + +- [ ] `fix` — bug fix +- [ ] `feat` — new feature +- [ ] `perf` — performance improvement +- [ ] `refactor` — code change that's neither a fix nor a feature +- [ ] `docs` — documentation only +- [ ] `chore` / `ci` / `build` / `test` — tooling, no user-facing change +- [ ] Breaking change (title has `!` or PR notes a `BREAKING CHANGE:`) + +## Checklist + +- [ ] PR title follows the Conventional Commits format (see comment above) +- [ ] `pnpm lint` passes +- [ ] `pnpm typecheck` passes +- [ ] `pnpm build` passes +- [ ] I have **not** bumped the version or edited `CHANGELOG.md` (release-please handles this) +- [ ] I have signed the CLA (the bot will prompt on first contribution) +- [ ] Docs / `README.md` / `STYLE_GUIDE.md` updated if behaviour or output changed + +## How to test + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e792454 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 +updates: + # npm / pnpm dependencies. + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + target-branch: dev + open-pull-requests-limit: 10 + commit-message: + # Conventional Commit prefix so the squashed PR title matches our PR-title + # lint and release-please picks dependency bumps into the changelog. + prefix: deps + prefix-development: chore + groups: + # Collapse the noise: one PR for all non-major updates. + minor-and-patch: + update-types: + - minor + - patch + + # GitHub Actions used by our workflows. + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + target-branch: dev + commit-message: + prefix: ci + groups: + actions: + update-types: + - minor + - patch diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..bcab57c --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,49 @@ +name: CLA Assistant + +# Gates merges on a signed Contributor License Agreement. +# +# Uses CLA Assistant Lite (contributor-assistant/github-action): signatures are +# stored as a JSON file committed to a branch of THIS repo (no third-party +# service holds the data). Contributors sign by commenting the configured phrase +# on their PR; the action records it and flips the check green. +# +# SETUP REQUIRED before this can work: +# 1. Create a token with repo write access and add it as the `PERSONAL_ACCESS_TOKEN` +# secret (a fine-grained PAT or the release GitHub App token both work). The +# default GITHUB_TOKEN is also passed, but a PAT is needed to commit the +# signature file back to the repo. +# 2. Create the `cla-signatures` branch (e.g. an empty orphan branch) so the +# action has somewhere to write `signatures/version1/cla.json`. +# 3. Finalise CLA.md (legal review) — it's the document contributors agree to. +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + cla: + runs-on: ubuntu-latest + # Only act on the signature comment or on PR events (not every comment). + if: (github.event.issue.pull_request && contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target' + steps: + - uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: "signatures/version1/cla.json" + path-to-document: "https://github.com/devicecloud-dev/dcd-cli/blob/dev/CLA.md" + branch: "cla-signatures" + # PR target branches the CLA applies to. + allowlist: dependabot[bot],renovate[bot],*[bot] + # Customise the bot's prompts if desired: + custom-notsigned-prompt: "Thanks for your contribution! Please sign our Contributor License Agreement before we can merge. Comment the line below to sign:" + custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" + custom-allsigned-prompt: "All contributors have signed the CLA. ✍️ ✅" diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 71d6ae3..d4bb529 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -2,9 +2,11 @@ name: CLI CI on: push: - branches: [ dev ] + branches: [ dev, production ] pull_request: - branches: [ dev ] + # `production` is included so the dev→production promotion PR is also gated + # by lint/typecheck/build (and is required by the production ruleset). + branches: [ dev, production ] workflow_dispatch: permissions: diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml new file mode 100644 index 0000000..c16aa6d --- /dev/null +++ b/.github/workflows/pr-title-lint.yml @@ -0,0 +1,43 @@ +name: PR Title + +# Enforces Conventional Commits on the PR *title*. Because PRs are squash-merged +# with the title as the commit subject, this is what release-please parses to +# compute version bumps and the changelog — so the allowed types below must stay +# in sync with `changelog-sections` in release-please-config.json. +# +# Uses pull_request_target so it also runs (and reports a required status check) +# on PRs from forks. It only reads the title — no untrusted code is checked out. +on: + pull_request_target: + types: + - opened + - edited + - synchronize + - reopened + +permissions: + pull-requests: read + +jobs: + validate: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Keep in lockstep with release-please-config.json changelog-sections. + types: | + feat + fix + perf + deps + revert + refactor + docs + chore + test + ci + build + style diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 284b290..f28914a 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -22,15 +22,28 @@ jobs: release-please-prod: if: github.ref_name == 'production' runs-on: ubuntu-latest + # Empty until the release GitHub App secrets are configured. We use an App + # token (not GITHUB_TOKEN) so the Release PR triggers CI / PR-title / CLA + # checks — PRs opened by GITHUB_TOKEN do not, which would deadlock branch + # protection. Falls back to GITHUB_TOKEN (today's behaviour) until the App + # is set up, so this is safe to merge before then. + env: + RELEASE_PLEASE_APP_ID: ${{ secrets.RELEASE_PLEASE_APP_ID }} outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} version: ${{ steps.release.outputs.version }} steps: + - uses: actions/create-github-app-token@v2 + id: app-token + if: env.RELEASE_PLEASE_APP_ID != '' + with: + app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} - uses: googleapis/release-please-action@v4 id: release with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} target-branch: production config-file: release-please-config.json manifest-file: .release-please-manifest.json @@ -38,15 +51,25 @@ jobs: release-please-beta: if: github.ref_name == 'dev' runs-on: ubuntu-latest + # See release-please-prod above for why this uses an App token with a + # GITHUB_TOKEN fallback. + env: + RELEASE_PLEASE_APP_ID: ${{ secrets.RELEASE_PLEASE_APP_ID }} outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} version: ${{ steps.release.outputs.version }} steps: + - uses: actions/create-github-app-token@v2 + id: app-token + if: env.RELEASE_PLEASE_APP_ID != '' + with: + app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} - uses: googleapis/release-please-action@v4 id: release with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} target-branch: dev config-file: release-please-config-beta.json manifest-file: .release-please-manifest-beta.json diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..1c8930d --- /dev/null +++ b/CLA.md @@ -0,0 +1,140 @@ +# Contributor License Agreement (CLA) + +> [!IMPORTANT] +> **This is a starting-point template, not final legal text.** It is adapted from +> the Apache Software Foundation Individual and Corporate CLAs. **Have it reviewed +> by legal counsel and replace the bracketed placeholders before relying on it.** +> Once finalised, this document is what the CLA Assistant bot links contributors +> to when they sign on a pull request. + +Thank you for your interest in contributing to software projects managed by +**[Legal Entity Name] ("devicecloud.dev", "we", "us")**. To clarify the +intellectual property licence granted with contributions from any person or +entity, we must have a Contributor License Agreement (CLA) on file that has been +signed by each contributor, indicating agreement to the licence terms below. + +This licence is for your protection as a contributor as well as the protection of +devicecloud.dev and its users; it does not change your rights to use your own +contributions for any other purpose. + +By signing via the CLA Assistant bot on a pull request, you accept and agree to +the applicable terms below for your present and future contributions submitted to +devicecloud.dev. + +--- + +## Individual Contributor License Agreement + +You accept and agree to the following terms and conditions for your present and +future Contributions submitted to devicecloud.dev. Except for the licence granted +herein to devicecloud.dev and recipients of software distributed by +devicecloud.dev, you reserve all right, title, and interest in and to your +Contributions. + +1. **Definitions.** "You" (or "Your") means the copyright owner or legal entity + authorised by the copyright owner that is making this Agreement. "Contribution" + means any original work of authorship, including any modifications or additions + to an existing work, that is intentionally submitted by You to devicecloud.dev + for inclusion in, or documentation of, any of the products owned or managed by + devicecloud.dev (the "Work"). "Submitted" means any form of electronic, verbal, + or written communication sent to devicecloud.dev or its representatives, + including but not limited to communication on electronic mailing lists, source + code control systems, and issue tracking systems that are managed by, or on + behalf of, devicecloud.dev for the purpose of discussing and improving the + Work, but excluding communication that is conspicuously marked or otherwise + designated in writing by You as "Not a Contribution." + +2. **Grant of Copyright Licence.** Subject to the terms and conditions of this + Agreement, You hereby grant to devicecloud.dev and to recipients of software + distributed by devicecloud.dev a perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable copyright licence to reproduce, prepare + derivative works of, publicly display, publicly perform, sublicense, and + distribute Your Contributions and such derivative works. + +3. **Grant of Patent Licence.** Subject to the terms and conditions of this + Agreement, You hereby grant to devicecloud.dev and to recipients of software + distributed by devicecloud.dev a perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable (except as stated in this section) patent + licence to make, have made, use, offer to sell, sell, import, and otherwise + transfer the Work, where such licence applies only to those patent claims + licensable by You that are necessarily infringed by Your Contribution(s) alone + or by combination of Your Contribution(s) with the Work to which such + Contribution(s) was submitted. If any entity institutes patent litigation + against You or any other entity (including a cross-claim or counterclaim in a + lawsuit) alleging that Your Contribution, or the Work to which You have + contributed, constitutes direct or contributory patent infringement, then any + patent licences granted to that entity under this Agreement for that + Contribution or Work shall terminate as of the date such litigation is filed. + +4. **Representations.** You represent that You are legally entitled to grant the + above licence. If Your employer(s) has rights to intellectual property that You + create that includes Your Contributions, You represent that You have received + permission to make Contributions on behalf of that employer, that Your employer + has waived such rights for Your Contributions to devicecloud.dev, or that Your + employer has executed a separate Corporate CLA with devicecloud.dev. + +5. **Original Work.** You represent that each of Your Contributions is Your + original creation (see section 7 for submissions on behalf of others). You + represent that Your Contribution submissions include complete details of any + third-party licence or other restriction (including, but not limited to, + related patents and trademarks) of which You are personally aware and which are + associated with any part of Your Contributions. + +6. **No Warranty.** You are not expected to provide support for Your + Contributions, except to the extent You desire to provide support. You may + provide support for free, for a fee, or not at all. Unless required by + applicable law or agreed to in writing, You provide Your Contributions on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions of TITLE, + NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. **Third-Party Works.** Should You wish to submit work that is not Your original + creation, You may submit it to devicecloud.dev separately from any + Contribution, identifying the complete details of its source and of any + licence or other restriction (including, but not limited to, related patents, + trademarks, and licence agreements) of which You are personally aware, and + conspicuously marking the work as "Submitted on behalf of a third-party: + [named here]". + +8. **Notification.** You agree to notify devicecloud.dev of any facts or + circumstances of which You become aware that would make these representations + inaccurate in any respect. + +--- + +## Corporate Contributor License Agreement + +This version is for a corporation (or other legal entity) that wishes to authorise +employees to submit Contributions. It covers the same copyright and patent grants, +representations, and disclaimers as the Individual CLA above, made on behalf of the +entity, plus a schedule of authorised contributors. + +1. The definitions, copyright licence, patent licence, "no warranty", and + third-party works provisions in sections 1–3 and 6–7 of the Individual CLA + above apply equally to this Corporate CLA, with "You" referring to the + **Corporation** identified below. + +2. **Authorisation.** The Corporation represents that each employee designated on + **Schedule A** is authorised to submit Contributions on behalf of the + Corporation. The Corporation agrees to maintain Schedule A and to notify + devicecloud.dev when an individual's authorisation to submit Contributions on + behalf of the Corporation is terminated. + +3. **Representations.** The Corporation represents that each Contribution is an + original creation (per section 5 of the Individual CLA) and that it is legally + entitled to grant the above licences. The Corporation agrees to notify + devicecloud.dev of any facts or circumstances of which it becomes aware that + would make these representations inaccurate. + +**Schedule A — Designated Employees** + +| Full name | GitHub username | Email | +| --- | --- | --- | +| | | | + +**Corporation details** + +- Corporation name: ______________________________ +- Corporation address: ___________________________ +- Authorised signatory (name & title): ___________ +- Signature / date: ______________________________ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fd6b573 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**conduct@devicecloud.dev**. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1761173 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,147 @@ +# Contributing to the devicecloud.dev CLI + +Thanks for your interest in improving `@devicecloud.dev/dcd`! This guide covers +everything you need to land a change: local setup, our commit/PR conventions, and +how releases work. + +By participating you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). + +## Contributor License Agreement (CLA) + +Before your first contribution can be merged, you must sign our Contributor +License Agreement. When you open your first pull request, the **CLA Assistant** +bot will comment with a link and instructions — signing takes under a minute and +is a one-time step. PRs cannot be merged until the CLA check is green. + +- Individuals: sign the [Individual CLA](CLA.md#individual-contributor-license-agreement). +- Contributing on behalf of an employer? Have an authorised signatory complete the + [Corporate CLA](CLA.md#corporate-contributor-license-agreement). + +## Getting started + +You need **Node.js 22+** and **[pnpm](https://pnpm.io)** (`packageManager` pins +the exact version; [Corepack](https://nodejs.org/api/corepack.html) will pick it +up automatically). + +```sh-session +$ git clone https://github.com/devicecloud-dev/dcd-cli.git +$ cd dcd-cli +$ pnpm install # installs deps, builds, and sets up git hooks +$ pnpm dcd # run the CLI from source +``` + +Useful scripts: + +| Command | What it does | +| --- | --- | +| `pnpm lint` | ESLint over `src/` and `test/` | +| `pnpm typecheck` | Strict `tsc --noEmit` over `src/` and `test/` | +| `pnpm build` | Compile to `dist/` | +| `pnpm test` | Build + boot the mock API + run integration/unit tests | + +**Before pushing, make sure `pnpm lint`, `pnpm typecheck`, and `pnpm build` +pass.** These run for every PR (including from forks) and are required to merge. + +### About the test suite + +`pnpm test` boots a **mock API that lives in a private repository**, so the full +integration suite only runs on branches inside this repo. **On pull requests from +forks the integration tests are automatically skipped** — you'll see a CI notice +saying so. That's expected: lint, typecheck, and build still run and gate your +PR, and a maintainer runs the full suite before merge. You don't need backend +access to contribute. + +### Secret scanning + +A [gitleaks](https://github.com/gitleaks/gitleaks) scan runs as a pre-commit hook +and in CI (sharing the allowlist in `.gitleaks.toml`). Installing the binary +locally (`brew install gitleaks`) catches secrets before you commit; without it +the hook skips with a warning and CI remains the backstop. **Never commit real +credentials.** + +## Branching & pull requests + +1. Branch off **`dev`** (the default branch). Name it descriptively, e.g. + `fix/upload-retry` or `feat/json-output`. +2. Open your pull request **against `dev`**. (The `production` branch is the + stable release track and is maintainer-only — don't target it.) +3. Keep PRs focused. Smaller, single-purpose PRs are reviewed and merged faster. +4. Fill in the PR template, including the checklist. +5. PRs are merged via **squash merge**, so your PR ends up as a single commit on + `dev` whose message is your **PR title** — which is why the title must follow + the Conventional Commits format below. + +## Commit & PR title conventions + +We use [Conventional Commits](https://www.conventionalcommits.org). Because we +squash-merge, **only your PR title needs to follow the format** — individual +commit messages on your branch are squashed away, so commit however you like +while developing. A CI check (`PR Title`) validates the title and must pass to +merge. + +Format: + +``` +(): +``` + +Allowed types and how they affect the next release: + +| Type | Use for | Changelog | Version bump | +| --- | --- | --- | --- | +| `feat` | A new feature | **Features** | minor | +| `fix` | A bug fix | **Bug Fixes** | patch | +| `perf` | A performance improvement | **Performance** | patch | +| `deps` | Dependency updates | **Dependencies** | patch | +| `revert` | Reverting a previous change | **Reverts** | patch | +| `refactor` | Code change that neither fixes a bug nor adds a feature | **Code Refactoring** | patch | +| `docs` | Documentation only | hidden | none | +| `chore` | Tooling/maintenance | hidden | none | +| `test` | Adding or fixing tests | hidden | none | +| `ci` | CI configuration | hidden | none | +| `build` | Build system | hidden | none | +| `style` | Formatting, whitespace | hidden | none | + +**Breaking changes:** append `!` after the type (e.g. `feat!: drop Node 20`) or +add a `BREAKING CHANGE:` footer in the PR description. While the CLI is pre-1.0, +`feat` bumps the minor version and breaking changes bump the minor too. + +Examples: + +``` +feat(cloud): add --json output for run results +fix: retry binary upload on transient 5xx +docs: clarify dcd login flow in README +deps: bump @modelcontextprotocol/sdk to 1.x +``` + +## Code style + +- TypeScript, strict mode. Run `pnpm lint` and `pnpm typecheck` before pushing. +- Formatting is handled by Prettier (config in `.prettierrc`); an `.editorconfig` + keeps editors consistent. +- All human-facing CLI output goes through the rendering layer described in + [`STYLE_GUIDE.md`](STYLE_GUIDE.md) — please read it before adding output. Don't + hand-roll layouts or call `console.log` directly. + +## How releases work + +You don't need to do anything for releases — **do not bump the version in +`package.json` or edit `CHANGELOG.md`** in your PR. + +Releases are automated by [release-please](https://github.com/googleapis/release-please): + +- Merges to `dev` accumulate into a **beta** release (published to npm under the + `beta` tag). +- Maintainers promote `dev` → `production` for **stable** releases (npm `latest`). + +release-please reads the Conventional Commit titles of merged PRs to compute the +next version and generate the changelog — which is exactly why the PR title +convention matters. + +## Questions + +- General questions and help: [Discord](https://discord.gg/gm3mJwcNw8). +- Security vulnerabilities: **do not** open an issue — see [SECURITY.md](SECURITY.md). + +Thanks for contributing! 🎉 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e983d13 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 devicecloud.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a656931..b1acb0c 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,20 @@ A [gitleaks](https://github.com/gitleaks/gitleaks) scan runs in two places, both - **CI** — the `secret-scan` job scans the full history on every push and pull request, and is the enforced backstop regardless of local setup. +## Contributing + +Contributions are welcome! Read **[CONTRIBUTING.md](CONTRIBUTING.md)** for local +setup, our commit/PR conventions (Conventional Commit PR titles, squash-merge), +and how releases work. All contributors sign our +[Contributor License Agreement](CLA.md) — the bot prompts you on your first PR — +and follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +Found a security issue? Please **don't** open a public issue — see +[SECURITY.md](SECURITY.md). + + +## License + +[MIT](LICENSE) © devicecloud.dev + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..047affc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,55 @@ +# Security Policy + +We take the security of the devicecloud.dev CLI (`@devicecloud.dev/dcd`) +seriously. Thank you for helping keep our users safe. + +## Supported Versions + +Security fixes are released against the latest published major version on npm. +Always upgrade to the newest release before reporting: + +```sh-session +$ npm install -g @devicecloud.dev/dcd@latest # npm install +$ dcd upgrade # binary install +``` + +| Version | Supported | +| -------------- | ------------------ | +| Latest `5.x` | :white_check_mark: | +| Older majors | :x: | + +## Reporting a Vulnerability + +**Please do not open a public GitHub issue, pull request, or Discord message for +security vulnerabilities.** Public reports put users at risk before a fix is +available. + +Instead, email **security@devicecloud.dev** with: + +- A description of the vulnerability and its impact. +- Steps to reproduce (a proof of concept is ideal). +- The CLI version (`dcd --version`), OS, and Node.js version where applicable. +- Any suggested remediation, if you have one. + +### What to expect + +- **Acknowledgement** within 3 business days. +- An initial assessment and severity triage within 7 business days. +- Coordinated disclosure: we will work with you on a fix and a disclosure + timeline, and credit you in the release notes if you wish. + +Please give us a reasonable opportunity to remediate before any public +disclosure. + +## Scope + +This policy covers the code in this repository — the `dcd` CLI and the `dcd-mcp` +server. Vulnerabilities in the devicecloud.dev backend or web console should also +be sent to **security@devicecloud.dev** and will be routed to the right team. + +## Secrets + +This repository is scanned for committed secrets by [gitleaks](https://github.com/gitleaks/gitleaks) +on every push and pull request, and via a local pre-commit hook. If you believe +a secret has been committed, email **security@devicecloud.dev** immediately +rather than opening an issue. From 129f802729c79e436bb7dc2fd04b12cf742ed663 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:23:08 +0100 Subject: [PATCH 07/16] 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. --- .github/CODEOWNERS | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 1867eb6..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,16 +0,0 @@ -# Code owners for dcd-cli. -# Listed owners are requested for review automatically and — when the branch -# ruleset has "Require review from Code Owners" enabled — must approve before a -# PR can merge. -# -# NOTE: replace @devicecloud-dev/cli-maintainers with the real maintainer team -# slug (or individual @handles) before enabling Code Owner review in the ruleset. - -* @devicecloud-dev/cli-maintainers - -# Release pipeline and CI are sensitive — keep them owned by maintainers. -/.github/ @devicecloud-dev/cli-maintainers -/release-please-config.json @devicecloud-dev/cli-maintainers -/release-please-config-beta.json @devicecloud-dev/cli-maintainers -/.release-please-manifest.json @devicecloud-dev/cli-maintainers -/.release-please-manifest-beta.json @devicecloud-dev/cli-maintainers From dc872572846fbe0d9760902cda3380edee1ef2ff Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:34:26 +0100 Subject: [PATCH 08/16] 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. --- .github/dependabot.yml | 7 ++++--- .github/workflows/cla.yml | 11 +++++++++-- .github/workflows/claude-code-review.yml | 10 ++++------ .github/workflows/cli-ci.yml | 6 +++++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e792454..e93cbd3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,7 +28,8 @@ updates: commit-message: prefix: ci groups: + # One PR per week for ALL action bumps (including majors). Actions are + # low-risk and quick to eyeball together; no need for a PR each. actions: - update-types: - - minor - - patch + patterns: + - "*" diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index bcab57c..215c76b 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -30,10 +30,17 @@ permissions: jobs: cla: runs-on: ubuntu-latest + # Empty until the PERSONAL_ACCESS_TOKEN secret is configured (see SETUP above). + # While empty, the action step below is skipped so this check passes (green) + # instead of failing on every PR with "Branch cla-signatures not found". It + # auto-activates once the secret + cla-signatures branch exist. + env: + HAS_CLA_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN != '' }} # Only act on the signature comment or on PR events (not every comment). if: (github.event.issue.pull_request && contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target' steps: - uses: contributor-assistant/github-action@v2.6.1 + if: env.HAS_CLA_TOKEN == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} @@ -44,6 +51,6 @@ jobs: # PR target branches the CLA applies to. allowlist: dependabot[bot],renovate[bot],*[bot] # Customise the bot's prompts if desired: - custom-notsigned-prompt: "Thanks for your contribution! Please sign our Contributor License Agreement before we can merge. Comment the line below to sign:" + custom-notsigned-prcomment: "Thanks for your contribution! Please sign our Contributor License Agreement before we can merge. Comment the line below to sign:" custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" - custom-allsigned-prompt: "All contributors have signed the CLA. ✍️ ✅" + custom-allsigned-prcomment: "All contributors have signed the CLA. ✍️ ✅" diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index c396185..5474be8 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,12 +12,10 @@ on: jobs: claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + # The review needs CLAUDE_CODE_OAUTH_TOKEN, which is NOT exposed to PRs that + # run without secrets — Dependabot PRs and PRs from forks. Skip them so the + # check doesn't fail with an empty token; same-repo PRs only. + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index d4bb529..9febaea 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -40,8 +40,12 @@ jobs: # SSH deploy key. GitHub does NOT expose secrets to pull_request workflows # triggered from forks, so that checkout (and the integration tests that need # it) can only run for same-repo events. Fork PRs still run lint/typecheck/build. + # + # Dependabot PRs branch from this repo (so the fork check passes) but ALSO run + # without secrets — treat them like forks and skip the private checkout, or + # the mock-api clone fails with an empty DCD_SSH_DEPLOY_KEY. env: - HAS_PRIVATE_ACCESS: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + HAS_PRIVATE_ACCESS: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' }} steps: - name: Checkout CLI From 07074d312be16e20d3053cd04314ac9919fc048c Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:17:08 +0100 Subject: [PATCH 09/16] ci: power CLA via the shared automation GitHub App (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .github/workflows/cla.yml | 43 +++++++++++++++++----------- .github/workflows/release-please.yml | 21 +++++++------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 215c76b..395a4f1 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -7,14 +7,20 @@ name: CLA Assistant # service holds the data). Contributors sign by commenting the configured phrase # on their PR; the action records it and flips the check green. # -# SETUP REQUIRED before this can work: -# 1. Create a token with repo write access and add it as the `PERSONAL_ACCESS_TOKEN` -# secret (a fine-grained PAT or the release GitHub App token both work). The -# default GITHUB_TOKEN is also passed, but a PAT is needed to commit the -# signature file back to the repo. -# 2. Create the `cla-signatures` branch (e.g. an empty orphan branch) so the -# action has somewhere to write `signatures/version1/cla.json`. +# AUTH: mints a token from the shared automation GitHub App (the same App +# release-please uses), so signature commits show as the bot and there's no +# personal token to expire. +# +# SETUP REQUIRED before this enforces anything: +# 1. Create/install the automation GitHub App (Contents R/W, Pull requests R/W, +# Issues R/W) and add BOT_APP_ID + BOT_APP_PRIVATE_KEY repo secrets — the +# same secrets release-please uses. +# 2. Create the `cla-signatures` branch (empty orphan) so the action has +# somewhere to write `signatures/version1/cla.json`. # 3. Finalise CLA.md (legal review) — it's the document contributors agree to. +# +# Until the App secrets exist the CLA step self-skips, so the check is green +# (not failing) on every PR and auto-activates once they're set. on: issue_comment: types: [created] @@ -30,26 +36,31 @@ permissions: jobs: cla: runs-on: ubuntu-latest - # Empty until the PERSONAL_ACCESS_TOKEN secret is configured (see SETUP above). - # While empty, the action step below is skipped so this check passes (green) - # instead of failing on every PR with "Branch cla-signatures not found". It - # auto-activates once the secret + cla-signatures branch exist. + # Empty until the automation App secrets are configured (see SETUP above). + # While empty, the steps below self-skip so this check passes (green) instead + # of failing on every PR with "Branch cla-signatures not found". env: - HAS_CLA_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN != '' }} + HAS_APP: ${{ secrets.BOT_APP_ID != '' }} # Only act on the signature comment or on PR events (not every comment). if: (github.event.issue.pull_request && contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target' steps: + - uses: actions/create-github-app-token@v2 + id: app-token + if: env.HAS_APP == 'true' + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - uses: contributor-assistant/github-action@v2.6.1 - if: env.HAS_CLA_TOKEN == 'true' + if: env.HAS_APP == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} with: path-to-signatures: "signatures/version1/cla.json" path-to-document: "https://github.com/devicecloud-dev/dcd-cli/blob/dev/CLA.md" branch: "cla-signatures" - # PR target branches the CLA applies to. - allowlist: dependabot[bot],renovate[bot],*[bot] + # Internal maintainers (covered by employment/CCLA) + bots skip the prompt. + allowlist: riglar,finalerock44,dependabot[bot],renovate[bot],*[bot] # Customise the bot's prompts if desired: custom-notsigned-prcomment: "Thanks for your contribution! Please sign our Contributor License Agreement before we can merge. Comment the line below to sign:" custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index f28914a..0745137 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -22,13 +22,14 @@ jobs: release-please-prod: if: github.ref_name == 'production' runs-on: ubuntu-latest - # Empty until the release GitHub App secrets are configured. We use an App - # token (not GITHUB_TOKEN) so the Release PR triggers CI / PR-title / CLA + # Empty until the automation GitHub App secrets are configured (the same App + # powers the CLA workflow). We use an App token (not GITHUB_TOKEN) so the + # Release PR triggers CI / PR-title / CLA # checks — PRs opened by GITHUB_TOKEN do not, which would deadlock branch # protection. Falls back to GITHUB_TOKEN (today's behaviour) until the App # is set up, so this is safe to merge before then. env: - RELEASE_PLEASE_APP_ID: ${{ secrets.RELEASE_PLEASE_APP_ID }} + BOT_APP_ID: ${{ secrets.BOT_APP_ID }} outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} @@ -36,10 +37,10 @@ jobs: steps: - uses: actions/create-github-app-token@v2 id: app-token - if: env.RELEASE_PLEASE_APP_ID != '' + if: env.BOT_APP_ID != '' with: - app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - uses: googleapis/release-please-action@v4 id: release with: @@ -54,7 +55,7 @@ jobs: # See release-please-prod above for why this uses an App token with a # GITHUB_TOKEN fallback. env: - RELEASE_PLEASE_APP_ID: ${{ secrets.RELEASE_PLEASE_APP_ID }} + BOT_APP_ID: ${{ secrets.BOT_APP_ID }} outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} @@ -62,10 +63,10 @@ jobs: steps: - uses: actions/create-github-app-token@v2 id: app-token - if: env.RELEASE_PLEASE_APP_ID != '' + if: env.BOT_APP_ID != '' with: - app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - uses: googleapis/release-please-action@v4 id: release with: From d3b0accecd5b3283d7cf43cc34611fcf9194227c Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:47:23 +0100 Subject: [PATCH 10/16] 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. --- CLA.md | 40 ++++++++++++++++++++-------------------- LICENSE | 2 +- README.md | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CLA.md b/CLA.md index 1c8930d..81ea8e4 100644 --- a/CLA.md +++ b/CLA.md @@ -8,52 +8,52 @@ > to when they sign on a pull request. Thank you for your interest in contributing to software projects managed by -**[Legal Entity Name] ("devicecloud.dev", "we", "us")**. To clarify the +**Moropo Ltd t/a DeviceCloud ("DeviceCloud", "we", "us")**. To clarify the intellectual property licence granted with contributions from any person or entity, we must have a Contributor License Agreement (CLA) on file that has been signed by each contributor, indicating agreement to the licence terms below. This licence is for your protection as a contributor as well as the protection of -devicecloud.dev and its users; it does not change your rights to use your own +DeviceCloud and its users; it does not change your rights to use your own contributions for any other purpose. By signing via the CLA Assistant bot on a pull request, you accept and agree to the applicable terms below for your present and future contributions submitted to -devicecloud.dev. +DeviceCloud. --- ## Individual Contributor License Agreement You accept and agree to the following terms and conditions for your present and -future Contributions submitted to devicecloud.dev. Except for the licence granted -herein to devicecloud.dev and recipients of software distributed by -devicecloud.dev, you reserve all right, title, and interest in and to your +future Contributions submitted to DeviceCloud. Except for the licence granted +herein to DeviceCloud and recipients of software distributed by +DeviceCloud, you reserve all right, title, and interest in and to your Contributions. 1. **Definitions.** "You" (or "Your") means the copyright owner or legal entity authorised by the copyright owner that is making this Agreement. "Contribution" means any original work of authorship, including any modifications or additions - to an existing work, that is intentionally submitted by You to devicecloud.dev + to an existing work, that is intentionally submitted by You to DeviceCloud for inclusion in, or documentation of, any of the products owned or managed by - devicecloud.dev (the "Work"). "Submitted" means any form of electronic, verbal, - or written communication sent to devicecloud.dev or its representatives, + DeviceCloud (the "Work"). "Submitted" means any form of electronic, verbal, + or written communication sent to DeviceCloud or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on - behalf of, devicecloud.dev for the purpose of discussing and improving the + behalf of, DeviceCloud for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 2. **Grant of Copyright Licence.** Subject to the terms and conditions of this - Agreement, You hereby grant to devicecloud.dev and to recipients of software - distributed by devicecloud.dev a perpetual, worldwide, non-exclusive, + Agreement, You hereby grant to DeviceCloud and to recipients of software + distributed by DeviceCloud a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright licence to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 3. **Grant of Patent Licence.** Subject to the terms and conditions of this - Agreement, You hereby grant to devicecloud.dev and to recipients of software - distributed by devicecloud.dev a perpetual, worldwide, non-exclusive, + Agreement, You hereby grant to DeviceCloud and to recipients of software + distributed by DeviceCloud a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent licence to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such licence applies only to those patent claims @@ -70,8 +70,8 @@ Contributions. above licence. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer - has waived such rights for Your Contributions to devicecloud.dev, or that Your - employer has executed a separate Corporate CLA with devicecloud.dev. + has waived such rights for Your Contributions to DeviceCloud, or that Your + employer has executed a separate Corporate CLA with DeviceCloud. 5. **Original Work.** You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You @@ -89,14 +89,14 @@ Contributions. NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 7. **Third-Party Works.** Should You wish to submit work that is not Your original - creation, You may submit it to devicecloud.dev separately from any + creation, You may submit it to DeviceCloud separately from any Contribution, identifying the complete details of its source and of any licence or other restriction (including, but not limited to, related patents, trademarks, and licence agreements) of which You are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. **Notification.** You agree to notify devicecloud.dev of any facts or +8. **Notification.** You agree to notify DeviceCloud of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect. @@ -117,13 +117,13 @@ entity, plus a schedule of authorised contributors. 2. **Authorisation.** The Corporation represents that each employee designated on **Schedule A** is authorised to submit Contributions on behalf of the Corporation. The Corporation agrees to maintain Schedule A and to notify - devicecloud.dev when an individual's authorisation to submit Contributions on + DeviceCloud when an individual's authorisation to submit Contributions on behalf of the Corporation is terminated. 3. **Representations.** The Corporation represents that each Contribution is an original creation (per section 5 of the Individual CLA) and that it is legally entitled to grant the above licences. The Corporation agrees to notify - devicecloud.dev of any facts or circumstances of which it becomes aware that + DeviceCloud of any facts or circumstances of which it becomes aware that would make these representations inaccurate. **Schedule A — Designated Employees** diff --git a/LICENSE b/LICENSE index e983d13..278d44f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 devicecloud.dev +Copyright (c) 2026 Moropo Ltd t/a DeviceCloud Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b1acb0c..f0da192 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,6 @@ Found a security issue? Please **don't** open a public issue — see ## License -[MIT](LICENSE) © devicecloud.dev +[MIT](LICENSE) © Moropo Ltd t/a DeviceCloud From 3fe1f2191c69261d5c5d48b897888e9e051dc7a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:14:13 +0100 Subject: [PATCH 11/16] 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> --- .github/workflows/cla.yml | 2 +- .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/cli-ci.yml | 10 +++++----- .github/workflows/npm-publish.yml | 6 +++--- .github/workflows/pr-title-lint.yml | 2 +- .github/workflows/release-binaries.yml | 6 +++--- .github/workflows/release-please.yml | 8 ++++---- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 395a4f1..a0306ed 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -44,7 +44,7 @@ jobs: # Only act on the signature comment or on PR events (not every comment). if: (github.event.issue.pull_request && contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA')) || github.event_name == 'pull_request_target' steps: - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@v3 id: app-token if: env.HAS_APP == 'true' with: diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5474be8..3419419 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 68c8f95..56235d1 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,7 +26,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 1 diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 9febaea..1cb3bc1 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (full history) - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: # Full history so gitleaks scans every commit, not just the tip. fetch-depth: 0 @@ -49,13 +49,13 @@ jobs: steps: - name: Checkout CLI - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: path: cli - name: Checkout dcd (mock-api) if: env.HAS_PRIVATE_ACCESS == 'true' - uses: actions/checkout@v5 + uses: actions/checkout@v7 with: repository: moropo-com/dcd path: dcd @@ -68,13 +68,13 @@ jobs: /api/swagger.json - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: 10 run_install: false - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22' cache: 'pnpm' diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index dbee47d..8ff9b19 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -24,14 +24,14 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 # Setup .npmrc file to publish to npm - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index c16aa6d..e3f8cb8 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -23,7 +23,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 32467ed..ba1b227 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -24,14 +24,14 @@ jobs: permissions: contents: write # Required to upload release assets steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: '22.x' cache: 'pnpm' diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0745137..3913334 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -35,13 +35,13 @@ jobs: tag_name: ${{ steps.release.outputs.tag_name }} version: ${{ steps.release.outputs.version }} steps: - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@v3 id: app-token if: env.BOT_APP_ID != '' with: app-id: ${{ secrets.BOT_APP_ID }} private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@v5 id: release with: token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} @@ -61,13 +61,13 @@ jobs: tag_name: ${{ steps.release.outputs.tag_name }} version: ${{ steps.release.outputs.version }} steps: - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@v3 id: app-token if: env.BOT_APP_ID != '' with: app-id: ${{ secrets.BOT_APP_ID }} private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@v5 id: release with: token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} From d780e55093b314fe9855890db145188e2735beb3 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:00:13 +0100 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20v5=20release=20blockers=20?= =?UTF-8?q?=E2=80=94=20installer,=20binary=20version,=20repeated=20flags,?= =?UTF-8?q?=E2=80=A6=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- install.ps1 | 12 +++++- scripts/build-binaries.mjs | 21 +++++++++- src/commands/cloud.ts | 50 +++++++++++++++++++----- src/commands/list.ts | 11 ++++-- src/commands/status.ts | 11 +++++- src/commands/upgrade.ts | 21 ++++++++-- src/global.d.ts | 10 +++++ src/methods.ts | 20 ++++------ src/services/results-polling.service.ts | 16 +++++++- src/services/version.service.ts | 49 ++++++++++++++++++++---- src/utils/cli.ts | 51 +++++++++++++++++++++++-- src/utils/progress.ts | 48 +++++++++++++++++++++-- 12 files changed, 271 insertions(+), 49 deletions(-) create mode 100644 src/global.d.ts diff --git a/install.ps1 b/install.ps1 index 07a70b7..dab6b1f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -75,7 +75,15 @@ try { Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing # --- verify checksum --- - $sums = (Invoke-WebRequest -Uri $sumsUrl -UseBasicParsing).Content + # GitHub serves SHA256SUMS as application/octet-stream, so under + # -UseBasicParsing on Windows PowerShell 5.x .Content comes back as a + # Byte[] (not a string) and -split would never match. Decode to UTF-8 text. + $sumsResp = Invoke-WebRequest -Uri $sumsUrl -UseBasicParsing + $sums = if ($sumsResp.Content -is [byte[]]) { + [System.Text.Encoding]::UTF8.GetString($sumsResp.Content) + } else { + [string]$sumsResp.Content + } $expected = ($sums -split "`n" | Where-Object { $_ -match "^([a-f0-9]{64})\s+$([regex]::Escape($asset))\s*$" } | ForEach-Object { $matches[1] } | @@ -83,7 +91,7 @@ try { if (-not $expected) { throw "SHA256SUMS has no entry for $asset" } $actual = (Get-FileHash -Path $tmp -Algorithm SHA256).Hash.ToLower() if ($expected -ne $actual) { - throw "Checksum mismatch for $asset: expected $expected, got $actual" + throw "Checksum mismatch for ${asset}: expected $expected, got $actual" } # --- install --- diff --git a/scripts/build-binaries.mjs b/scripts/build-binaries.mjs index 008a52f..f9dae78 100644 --- a/scripts/build-binaries.mjs +++ b/scripts/build-binaries.mjs @@ -23,6 +23,13 @@ import { fileURLToPath } from 'node:url'; const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); const outDir = join(repoRoot, 'dist-bin'); +// The compiled binary can't read package.json off disk (it isn't bundled), so +// stamp the version in at compile time via `bun --define`. getCliVersion() +// prefers this constant and falls back to reading package.json on the npm path. +const { version } = JSON.parse( + readFileSync(join(repoRoot, 'package.json'), 'utf8'), +); + // Each entry maps a Bun cross-compile target to the GitHub Release asset name. // outName excludes `.exe` because Bun appends it automatically for windows targets. const targets = [ @@ -41,7 +48,19 @@ for (const { target, outName, asset } of targets) { console.log(`→ ${target}`); execFileSync( 'bun', - ['build', '--compile', `--target=${target}`, 'src/index.ts', '--outfile', out], + [ + 'build', + '--compile', + `--target=${target}`, + // bun wants the space-separated `--define KEY=value` form (the colon form + // `--define:KEY=value` silently no-ops). JSON.stringify supplies the + // surrounding quotes bun expects for a string-literal replacement. + '--define', + `__DCD_CLI_VERSION__=${JSON.stringify(version)}`, + 'src/index.ts', + '--outfile', + out, + ], { cwd: repoRoot, stdio: 'inherit' }, ); const produced = join(outDir, asset); diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index c7bcc00..e8358cf 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -1,5 +1,6 @@ /* eslint-disable complexity */ import { defineCommand } from 'citty'; +import { existsSync } from 'node:fs'; import * as path from 'node:path'; import { flags as allFlags } from '../constants.js'; @@ -36,6 +37,7 @@ import { isCI } from '../utils/ci.js'; import { CliError, coerceArray, + collectRepeatedFlag, getCliVersion, getUpgradeCommand, logger, @@ -109,7 +111,7 @@ export const cloudCommand = defineCommand({ }, }, // eslint-disable-next-line complexity - async run({ args }) { + async run({ args, rawArgs }) { const cliVersion = getCliVersion(); const deviceValidationService = new DeviceValidationService(); const moropoService = new MoropoService(); @@ -119,12 +121,16 @@ export const cloudCommand = defineCommand({ const versionService = new VersionService(); const versionCheck = async () => { - const latestVersion = await versionService.checkLatestCliVersion(); - if (latestVersion && versionService.isOutdated(cliVersion, latestVersion)) { + const result = await versionService.checkLatestCliVersion(cliVersion); + if ( + result.ok && + result.version && + versionService.isOutdated(cliVersion, result.version) + ) { out(ui.warn(colors.bold('Update available'))); out( ui.branch([ - `A new version of the DeviceCloud CLI is available: ${colors.highlight(latestVersion)}`, + `A new version of the DeviceCloud CLI is available: ${colors.highlight(result.version)}`, `${colors.dim('Run:')} ${colors.info(getUpgradeCommand())}`, ]), ); @@ -165,18 +171,23 @@ export const cloudCommand = defineCommand({ 'download-artifacts', ); const dryRun = Boolean(args['dry-run']); - const env = coerceArray(args.env as string | string[] | undefined, false); + // Repeatable flags are collected from rawArgs: citty/parseArgs only keeps + // the last occurrence, so reading args.* directly drops earlier values. + const env = coerceArray( + collectRepeatedFlag(rawArgs, ['--env', '-e']), + false, + ); const excludeFlows = coerceArray( - args['exclude-flows'] as string | string[] | undefined, + collectRepeatedFlag(rawArgs, ['--exclude-flows']), ); const excludeTags = coerceArray( - args['exclude-tags'] as string | string[] | undefined, + collectRepeatedFlag(rawArgs, ['--exclude-tags']), ); let flows = args.flows as string | undefined; const googlePlay = Boolean(args['google-play']); const ignoreShaCheck = Boolean(args['ignore-sha-check']); const includeTags = coerceArray( - args['include-tags'] as string | string[] | undefined, + collectRepeatedFlag(rawArgs, ['--include-tags']), ); const iOSDevice = validateEnum( args['ios-device'] as string | undefined, @@ -204,7 +215,7 @@ export const cloudCommand = defineCommand({ const jsonFileName = args['json-file-name'] as string | undefined; const maestroVersion = args['maestro-version'] as string | undefined; const metadata = coerceArray( - args.metadata as string | string[] | undefined, + collectRepeatedFlag(rawArgs, ['--metadata', '-m']), false, ); const mitmHost = args.mitmHost as string | undefined; @@ -437,6 +448,14 @@ export const cloudCommand = defineCommand({ out(`[DEBUG] Found .app bundle at: ${finalAppFile}`); } } + + // Validate the resolved local app file early — dry-run otherwise skips + // the upload that would surface a missing file, so a typo'd path would + // pass a dry-run and only fail on the real run. (URL/.tar.gz inputs are + // already resolved to existing temp paths by this point.) + if (!existsSync(finalAppFile)) { + throw new CliError(`App file does not exist: ${finalAppFile}`); + } } if (debug) { @@ -619,14 +638,27 @@ export const cloudCommand = defineCommand({ ]); // Only log canonical flag keys (skip citty-populated alias duplicates like apiURL/apiUrl). const canonicalFlagKeys = new Set(Object.keys(allFlags)); + // Repeatable flags are recovered from rawArgs (args.* only holds the last + // occurrence), so echo the fully-collected values rather than args.*. + const repeatableDisplay: Record = { + env, + metadata, + 'include-tags': includeTags, + 'exclude-tags': excludeTags, + 'exclude-flows': excludeFlows, + }; for (const [k, v] of Object.entries(args)) { if (!canonicalFlagKeys.has(k)) continue; + if (k in repeatableDisplay) continue; if (v === undefined || v === null || v === false) continue; const asString = String(v); if (asString.length > 0 && !sensitiveFlags.has(k)) { flagLogs.push(`${k}: ${asString}`); } } + for (const [k, values] of Object.entries(repeatableDisplay)) { + if (values.length > 0) flagLogs.push(`${k}: ${values.join(', ')}`); + } const overridesEntries = Object.entries(flowOverrides); const hasOverrides = overridesEntries.some( diff --git a/src/commands/list.ts b/src/commands/list.ts index 669acc0..c057b38 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,6 +1,7 @@ import { defineCommand } from 'citty'; import { apiFlags } from '../config/flags/api.flags.js'; +import { resolveFrontendUrl } from '../config/environments.js'; import { ApiGateway } from '../gateways/api-gateway.js'; import { resolveAuth } from '../utils/auth.js'; import { CliError, logger, parseIntFlag } from '../utils/cli.js'; @@ -41,8 +42,12 @@ function detectShellExpansion(name: string): void { } } -function displayResults(response: ListResponse): void { +function displayResults(response: ListResponse, apiUrl: string): void { const { uploads, total, limit, offset } = response; + // Build console links from the env the CLI is pointed at, rather than the + // API-supplied consoleUrl (which is hardcoded to prod) — so dev/staging users + // get links that actually resolve. + const frontendUrl = resolveFrontendUrl(apiUrl); if (uploads.length === 0) { logger.log(ui.info('No uploads found matching your criteria.')); @@ -70,7 +75,7 @@ function displayResults(response: ListResponse): void { ...ui.fields([ ['id', formatId(upload.id)], ['created', formattedDate], - ['console', formatUrl(upload.consoleUrl)], + ['console', formatUrl(`${frontendUrl}/results?upload=${upload.id}`)], ]), ]), ); @@ -169,7 +174,7 @@ export const listCommand = defineCommand({ return; } - displayResults(response); + displayResults(response, apiUrl); } catch (error) { throw new CliError( `Failed to list uploads: ${(error as Error).message}`, diff --git a/src/commands/status.ts b/src/commands/status.ts index 6deb425..9724897 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -1,6 +1,7 @@ import { defineCommand } from 'citty'; import { apiFlags } from '../config/flags/api.flags.js'; +import { resolveFrontendUrl } from '../config/environments.js'; import { ApiGateway } from '../gateways/api-gateway.js'; import { formatDurationSeconds } from '../methods.js'; import { resolveAuth } from '../utils/auth.js'; @@ -248,8 +249,14 @@ async function statusMain({ if (status.createdAt) { fields.push(['created', formatDateTime(status.createdAt)]); } - if (status.consoleUrl) { - fields.push(['console', formatUrl(status.consoleUrl)]); + // Prefer a console link built from the env the CLI is pointed at (the + // API-supplied consoleUrl is hardcoded to prod, so it misdirects + // dev/staging users); fall back to the API value if we have no uploadId. + const consoleUrl = status.uploadId + ? `${resolveFrontendUrl(apiUrl)}/results?upload=${status.uploadId}` + : status.consoleUrl; + if (consoleUrl) { + fields.push(['console', formatUrl(consoleUrl)]); } logger.log(ui.section('Upload Status')); diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts index 0aa2232..719ab60 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/upgrade.ts @@ -57,14 +57,24 @@ export const upgradeCommand = defineCommand({ const current = getCliVersion(); const versionService = new VersionService(); - const latest = await versionService.checkLatestCliVersion(); + const result = await versionService.checkLatestCliVersion(current); - if (!latest) { + if (!result.ok) { throw new CliError( - 'Could not reach the update manifest. Check your network connection and try again.', + `Could not reach the update manifest (${result.error}). Check your network connection and try again.`, ); } + // Reachable, but nothing published on this channel yet (e.g. a stable + // install while only betas exist). Not an error — just nothing to do. + if (result.version === null) { + logger.log( + ui.info(`No newer release available on the ${result.channel} channel.`), + ); + return; + } + + const latest = result.version; if (!versionService.isOutdated(current, latest)) { logger.log( ui.success(`Already on the latest version (${colors.highlight(current)})`), @@ -85,8 +95,11 @@ export const upgradeCommand = defineCommand({ if (process.platform === 'win32') { // Windows can't replace a running .exe; defer to a re-run of the installer. const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE; + // Prerelease users need the beta channel opt-in or the installer resolves + // the (currently non-existent) stable release. + const betaHint = result.channel === 'beta' ? '$env:DCD_BETA=1; ' : ''; throw new CliError( - `Automatic upgrade on Windows is not yet supported. Re-run the installer:\n irm ${base}/install.ps1 | iex`, + `Automatic upgrade on Windows is not yet supported. Re-run the installer:\n ${betaHint}irm ${base}/install.ps1 | iex`, ); } diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..bfa4354 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,10 @@ +/** + * Build-time constants injected by `bun --define` (see + * scripts/build-binaries.mjs). Declared as an ambient global — this file must + * stay free of top-level import/export or it stops being a global declaration. + * + * `__DCD_CLI_VERSION__` holds the package version stamped into the compiled + * standalone binary. On the npm/tsx path it is never defined; `getCliVersion()` + * guards every read with `typeof`. + */ +declare const __DCD_CLI_VERSION__: string | undefined; diff --git a/src/methods.ts b/src/methods.ts index 42ab30d..ebc7a0f 100644 --- a/src/methods.ts +++ b/src/methods.ts @@ -924,8 +924,9 @@ async function uploadToBackblaze( console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`); } - // Don't throw - we don't want Backblaze failures to block the primary upload - console.warn(`Warning: Backblaze upload failed with status ${response.status}`); + // Don't throw and don't warn — Backblaze is the primary attempt and the + // Supabase fallback usually recovers. A user-facing error is raised only + // if every strategy fails (see validateUploadResults). return false; } @@ -950,13 +951,10 @@ async function uploadToBackblaze( if (debug) { console.error('[DEBUG] Network error detected - could be DNS, connection, or SSL issue'); } - - console.warn('Warning: Backblaze upload failed due to network error'); - } else { - // Don't throw - we don't want Backblaze failures to block the primary upload - console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`); } + // Don't throw and don't warn — the Supabase fallback usually recovers, and + // validateUploadResults raises a user-facing error only if all fail. return false; } } @@ -1088,11 +1086,9 @@ function logBackblazeUploadError(error: unknown, debug: boolean): void { } } - if (error instanceof Error && error.message.includes('network error')) { - console.warn('Warning: Backblaze large file upload failed due to network error'); - } else { - console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`); - } + // No user-facing warning: Backblaze is the primary attempt and the Supabase + // fallback usually recovers; validateUploadResults raises the only + // user-facing error, and only when every strategy fails. } /** diff --git a/src/services/results-polling.service.ts b/src/services/results-polling.service.ts index 881af60..c1cf800 100644 --- a/src/services/results-polling.service.ts +++ b/src/services/results-polling.service.ts @@ -9,6 +9,7 @@ import { formatDurationSeconds } from '../methods.js'; import type { AuthContext } from '../types/domain/auth.types.js'; import { paths } from '../types/generated/schema.types.js'; import { checkInternetConnectivity } from '../utils/connectivity.js'; +import { isCI } from '../utils/ci.js'; import { ux } from '../utils/progress.js'; import { colors, formatTestSummary, statusPalette, table } from '../utils/styling.js'; import { type Field, ui } from '../utils/ui.js'; @@ -160,8 +161,16 @@ export class ResultsPollingService { let realtimeEnabled = false; let statusBody = ''; let nextPollAt: null | number = null; + // The animated footer/countdown only makes sense on a TTY. In CI/pipes it + // would flood logs (a fresh line per frame), so we drop it and let the + // progress adapter print one line per distinct status change instead. + const interactive = !json && !isCI(); const renderStatus = () => { if (json) return; + if (!interactive) { + ux.action.status = statusBody; + return; + } const footer = this.buildStatusFooter( realtimeEnabled, subscription?.isConnected() ?? false, @@ -201,8 +210,11 @@ export class ResultsPollingService { // Tick the live footer once a second so the countdown actually counts down // (the spinner's own frames don't recompute our message). Unref'd so it - // never keeps the process alive on its own. - const ticker: NodeJS.Timeout | null = json ? null : setInterval(renderStatus, 1000); + // never keeps the process alive on its own. Only on an interactive TTY — + // a 1s ticker in CI would reprint the status every second. + const ticker: NodeJS.Timeout | null = interactive + ? setInterval(renderStatus, 1000) + : null; ticker?.unref?.(); try { diff --git a/src/services/version.service.ts b/src/services/version.service.ts index 953a4b9..1ed8239 100644 --- a/src/services/version.service.ts +++ b/src/services/version.service.ts @@ -3,6 +3,18 @@ import { CompatibilityData } from '../utils/compatibility.js'; const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json'; const MANIFEST_TIMEOUT_MS = 3000; +export type ReleaseChannel = 'beta' | 'stable'; + +/** + * Outcome of a release-manifest lookup. `ok: true` means the manifest was + * reachable — `version` is the published version on the channel, or `null` when + * nothing is published there yet. `ok: false` means the lookup itself failed + * (network/timeout/non-2xx). + */ +export type LatestVersionResult = + | { ok: true; channel: ReleaseChannel; version: null | string } + | { ok: false; error: string }; + /** * Compare two semantic versions per SemVer 2.0.0 precedence rules. * Returns a negative number if `a < b`, positive if `a > b`, and 0 if equal. @@ -62,19 +74,42 @@ export class VersionService { /** * Fetch the latest published CLI version from the release manifest. * Works for both npm- and binary-installed users (no `npm` shell-out). - * Silently returns null on any failure — this check is informational only. + * + * The result is discriminated so callers can tell "reachable, but no release + * on this channel yet" (`ok: true, version: null`) apart from an actual + * network/manifest failure (`ok: false`) — the old single-`null` return + * conflated the two and produced a misleading "check your network" error + * during the beta. Prerelease installs (current version contains `-`) query + * the opt-in beta channel; everyone else gets the stable channel. */ - async checkLatestCliVersion(): Promise { - const url = process.env.DCD_MANIFEST_URL ?? DEFAULT_MANIFEST_URL; + async checkLatestCliVersion( + currentVersion?: string, + ): Promise { + const channel: ReleaseChannel = + currentVersion?.includes('-') ? 'beta' : 'stable'; + const base = process.env.DCD_MANIFEST_URL ?? DEFAULT_MANIFEST_URL; + const url = + channel === 'beta' + ? `${base}${base.includes('?') ? '&' : '?'}channel=beta` + : base; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), MANIFEST_TIMEOUT_MS); try { const res = await fetch(url, { signal: controller.signal }); - if (!res.ok) return null; + if (!res.ok) { + return { ok: false, error: `manifest responded with HTTP ${res.status}` }; + } const data = (await res.json()) as { version?: unknown }; - return typeof data.version === 'string' ? data.version : null; - } catch { - return null; + return { + ok: true, + channel, + version: typeof data.version === 'string' ? data.version : null, + }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; } finally { clearTimeout(timer); } diff --git a/src/utils/cli.ts b/src/utils/cli.ts index 1a320f8..6b152cd 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -12,9 +12,19 @@ import { telemetry } from '../services/telemetry.service.js'; import { symbols } from './styling.js'; -// Resolve version at runtime — read the file rather than importing it, so -// package.json never gets pulled into the tsc program / dist rootDir. +// Resolve version at runtime. The bun-compiled binary can't read package.json +// off disk (it isn't bundled next to the embedded module), so the build stamps +// the version in via `bun --define __DCD_CLI_VERSION__` (see +// scripts/build-binaries.mjs). Prefer that constant; on the npm/tsx path the +// identifier was never defined, so `typeof` is 'undefined' (no ReferenceError) +// and we fall back to reading package.json. export function getCliVersion(): string { + if ( + typeof __DCD_CLI_VERSION__ === 'string' && + __DCD_CLI_VERSION__.length > 0 + ) { + return __DCD_CLI_VERSION__; + } try { const pkg = JSON.parse( readFileSync(new URL('../../package.json', import.meta.url), 'utf8'), @@ -114,8 +124,7 @@ export function validateEnum( /** * Coerce a flag value (possibly a single string, array, or undefined) into a * flat string array. Comma-separated values inside each entry are split out. - * Used for repeatable flags like --include-tags, --env, --metadata where citty - * surfaces a string (single use) or string[] (repeated). + * Pair with {@link collectRepeatedFlag} for repeatable flags. */ export function coerceArray( value: string | string[] | undefined, @@ -127,6 +136,40 @@ export function coerceArray( return arr.flatMap((v) => v.split(',')); } +/** + * Collect every occurrence of a repeatable flag from raw argv, in order. + * + * citty 0.2.2 delegates to Node's `parseArgs`, which — without `multiple: true` + * (unsupported by citty's ArgsDef) — keeps only the LAST value of a repeated + * `type: 'string'` flag. So `-e A=1 -e B=2` collapses to just `B=2`. We recover + * all occurrences by scanning rawArgs ourselves (same approach as + * `recoverFlagValue` in commands/live.ts). + * + * `names` lists every spelling of one logical flag, e.g. ['--env', '-e']. + * Handles both `--flag value` (consuming the next token, so values starting + * with `-` survive) and `--flag=value`. Feed the result through + * {@link coerceArray} for comma-splitting where appropriate. + */ +export function collectRepeatedFlag( + rawArgs: string[], + names: string[], +): string[] { + const out: string[] = []; + for (let i = 0; i < rawArgs.length; i++) { + const arg = rawArgs[i]; + const eqName = names.find((n) => arg.startsWith(`${n}=`)); + if (eqName) { + out.push(arg.slice(eqName.length + 1)); + continue; + } + if (names.includes(arg) && i + 1 < rawArgs.length) { + out.push(rawArgs[i + 1]); + i++; // consume the value so a leading-dash value isn't re-read as a flag + } + } + return out; +} + /** * Parse an integer flag. Returns undefined if the value is undefined/empty. * Throws CliError if the value is not a valid integer. diff --git a/src/utils/progress.ts b/src/utils/progress.ts index c4d33ed..fcf8d45 100644 --- a/src/utils/progress.ts +++ b/src/utils/progress.ts @@ -3,25 +3,53 @@ * drop-in API for existing services that used oclif's `ux.action` / `ux.info`. * * Keeps call sites unchanged while migrating away from @oclif/core. + * + * TTY-awareness: @clack/prompts' spinner animates on a timer and, when stdout + * isn't a TTY (CI, pipes, redirects), it can't rewrite a line in place — every + * frame becomes a fresh line, flooding logs with hundreds of duplicates. In + * non-interactive environments we skip the spinner entirely and instead print a + * plain line once per *distinct* status, so CI logs show real progress without + * the flood. */ import * as p from '@clack/prompts'; +import { isCI } from './ci.js'; + type ClackSpinner = ReturnType; class Action { private current: ClackSpinner | null = null; private _status = ''; + // Last line emitted in non-interactive mode, for de-duplication. + private _lastPrinted = ''; + + private interactive(): boolean { + return process.stdout.isTTY === true && !isCI(); + } start(title: string, initialStatus?: string, _opts?: unknown): void { + this._status = initialStatus ?? ''; + const line = initialStatus ? `${title} — ${initialStatus}` : title; + if (!this.interactive()) { + this.current = null; + this.print(line); + return; + } if (this.current) { this.current.stop(); } this.current = p.spinner(); - this._status = initialStatus ?? ''; - this.current.start(initialStatus ? `${title} — ${initialStatus}` : title); + this.current.start(line); } stop(message?: string): void { + if (!this.interactive()) { + if (message) this.print(message); + this.current = null; + this._status = ''; + this._lastPrinted = ''; + return; + } if (!this.current) { if (message) { // eslint-disable-next-line no-console @@ -36,7 +64,12 @@ class Action { set status(value: string) { this._status = value; - if (this.current && value) { + if (!value) return; + if (!this.interactive()) { + this.print(value); + return; + } + if (this.current) { this.current.message(value); } } @@ -44,6 +77,15 @@ class Action { get status(): string { return this._status; } + + // Emit a line only when it differs from the previous one, so repeated polls + // with no state change stay quiet. + private print(line: string): void { + if (line === this._lastPrinted) return; + this._lastPrinted = line; + // eslint-disable-next-line no-console + console.log(line); + } } export const ux = { From 7fd253a73cd5bcf2d32e1b0459b732d957c877fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:18:09 +0100 Subject: [PATCH 13/16] 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> --- .release-please-manifest-beta.json | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json index 86cb1d6..26587a1 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.0.0-beta.1" + ".": "5.0.0-beta.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 25de72f..de0dc2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [5.0.0-beta.2](https://github.com/devicecloud-dev/dcd-cli/compare/v5.0.0-beta.1...v5.0.0-beta.2) (2026-06-24) + + +### Features + +* **cloud:** drop legacy Maestro removed-versions block; soft-warn on deprecated 1.39.5/1.41.0 ([62c7672](https://github.com/devicecloud-dev/dcd-cli/commit/62c767295cb99339cbc3326c6bf319caf637d649)) +* **cloud:** Maestro deprecation — drop legacy hard-block, soft-warn 1.39.5/1.41.0 ([ee995e7](https://github.com/devicecloud-dev/dcd-cli/commit/ee995e7ebefb7ddfd3678bea27adf4751a39e879)) +* **cloud:** warn on deprecated iOS 16 (removal 2026-08-23) ([d794695](https://github.com/devicecloud-dev/dcd-cli/commit/d794695529c9dce938d16199e336d6698e21bff9)) +* **cloud:** warn on deprecated iOS 16 (removal 2026-08-23) ([ea62f72](https://github.com/devicecloud-dev/dcd-cli/commit/ea62f724653b3e1173036c4abe66aa4e110c0a0e)) + + +### Bug Fixes + +* **ci:** keep dependabot and fork PRs green ([#46](https://github.com/devicecloud-dev/dcd-cli/issues/46)) ([dc87257](https://github.com/devicecloud-dev/dcd-cli/commit/dc872572846fbe0d9760902cda3380edee1ef2ff)) +* **installer:** make beta opt-in, add stable/beta channels ([ec16bcc](https://github.com/devicecloud-dev/dcd-cli/commit/ec16bccd044f892f7fd1997aac977c77aa14376d)) +* **installer:** make beta opt-in, default to stable channel ([88c3532](https://github.com/devicecloud-dev/dcd-cli/commit/88c3532f8a3c6de7210c2d66d819838ea4c99fad)) +* suppress refresh countdown in quiet mode ([10eade4](https://github.com/devicecloud-dev/dcd-cli/commit/10eade42c80f42df05b165e3f83e1190aeabfd80)) +* suppress refresh countdown in quiet mode ([d543981](https://github.com/devicecloud-dev/dcd-cli/commit/d543981b0e2dc274d664bf378da4154a77a6d2e8)) +* **upgrade:** compare prerelease versions per SemVer ([f77841f](https://github.com/devicecloud-dev/dcd-cli/commit/f77841fae397e3b8d88b7ba6876b98f65026f089)) +* **upgrade:** compare prerelease versions per SemVer ([6c533e7](https://github.com/devicecloud-dev/dcd-cli/commit/6c533e7ae9e55b64e5ffe63a9c9a6934e9250938)) +* v5 release blockers — installer, binary version, repeated flags, upgrade, CI output ([d780e55](https://github.com/devicecloud-dev/dcd-cli/commit/d780e55093b314fe9855890db145188e2735beb3)) +* v5 release blockers — installer, binary version, repeated flags,… ([#51](https://github.com/devicecloud-dev/dcd-cli/issues/51)) ([d780e55](https://github.com/devicecloud-dev/dcd-cli/commit/d780e55093b314fe9855890db145188e2735beb3)) + ## [5.0.0-beta.1](https://github.com/devicecloud-dev/dcd-cli/compare/v5.0.0-beta.0...v5.0.0-beta.1) (2026-06-23) diff --git a/package.json b/package.json index 32464d8..a5ee017 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test": "node scripts/test-runner.mjs", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" }, From bd6029847b9eccfe9078ae21b40ad548e4ef8985 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:35:04 +0100 Subject: [PATCH 14/16] fix: stop CLA locking release PRs (breaks release pipeline) (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .github/workflows/cla.yml | 5 +++++ .github/workflows/claude-code-review.yml | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index a0306ed..ba697bc 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -59,6 +59,11 @@ jobs: path-to-signatures: "signatures/version1/cla.json" path-to-document: "https://github.com/devicecloud-dev/dcd-cli/blob/dev/CLA.md" branch: "cla-signatures" + # Do NOT lock the PR on merge (the action's default is true). release-please + # comments on its release PR *after* merge; a locked conversation makes that + # comment fail and takes down the whole Release job (npm publish + binaries + # never run). Keeping this false is load-bearing for the release pipeline. + lock-pullrequest-aftermerge: false # Internal maintainers (covered by employment/CCLA) + bots skip the prompt. allowlist: riglar,finalerock44,dependabot[bot],renovate[bot],*[bot] # Customise the bot's prompts if desired: diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 3419419..1eac5d0 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,10 +12,11 @@ on: jobs: claude-review: - # The review needs CLAUDE_CODE_OAUTH_TOKEN, which is NOT exposed to PRs that - # run without secrets — Dependabot PRs and PRs from forks. Skip them so the - # check doesn't fail with an empty token; same-repo PRs only. - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} + # Skip PRs we shouldn't (or can't) review: + # - Dependabot / forks: no CLAUDE_CODE_OAUTH_TOKEN, so the action would fail. + # - release-please release PRs: just version bumps + changelog — nothing to + # review, and it must never block a release. + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release-please--') }} runs-on: ubuntu-latest permissions: contents: read From a02584fe574fd687539d0be424c658c743aaae8a Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:31:42 +0100 Subject: [PATCH 15/16] 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. --- src/commands/live.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/live.ts b/src/commands/live.ts index 2e2e017..cf925a5 100644 --- a/src/commands/live.ts +++ b/src/commands/live.ts @@ -171,6 +171,14 @@ const startSub = defineCommand({ throw new CliError('--android-device and --android-api-level must be provided together.'); } + logger.log(ui.warn(colors.bold('Live is in beta'))); + logger.log( + ui.branch([ + 'Live device sessions are a beta feature, billed at $0.03/min.', + `${colors.dim('Not enrolled?')} Contact support to request access.`, + ]), + ); + logger.log(ui.running(`Starting ${platform} live session…`)); const session = await ApiGateway.startLiveSession(apiUrl, auth, { From 3eedde30551dfc74a39d9860ed41bbe98395b1c8 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, 25 Jun 2026 10:40:06 +0100 Subject: [PATCH 16/16] 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> --- .release-please-manifest-beta.json | 2 +- CHANGELOG.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 26587a1..a690fc9 100644 --- a/.release-please-manifest-beta.json +++ b/.release-please-manifest-beta.json @@ -1,3 +1,3 @@ { - ".": "5.0.0-beta.2" + ".": "5.0.0-beta.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index de0dc2d..f4548ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [5.0.0-beta.3](https://github.com/devicecloud-dev/dcd-cli/compare/v5.0.0-beta.2...v5.0.0-beta.3) (2026-06-25) + + +### Features + +* **live:** add a beta warning to `dcd live start` ([#54](https://github.com/devicecloud-dev/dcd-cli/issues/54)) ([a02584f](https://github.com/devicecloud-dev/dcd-cli/commit/a02584fe574fd687539d0be424c658c743aaae8a)) + + +### Bug Fixes + +* stop CLA locking release PRs (breaks release pipeline) ([#52](https://github.com/devicecloud-dev/dcd-cli/issues/52)) ([bd60298](https://github.com/devicecloud-dev/dcd-cli/commit/bd6029847b9eccfe9078ae21b40ad548e4ef8985)) + ## [5.0.0-beta.2](https://github.com/devicecloud-dev/dcd-cli/compare/v5.0.0-beta.1...v5.0.0-beta.2) (2026-06-24) diff --git a/package.json b/package.json index a5ee017..7dd3ebe 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test": "node scripts/test-runner.mjs", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, - "version": "5.0.0-beta.2", + "version": "5.0.0-beta.3", "bugs": { "url": "https://discord.gg/gm3mJwcNw8" },