Skip to content

[App Service] Fix #16111: Add az webapp deployment slot copy command (preview)#33069

Closed
seligj95 wants to merge 1 commit intoAzure:devfrom
seligj95:fix/16111-slot-copy-command
Closed

[App Service] Fix #16111: Add az webapp deployment slot copy command (preview)#33069
seligj95 wants to merge 1 commit intoAzure:devfrom
seligj95:fix/16111-slot-copy-command

Conversation

@seligj95
Copy link
Contributor

Description

Fixes #16111

Problem

The Azure REST API exposes a slotcopy endpoint for copying content and configuration from one deployment slot to another, but no CLI command exposed it. This was requested by the App Service team.

Solution

Add a new az webapp deployment slot copy command (marked as preview) that:

  • Calls the slotcopy REST API endpoint directly (SDK does not yet expose this method)
  • Copies content from --slot (source) to --target-slot (destination, defaults to production)
  • Is a one-way operation — unlike swap, it overwrites the target without exchanging

Usage

# Copy staging slot content to production
az webapp deployment slot copy -g MyRG -n MyApp --slot staging --target-slot production

# Copy production content to staging (refresh staging)
az webapp deployment slot copy -g MyRG -n MyApp --slot production --target-slot staging

Changes

  • commands.py: Register copy command with is_preview=True
  • _params.py: Add --slot and --target-slot parameters for the copy command
  • custom.py: copy_slot() implementation using send_raw_request to the slotcopy REST API
  • _help.py: Help text with examples
  • tests: 3 unit tests covering success, accepted (202), and default target slot behavior

Testing

  • All 32 existing + 3 new unit tests pass

…ommand (preview)

Add new command to copy content and configuration from one deployment
slot to another using the REST API slotcopy endpoint. Unlike swap, this
is a one-way operation that overwrites the target slot content.

- Register command with is_preview=True in commands.py
- Add --slot (source) and --target-slot (destination) parameters
- Implementation uses send_raw_request since SDK lacks copy_slot method
- Add help text with examples
- Add unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 26, 2026 14:54
@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Mar 26, 2026

️✔️AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link

Hi @seligj95,
Since the current milestone time is less than 7 days, this pr will be reviewed in the next milestone.

@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Mar 26, 2026

⚠️AzureCLI-BreakingChangeTest
⚠️appservice
rule cmd_name rule_message suggest_message
⚠️ 1001 - CmdAdd webapp deployment slot copy cmd webapp deployment slot copy added

@yonzhan
Copy link
Collaborator

yonzhan commented Mar 26, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new App Service CLI surface for copying deployment slot content/config using the underlying slotcopy ARM endpoint, exposing it as a preview command under az webapp deployment slot.

Changes:

  • Registers new preview command az webapp deployment slot copy.
  • Adds parameters and help content for the new command.
  • Implements copy_slot() via send_raw_request and adds unit tests for 200/202 and default target behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/azure-cli/azure/cli/command_modules/appservice/commands.py Registers the new copy slot command as preview.
src/azure-cli/azure/cli/command_modules/appservice/_params.py Adds argument context for webapp deployment slot copy.
src/azure-cli/azure/cli/command_modules/appservice/custom.py Implements copy_slot() calling the slotcopy REST API.
src/azure-cli/azure/cli/command_modules/appservice/_help.py Adds help text and examples for the new command.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py Adds unit tests validating basic request/response behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5618 to +5621
url = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}"
"/slots/{}/slotcopy?api-version={}").format(
subscription_id, resource_group_name, webapp, slot,
client.DEFAULT_API_VERSION)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--slot production is documented as a supported source (see help examples), but this implementation always calls the slot-scoped ARM resource /sites/{name}/slots/{slot}/slotcopy. In App Service, the production slot is the site resource itself (not a slots/production child), so this will likely 404 or invoke the wrong endpoint when slot is production. Consider normalizing slot=='production' (or empty) to the production endpoint (typically /sites/{name}/slotcopy) or explicitly validating and rejecting production if the REST API only supports slot-to-slot copy.

Suggested change
url = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}"
"/slots/{}/slotcopy?api-version={}").format(
subscription_id, resource_group_name, webapp, slot,
client.DEFAULT_API_VERSION)
# In App Service, the production slot is the site resource itself (no 'slots/production' child).
# Use the site-level slotcopy endpoint when the source is production, otherwise use the slot-scoped endpoint.
is_production_source = not slot or str(slot).lower() == 'production'
base_path = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}"
.format(subscription_id, resource_group_name, webapp))
if is_production_source:
url = "{}/slotcopy?api-version={}".format(base_path, client.DEFAULT_API_VERSION)
else:
url = "{}/slots/{}/slotcopy?api-version={}".format(base_path, slot, client.DEFAULT_API_VERSION)

Copilot uses AI. Check for mistakes.
Comment on lines +5616 to +5617
target_slot = target_slot or 'production'
subscription_id = get_subscription_id(cmd.cli_ctx)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked issue #16111’s proposed behavior states the target slot cannot be production, but this command defaults target_slot to 'production' and the help text/examples encourage copying to production. Please confirm the REST API actually allows production as a target; if it doesn't, add validation to block --target-slot production and update the default/help accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +724 to +725
c.argument('slot', help='the name of the source slot to copy from')
c.argument('target_slot', help="the name of the destination slot to copy to, default to 'production'")
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help text says "default to 'production'"; grammatically this should be "defaults to 'production'" (and similarly in other new/updated strings) to read correctly in --help output.

Copilot uses AI. Check for mistakes.
Comment on lines +2069 to +2070
az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --slot production \\
--target-slot staging
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This help example uses --slot production, but elsewhere in this module production is represented by omitting --slot / passing slot=None (e.g., swap preview uses if slot is None:). Unless copy_slot explicitly handles production as a special case, this example will likely not work and should be adjusted to match the actual supported syntax/behavior.

Suggested change
az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --slot production \\
--target-slot staging
az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --target-slot staging

Copilot uses AI. Check for mistakes.
Comment on lines +668 to +713
result = copy_slot(cmd_mock, 'rg1', 'myapp', 'staging', 'production')
self.assertEqual(result, {"status": "completed"})
send_raw_request_mock.assert_called_once()
call_args = send_raw_request_mock.call_args
self.assertIn('/slotcopy', call_args[1].get('url', '') or str(call_args))

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
def test_copy_slot_accepted(self, client_factory_mock, send_raw_request_mock):
"""Test copy_slot returns None on 202 accepted."""
client = mock.MagicMock()
client.DEFAULT_API_VERSION = '2024-04-01'
client_factory_mock.return_value = client
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cli_ctx_mock.data = {'subscription_id': 'sub1'}
cmd_mock.cli_ctx = cli_ctx_mock

response = mock.MagicMock()
response.status_code = 202
response.text = ''
send_raw_request_mock.return_value = response

result = copy_slot(cmd_mock, 'rg1', 'myapp', 'staging')
self.assertIsNone(result)

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
def test_copy_slot_default_target(self, client_factory_mock, send_raw_request_mock):
"""Test copy_slot defaults target_slot to 'production'."""
client = mock.MagicMock()
client.DEFAULT_API_VERSION = '2024-04-01'
client_factory_mock.return_value = client
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cli_ctx_mock.data = {'subscription_id': 'sub1'}
cmd_mock.cli_ctx = cli_ctx_mock

response = mock.MagicMock()
response.status_code = 200
response.text = '{}'
response.json.return_value = {}
send_raw_request_mock.return_value = response

copy_slot(cmd_mock, 'rg1', 'myapp', 'staging')
call_args = send_raw_request_mock.call_args
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests cover 200/202 and default target_slot, but they don't cover the documented --slot production case. Add a unit test asserting the correct URL/path behavior for a production source (or, if production source isn't supported, a test that it fails/validates accordingly) so this doesn't regress.

Copilot uses AI. Check for mistakes.
@seligj95
Copy link
Contributor Author

Consolidated into #33066

@seligj95 seligj95 closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebApp: Add command for copying slots on Azure Web Apps (in preview)

4 participants