lib/release_tools/pipeline_tracer/pipeline.rb (132 lines of code) (raw):
# frozen_string_literal: true
module ReleaseTools
module PipelineTracer
# Helper class for the PipelineTracer module
class Pipeline
include ::SemanticLogger::Loggable
PIPELINE_URL_REGEX = %r{https://(?<instance>dev\.gitlab\.org|ops\.gitlab\.net|gitlab\.com)/(?<project>.+)/-/pipelines/(?<pipeline_id>\d+)}
FINISHED_STATUSES = %w[success failed canceled].freeze
# Jobs that should be ignored when calculating end_time of the pipeline.
# This is used to ignore the metrics:trace-pipeline job since the job runs much later than
# the rest of the pipeline and should not count towards the duration of the pipeline.
IGNORED_JOBS_FOR_END_TIME_CALCULATION = %w[metrics:trace-pipeline].freeze
attr_reader :gitlab_instance, :project, :id
# @param [string] pipeline_url
def self.from_url(pipeline_url)
match = PIPELINE_URL_REGEX.match(pipeline_url)
new(match[:instance], match[:project], match[:pipeline_id])
end
# @param [string] gitlab_instance Ex: 'ops.gitlab.net', 'dev.gitlab.org', 'gitlab.com'
# @param [string] project Ex: 'gitlab-org/release/tools'
# @param [string, integer] pipeline_id Ex: '12345' or 12345
def initialize(gitlab_instance, project, pipeline_id)
@gitlab_instance = gitlab_instance
@project = project
@id = pipeline_id
end
def details
@details ||=
Retriable.with_context(:api) do
client.pipeline(project, id)
end
end
def bridge_jobs
@bridge_jobs ||=
Retriable.with_context(:api) do
client.pipeline_bridges(project, id)
end
end
def jobs
@jobs ||=
Retriable.with_context(:api) do
client.pipeline_jobs(project, id, { include_retried: true, per_page: 100 })
end
end
def url
"https://#{gitlab_instance}/#{project}/-/pipelines/#{id}"
end
def start_time
return @start_time if defined?(@start_time)
logger.info(
'Start time of pipeline',
pipeline_url: url,
started_at: details.started_at,
created_at: details.created_at
)
@start_time ||= details.started_at || details.created_at
end
def end_time
@end_time ||=
if valid_finished_at?
logger.info(
'Has valid finished_at',
pipeline_url: url,
finished_at: details.finished_at,
updated_at: details.updated_at
)
details.finished_at || details.updated_at
else
calculate_end_time
end
end
def real_time_duration
Time.parse(end_time) - Time.parse(start_time)
end
def root_attributes
{
user: details.user.username,
web_url: details.web_url,
created_at: details.created_at,
updated_at: details.updated_at,
ref: details.ref,
duration: details.duration || 0,
queued_duration: details.queued_duration || 0,
status: details.status,
started_at: details.started_at,
finished_at: details.finished_at || 0
}
end
def valid_finished_at?
FINISHED_STATUSES.include?(details.status)
end
def deploy_environment
deploy_env = pipeline_variables.detect { |v| v.key == 'DEPLOY_ENVIRONMENT' }
return unless deploy_env
deploy_env.value
end
def client
@client ||=
case gitlab_instance
when 'dev.gitlab.org'
GitlabDevClient
when 'ops.gitlab.net'
GitlabOpsClient
when 'gitlab.com'
GitlabClient
else
raise "Unknown Gitlab instance: #{gitlab_instance}"
end
end
private
# The end_time of the pipeline should be the finished_at value of the last job to complete.
def calculate_end_time
last_job_end_time = last_job_finished_at
last_bridge_job_end_time = last_bridge_job_finished_at
logger.info(
'Calculating end time',
pipeline_url: url,
last_job_finished_at: last_job_end_time&.iso8601,
last_bridge_job_finished_at: last_bridge_job_end_time&.iso8601
)
[last_job_end_time, last_bridge_job_end_time].compact.max&.iso8601
end
def last_job_finished_at
all_finished_at = jobs.auto_paginate.collect do |job|
next if IGNORED_JOBS_FOR_END_TIME_CALCULATION.include?(job.name)
next unless job.finished_at
Time.parse(job.finished_at)
end
all_finished_at.compact.max
end
def last_bridge_job_finished_at
bridge_jobs
.auto_paginate
.filter_map { |job| Time.parse(job.finished_at) if job.finished_at }
.max
end
def pipeline_variables
client.pipeline_variables(project, id)
end
end
end
end