Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
18 changes: 11 additions & 7 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<never> {
const packageError = await channelUpdatePackageCliError(error)
uploadFail(packageError || 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 @@ -1026,7 +1031,7 @@ async function promoteExistingChannel(
})

if (error)
uploadFail(`Cannot set channel because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
await uploadFailIfChannelError(error, `Cannot set channel because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`)
Comment thread
riderx marked this conversation as resolved.
Outdated

const bundleUrl = `${localConfig.hostWeb}/app/${appid}/channel/${targetChannel.id}`
if (targetChannel.public)
Expand Down Expand Up @@ -1089,7 +1094,7 @@ async function setVersionInChannel(
...(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)}`)
await uploadFailIfChannelError(dbError3, `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 🎉')
Expand All @@ -1116,9 +1121,8 @@ async function setVersionInChannel(
supaHost: cliHost?.supaHost,
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)}`)
}
if (error)
await uploadFailIfChannelError(error, `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
let createdChannelId = Number(createdChannel?.id)
Expand Down Expand Up @@ -1205,7 +1209,7 @@ async function setRolloutVersionInChannel(
})

if (rolloutError)
uploadFail(`Cannot set rollout in channel ${await formatFunctionInvokeError(rolloutError)}`)
await uploadFailIfChannelError(rolloutError, `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.
9 changes: 9 additions & 0 deletions messages/en.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -2594,6 +2594,15 @@
"update-delivery-samples": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-delivery-trend": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-delivery-trend-help": "Used in Capgo web console areas: components/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-all": "Used in Capgo web console areas: pages/app. Role: dropdown option label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-delta": "Used in Capgo web console areas: pages/app. Role: dropdown option label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-delta-from-builtin": "Used in Capgo web console areas: pages/app. Role: dropdown option label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-delta-required": "Used in Capgo web console areas: pages/app, components/tables. Role: error toast. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-help": "Used in Capgo web console areas: pages/app. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-zip": "Used in Capgo web console areas: pages/app. Role: dropdown option label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-zip-from-builtin": "Used in Capgo web console areas: pages/app. Role: dropdown option label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-package-zip-required": "Used in Capgo web console areas: pages/app, components/tables. Role: error toast. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update-password-now": "Used in Capgo web console areas: components. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"update_statistics": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"updated-at": "Used in Capgo web console areas: components/tables, pages/app, pages/settings/organization. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down
Loading
Loading