diff --git a/scripts/api/download_pipeline_artifact.rb b/scripts/api/download_pipeline_artifact.rb new file mode 100644 index 0000000000000000000000000000000000000000..c440880d5fd9de902be6d6bb646593b14671023b --- /dev/null +++ b/scripts/api/download_pipeline_artifact.rb @@ -0,0 +1,99 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'optparse' +require 'gitlab' +require_relative 'default_options' + +class PipelineArtifactFinder + def initialize(options) + @project_id = options.delete(:project_id) + @pipeline_id = options.delete(:pipeline_id) + # Force the token to be a string so that if api_token is nil, it's set to '', allowing unauthenticated requests (for forks). + @api_token = options.delete(:api_token).to_s + @endpoint = options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint] + @artifact_path = options.delete(:artifact_path) + @job_query_params = { per_page: 100 } + + raise ArgumentError, 'artifact_path argument missing' if artifact_path.nil? + + @client = Gitlab.client( + endpoint: endpoint, + private_token: api_token + ) + + warn "No API token given." unless api_token + end + + def execute + client.pipeline_jobs(project_id, pipeline_id, job_query_params).auto_paginate do |job| + url = "#{endpoint}/projects/#{project_id}/jobs/#{job.id}/artifacts/#{artifact_path}" + + fetch(url, "#{job.id}_#{File.basename(artifact_path)}") + end + end + + private + + attr_reader :client, :project_id, :pipeline_id, :api_token, :endpoint, :artifact_path, :job_query_params + + def fetch(uri_str, output_file_path, limit = 10) + raise 'Too many HTTP redirects' if limit == 0 + + uri = URI(uri_str) + request = Net::HTTP::Get.new(uri) + request['Private-Token'] = api_token if api_token + + Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| + http.request(request) do |response| + case response + when Net::HTTPSuccess then + File.open(output_file_path, 'w') do |file| + response.read_body { |body| file.write(body) } + end + when Net::HTTPRedirection then + location = response['location'] + warn "Redirected (#{limit - 1} redirections remaining)." + fetch(location, limit - 1) + when Net::HTTPNotFound then + warn "Not found for #{uri_str}" + else + raise "Unexpected response: #{response.value}" + end + end + end + end +end + +if $0 == __FILE__ + options = API::DEFAULT_OPTIONS.dup + + OptionParser.new do |opts| + opts.on("-p", "--project PROJECT", String, "Project where to find the job (defaults to $CI_PROJECT_ID)") do |value| + options[:project_id] = value + end + + opts.on("--pipeline-id PIPELINE_ID", String, "A pipeline ID") do |value| + options[:pipeline_id] = value + end + + opts.on("-a", "--artifact-path ARTIFACT_PATH", String, "A valid artifact path") do |value| + options[:artifact_path] = value + end + + opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope") do |value| + options[:api_token] = value + end + + opts.on("-E", "--endpoint ENDPOINT", String, "The API endpoint for the API token. (defaults to $CI_API_V4_URL and fallback to https://gitlab.com/api/v4)") do |value| + options[:endpoint] = value + end + + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end + end.parse! + + PipelineArtifactFinder.new(options).execute +end