Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"test:build-zip-filter": "bun test/test-build-zip-filter.mjs",
"test:checksum": "bun test/test-checksum-algorithm.mjs",
"test:build-needed": "bun test/test-build-needed.mjs",
"test:build-cancellation": "bun test/test-build-cancellation.mjs",
"test:ci-prompts": "bun test/test-ci-prompts.mjs",
"test:ci-secrets": "bun test/test-ci-secrets.mjs",
"test:android-onboarding-progress": "bun test/test-android-onboarding-progress.mjs",
Expand Down Expand Up @@ -174,7 +175,7 @@
"test:android-version": "bun test/test-android-version.mjs",
"test:platform-flow-contract": "bun test/test-platform-flow-contract.mjs",
"test:tail-engine-shared": "bun test/test-tail-engine-shared.mjs",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:bundle-validation && bun run test:functional && bun run test:semver && bun run test:auto-bump-version && bun run test:auto-bump-ai-diff && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:package-json-guard && bun run test:credentials && bun run test:credentials-export && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:cli-headers && bun run test:min-cli-version && bun run test:authenticated-command-invocation && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:app-list-output-text && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:cli-recovery && bun run test:create-supabase-client && bun run test:build-platform-selection && bun run test:builder-project-discovery && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:channel-add-exists && bun run test:wait-log && bun run test:init-guardrails && bun run test:init-replay && bun run test:init-telemetry && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:android-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:2fa-compliance-network && bun run test:organization-set-api-host && bun run test:trial-warning && bun run test:plan-validation",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:bundle-validation && bun run test:functional && bun run test:semver && bun run test:auto-bump-version && bun run test:auto-bump-ai-diff && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:package-json-guard && bun run test:credentials && bun run test:credentials-export && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:build-cancellation && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:cli-headers && bun run test:min-cli-version && bun run test:authenticated-command-invocation && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:app-list-output-text && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:cli-recovery && bun run test:create-supabase-client && bun run test:build-platform-selection && bun run test:builder-project-discovery && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:channel-add-exists && bun run test:wait-log && bun run test:init-guardrails && bun run test:init-replay && bun run test:init-telemetry && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:android-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:2fa-compliance-network && bun run test:organization-set-api-host && bun run test:trial-warning && bun run test:plan-validation",
"test:build-platform-selection": "bun test/test-build-platform-selection.mjs",
"test:builder-project-discovery": "bun test/test-builder-project-discovery.mjs",
"test:ai-log-capture": "bun test/test-ai-log-capture.mjs",
Expand Down
73 changes: 73 additions & 0 deletions cli/src/build/cancellation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export type BuildCancellationResult
= | { ok: true }
| { ok: false, message: string }

// npx can deliver one physical Ctrl+C to the CLI twice: once through the
// foreground process group and once when its wrapper forwards SIGINT.
export const DUPLICATE_SIGINT_WINDOW_MS = 500

export interface BuildCancellationRequestOptions {
url: string
headers: Record<string, string>
appId: string
fetchImpl?: typeof fetch
timeoutMs?: number
}

export async function requestBuildCancellation(options: BuildCancellationRequestOptions): Promise<BuildCancellationResult> {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), options.timeoutMs ?? 4_000)

try {
const response = await (options.fetchImpl ?? fetch)(options.url, {
method: 'POST',
headers: options.headers,
body: JSON.stringify({ app_id: options.appId }),
signal: abortController.signal,
})
if (!response.ok) {
const status = `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
return { ok: false, message: `Build cancellation request failed: HTTP ${status}` }
}
return { ok: true }
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { ok: false, message: `Build cancellation request failed: ${message}` }
}
finally {
clearTimeout(timeout)
}
}

export interface BuildCancellationSignalHandlerOptions {
requestCancellation: () => Promise<BuildCancellationResult>
onCancellationStarted: () => void
onCancellationResult: (result: BuildCancellationResult) => void
abortLogStream: () => void
forceExit: (code: number) => void
now?: () => number
duplicateSignalWindowMs?: number
}

export function createBuildCancellationSignalHandler(options: BuildCancellationSignalHandlerOptions): () => Promise<void> {
const now = options.now ?? Date.now
const duplicateSignalWindowMs = options.duplicateSignalWindowMs ?? DUPLICATE_SIGINT_WINDOW_MS
let firstSignalAt: number | null = null

return async () => {
const signalAt = now()
if (firstSignalAt !== null) {
if (signalAt - firstSignalAt <= duplicateSignalWindowMs)
return
options.forceExit(1)
return
}

firstSignalAt = signalAt
options.onCancellationStarted()
const result = await options.requestCancellation()
options.onCancellationResult(result)
options.abortLogStream()
}
}
62 changes: 23 additions & 39 deletions cli/src/build/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { offerSupportUploadBeforeAi } from '../support/support-upload-prompt.js'
import { buildCliRequestHeaders } from '../analytics/cli-headers'
import { assertCliPermission, canPromptInteractively, createSupabaseClient, findSavedKey, getConfig, getOrganizationId, getRemoteConfig, sendEvent, trimTrailingSlashes, TUS_UPLOAD_RETRY_DELAYS } from '../utils'
import { syncAndroidVersion } from './android-version'
import { createBuildCancellationSignalHandler, requestBuildCancellation } from './cancellation'
import { mergeCredentials, MIN_OUTPUT_RETENTION_SECONDS, parseAndroidPlayStoreReleaseStatus, parseAndroidPlayStoreTrack, parseInAppUpdatePriority, parseOptionalBoolean, parseOutputRetentionSeconds } from './credentials'
import { buildProvisioningMap } from './credentials-command'
import { withCwd } from './cwd'
Expand Down Expand Up @@ -2212,46 +2213,29 @@ export async function requestBuildInternal(appId: string, options: BuildRequestO
log.info('Streaming build logs...')

const abortController = new AbortController()
let cancelRequested = false
const cancelBuild = async () => {
if (cancelRequested)
return
cancelRequested = true
const cancelAbort = new AbortController()
const timeout = setTimeout(() => cancelAbort.abort(), 4000)
try {
await fetch(`${host}/build/cancel/${buildRequest.job_id}`, {
method: 'POST',
headers: buildCliRequestHeaders({
'Content-Type': 'application/json',
authorization: options.apikey,
}),
body: JSON.stringify({ app_id: appId }),
signal: cancelAbort.signal,
})
}
catch (err) {
appendInternalLog(`build cancel request errored (ignored): ${err instanceof Error ? err.message : String(err)}`)
// ignore cancellation errors
}
finally {
clearTimeout(timeout)
}
}

const onSigint = async () => {
try {
if (cancelRequested) {
process.exit(1)
}
const onSigint = createBuildCancellationSignalHandler({
requestCancellation: () => requestBuildCancellation({
url: `${host}/build/cancel/${buildRequest.job_id}`,
headers: buildCliRequestHeaders({
'Content-Type': 'application/json',
authorization: options.apikey,
}),
appId,
}),
onCancellationStarted: () => {
log.warn('Canceling build... (press Ctrl+C again to force quit)')
await cancelBuild()
abortController.abort()
}
catch {
// Prevent unhandled rejection from crashing the process
}
}
},
onCancellationResult: (result) => {
if (result.ok) {
log.success('Build cancellation requested.')
return
}
appendInternalLog(result.message)
log.warn(`${result.message}. The remote build may still be running.`)
},
abortLogStream: () => abortController.abort(),
forceExit: code => process.exit(code),
})

process.on('SIGINT', onSigint)

Expand Down
Loading
Loading