Commit 0130c0fb authored by Sean Carroll's avatar Sean Carroll Committed by Kamil Trzciński

Support for release key in gitlab-ci.yaml

Part of https://gitlab.com/gitlab-org/gitlab/issues/26013

See merge request https://gitlab.com/gitlab-org/gitlab/merge_requests/19298
parent 8ebf18da
......@@ -17,7 +17,7 @@ module Gitlab
allow_failure type stage when start_in artifacts cache
dependencies before_script needs after_script variables
environment coverage retry parallel extends interruptible timeout
resource_group].freeze
resource_group release].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze
......@@ -151,14 +151,18 @@ module Gitlab
description: 'Coverage configuration for this job.',
inherit: false
entry :release, Entry::Release,
description: 'This job will produce a release.',
inherit: false
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts, :environment, :coverage, :retry, :rules,
:parallel, :needs, :interruptible
:parallel, :needs, :interruptible, :release
attributes :script, :tags, :allow_failure, :when, :dependencies,
:needs, :retry, :parallel, :extends, :start_in, :rules,
:interruptible, :timeout, :resource_group
:interruptible, :timeout, :resource_group, :release
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
......@@ -243,6 +247,7 @@ module Gitlab
interruptible: interruptible_defined? ? interruptible_value : nil,
timeout: has_timeout? ? ChronicDuration.parse(timeout.to_s) : nil,
artifacts: artifacts_value,
release: release_value,
after_script: after_script_value,
ignore: ignored?,
needs: needs_defined? ? needs_value : nil,
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a release configuration.
#
class Release < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[tag_name name description assets].freeze
attributes %i[tag_name name assets].freeze
# Attributable description conflicts with
# ::Gitlab::Config::Entry::Node.description
def has_description?
true
end
def description
config[:description]
end
entry :assets, Entry::Release::Assets, description: 'Release assets.'
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :tag_name, presence: true
validates :description, type: String, presence: true
end
helpers :assets
def value
@config[:assets] = assets_value if @config.key?(:assets)
@config
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of release assets.
#
class Release
class Assets < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[links].freeze
attributes ALLOWED_KEYS
entry :links, Entry::Release::Assets::Links, description: 'Release assets:links.'
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :links, array_of_hashes: true, presence: true
end
helpers :links
def value
@config[:links] = links_value if @config.key?(:links)
@config
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of release:assets:links.
#
class Release
class Assets
class Link < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[name url].freeze
attributes ALLOWED_KEYS
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :name, type: String, presence: true
validates :url, presence: true, addressable_url: true
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of release:assets:links.
#
class Release
class Assets
class Links < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Validatable
entry :link, Entry::Release::Assets::Link, description: 'Release assets:links:link.'
validations do
validates :config, type: Array, presence: true
end
def skip_config_hash_validation?
true
end
end
end
end
end
end
end
end
......@@ -81,10 +81,15 @@ module Gitlab
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
}.compact }.compact
end
def release(job)
job[:release] if Feature.enabled?(:ci_release_generation, default_enabled: false)
end
def stage_builds_attributes(stage)
@jobs.values
.select { |job| job[:stage] == stage }
......@@ -133,7 +138,6 @@ module Gitlab
@jobs.each do |name, job|
# logical validation for job
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
validate_job_needs!(name, job)
......
......@@ -10,7 +10,7 @@ module Gitlab
def attributes(*attributes)
attributes.flatten.each do |attribute|
if method_defined?(attribute)
raise ArgumentError, 'Method already defined!'
raise ArgumentError, "Method already defined: #{attribute}"
end
define_method(attribute) do
......
......@@ -5,7 +5,7 @@ module Gitlab
module Entry
##
# This mixin is responsible for adding DSL, which purpose is to
# simplifly process of adding child nodes.
# simplify the process of adding child nodes.
#
# This can be used only if parent node is a configuration entry that
# holds a hash as a configuration value, for example:
......
......@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
image services only except rules needs variables artifacts
environment coverage retry interruptible timeout tags]
environment coverage retry interruptible timeout release tags]
end
it { is_expected.to match_array result }
......@@ -122,6 +122,21 @@ describe Gitlab::Ci::Config::Entry::Job do
it { expect(entry).to be_valid }
end
context 'when it is a release' do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "v0.06",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it { expect(entry).to be_valid }
end
end
end
......@@ -443,6 +458,25 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.timeout).to eq('1m 1s')
end
end
context 'when it is a release' do
context 'when `release:description` is missing' do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "v0.06",
name: "Release $CI_TAG_NAME"
}
}
end
it "returns error" do
expect(entry).not_to be_valid
expect(entry.errors).to include "release description can't be blank"
end
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Release::Assets::Link do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when entry config value is correct' do
let(:config) do
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
}
end
describe '#value' do
it 'returns link configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when name is not a string' do
let(:config) { { name: 123, url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip" } }
it 'reports error' do
expect(entry.errors)
.to include 'link name should be a string'
end
end
context 'when name is not present' do
let(:config) { { url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip" } }
it 'reports error' do
expect(entry.errors)
.to include "link name can't be blank"
end
end
context 'when url is not addressable' do
let(:config) { { name: "cool-app.zip", url: "xyz" } }
it 'reports error' do
expect(entry.errors)
.to include "link url is blocked: only allowed schemes are http, https"
end
end
context 'when url is not present' do
let(:config) { { name: "cool-app.zip" } }
it 'reports error' do
expect(entry.errors)
.to include "link url can't be blank"
end
end
context 'when there is an unknown key present' do
let(:config) { { test: 100 } }
it 'reports error' do
expect(entry.errors)
.to include 'link config contains unknown keys: test'
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Release::Assets::Links do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when entry config value is correct' do
let(:config) do
[
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
},
{
name: "cool-app.exe",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.exe"
}
]
end
describe '#value' do
it 'returns links configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when value of link is invalid' do
let(:config) { { link: 'xyz' } }
it 'reports error' do
expect(entry.errors)
.to include 'links config should be a array'
end
end
context 'when value of links link is empty' do
let(:config) { { link: [] } }
it 'reports error' do
expect(entry.errors)
.to include "links config should be a array"
end
end
context 'when there is an unknown key present' do
let(:config) { { test: 100 } }
it 'reports error' do
expect(entry.errors)
.to include 'links config should be a array'
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Release::Assets do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when entry config value is correct' do
let(:config) do
{
links: [
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
},
{
name: "cool-app.exe",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.exe"
}
]
}
end
describe '#value' do
it 'returns assets configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when value of assets is invalid' do
let(:config) { { links: 'xyz' } }
it 'reports error' do
expect(entry.errors)
.to include 'assets links should be an array of hashes'
end
end
context 'when value of assets:links is empty' do
let(:config) { { links: [] } }
it 'reports error' do
expect(entry.errors)
.to include "assets links can't be blank"
end
end
context 'when there is an unknown key present' do
let(:config) { { test: 100 } }
it 'reports error' do
expect(entry.errors)
.to include 'assets config contains unknown keys: test'
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Release do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when entry config value is correct' do
let(:config) { { tag_name: 'v0.06', description: "./release_changelog.txt" } }
describe '#value' do
it 'returns release configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context "when value includes 'assets' keyword" do
let(:config) do
{
tag_name: 'v0.06',
description: "./release_changelog.txt",
assets: [
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
}
]
}
end
describe '#value' do
it 'returns release configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context "when value includes 'name' keyword" do
let(:config) do
{
tag_name: 'v0.06',
description: "./release_changelog.txt",
name: "Release $CI_TAG_NAME"
}
end
describe '#value' do
it 'returns release configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when value of attribute is invalid' do
let(:config) { { description: 10 } }
it 'reports error' do
expect(entry.errors)
.to include 'release description should be a string'
end
end
context 'when release description is missing' do
let(:config) { { tag_name: 'v0.06' } }
it 'reports error' do
expect(entry.errors)
.to include "release description can't be blank"
end
end
context 'when release tag_name is missing' do
let(:config) { { description: "./release_changelog.txt" } }
it 'reports error' do
expect(entry.errors)
.to include "release tag name can't be blank"
end
end
context 'when there is an unknown key present' do
let(:config) { { test: 100 } }
it 'reports error' do
expect(entry.errors)
.to include 'release config contains unknown keys: test'
end
end
end
end
end
end
......@@ -27,16 +27,29 @@ describe Gitlab::Ci::Config::Entry::Root do
context 'when configuration is valid' do
context 'when top-level entries are defined' do
let(:hash) do
{ before_script: %w(ls pwd),
{
before_script: %w(ls pwd),
image: 'ruby:2.2',
default: {},
services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'value' },
after_script: ['make clean'],
stages: %w(build pages),
stages: %w(build pages release),
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' } }
spinach: { before_script: [], variables: {}, script: 'spinach' },
release: {
stage: 'release',
before_script: [],
after_script: [],
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: 'v0.06',
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
}
end
describe '#compose!' do
......@@ -87,7 +100,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#stages_value' do
context 'when stages key defined' do
it 'returns array of stages' do
expect(root.stages_value).to eq %w[build pages]
expect(root.stages_value).to eq %w[build pages release]
end
end
......@@ -105,8 +118,9 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#jobs_value' do
it 'returns jobs configuration' do
expect(root.jobs_value).to eq(
rspec: { name: :rspec,
expect(root.jobs_value.keys).to eq([:rspec, :spinach, :release])
expect(root.jobs_value[:rspec]).to eq(
{ name: :rspec,
script: %w[rspec ls],
before_script: %w(ls pwd),
image: { name: 'ruby:2.2' },
......@@ -116,8 +130,10 @@ describe Gitlab::Ci::Config::Entry::Root do
variables: {},
ignore: false,
after_script: ['make clean'],
only: { refs: %w[branches tags] } },
spinach: { name: :spinach,
only: { refs: %w[branches tags] } }
)
expect(root.jobs_value[:spinach]).to eq(
{ name: :spinach,
before_script: [],
script: %w[spinach],
image: { name: 'ruby:2.2' },
......@@ -129,6 +145,20 @@ describe Gitlab::Ci::Config::Entry::Root do
after_script: ['make clean'],
only: { refs: %w[branches tags] } }
)
expect(root.jobs_value[:release]).to eq(
{ name: :release,
stage: 'release',
before_script: [],
script: ["make changelog | tee release_changelog.txt"],
release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
image: { name: "ruby:2.2" },
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" },
only: { refs: %w(branches tags) },
variables: {},
after_script: [],
ignore: false }
)
end
end
end
......@@ -261,7 +291,7 @@ describe Gitlab::Ci::Config::Entry::Root do
# despite the fact, that key is present. See issue #18775 for more
# details.
#
context 'when entires specified but not defined' do
context 'when entries are specified but not defined' do
before do
root.compose!
end
......
......@@ -1285,6 +1285,59 @@ module Gitlab
end
end
describe "release" do
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
let(:config) do
{
stages: ["build", "test", "release"], # rubocop:disable Style/WordArray
release: {
stage: "release",
only: ["tags"],
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "$CI_COMMIT_TAG",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt",
assets: {
links: [
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
},
{
name: "cool-app.exe",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.exe"
}
]
}
}
}
}
end
context 'with feature flag active' do
before do
stub_feature_flags(ci_release_generation: true)
end
it "returns release info" do
expect(processor.stage_builds_attributes('release').first[:options])
.to eq(config[:release].except(:stage, :only))
end
end
context 'with feature flag inactive' do
before do
stub_feature_flags(ci_release_generation: false)
end
it "returns release info" do
expect(processor.stage_builds_attributes('release').first[:options].include?(config[:release]))
.to be false
end
end
end
describe '#environment' do
let(:config) do
{
......
......@@ -59,7 +59,7 @@ describe Gitlab::Config::Entry::Attributable do
end
end
expectation.to raise_error(ArgumentError, 'Method already defined!')
expectation.to raise_error(ArgumentError, 'Method already defined: length')
end
end
end
......@@ -940,7 +940,7 @@ describe Ci::CreatePipelineService do
expect(resource_group.resources.first.build).to eq(nil)
end
context 'when resourc group key includes predefined variables' do
context 'when resource group key includes predefined variables' do
let(:resource_group_key) { '$CI_COMMIT_REF_NAME-$CI_JOB_NAME' }
it 'interpolates the variables into the key correctly' do
......@@ -969,6 +969,70 @@ describe Ci::CreatePipelineService do
end
end
context 'with release' do
shared_examples_for 'a successful release pipeline' do
before do
stub_feature_flags(ci_release_generation: true)
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
it 'is valid config' do
pipeline = execute_service
build = pipeline.builds.first
expect(pipeline).to be_kind_of(Ci::Pipeline)
expect(pipeline).to be_valid
expect(pipeline.yaml_errors).not_to be_present
expect(pipeline).to be_persisted
expect(build).to be_kind_of(Ci::Build)
expect(build.options).to eq(config[:release].except(:stage, :only).with_indifferent_access)
end
end
context 'simple example' do
it_behaves_like 'a successful release pipeline' do
let(:config) do
{
release: {
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "v0.06",
description: "./release_changelog.txt"
}
}
}
end
end
end
context 'example with all release metadata' do
it_behaves_like 'a successful release pipeline' do
let(:config) do
{
release: {
script: ["make changelog | tee release_changelog.txt"],
release: {
name: "Release $CI_TAG_NAME",
tag_name: "v0.06",
description: "./release_changelog.txt",
assets: {
links: [
{
name: "cool-app.zip",
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.zip"
},
{
url: "http://my.awesome.download.site/1.0-$CI_COMMIT_SHORT_SHA.exe"
}
]
}
}
}
}
end
end
end
end
shared_examples 'when ref is protected' do
let(:user) { create(:user) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment