#!/usr/bin/env ruby require "date" require "json" require "net/http" require "open3" require "optparse" require "pathname" require "tmpdir" options = { assets_file: "azure_devops.tgz", pipeline_assets_dir: "pipelines", root_dir: File.join(ENV["CODESPACE_VSCODE_FOLDER"], "azure_devops/bootstrap"), forecast_source_file: "jobs.json" } OptionParser.new do |opt| opt.on('--organization ORGANIZATION') { |o| options[:organization] = o } opt.on('--project PROJECT') { |o| options[:project] = o } opt.on('--access-token ACCESS_TOKEN') { |o| options[:access_token] = o } end.parse! [:organization, :project, :access_token].each do |key| raise OptionParser::MissingArgument, key if options[key].nil? end def post_request(uri, body, access_token) Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Post.new(uri) request.basic_auth "", access_token request.body = body.to_json request.content_type = 'application/json' response = http.request request abort_on_ado_error!(response, uri) yield(response) if block_given? sleep(5) JSON.parse(response.body) end end def get_request(uri, access_token) Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Get.new(uri) request.basic_auth "", access_token response = http.request request yield(response) if block_given? JSON.parse(response.body) end end def abort_on_ado_error!(response, uri) return if response.code.start_with?("20") message = case response.code when "401" "Please ensure that your Azure DevOps access token has the correct permissions." when "404" JSON.parse(response.body)["message"] when "302" "Please ensure that your Azure DevOps access token is entered correctly" else begin JSON.parse(response.body)["message"] rescue JSON::ParserError response.body end end abort "Error encountered on request:#{uri}\n StatusCode:#{response.code}\n Message: '#{message}'" end def project_exists?(options) organization = options[:organization] project = options[:project] access_token = options[:access_token] uri = URI("https://dev.azure.com/#{organization}/_apis/projects/#{project}?api-version=6.0") get_request(uri, access_token) do |response| return response.code.start_with?("20") end end def create_project(options) organization = options[:organization] project = options[:project] access_token = options[:access_token] puts "Creating project #{project} in organization #{organization}..." uri = URI("https://dev.azure.com/#{organization}/_apis/projects?api-version=6.0") project_to_create = { name: project, description: "Project to be used for GitHub Actions Importer Labs", capabilities: { versioncontrol: { sourceControlType: "Git" }, processTemplate: { templateTypeId: "6b724908-ef14-45cf-84f8-768b5384da45" } } } post_request(uri, project_to_create, access_token) end def create_repository(options) organization = options[:organization] project = options[:project] # use the default repository created when the project is created repository = options[:project] access_token = options[:access_token] root_dir = options[:root_dir] assets_file = options[:assets_file] tmp_dir = Dir.mktmpdir puts "Extracting assets..." _, _, st = Open3.capture3("tar", "-xzf", File.join(root_dir, assets_file), "-C", tmp_dir) abort unless st.exitstatus == 0 uri = URI("https://dev.azure.com/#{organization}/#{project}/_apis/git/repositories/#{repository}/pushes?api-version=7.1-preview.2") puts "Creating repository #{repository}..." root = Pathname.new(root_dir) repository_to_create = { refUpdates: [{ name: "refs/heads/main", oldObjectId: "0000000000000000000000000000000000000000" }], commits: [{ comment: "Initial commit.", changes: Dir[File.join(tmp_dir, "**/*.{cs,csproj,sln,yml}")].reject {|f| File.directory?(f) }.map do |entry| { changeType: "add", item: { path: File.join("/", Pathname.new(entry).relative_path_from(tmp_dir).to_s) }, newContent: { content: File.read(entry), contentType: "rawText" } } end.flatten }] } result = post_request(uri, repository_to_create, access_token) [result, tmp_dir] end def create_yaml_pipelines(options, repository, tmp_dir) organization = options[:organization] project = options[:project] repository_name = repository.dig("repository", "name") repository_id = repository.dig("repository", "id") access_token = options[:access_token] pipeline_assets_dir = options[:pipeline_assets_dir] puts "Creating yaml pipelines..." uri = URI("https://dev.azure.com/#{organization}/#{project}/_apis/pipelines?api-version=6.0-preview.1") root = Pathname.new(tmp_dir) Dir[File.join(tmp_dir, pipeline_assets_dir, "yml", "*")].each do |entry| puts "Creating pipeline #{entry}..." pipeline_to_create = { folder: "pipelines", name: File.basename(entry, ".yml"), configuration: { type: "yaml", path: File.join("/", Pathname.new(entry).relative_path_from(root).to_s), repository: { id: repository_id, name: repository_name, type: "azureReposGit" } } } post_request(uri, pipeline_to_create, access_token) end end def create_classic_pipelines(options, repository) organization = options[:organization] project = options[:project] repository_name = repository.dig("repository", "name") repository_id = repository.dig("repository", "id") access_token = options[:access_token] root_dir = options[:root_dir] pipeline_assets_dir = options[:pipeline_assets_dir] puts "Creating classic pipelines..." uri = URI("https://dev.azure.com/#{organization}/#{project}/_apis/build/definitions?api-version=7.1-preview.7") root = Pathname.new(root_dir) Dir[File.join(root_dir, pipeline_assets_dir, "classic", "*")].each do |entry| puts "Creating pipeline #{entry}..." pipeline_to_create = JSON.parse(File.read(entry)) pipeline_to_create["repository"]["name"] = repository_name pipeline_to_create["repository"]["id"] = repository_id post_request(uri, pipeline_to_create, access_token) end end def update_forecast_source_file(options, tmp_dir) puts "Updating forecast data" root_dir = options[:root_dir] jobs_data_file = options[:forecast_source_file] jobs_data = File.read(File.join(tmp_dir, jobs_data_file)) today = Date.today.strftime("%Y-%m-%d") jobs_data.gsub!(/20[0-2][0-9]-[0-1][0-9]-[0-3][0-9]/, today) File.write(File.join(root_dir, jobs_data_file), jobs_data) end if project_exists?(options) print "Project '#{options[:project]}' already exists. Would you like to continue? [y/N]" abort unless gets.chomp.casecmp? "y" else create_project(options) end repository, tmp_dir = create_repository(options) create_yaml_pipelines(options, repository, tmp_dir) create_classic_pipelines(options, repository) update_forecast_source_file(options, tmp_dir) puts "Success!"