Skip to content

Commit 43564aa

Browse files
paoptu023jfuss
authored andcommitted
feat(publish): Add --semantic-version option to sam publish command (#1020)
1 parent d7ff936 commit 43564aa

File tree

6 files changed

+119
-82
lines changed

6 files changed

+119
-82
lines changed

designs/sam_publish_cmd.rst

+14-7
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ Create new version of an existing SAR application
132132
Click the link below to view your application in AWS console:
133133
https://console.aws.amazon.com/serverlessrepo/home?region=<region>#/published-applications/<arn>
134134

135+
Alternatively, you can provide the new version number through the --semantic-version option without manually modifying
136+
the template. The command will publish a new application version using the specified value.
137+
138+
>>> sam publish -t ./packaged.yaml --semantic-version 0.0.2
139+
135140
Update the metadata of an existing application without creating new version
136141
Keep SemanticVersion unchanged, then modify metadata fields like Description or ReadmeUrl, and run
137142
``sam publish -t ./packaged.yaml``. SAM CLI prints application metadata updated message, values of updated
@@ -184,13 +189,15 @@ CLI Changes
184189
$ sam publish -t packaged.yaml --region <region>
185190
186191
Options:
187-
-t, --template PATH AWS SAM template file [default: template.[yaml|yml]]
188-
--profile TEXT Select a specific profile from your credential file to
189-
get AWS credentials.
190-
--region TEXT Set the AWS Region of the service (e.g. us-east-1).
191-
--debug Turn on debug logging to print debug message generated
192-
by SAM CLI.
193-
--help Show this message and exit.
192+
-t, --template PATH AWS SAM template file [default: template.[yaml|yml]]
193+
--semantic-version TEXT Optional. The value provided here overrides SemanticVersion
194+
in the template metadata.
195+
--profile TEXT Select a specific profile from your credential file to
196+
get AWS credentials.
197+
--region TEXT Set the AWS Region of the service (e.g. us-east-1).
198+
--debug Turn on debug logging to print debug message generated
199+
by SAM CLI.
200+
--help Show this message and exit.
194201
195202
2. Update ``sam package`` (``aws cloudformation package``) command to support uploading locally referenced readme and
196203
license files to S3.

requirements/base.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ python-dateutil~=2.6
1313
pathlib2~=2.3.2; python_version<"3.4"
1414
requests==2.20.1
1515
aws_lambda_builders==0.2.0
16-
serverlessrepo==0.1.5
16+
serverlessrepo==0.1.8

samcli/commands/publish/command.py

+28-37
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
"""CLI command for "publish" command."""
22

33
import json
4+
import logging
45

56
import click
67
import boto3
7-
from botocore.exceptions import ClientError
88
from serverlessrepo import publish_application
99
from serverlessrepo.publish import CREATE_APPLICATION
10-
from serverlessrepo.exceptions import ServerlessRepoError
10+
from serverlessrepo.parser import METADATA, SERVERLESS_REPO_APPLICATION
11+
from serverlessrepo.exceptions import ServerlessRepoError, InvalidS3UriError
1112

1213
from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options
1314
from samcli.commands._utils.options import template_common_option
1415
from samcli.commands._utils.template import get_template_data
1516
from samcli.commands.exceptions import UserException
1617

18+
LOG = logging.getLogger(__name__)
19+
20+
SAM_PUBLISH_DOC = "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-publishing-applications.html" # noqa
21+
SAM_PACKAGE_DOC = "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-package.html" # noqa
1722
HELP_TEXT = """
1823
Use this command to publish a packaged AWS SAM template to
1924
the AWS Serverless Application Repository to share within your team,
@@ -22,46 +27,57 @@
2227
This command expects the template's Metadata section to contain an
2328
AWS::ServerlessRepo::Application section with application metadata
2429
for publishing. For more details on this metadata section, see
25-
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-publishing-applications.html
30+
{}
2631
\b
2732
Examples
2833
--------
2934
To publish an application
3035
$ sam publish -t packaged.yaml --region <region>
31-
"""
36+
""".format(SAM_PUBLISH_DOC)
3237
SHORT_HELP = "Publish a packaged AWS SAM template to the AWS Serverless Application Repository."
3338
SERVERLESSREPO_CONSOLE_URL = "https://console.aws.amazon.com/serverlessrepo/home?region={}#/published-applications/{}"
39+
SEMANTIC_VERSION_HELP = "Optional. The value provided here overrides SemanticVersion in the template metadata."
40+
SEMANTIC_VERSION = 'SemanticVersion'
3441

3542

3643
@click.command("publish", help=HELP_TEXT, short_help=SHORT_HELP)
3744
@template_common_option
45+
@click.option('--semantic-version', help=SEMANTIC_VERSION_HELP)
3846
@aws_creds_options
3947
@cli_framework_options
4048
@pass_context
41-
def cli(ctx, template):
49+
def cli(ctx, template, semantic_version):
4250
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing
4351

44-
do_cli(ctx, template) # pragma: no cover
52+
do_cli(ctx, template, semantic_version) # pragma: no cover
4553

4654

47-
def do_cli(ctx, template):
55+
def do_cli(ctx, template, semantic_version):
4856
"""Publish the application based on command line inputs."""
4957
try:
5058
template_data = get_template_data(template)
5159
except ValueError as ex:
5260
click.secho("Publish Failed", fg='red')
5361
raise UserException(str(ex))
5462

63+
# Override SemanticVersion in template metadata when provided in command input
64+
if semantic_version and SERVERLESS_REPO_APPLICATION in template_data.get(METADATA, {}):
65+
template_data.get(METADATA).get(SERVERLESS_REPO_APPLICATION)[SEMANTIC_VERSION] = semantic_version
66+
5567
try:
5668
publish_output = publish_application(template_data)
5769
click.secho("Publish Succeeded", fg="green")
58-
click.secho(_gen_success_message(publish_output), fg="yellow")
59-
except ServerlessRepoError as ex:
70+
click.secho(_gen_success_message(publish_output))
71+
except InvalidS3UriError:
6072
click.secho("Publish Failed", fg='red')
61-
raise UserException(str(ex))
62-
except ClientError as ex:
73+
raise UserException(
74+
"Your SAM template contains invalid S3 URIs. Please make sure that you have uploaded application "
75+
"artifacts to S3 by packaging the template. See more details in {}".format(SAM_PACKAGE_DOC))
76+
except ServerlessRepoError as ex:
6377
click.secho("Publish Failed", fg='red')
64-
raise _wrap_s3_uri_exception(ex)
78+
LOG.debug("Failed to publish application to serverlessrepo", exc_info=True)
79+
error_msg = '{}\nPlease follow the instructions in {}'.format(str(ex), SAM_PUBLISH_DOC)
80+
raise UserException(error_msg)
6581

6682
application_id = publish_output.get('application_id')
6783
_print_console_link(ctx.region, application_id)
@@ -108,28 +124,3 @@ def _print_console_link(region, application_id):
108124
console_link = SERVERLESSREPO_CONSOLE_URL.format(region, application_id.replace('/', '~'))
109125
msg = "Click the link below to view your application in AWS console:\n{}".format(console_link)
110126
click.secho(msg, fg="yellow")
111-
112-
113-
def _wrap_s3_uri_exception(ex):
114-
"""
115-
Wrap invalid S3 URI exception with a better error message.
116-
117-
Parameters
118-
----------
119-
ex : ClientError
120-
boto3 exception
121-
122-
Returns
123-
-------
124-
Exception
125-
UserException if found invalid S3 URI or ClientError
126-
"""
127-
error_code = ex.response.get('Error').get('Code')
128-
message = ex.response.get('Error').get('Message')
129-
130-
if error_code == 'BadRequestException' and "Invalid S3 URI" in message:
131-
return UserException(
132-
"Your SAM template contains invalid S3 URIs. Please make sure that you have uploaded application "
133-
"artifacts to S3 by packaging the template: 'sam package --template-file <file-path>'.")
134-
135-
return ex

tests/integration/publish/publish_app_integ_base.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def base_command(self):
9090

9191
return command
9292

93-
def get_command_list(self, template_path=None, region=None, profile=None):
93+
def get_command_list(self, template_path=None, region=None, profile=None, semantic_version=None):
9494
command_list = [self.base_command(), "publish"]
9595

9696
if template_path:
@@ -102,4 +102,7 @@ def get_command_list(self, template_path=None, region=None, profile=None):
102102
if profile:
103103
command_list = command_list + ["--profile", profile]
104104

105+
if semantic_version:
106+
command_list = command_list + ["--semantic-version", semantic_version]
107+
105108
return command_list

tests/integration/publish/test_command_integ.py

+18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from unittest import skipIf
77

8+
from samcli.commands.publish.command import SEMANTIC_VERSION
89
from .publish_app_integ_base import PublishAppIntegBase
910

1011
# Publish tests require credentials and Travis will only add credentials to the env if the PR is from the same repo.
@@ -59,6 +60,23 @@ def test_create_application_version(self):
5960
app_metadata = json.loads(app_metadata_text)
6061
self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8'))
6162

63+
def test_create_application_version_with_semantic_version_option(self):
64+
template_path = self.temp_dir.joinpath("template_create_app_version.yaml")
65+
command_list = self.get_command_list(
66+
template_path=template_path, region=self.region_name, semantic_version='0.1.0')
67+
68+
process = Popen(command_list, stdout=PIPE)
69+
process.wait()
70+
process_stdout = b"".join(process.stdout.readlines()).strip()
71+
72+
expected_msg = 'The following metadata of application "{}" has been updated:'.format(self.application_id)
73+
self.assertIn(expected_msg, process_stdout.decode('utf-8'))
74+
75+
app_metadata_text = self.temp_dir.joinpath("metadata_create_app_version.json").read_text()
76+
app_metadata = json.loads(app_metadata_text)
77+
app_metadata[SEMANTIC_VERSION] = '0.1.0'
78+
self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8'))
79+
6280

6381
@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only")
6482
class TestPublishNewApp(PublishAppIntegBase):

tests/unit/commands/publish/test_command.py

+54-36
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from unittest import TestCase
44
from mock import patch, call, Mock
55

6-
from botocore.exceptions import ClientError
7-
from serverlessrepo.exceptions import ServerlessRepoError
6+
from serverlessrepo.exceptions import ServerlessRepoError, InvalidS3UriError
87
from serverlessrepo.publish import CREATE_APPLICATION, UPDATE_APPLICATION
8+
from serverlessrepo.parser import METADATA, SERVERLESS_REPO_APPLICATION
99

10-
from samcli.commands.publish.command import do_cli as publish_cli
10+
from samcli.commands.publish.command import do_cli as publish_cli, SEMANTIC_VERSION
1111
from samcli.commands.exceptions import UserException
1212

1313

@@ -26,7 +26,7 @@ def setUp(self):
2626
def test_must_raise_if_value_error(self, click_mock, get_template_data_mock):
2727
get_template_data_mock.side_effect = ValueError("Template not found")
2828
with self.assertRaises(UserException) as context:
29-
publish_cli(self.ctx_mock, self.template)
29+
publish_cli(self.ctx_mock, self.template, None)
3030

3131
message = str(context.exception)
3232
self.assertEqual("Template not found", message)
@@ -38,42 +38,20 @@ def test_must_raise_if_value_error(self, click_mock, get_template_data_mock):
3838
def test_must_raise_if_serverlessrepo_error(self, click_mock, publish_application_mock):
3939
publish_application_mock.side_effect = ServerlessRepoError()
4040
with self.assertRaises(UserException):
41-
publish_cli(self.ctx_mock, self.template)
41+
publish_cli(self.ctx_mock, self.template, None)
4242

4343
click_mock.secho.assert_called_with("Publish Failed", fg="red")
4444

4545
@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
4646
@patch('samcli.commands.publish.command.publish_application')
4747
@patch('samcli.commands.publish.command.click')
48-
def test_must_raise_if_s3_uri_error(self, click_mock, publish_application_mock):
49-
publish_application_mock.side_effect = ClientError(
50-
{
51-
'Error': {
52-
'Code': 'BadRequestException',
53-
'Message': 'Invalid S3 URI'
54-
}
55-
},
56-
'create_application'
57-
)
48+
def test_must_raise_if_invalid_S3_uri_error(self, click_mock, publish_application_mock):
49+
publish_application_mock.side_effect = InvalidS3UriError(message="")
5850
with self.assertRaises(UserException) as context:
59-
publish_cli(self.ctx_mock, self.template)
51+
publish_cli(self.ctx_mock, self.template, None)
6052

6153
message = str(context.exception)
62-
self.assertIn("Please make sure that you have uploaded application artifacts "
63-
"to S3 by packaging the template", message)
64-
click_mock.secho.assert_called_with("Publish Failed", fg="red")
65-
66-
@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
67-
@patch('samcli.commands.publish.command.publish_application')
68-
@patch('samcli.commands.publish.command.click')
69-
def test_must_raise_if_not_s3_uri_error(self, click_mock, publish_application_mock):
70-
publish_application_mock.side_effect = ClientError(
71-
{'Error': {'Code': 'OtherError', 'Message': 'OtherMessage'}},
72-
'other_operation'
73-
)
74-
with self.assertRaises(ClientError):
75-
publish_cli(self.ctx_mock, self.template)
76-
54+
self.assertTrue("Your SAM template contains invalid S3 URIs" in message)
7755
click_mock.secho.assert_called_with("Publish Failed", fg="red")
7856

7957
@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
@@ -86,7 +64,7 @@ def test_must_succeed_to_create_application(self, click_mock, publish_applicatio
8664
'actions': [CREATE_APPLICATION]
8765
}
8866

89-
publish_cli(self.ctx_mock, self.template)
67+
publish_cli(self.ctx_mock, self.template, None)
9068
details_str = json.dumps({'attr1': 'value1'}, indent=2)
9169
expected_msg = "Created new application with the following metadata:\n{}"
9270
expected_link = self.console_link.format(
@@ -95,7 +73,7 @@ def test_must_succeed_to_create_application(self, click_mock, publish_applicatio
9573
)
9674
click_mock.secho.assert_has_calls([
9775
call("Publish Succeeded", fg="green"),
98-
call(expected_msg.format(details_str), fg="yellow"),
76+
call(expected_msg.format(details_str)),
9977
call(expected_link, fg="yellow")
10078
])
10179

@@ -109,7 +87,7 @@ def test_must_succeed_to_update_application(self, click_mock, publish_applicatio
10987
'actions': [UPDATE_APPLICATION]
11088
}
11189

112-
publish_cli(self.ctx_mock, self.template)
90+
publish_cli(self.ctx_mock, self.template, None)
11391
details_str = json.dumps({'attr1': 'value1'}, indent=2)
11492
expected_msg = 'The following metadata of application "{}" has been updated:\n{}'
11593
expected_link = self.console_link.format(
@@ -118,7 +96,7 @@ def test_must_succeed_to_update_application(self, click_mock, publish_applicatio
11896
)
11997
click_mock.secho.assert_has_calls([
12098
call("Publish Succeeded", fg="green"),
121-
call(expected_msg.format(self.application_id, details_str), fg="yellow"),
99+
call(expected_msg.format(self.application_id, details_str)),
122100
call(expected_link, fg="yellow")
123101
])
124102

@@ -139,9 +117,49 @@ def test_print_console_link_if_context_region_not_set(self, click_mock, boto3_mo
139117
session_mock.region_name = "us-west-1"
140118
boto3_mock.Session.return_value = session_mock
141119

142-
publish_cli(self.ctx_mock, self.template)
120+
publish_cli(self.ctx_mock, self.template, None)
143121
expected_link = self.console_link.format(
144122
session_mock.region_name,
145123
self.application_id.replace('/', '~')
146124
)
147125
click_mock.secho.assert_called_with(expected_link, fg="yellow")
126+
127+
@patch('samcli.commands.publish.command.get_template_data')
128+
@patch('samcli.commands.publish.command.publish_application')
129+
def test_must_use_template_semantic_version(self, publish_application_mock,
130+
get_template_data_mock):
131+
template_data = {
132+
METADATA: {
133+
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.1'}
134+
}
135+
}
136+
get_template_data_mock.return_value = template_data
137+
publish_application_mock.return_value = {
138+
'application_id': self.application_id,
139+
'details': {}, 'actions': {}
140+
}
141+
publish_cli(self.ctx_mock, self.template, None)
142+
publish_application_mock.assert_called_with(template_data)
143+
144+
@patch('samcli.commands.publish.command.get_template_data')
145+
@patch('samcli.commands.publish.command.publish_application')
146+
def test_must_override_template_semantic_version(self, publish_application_mock,
147+
get_template_data_mock):
148+
template_data = {
149+
METADATA: {
150+
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.1'}
151+
}
152+
}
153+
get_template_data_mock.return_value = template_data
154+
publish_application_mock.return_value = {
155+
'application_id': self.application_id,
156+
'details': {}, 'actions': {}
157+
}
158+
159+
publish_cli(self.ctx_mock, self.template, '0.2')
160+
expected_template_data = {
161+
METADATA: {
162+
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.2'}
163+
}
164+
}
165+
publish_application_mock.assert_called_with(expected_template_data)

0 commit comments

Comments
 (0)