Skip to content
Snippets Groups Projects
Commit 270dea95 authored by Sean Carroll's avatar Sean Carroll :rocket:
Browse files

Support for release key in gitlab-ci.yaml

Part of #26013

See merge request !19298
parent b5832553
No related branches found
No related tags found
1 merge request!22081WIP: Resolve "Release generation from within `.gitlab-ci.yml` - Release Configuration in .yml + Job Token"
Pipeline #104288103 failed
Showing
with 795 additions and 18 deletions
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of release assets.
#
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::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
......@@ -16,7 +16,8 @@ class Job < ::Gitlab::Config::Entry::Node
ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
dependencies before_script needs after_script variables
environment coverage retry parallel extends interruptible timeout].freeze
environment coverage retry parallel extends interruptible
timeout release].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze
......@@ -62,6 +63,14 @@ class Job < ::Gitlab::Config::Entry::Node
errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs")
end
end
validate do
next unless has_commit_tag?
unless only_tags_present?
errors.add(:config, '`only: tags` must be specified with $CI_COMMIT_TAG')
end
end
end
entry :before_script, Entry::Script,
......@@ -149,14 +158,18 @@ class Job < ::Gitlab::Config::Entry::Node
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
:interruptible, :timeout, :release
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
......@@ -211,6 +224,16 @@ def has_rules?
@config.try(:key?, :rules)
end
def has_commit_tag?
return false unless @config.try(:key?, :release)
@config.dig(:release, :tag_name)&.include?('$CI_COMMIT_TAG')
end
def only_tags_present?
@config.try(:key?, :only) && @config[:only].map(&:to_sym).include?(:tags)
end
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
......@@ -241,6 +264,7 @@ def to_hash
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 configuration of release:assets:links.
#
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
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of release:assets:links.
#
class Links < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Validatable
entry :link, Entry::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
# 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::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
......@@ -18,6 +18,8 @@ def initialize(pipeline, attributes, previous_stages)
@seed_attributes = attributes
@previous_stages = previous_stages
@needs_attributes = dig(:needs_attributes)
@resource_group_key = attributes.delete(:resource_group_key)
attributes.fetch(:options, {})&.delete(:release) unless Feature.enabled?(:ci_release_generation, pipeline.project)
@using_rules = attributes.key?(:rules)
@using_only = attributes.key?(:only)
......
......@@ -80,7 +80,8 @@ def build_attributes(name)
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: job[:release]
}.compact }.compact
end
......@@ -132,7 +133,6 @@ def initial_parsing
@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 Attributable
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 Config
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:
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::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
......@@ -24,7 +24,7 @@
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,71 @@
it { expect(entry).to be_valid }
end
context 'when is a release' do
context "tag_name is not $CI_COMMIT_TAG" 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
context "tag_name is $CI_COMMIT_TAG and `only: tags` is present" do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
only: ["tags"],
release: {
tag_name: "$CI_COMMIT_TAG",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it { expect(entry).to be_valid }
end
context 'tag_name includes $CI_COMMIT_TAG in a longer string' do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
only: ["tags"],
release: {
tag_name: "v$CI_COMMIT_TAG December 2019",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it { expect(entry).to be_valid }
end
context "tag_name is $CI_COMMIT_TAG and `only: tags` is present with other options" do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
only: %w[tags web],
release: {
tag_name: "$CI_COMMIT_TAG",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it { expect(entry).to be_valid }
end
end
end
end
......@@ -443,6 +508,80 @@
expect(entry.timeout).to eq('1m 1s')
end
end
context 'when 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
context "tag_name is $CI_COMMIT_TAG and `only: tags` is absent" do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "$CI_COMMIT_TAG",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it "returns error about incorrect $CI_COMMIT_TAG configuration" do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job config `only: tags` must be specified with $CI_COMMIT_TAG'.downcase
end
end
context "tag_name includes $CI_COMMIT_TAG and `only: tags` is absent" do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "v$CI_COMMIT_TAG December 2019",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it "returns error about incorrect $CI_COMMIT_TAG configuration" do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job config `only: tags` must be specified with $CI_COMMIT_TAG'.downcase
end
end
context "tag_name is $CI_COMMIT_TAG and `only` is present but `tags` are absent" do
let(:config) do
{
script: ["make changelog | tee release_changelog.txt"],
only: ['web'],
release: {
tag_name: "$CI_COMMIT_TAG",
name: "Release $CI_TAG_NAME",
description: "./release_changelog.txt"
}
}
end
it "returns error about incorrect $CI_COMMIT_TAG configuration" do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job config `only: tags` must be specified with $CI_COMMIT_TAG'.downcase
end
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::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::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 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 @@
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 '#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 '#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 @@
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 @@
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 @@
# 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
......
......@@ -1257,6 +1257,42 @@ module Ci
end
end
describe "release" do
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
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
it "returns release info" do
expect(processor.stage_builds_attributes('release').first[:options])
.to eq(config[:release].except(:stage, :only))
end
end
describe '#environment' do
let(:config) do
{
......
......@@ -59,7 +59,7 @@
end
end
expectation.to raise_error(ArgumentError, 'Method already defined!')
expectation.to raise_error(ArgumentError, 'Method already defined: length')
end
end
end
......@@ -931,6 +931,87 @@ def previous_commit_sha_from_ref(ref)
end
end
context 'with release' do
shared_examples_for 'a successful release pipeline' do
before do
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 'without $CI_COMMIT_TAG' 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 'with $CI_COMMIT_TAG' do
it_behaves_like 'a successful release pipeline' do
let(:ref_name) { 'refs/tags/v1.1.0' }
let(:config) do
{
release: {
only: ['tags'],
script: ["make changelog | tee release_changelog.txt"],
release: {
tag_name: "$CI_COMMIT_TAG",
description: "./release_changelog.txt"
}
}
}
end
end
end
context 'correctly creates builds with 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) }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment