Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 7 additions & 2 deletions cli/src/channel/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export async function setChannelInternal(channel: string, appId: string, options
}

const hasStableBundlePromotion = bundle != null || latest === true || latestRemote === true
const hasRolloutTargetChange = rolloutBundle != null || rolloutRollback === true || rolloutPromote === true
const hasRolloutTargetChange = rolloutBundle != null || rolloutRollback === true || rolloutPromote === true || rolloutDisable === true
Comment thread
riderx marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
riderx marked this conversation as resolved.
Outdated
const hasRolloutConfiguration = rolloutPercentage != null
|| rolloutPercentageBps != null
|| rolloutEnable != null
Expand Down Expand Up @@ -424,8 +424,13 @@ export async function setChannelInternal(channel: string, appId: string, options

if (rolloutEnable != null)
channelPayload.rollout_enabled = !!rolloutEnable
if (rolloutDisable)
if (rolloutDisable) {
bundleLinkChanged = bundleLinkChanged || existingChannel.rollout_version != null
channelPayload.rollout_enabled = false
channelPayload.rollout_version = null
channelPayload.rollout_paused_at = null
channelPayload.rollout_pause_reason = null
}

if (rolloutPause) {
channelPayload.rollout_paused_at = new Date().toISOString()
Expand Down
2 changes: 1 addition & 1 deletion cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ Example: npx @capgo/cli@latest channel set production com.example.app --bundle 1
.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))
.option('--rollout-enable', `Enable the configured rollout`)
.option('--rollout-disable', `Disable the configured rollout`)
.option('--rollout-disable', `Disable the configured rollout and unlink the rollout bundle`)
.option('--rollout-pause', `Pause rollout exposure without rolling back selected devices`)
.option('--rollout-resume', `Resume a paused rollout`)
.option('--rollout-rollback', `Clear rollout state and return devices to stable`)
Expand Down
14 changes: 13 additions & 1 deletion src/pages/app/[app].channel.[channel].vue
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
async function saveChannelChanges(update: ChannelUpdate) {
const changesStableVersion = Object.prototype.hasOwnProperty.call(update, 'version')
const changesRolloutVersion = Object.prototype.hasOwnProperty.call(update, 'rollout_version')
// Unlinking rollout_version (including disable) requires promote_bundle — matches refresh_channel_rollout_id.
const canUpdate = changesStableVersion || changesRolloutVersion
? canPromoteBundle.value
: canUpdateChannelSettings.value
Expand Down Expand Up @@ -589,6 +590,17 @@
await saveChannelChange('rollout_enabled', true as any)
}

async function disableRollout() {
if (await saveChannelChanges({
rollout_enabled: false,
rollout_version: null,
rollout_paused_at: null,
rollout_pause_reason: null,
})) {
await askUpdateNotificationAfterBundleChange()
}
}

async function saveRolloutPercentage(value: string) {
const percentage = Number.parseFloat(value)
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Expand Down Expand Up @@ -1051,7 +1063,7 @@
<button class="min-h-11 d-btn d-btn-ghost" :disabled="!canPromoteBundle" @click="openSelectRolloutVersion()">
{{ t('set-rollout-target') }}
</button>
<button class="min-h-11 d-btn d-btn-outline" :disabled="rolloutActionsDisabled" @click="saveChannelChange('rollout_enabled', !channel.rollout_enabled as any)">
<button class="min-h-11 d-btn d-btn-outline" :disabled="channel.rollout_enabled ? rolloutTargetActionsDisabled : rolloutEnableDisabled" @click="channel.rollout_enabled ? disableRollout() : enableRollout()">

Check warning on line 1066 in src/pages/app/[app].channel.[channel].vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit "type" attribute to this button.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ_Ce9WfETN2Y3sJW_A0&open=AZ_Ce9WfETN2Y3sJW_A0&pullRequest=2821
Comment thread
riderx marked this conversation as resolved.
Outdated
{{ channel.rollout_enabled ? t('disable') : t('enable') }}
</button>
<button class="min-h-11 d-btn d-btn-outline" :disabled="rolloutPauseDisabled" @click="toggleRolloutPause()">
Expand Down
15 changes: 14 additions & 1 deletion supabase/functions/_backend/public/channel/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,12 @@ export async function post(c: Context<MiddlewareKeyVariables>, body: ChannelSet,
if (body.autoPauseAction && !['pause', 'rollback', 'notify'].includes(body.autoPauseAction)) {
throw simpleError('invalid_auto_pause_action', 'Auto-pause action must be pause, rollback, or notify', { autoPauseAction: body.autoPauseAction })
}
const changesRolloutTarget = body.rolloutVersion !== undefined || !!body.rollback || !!body.promoteToStable
const disablesRollout = body.rolloutEnabled === false && !body.rollback && !body.promoteToStable
// Clearing rollout_version (disable unlink / explicit target / rollback / promote) is gated by the DB trigger with channel.promote_bundle.
const changesRolloutTarget = body.rolloutVersion !== undefined
|| !!body.rollback
|| !!body.promoteToStable
|| (disablesRollout && existingRolloutVersion != null)
if (changesRolloutTarget) {
if (existingChannelId === null) {
throw simpleError('cannot_find_channel', 'Cannot find channel', { app_id: body.app_id, channel: body.channel })
Expand Down Expand Up @@ -470,6 +475,14 @@ export async function post(c: Context<MiddlewareKeyVariables>, body: ChannelSet,
channel.rollout_paused_at = null
channel.rollout_pause_reason = null
}

// Disabling progressive rollout always unlinks the second bundle (ignore any parallel rolloutVersion).
if (disablesRollout) {
channel.rollout_version = null
Comment thread
riderx marked this conversation as resolved.
channel.rollout_paused_at = null
channel.rollout_pause_reason = null
}

if (!existingChannel && requestedVersionName) {
const createdChannel = await createAndPromoteChannelInTransaction(c, channel, requestedVersionName, apikey)
return c.json({ ...BRES, ...createdChannel })
Expand Down
71 changes: 71 additions & 0 deletions tests/channel-post.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,77 @@ describe('public channel post', () => {
expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 456, rollout_version: null }), 42, false)
})

it('unlinks the rollout bundle when progressive rollout is disabled', async () => {
supabaseAdmin.mockImplementation(() => buildAdminChain({
existingChannelId: 42,
existingChannelVersion: 123,
existingRolloutVersion: 456,
}))
const { post } = await import('../supabase/functions/_backend/public/channel/post.ts')
const c = context()

await post(c, {
app_id: 'com.test.disable-rollout',
channel: 'production',
rolloutEnabled: false,
}, apiKey())

expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.disable-rollout', channelId: 42 })
expect(checkPermission).toHaveBeenCalledWith(c, 'channel.promote_bundle', { appId: 'com.test.disable-rollout', channelId: 42 })
expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({
rollout_enabled: false,
rollout_version: null,
rollout_paused_at: null,
rollout_pause_reason: null,
}), 42, true)
})

it('forces unlink on disable even when rolloutVersion is also sent', async () => {
supabaseAdmin.mockImplementation(() => buildAdminChain({
existingChannelId: 42,
existingChannelVersion: 123,
existingRolloutVersion: 456,
versionId: 789,
}))
const { post } = await import('../supabase/functions/_backend/public/channel/post.ts')
const c = context()

await post(c, {
app_id: 'com.test.disable-with-version',
channel: 'production',
rolloutEnabled: false,
rolloutVersion: '2.0.0',
}, apiKey())

expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({
rollout_enabled: false,
rollout_version: null,
}), 42, true)
})

it('does not require promote when disabling with no linked rollout bundle', async () => {
supabaseAdmin.mockImplementation(() => buildAdminChain({
existingChannelId: 42,
existingChannelVersion: 123,
existingRolloutVersion: null,
}))
const { post } = await import('../supabase/functions/_backend/public/channel/post.ts')
const c = context()

await post(c, {
app_id: 'com.test.disable-no-rollout',
channel: 'production',
rolloutEnabled: false,
}, apiKey())

expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.disable-no-rollout', channelId: 42 })
expect(checkPermission).not.toHaveBeenCalledWith(c, 'channel.promote_bundle', expect.anything())
expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({
rollout_enabled: false,
rollout_version: null,
}), 42, true)
})

it('creates and promotes a new channel in one transaction after its scoped grant exists', async () => {
const { post } = await import('../supabase/functions/_backend/public/channel/post.ts')
const c = context()
Expand Down
Loading