Commit 76923a3f authored by Jay Swain's avatar Jay Swain

Release Highlights Linter

Adding a linter/validator for the release highlights ("whats new") .yaml
files that will be added to the repo every month.

This code leverages ActiveModel::Validations and ties the errors back to
where the error occurs in the .yaml file.

I decided to actually run the `validate_all!` method in the spec in
which it's tested. Though I do feel like moving this to it's own linter
plugin could be a better place eventually.

part of:
https://gitlab.com/gitlab-org/growth/engineering/-/issues/5403
parent 11791b17
......@@ -63,7 +63,7 @@
stage: Release
self-managed: true
gitlab-com: true
packages: [starter, premium, ultimate]
packages: [Starter, Premium, Ultimate]
url: https://www.youtube.com/embed/1FBRaBQTQZk
image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg
published_at: 2020-09-22
......
......@@ -47,4 +47,5 @@
stage: Verify
body: |
Available today is the GitLab Runner container image for the [Red Hat OpenShift Container Platform](https://www.openshift.com/products/container-platform). To install the runner on OpenShift, you can use the new [GitLab Runner Operator](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator) available from the beta channel in Red Hat's Operator Hub - a web console for OpenShift cluster administrators to discover and select Operators to install on their cluster. Operator Hub is deployed by default in the OpenShift Container Platform. We plan to transition the GitLab Runner Operator to the stable channel, and by extension [GA](https://gitlab.com/gitlab-org/gl-openshift/gitlab-runner-operator/-/issues/6), in early 2021. Finally, we are also developing an operator for GitLab, so stay tuned to future release posts for those announcements.
published_at: 2020-12-22
release: 13.7
# frozen_string_literal: true
module ReleaseHighlights
class Validator
attr_reader :errors, :file
def initialize(file:)
@file = file
@errors = []
end
def valid?
document = YAML.parse(File.read(file))
document.root.children.each do |entry|
entry = ReleaseHighlights::Validator::Entry.new(entry)
errors.push(entry.errors.full_messages) unless entry.valid?
end
errors.none?
end
def self.validate_all!
@all_errors = []
ReleaseHighlight.file_paths.each do |file_path|
instance = self.new(file: file_path)
@all_errors.push([instance.errors, instance.file]) unless instance.valid?
end
@all_errors.none?
end
def self.error_message
io = StringIO.new
@all_errors.each do |errors, file|
message = "Validation failed for #{file}"
line = -> { io.puts "-" * message.length }
line.call
io.puts message
line.call
errors.flatten.each { |error| io.puts "* #{error}" }
io.puts
end
io.string
end
end
end
# frozen_string_literal: true
module ReleaseHighlights
class Validator::Entry
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
PACKAGES = %w(Core Starter Premium Ultimate).freeze
attr_reader :entry
validates :title, :body, :stage, presence: true
validates :'self-managed', :'gitlab-com', inclusion: { in: [true, false], message: "must be a boolean" }
validates :url, :image_url, format: { with: URI::DEFAULT_PARSER.make_regexp, message: 'must be a URL' }
validates :release, numericality: true
validate :validate_published_at
validate :validate_packages
after_validation :add_line_numbers_to_errors!
def initialize(entry)
@entry = entry
end
def validate_published_at
published_at = value_for('published_at')
return if published_at.is_a?(Date)
errors.add(:published_at, 'must be valid Date')
end
def validate_packages
packages = value_for('packages')
if !packages.is_a?(Array) || packages.empty? || packages.any? { |p| PACKAGES.exclude?(p) }
errors.add(:packages, "must be one of #{PACKAGES}")
end
end
def read_attribute_for_validation(key)
value_for(key)
end
private
def add_line_numbers_to_errors!
errors.messages.each do |attribute, messages|
messages.map! { |m| "#{m} (line #{line_number_for(attribute)})" }
end
end
def line_number_for(key)
node = find_node(key)
(node&.start_line || @entry.start_line) + 1
end
def value_for(key)
node = find_node(key)
return if node.nil?
index = entry.children.find_index(node)
next_node = entry.children[index + 1]
next_node&.to_ruby
end
def find_node(key)
entry.children.find {|node| node.try(:value) == key.to_s }
end
end
end
- title:
body:
stage:
self-managed:
gitlab-com:
url:
image_url:
published_at:
release:
- title: Create and view requirements in GitLab
body: The first step towards managing requirements from within GitLab is here! This initial release allows users to create and view requirements at a project level. As Requirements Management evolves in GitLab, stay tuned for support for traceability between all artifacts, creating a seamless workflow to visually demonstrate completeness and compliance.
stage: Plan
self-managed: true
gitlab-com: true
packages: [ALL]
url: https://docs.gitlab.com/ee/user/project/requirements/index.html
image_url: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png
published_at: 2020-04-22
release: 12.10
- title: Retrieve CI/CD secrets from HashiCorp Vault
body: In this release, GitLab adds support for lightweight JSON Web Token (JWT) authentication to integrate with your existing HashiCorp Vault. Now, you can seamlessly provide secrets to CI/CD jobs by taking advantage of HashiCorp's JWT authentication method rather than manually having to provide secrets as a variable in GitLab.
stage: Release
self-managed: true
gitlab-com: true
packages: [Starter]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
release: 12.10
- title: Create and view requirements in GitLab
body: The first step towards managing requirements from within GitLab is here! This initial release allows users to create and view requirements at a project level. As Requirements Management evolves in GitLab, stay tuned for support for traceability between all artifacts, creating a seamless workflow to visually demonstrate completeness and compliance.
stage: Plan
self-managed: true
gitlab-com: true
packages: [Ultimate]
url: https://docs.gitlab.com/ee/user/project/requirements/index.html
image_url: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png
published_at: 2020-04-22
release: 12.10
- title: Retrieve CI/CD secrets from HashiCorp Vault
body: In this release, GitLab adds support for lightweight JSON Web Token (JWT) authentication to integrate with your existing HashiCorp Vault. Now, you can seamlessly provide secrets to CI/CD jobs by taking advantage of HashiCorp's JWT authentication method rather than manually having to provide secrets as a variable in GitLab.
stage: Release
self-managed: true
gitlab-com: true
packages: [Starter]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
release: 12.10
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleaseHighlights::Validator::Entry do
subject(:entry) { described_class.new(document.root.children.first) }
let(:document) { YAML.parse(File.read(yaml_path)) }
let(:yaml_path) { 'spec/fixtures/whats_new/blank.yml' }
describe 'validations' do
before do
allow(entry).to receive(:value_for).and_call_original
end
context 'with a valid entry' do
let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' }
it { is_expected.to be_valid }
end
context 'with an invalid entry' do
let(:yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
it 'returns line numbers in errors' do
subject.valid?
expect(entry.errors[:packages].first).to match('(line 6)')
end
end
context 'with a blank entry' do
it 'validate presence of title, body and stage' do
subject.valid?
expect(subject.errors[:title]).not_to be_empty
expect(subject.errors[:body]).not_to be_empty
expect(subject.errors[:stage]).not_to be_empty
expect(subject.errors[:packages]).not_to be_empty
end
it 'validates boolean value of "self-managed" and "gitlab-com"' do
allow(entry).to receive(:value_for).with('self-managed').and_return('nope')
allow(entry).to receive(:value_for).with('gitlab-com').and_return('yerp')
subject.valid?
expect(subject.errors[:'self-managed']).to include(/must be a boolean/)
expect(subject.errors[:'gitlab-com']).to include(/must be a boolean/)
end
it 'validates URI of "url" and "image_url"' do
allow(entry).to receive(:value_for).with('image_url').and_return('imgur/gitlab_feature.gif')
allow(entry).to receive(:value_for).with('url').and_return('gitlab/newest_release.html')
subject.valid?
expect(subject.errors[:url]).to include(/must be a URL/)
expect(subject.errors[:image_url]).to include(/must be a URL/)
end
it 'validates release is numerical' do
allow(entry).to receive(:value_for).with('release').and_return('one')
subject.valid?
expect(subject.errors[:release]).to include(/is not a number/)
end
it 'validates published_at is a date' do
allow(entry).to receive(:value_for).with('published_at').and_return('christmas day')
subject.valid?
expect(subject.errors[:published_at]).to include(/must be valid Date/)
end
it 'validates packages are included in list' do
allow(entry).to receive(:value_for).with('packages').and_return(['ALL'])
subject.valid?
expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleaseHighlights::Validator do
let(:validator) { described_class.new(file: yaml_path) }
let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' }
let(:invalid_yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
describe '#valid?' do
subject { validator.valid? }
context 'with a valid file' do
it 'passes entries to entry validator and returns true' do
expect(ReleaseHighlights::Validator::Entry).to receive(:new).exactly(:twice).and_call_original
expect(subject).to be true
expect(validator.errors).to be_empty
end
end
context 'with invalid file' do
let(:yaml_path) { invalid_yaml_path }
it 'returns false and has errors' do
expect(subject).to be false
expect(validator.errors).not_to be_empty
end
end
end
describe '.validate_all!' do
subject { described_class.validate_all! }
before do
allow(ReleaseHighlight).to receive(:file_paths).and_return(yaml_paths)
end
context 'with valid files' do
let(:yaml_paths) { [yaml_path, yaml_path] }
it { is_expected.to be true }
end
context 'with an invalid file' do
let(:yaml_paths) { [invalid_yaml_path, yaml_path] }
it { is_expected.to be false }
end
end
describe '.error_message' do
subject do
described_class.validate_all!
described_class.error_message
end
before do
allow(ReleaseHighlight).to receive(:file_paths).and_return([yaml_path])
end
context 'with a valid file' do
it { is_expected.to be_empty }
end
context 'with an invalid file' do
let(:yaml_path) { invalid_yaml_path }
it 'returns a nice error message' do
expect(subject).to eq(<<-MESSAGE.strip_heredoc)
---------------------------------------------------------
Validation failed for spec/fixtures/whats_new/invalid.yml
---------------------------------------------------------
* Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6)
MESSAGE
end
end
end
describe 'when validating all files' do
it 'they should have no errors' do
expect(described_class.validate_all!).to be_truthy, described_class.error_message
end
end
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ReleaseHighlight do
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*\_(\d*\_\d*)\.yml$/) }
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
......
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