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
1 change: 1 addition & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ npx @capgo/cli@latest channel set production com.example.app --bundle 1.0.0 --st
| **--self-assign** | <code>boolean</code> | Allow device to self-assign to this channel |
| **--no-self-assign** | <code>boolean</code> | Disable devices to self-assign to this channel |
| **--disable-auto-update** | <code>string</code> | Block updates by type: major, minor, metadata, patch, or none (allows all) |
| **--update-package** | <code>string</code> | Serve zip, delta, or both: all, zip, delta, zip_from_builtin, or delta_from_builtin |
| **--rollout-bundle** | <code>string</code> | Bundle version to release gradually on this channel |
| **--rollout-percentage** | <code>string</code> | Rollout percentage from 0 to 100 |
| **--rollout-percentage-bps** | <code>string</code> | Rollout percentage in basis points from 0 to 10000 |
Expand Down
41 changes: 25 additions & 16 deletions cli/src/bundle/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { showReplicationProgress } from '../replicationProgress'
import { CliUserError } from '../shared/cli-user-error'
import { formatTable } from '../terminal-table'
import { usesAlwaysDirectUpdate } from '../updaterConfig'
import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, deltaManifestTooLargeMessage, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, invokeCapgoCliApi, isCompatible, isDeprecatedPluginVersion, MAX_MANIFEST_ENTRIES, regexSemver, resolveUserIdFromApiKey, sendEvent, setVersionManifest, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, UPLOAD_TIMEOUT_ERROR_NAME, uploadTimeoutMessage, uploadTUS, uploadUrl, zipFile } from '../utils'
import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, channelUpdatePackageCliError, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, deltaManifestTooLargeMessage, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, invokeCapgoCliApi, isCompatible, isDeprecatedPluginVersion, MAX_MANIFEST_ENTRIES, regexSemver, resolveUserIdFromApiKey, sendEvent, setVersionManifest, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, UPLOAD_TIMEOUT_ERROR_NAME, uploadTimeoutMessage, uploadTUS, uploadUrl, zipFile } from '../utils'
import type { AutoBumpLevel } from '../versionHelpers'
import { autoBumpVersionBy, getVersionSuggestions, interactiveVersionBump, normalizeAutoBumpInput } from '../versionHelpers'
import { resolveAutoBumpLevelFromAi } from './auto-bump-ai'
Expand Down Expand Up @@ -65,6 +65,11 @@ function uploadFail(message: string): never {
throw new CliUserError(message)
}

async function uploadFailIfChannelError(error: unknown, fallback: () => string | Promise<string>): Promise<never> {
const packageError = await channelUpdatePackageCliError(error)
uploadFail(packageError || await fallback())
}

// A user-initiated cancel is an expected exit, not a crash: warn instead of
// error, and throw `CliUserError` so error tracking skips it.
function uploadCancel(): never {
Expand Down Expand Up @@ -1025,8 +1030,9 @@ async function promoteExistingChannel(
supaAnon: options?.supaAnon,
})

if (error)
uploadFail(`Cannot set channel because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
if (error) {
await uploadFailIfChannelError(error, async () => `Cannot set channel because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
}

const bundleUrl = `${localConfig.hostWeb}/app/${appid}/channel/${targetChannel.id}`
if (targetChannel.public)
Expand Down Expand Up @@ -1088,16 +1094,18 @@ async function setVersionInChannel(
owner_org: orgId,
...(selfAssign ? { allow_device_self_set: true } : {}),
})
if (dbError3)
uploadFail(`Cannot set channel because this API key does not have the required RBAC permission. ${formatError(dbError3)}`)
const bundleUrl = `${localConfig.hostWeb}/app/${appid}/channel/${data.id}`
if (data?.public)
log.info('Your update is now available in your public channel 🎉')
else if (data?.id)
log.info(`Link device to this bundle to try it: ${bundleUrl}`)

if (displayBundleUrl)
log.info(`Bundle url: ${bundleUrl}`)
if (dbError3) {
await uploadFailIfChannelError(dbError3, () => `Cannot set channel because this API key does not have the required RBAC permission. ${formatError(dbError3)}`)
}
if (data?.id) {
const bundleUrl = `${localConfig.hostWeb}/app/${appid}/channel/${data.id}`
if (data.public)
log.info('Your update is now available in your public channel 🎉')
else
log.info(`Link device to this bundle to try it: ${bundleUrl}`)
if (displayBundleUrl)
log.info(`Bundle url: ${bundleUrl}`)
}
return true
}

Expand All @@ -1117,7 +1125,7 @@ async function setVersionInChannel(
supaAnon: cliHost?.supaAnon,
})
if (error) {
uploadFail(`Cannot create channel and set its bundle because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
await uploadFailIfChannelError(error, async () => `Cannot create channel and set its bundle because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
}

const createdChannel = data as { id?: unknown, public?: unknown } | null
Expand Down Expand Up @@ -1204,8 +1212,9 @@ async function setRolloutVersionInChannel(
supaAnon: cliHost?.supaAnon,
})

if (rolloutError)
uploadFail(`Cannot set rollout in channel ${await formatFunctionInvokeError(rolloutError)}`)
if (rolloutError) {
await uploadFailIfChannelError(rolloutError, async () => `Cannot set rollout in channel ${await formatFunctionInvokeError(rolloutError)}`)
}

const bundleUrl = `${localConfig.hostWeb}/app/${appid}/channel/${targetChannel.id}`
log.info(`Set ${appid} channel ${channel} rollout target to @${bundle} (${formatRolloutPercentage(rolloutPercentageBps)})`)
Expand Down
31 changes: 30 additions & 1 deletion cli/src/channel/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getActiveAppVersions, getVersionData } from '../api/versions'
import { sendUpdateNotificationsForChannels } from '../notifications/send-update'
import { printPreviewQrForResolvedTarget, resolveChannelPreviewTarget } from '../preview/qr'
import { formatTable } from '../terminal-table'
import { checkCompatibilityNativePackages, checkPlanValid, createSupabaseClient, findSavedKey, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getOrganizationId, invokeCapgoCliApi, isCompatible, resolveUserIdFromApiKey, sendEvent } from '../utils'
import { channelUpdatePackageCliError, checkCompatibilityNativePackages, checkPlanValid, createSupabaseClient, findSavedKey, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getOrganizationId, invokeCapgoCliApi, isCompatible, resolveUserIdFromApiKey, sendEvent } from '../utils'

/**
* Display a compatibility table for the given packages
Expand All @@ -34,6 +34,7 @@ function displayCompatibilityTable(packages: Compatibility[]) {
export type { OptionsSetChannel } from '../schemas/channel'

const disableAutoUpdatesPossibleOptions = ['major', 'minor', 'metadata', 'patch', 'none']
const updatePackagePossibleOptions = ['all', 'zip', 'delta', 'zip_from_builtin', 'delta_from_builtin'] as const

function assertIntegerInRange(value: number, label: string, min: number, max: number) {
if (!Number.isFinite(value) || !Number.isInteger(value) || value < min || value > max)
Expand Down Expand Up @@ -93,6 +94,7 @@ export async function setChannelInternal(channel: string, appId: string, options
android,
selfAssign,
disableAutoUpdate,
updatePackage,
dev,
emulator,
device,
Expand Down Expand Up @@ -152,6 +154,7 @@ export async function setChannelInternal(channel: string, appId: string, options
&& device == null
&& prod == null
&& disableAutoUpdate == null
&& updatePackage == null
&& rolloutBundle == null
&& rolloutPercentage == null
&& rolloutPercentageBps == null
Expand Down Expand Up @@ -201,6 +204,7 @@ export async function setChannelInternal(channel: string, appId: string, options
|| android != null
|| selfAssign != null
|| disableAutoUpdate != null
|| updatePackage != null
|| dev != null
|| emulator != null
|| device != null
Expand Down Expand Up @@ -577,6 +581,17 @@ export async function setChannelInternal(channel: string, appId: string, options
log.info(`Set ${appId} channel: ${channel} to ${finalDisableAutoUpdate} disable update strategy to this channel`)
}

if (updatePackage != null) {
if (!updatePackagePossibleOptions.includes(updatePackage)) {
if (!silent)
log.error(`Update package ${updatePackage} is not known. The possible values are: ${updatePackagePossibleOptions.join(', ')}.`)
throw new Error(`Unknown update package ${updatePackage}`)
}
channelPayload.update_package = updatePackage
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (!silent)
log.info(`Set ${appId} channel: ${channel} update package to ${updatePackage}`)
}

if (hasStableBundlePromotion && !hasSettingsUpdate) {
const { error } = await invokeCapgoCliApi('bundle', {
apikey: options.apikey!,
Expand All @@ -590,6 +605,12 @@ export async function setChannelInternal(channel: string, appId: string, options
supaAnon: options.supaAnon,
})
if (error) {
const packageError = await channelUpdatePackageCliError(error)
if (packageError) {
if (!silent)
log.error(packageError)
throw new Error(packageError)
}
if (!silent)
log.error('Cannot set channel because this API key does not have the required RBAC permission.')
throw new Error('API key is not allowed to set this channel')
Expand Down Expand Up @@ -627,6 +648,8 @@ export async function setChannelInternal(channel: string, appId: string, options
channelBody.disableAutoUpdateUnderNative = channelPayload.disable_auto_update_under_native
if (channelPayload.disable_auto_update !== undefined)
channelBody.disableAutoUpdate = channelPayload.disable_auto_update
if (channelPayload.update_package !== undefined)
channelBody.updatePackage = channelPayload.update_package
if (channelPayload.ios !== undefined)
channelBody.ios = channelPayload.ios
if (channelPayload.android !== undefined)
Expand Down Expand Up @@ -696,6 +719,12 @@ export async function setChannelInternal(channel: string, appId: string, options
supaAnon: options.supaAnon,
})
if (dbError) {
const packageError = await channelUpdatePackageCliError(dbError)
if (packageError) {
if (!silent)
log.error(packageError)
throw new Error(packageError)
}
if (!silent)
log.error('Cannot set channel because this API key does not have the required RBAC permission.')
throw new Error('API key is not allowed to set this channel')
Expand Down
1 change: 1 addition & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ Example: npx @capgo/cli@latest channel set production com.example.app --bundle 1
.option('--self-assign', `Allow device to self-assign to this channel`)
.option('--no-self-assign', `Disable devices to self-assign to this channel`)
.option('--disable-auto-update <disableAutoUpdate>', `Block updates by type: major, minor, metadata, patch, or none (allows all)`)
.option('--update-package <updatePackage>', `Serve zip, delta, or both: all, zip, delta, zip_from_builtin, or delta_from_builtin`)
.option('--rollout-bundle <rolloutBundle>', `Bundle version to release gradually on this channel`)
.option('--rollout-percentage <rolloutPercentage>', `Rollout percentage from 0 to 100`, value => Number.parseFloat(value))
.option('--rollout-percentage-bps <rolloutPercentageBps>', `Rollout percentage in basis points from 0 to 10000`, value => Number.parseInt(value, 10))
Expand Down
1 change: 1 addition & 0 deletions cli/src/schemas/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const optionsSetChannelSchema = optionsBaseSchema.extend({
android: z.boolean().optional(),
selfAssign: z.boolean().optional(),
disableAutoUpdate: z.string().optional(),
updatePackage: z.enum(['all', 'zip', 'delta', 'zip_from_builtin', 'delta_from_builtin']).optional(),
dev: z.boolean().optional(),
emulator: z.boolean().optional(),
device: z.boolean().optional(),
Expand Down
1 change: 1 addition & 0 deletions cli/src/schemas/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const updateChannelOptionsBaseSchema = z.object({
android: z.boolean().optional(),
selfAssign: z.boolean().optional(),
disableAutoUpdate: z.string().optional(),
updatePackage: z.enum(['all', 'zip', 'delta', 'zip_from_builtin', 'delta_from_builtin']).optional(),
dev: z.boolean().optional(),
emulator: z.boolean().optional(),
device: z.boolean().optional(),
Expand Down
5 changes: 3 additions & 2 deletions cli/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {
ZipBundleOptions,
} from './schemas/sdk'
import type { Organization } from './utils'
import { buildCliRequestHeaders } from './analytics/cli-headers'
import { checkAppExistsAndHasPermissionOrgErr } from './api/app'
import { getActiveAppVersions } from './api/versions'
import { addAppInternal } from './app/add'
Expand Down Expand Up @@ -74,7 +75,6 @@ import { deleteOrganizationInternal } from './organization/delete'
import { listOrganizationsInternal } from './organization/list'
import { setOrganizationInternal } from './organization/set'
import { getUserIdInternal } from './user/account'
import { buildCliRequestHeaders } from './analytics/cli-headers'
import { createSupabaseClient, findSavedKey, getConfig, getLocalConfig } from './utils'
import { parseSecurityPolicyError } from './utils/security_policy_errors'
import { normalizeAutoBumpInput } from './versionHelpers'
Expand Down Expand Up @@ -873,6 +873,7 @@ export class CapgoSDK {
android: options.android,
selfAssign: options.selfAssign,
disableAutoUpdate: options.disableAutoUpdate ?? undefined,
updatePackage: options.updatePackage,
dev: options.dev,
emulator: options.emulator,
device: options.device,
Expand Down Expand Up @@ -1209,7 +1210,7 @@ export class CapgoSDK {

const response = await fetch(`${localConfig.hostApi}/private/stats`, {
method: 'POST',
headers: buildCliRequestHeaders({ 'Content-Type': 'application/json', capgkey: apikey }),
headers: buildCliRequestHeaders({ 'Content-Type': 'application/json', 'capgkey': apikey }),
body: JSON.stringify(query),
})

Expand Down
16 changes: 16 additions & 0 deletions cli/src/types/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export type Database = {
rollout_percentage_bps: number
rollout_version: number | null
rbac_id: string
update_package: Database["public"]["Enums"]["channel_update_package"]
updated_at: string
version: number | null
}
Expand Down Expand Up @@ -879,6 +880,7 @@ export type Database = {
rollout_percentage_bps?: number
rollout_version?: number | null
rbac_id?: string
update_package?: Database["public"]["Enums"]["channel_update_package"]
updated_at?: string
version?: number | null
}
Expand Down Expand Up @@ -918,6 +920,7 @@ export type Database = {
rollout_percentage_bps?: number
rollout_version?: number | null
rbac_id?: string
update_package?: Database["public"]["Enums"]["channel_update_package"]
updated_at?: string
version?: number | null
}
Expand Down Expand Up @@ -4775,6 +4778,12 @@ export type Database = {
}
Enums: {
action_type: "mau" | "storage" | "bandwidth" | "build_time"
channel_update_package:
| "all"
| "zip"
| "delta"
| "zip_from_builtin"
| "delta_from_builtin"
credit_metric_type: "mau" | "bandwidth" | "storage" | "build_time"
credit_transaction_type:
| "grant"
Expand Down Expand Up @@ -5036,6 +5045,13 @@ export const Constants = {
public: {
Enums: {
action_type: ["mau", "storage", "bandwidth", "build_time"],
channel_update_package: [
"all",
"zip",
"delta",
"zip_from_builtin",
"delta_from_builtin",
],
credit_metric_type: ["mau", "bandwidth", "storage", "build_time"],
credit_transaction_type: [
"grant",
Expand Down
19 changes: 19 additions & 0 deletions cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,25 @@ export function formatCapgoApiErrorBody(body: unknown): string {
return [record.error, record.message, record.status].filter(Boolean).join(' | ')
}

function messageAfterPrefix(text: string, prefix: string): string {
const index = text.indexOf(prefix)
if (index < 0)
return text
return text.slice(index + prefix.length).replace(/^:\s*/, '').split('\n')[0]!.trim()
}

export async function channelUpdatePackageCliError(error: unknown): Promise<string | null> {
const payload = await readCapgoCliApiErrorPayload(error)
if (payload?.error === 'channel_zip_required' || payload?.error === 'channel_delta_required')
return payload.message || payload.error
const text = error instanceof Error ? error.message : String(error ?? '')
if (text.includes('CHANNEL_ZIP_REQUIRED'))
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return messageAfterPrefix(text, 'CHANNEL_ZIP_REQUIRED')
if (text.includes('CHANNEL_DELTA_REQUIRED'))
return messageAfterPrefix(text, 'CHANNEL_DELTA_REQUIRED')
return null
}

/** Capgo-managed Supabase hosts (cloud). Match hostname exactly. */
export function isCapgoManagedSupabaseHost(supaHost?: string): boolean {
if (!supaHost)
Expand Down
Binary file added docs/pr/channel-update-package.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading