Skip to content

Commit

Permalink
(CONT-1026) Add publish top-level command
Browse files Browse the repository at this point in the history
To reduce complexity, this change introduces a new `publish` top-level
command.

The intention is to replace `pdk release` and `pdk release publish`
  • Loading branch information
chelnak committed Jun 13, 2023
1 parent dad97b3 commit 4281655
Show file tree
Hide file tree
Showing 2 changed files with 311 additions and 0 deletions.
188 changes: 188 additions & 0 deletions lib/pdk/cli/publish.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
require 'pdk/cli/util'
require 'pdk/validate'
require 'pdk/util/bundler'
require 'pdk/cli/util/interview'
require 'pdk/util/changelog_generator'
require 'pdk/module/build'

module PDK
module CLI
@publish_cmd = @base_cmd.define_command do # rubocop:disable Metrics/BlockLength
name 'publish'
usage 'publish [options] <tarball>'
summary 'Publishes the module <tarball> to the Forge.'

flag nil, :force, 'Publish the module automatically, with no prompts.'
flag nil, :'skip-validation', 'Skips the module validation check.'
flag nil, :'skip-changelog', 'Skips the automatic changelog generation.'
flag nil, :'skip-dependency', 'Skips the module dependency check.'
flag nil, :'skip-documentation', 'Skips the documentation update.'
flag nil, :'skip-build', 'Skips module build.'
flag nil, :'skip-publish', 'Skips publishing the module to the forge.'

option nil, :'forge-upload-url', 'Set forge upload url path.',
argument: :required, default: 'https://forgeapi.puppetlabs.com/v3/releases'

option nil, :'forge-token', 'Set Forge API token (you may also set via environment variable PDK_FORGE_TOKEN)', argument: :required

run do |opts, _args, _cmd|
# Make sure build is being run in a valid module directory with a metadata.json
PDK::CLI::Util.ensure_in_module!(
message: '`pdk publish` can only be run from inside a valid module with a metadata.json.',
log_level: :info
)
opts[:force] = true unless PDK::CLI::Util.interactive?
opts[:'forge-token'] ||= PDK::Util::Env['PDK_FORGE_TOKEN']

if opts[:'forge-token'].nil? || opts[:'forge-token'].empty?
PDK.logger.error 'You must supply a Forge API token either via `--forge-token` option or PDK_FORGE_TOKEN environment variable.'
exit 1
end

Release.prepare_interview(opts) unless opts[:force]

Release.send_analytics('publish', opts)

release = PDK::Module::Release.new(nil, opts)

release.run
end

module Release # rubocop:disable Lint/ConstantDefinitionInBlock
# Checks whether the module is compatible with PDK release process
# @param release PDK::Module::Release Object representing the release
# @param opts Options Hash from Cri
def self.module_compatibility_checks!(release, opts)
unless release.module_metadata.forge_ready?
if opts[:force]
PDK.logger.warn "This module is missing the following fields in the metadata.json: #{release.module_metadata.missing_fields.join(', ')}. " \
'These missing fields may affect the visibility of the module on the Forge.'
else
release.module_metadata.interview_for_forge!
release.write_module_metadata!
end
end

unless release.pdk_compatible? # rubocop:disable Style/GuardClause Nope!
if opts[:force]
PDK.logger.warn 'This module is not compatible with PDK, so PDK can not validate or test this build.'
else
PDK.logger.info 'This module is not compatible with PDK, so PDK can not validate or test this build. ' \
'Unvalidated modules may have errors when uploading to the Forge. ' \
'To make this module PDK compatible and use validate features, cancel the build and run `pdk convert`.'
unless PDK::CLI::Util.prompt_for_yes('Continue build without converting?')
PDK.logger.info 'Build cancelled; exiting.'
PDK::Util.exit_process(1)
end
end
end
end

# Send_analytics for the given command and Cri options
def self.send_analytics(command, opts)
# Don't pass tokens to analytics
PDK::CLI::Util.analytics_screen_view(command, opts.reject { |k, _| k == :'forge-token' })
end

def self.prepare_interview(opts)
questions = []

unless opts[:'skip-validation']
questions << {
name: 'validation',
question: 'Do you want to run the module validation ?',
type: :yes
}
end
unless opts[:'skip-changelog']
questions << {
name: 'changelog',
question: 'Do you want to run the automatic changelog generation ?',
type: :yes
}
end
unless opts[:version]
questions << {
name: 'setversion',
question: 'Do you want to set the module version ?',
type: :yes
}
end
unless opts[:'skip-dependency']
questions << {
name: 'dependency',
question: 'Do you want to run the dependency-checker on this module?',
type: :yes
}
end
unless opts[:'skip-documentation']
questions << {
name: 'documentation',
question: 'Do you want to update the documentation for this module?',
type: :yes
}
end
unless opts[:'skip-publish']
questions << {
name: 'publish',
question: 'Do you want to publish the module on the Puppet Forge?',
type: :yes
}
end

prompt = TTY::Prompt.new(help_color: :cyan)
interview = PDK::CLI::Util::Interview.new(prompt)
interview.add_questions(questions)
answers = interview.run

unless answers.nil?
opts[:'skip-validation'] = !answers['validation']
opts[:'skip-changelog'] = !answers['changelog']
opts[:'skip-dependency'] = !answers['dependency']
opts[:'skip-documentation'] = !answers['documentation']
opts[:'skip-publish'] = !answers['publish']

prepare_version_interview(prompt, opts) if answers['setversion']

prepare_publish_interview(prompt, opts) if answers['publish']
end
answers
end

def self.prepare_version_interview(prompt, opts)
questions = [
{
name: 'version',
question: 'Please set the module version',
help: 'This value is the version that will be used in the changelog generator and building of the module.',
required: true,
validate_pattern: /(\*|\d+(\.\d+){0,2}(\.\*)?)$/i,
validate_message: 'The version format should be in the format x.y.z where x represents the major version, y the minor version and z the build number.'
}
]
interview = PDK::CLI::Util::Interview.new(prompt)
interview.add_questions(questions)
ver_answer = interview.run
opts[:version] = ver_answer['version']
end

def self.prepare_publish_interview(prompt, opts)
return if opts[:'forge-token']

questions = [
{
name: 'apikey',
question: 'Please set the api key(authorization token) to upload on the Puppet Forge',
help: 'This value is used for authentication on the Puppet Forge to upload your module tarball.',
required: true
}
]
interview = PDK::CLI::Util::Interview.new(prompt)
interview.add_questions(questions)
api_answer = interview.run
opts[:'forge-token'] = api_answer['apikey']
end
end
end
end
end
123 changes: 123 additions & 0 deletions spec/unit/pdk/cli/publish_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require 'spec_helper'
require 'pdk/cli'

describe 'PDK::CLI release publish' do
let(:help_text) { a_string_matching(/^USAGE\s+pdk release publish/m) }
let(:base_cli_args) { ['release', 'publish'] }

context 'when not run from inside a module' do
include_context 'run outside module'

let(:cli_args) { base_cli_args }

it 'exits with an error' do
expect(logger).to receive(:error).with(a_string_matching(/must be run from inside a valid module/))

expect { PDK::CLI.run(cli_args) }.to exit_nonzero
end

it 'does not submit the command to analytics' do
expect(analytics).not_to receive(:screen_view)

expect { PDK::CLI.run(cli_args) }.to exit_nonzero
end
end

context 'when run from inside a module' do
let(:release_object) do
instance_double(
PDK::Module::Release,
pdk_compatible?: true,
module_metadata: mock_metadata_obj,
run: nil
)
end

let(:mock_metadata_obj) do
instance_double(
PDK::Module::Metadata,
forge_ready?: true
)
end

let(:cli_args) { base_cli_args << '--forge-token=cli123' }

before do
allow(PDK::CLI::Util).to receive(:ensure_in_module!).and_return(nil)
allow(PDK::Module::Release).to receive(:new).and_return(release_object)
allow(PDK::Util).to receive(:exit_process).and_raise('exit_process mock should not be called')
end

it 'calls PDK::Module::Release.run' do
expect(release_object).to receive(:run)

expect { PDK::CLI.run(cli_args.push('--force')) }.not_to raise_error
end

it 'skips all but publishing' do
expect(PDK::Module::Release).to receive(:new).with(
Object,
hash_including(
'skip-validation': true,
'skip-changelog': true,
'skip-dependency': true,
'skip-documentation': true,
'skip-build': true
)
)

expect { PDK::CLI.run(cli_args.push('--force')) }.not_to raise_error
end

it 'does not start an interview when --force is used' do
expect(PDK::CLI::Util::Interview).not_to receive(:new)

PDK::CLI.run(cli_args.push('--force'))
end

it 'implicitly uses --force in non-interactive environments' do
allow(PDK::CLI::Util).to receive(:interactive?).and_return(false)
expect(PDK::Module::Release).to receive(:new).with(Object, hash_including(force: true))

expect { PDK::CLI.run(cli_args) }.not_to raise_error
end

context 'when not passed a forge-token on the command line' do
let(:cli_args) { base_cli_args }

it 'exits with an error' do
expect(logger).to receive(:error).with(a_string_matching(/must supply a forge api token/i))

expect { PDK::CLI.run(cli_args) }.to exit_nonzero
end

context 'when passed a forge-token via PDK_FORGE_TOKEN' do
before do
allow(PDK::Util::Env).to receive(:[]).with('PDK_DISABLE_ANALYTICS').and_return(true)
allow(PDK::Util::Env).to receive(:[]).with('PDK_FORGE_TOKEN').and_return('env123')
end

it 'uses forge-token from environment' do
expect(PDK::Module::Release).to receive(:new).with(Object, hash_including('forge-token': 'env123'))

expect { PDK::CLI.run(cli_args) }.not_to raise_error
end
end
end

context 'when passed a forge-token on both the command line and via PDK_FORGE_TOKEN' do
let(:cli_args) { base_cli_args << '--forge-token=cli123' }

before do
allow(PDK::Util::Env).to receive(:[]).with('PDK_DISABLE_ANALYTICS').and_return(true)
allow(PDK::Util::Env).to receive(:[]).with('PDK_FORGE_TOKEN').and_return('env123')
end

it 'value from command line takes precedence' do
expect(PDK::Module::Release).to receive(:new).with(Object, hash_including('forge-token': 'cli123'))

expect { PDK::CLI.run(cli_args) }.not_to raise_error
end
end
end
end

0 comments on commit 4281655

Please sign in to comment.