Updating distribution

This commit is contained in:
Valet Bot
2022-01-07 19:45:53 +00:00
parent 74be75ac84
commit 51b31a805e
60 changed files with 1895 additions and 1 deletions
+27
View File
@@ -0,0 +1,27 @@
---
name: Azure DevOps
about: Migrate Azure DevOps pipelines to GitHub Actions with Valet
title: "[Azure DevOps]:"
labels: azure-devops
assignees: ""
---
## Inputs
Provide the following required inputs:
Organization:
_The Azure DevOps organization to migrate pipelines from._
Project:
_The Azure DevOps project to migrate pipelines from._
## Available commands
The following commands can be executed by adding a comment to this issue:
- `/audit`
- `/dry-run --pipeline-type pipeline|release --pipeline-id :pipeline-id`
- `/migrate --pipeline-type pipeline|release --pipeline-id :pipeline-id --target-url :github-repository-url`
**Note:** The `pipeline-type` option will default to `pipeline` if omitted. If any remaining options are missing, the command will not be successful.
+24
View File
@@ -0,0 +1,24 @@
---
name: Circle CI
about: Migrate Circle CI pipelines to GitHub Actions with Valet
title: "[Circle CI]:"
labels: circle-ci
assignees: ""
---
## Inputs
Provide the following required inputs:
Organization:
_The Circle CI organization to migrate pipelines from._
## Available commands
The following commands can be executed by adding a comment to this issue:
- `/audit`
- `/dry-run --repository :repository-name`
- `/migrate --repository :repository-name --target-url :github-repository-url`
**Note**: If any options are missing, the command will not be successful.
+24
View File
@@ -0,0 +1,24 @@
---
name: GitLab CI
about: Migrate GitLab CI pipelines to GitHub Actions with Valet
title: "[GitLab CI]:"
labels: gitlab
assignees: ""
---
## Inputs
Provide the following required inputs:
Namespace:
_The GitLab CI namespace (or group) to migrate pipelines from._
## Available commands
The following commands can be executed by adding a comment to this issue:
- `/audit`
- `/dry-run --project :project-name`
- `/migrate --project :project-name --target-url :github-repository-url`
**Note**: If any options are missing, the command will not be successful.
+24
View File
@@ -0,0 +1,24 @@
---
name: Jenkins
about: Migrate Jenkins jobs to GitHub Actions with Valet
title: "[Jenkins]:"
labels: jenkins
assignees: ""
---
## Inputs
Provide the following optional inputs:
Folders:
_Include specific folders in an audit_
## Available commands
The following commands can be executed by adding a comment to this issue:
- `/audit`
- `/dry-run --source-url :jenkins-job-url`
- `/migrate --source-url :jenkins-job-url --target-url :github-repository-url`
**Note**: If any options are missing, the command will not be successful.
+24
View File
@@ -0,0 +1,24 @@
---
name: Travis CI
about: Migrate Travis CI pipelines to GitHub Actions with Valet
title: "[Travis CI]:"
labels: travis-ci
assignees: ""
---
## Inputs
Provide the following required inputs:
Organization:
_The Travis CI organization to migrate pipelines from._
## Available commands
The following commands can be executed by adding a comment to this issue:
- `/audit`
- `/dry-run --repository :repository-name`
- `/migrate --repository :repository-name --target-url :github-repository-url`
**Note**: If any options are missing, the command will not be successful.
+33
View File
@@ -0,0 +1,33 @@
name: ci
on:
push:
branches: [main]
pull_request:
env:
ruby_version: 2.7.1
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.ruby_version }}
- name: Install dependencies
run: bundle install
- name: Run specs
run: bin/rspec
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1.47.0
with:
ruby-version: ${{ env.ruby_version }}
- name: Install dependencies
run: bundle install
- name: Lint
run: bin/rubocop
+227
View File
@@ -0,0 +1,227 @@
name: valet-issue-ops
on:
issue_comment:
types: [created]
env:
GITHUB_INSTANCE_URL: https://github.com
GITHUB_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
JENKINS_INSTANCE_URL: https://jenkout.westus2.cloudapp.azure.com
JENKINS_USERNAME: ${{ secrets.jenkins_username }}
JENKINS_ACCESS_TOKEN: ${{ secrets.jenkins_access_token }}
JENKINSFILE_ACCESS_TOKEN: ${{ secrets.jenkinsfile_access_token }}
AZURE_DEVOPS_ACCESS_TOKEN: ${{ secrets.azure_devops_access_token }}
TRAVIS_CI_ACCESS_TOKEN: ${{ secrets.travis_ci_access_token }}
TRAVIS_CI_SOURCE_GITHUB_ACCESS_TOKEN: ${{ secrets.travis_ci_source_github_access_token }}
GITLAB_ACCESS_TOKEN: ${{ secrets.gitlab_access_token }}
CIRCLE_CI_ACCESS_TOKEN: ${{ secrets.circle_ci_access_token }}
CIRCLE_CI_SOURCE_GITHUB_ACCESS_TOKEN: ${{ secrets.circle_ci_source_github_access_token }}
jobs:
execute-valet:
runs-on: ubuntu-latest
outputs:
command: ${{ steps.prepare.outputs.command }}
log-filename: ${{ steps.logs.outputs.filename }}
container:
image: ghcr.io/valet-customers/valet-cli:latest
credentials:
username: ${{ secrets.valet_ghcr_username }}
password: ${{ secrets.valet_ghcr_password }}
steps:
- uses: actions-ecosystem/action-add-labels@v1
if: always()
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: valet-running
- uses: actions/checkout@v2
- name: Install dependencies
run: bundle install --without development
- name: Prepare arguments
id: prepare
run: |
echo "${{ toJSON(github.event.issue.labels.*.name) }}"
./bin/parse_issue "${{ github.event.issue.body }}" "${{ github.event.comment.body }}" "${{ toJSON(github.event.issue.labels.*.name) }}"
- name: Validate arguments
run: |
if [ -z "${{ steps.prepare.outputs.provider }}" ]; then
echo "Unable to determine provider"
exit 1
elif [ -z "${{ steps.prepare.outputs.command }}" ]; then
echo "Unable to determine command"
exit 1
fi
- name: execute valet
run: |
valet ${{ steps.prepare.outputs.command }} ${{ steps.prepare.outputs.provider }} \
${{ steps.prepare.outputs.args }} \
--output-dir /data/output \
--no-telemetry
- uses: actions/upload-artifact@v2
if: always()
with:
path: /data/output/
name: output
- if: always()
id: logs
run: |
path=$(ls /data/output/log/*.log | head -1)
filename=$(basename "$path")
echo "LOG_FILE_PATH=$path" >> $GITHUB_ENV
echo "::set-output name=filename::$filename"
- uses: actions/upload-artifact@v2
if: always()
with:
path: ${{ env.LOG_FILE_PATH }}
name: logs
audit:
runs-on: ubuntu-latest
if: needs.execute-valet.outputs.command == 'audit'
needs: execute-valet
steps:
- uses: actions/download-artifact@v2
if: always()
with:
name: output
- uses: actions/github-script@v5
with:
script: |
const fs = require('fs')
const summaryText = fs.readFileSync("./audit_summary.md", "utf8")
const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`
const body = `Audit successfully completed :rocket:
<details>
<summary>Audit summary :point_down:</summary>
\`\`\`
${summaryText}
\`\`\`
</details>
Download full results [here](${artifactUrl})
`
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
dry-run:
runs-on: ubuntu-latest
if: needs.execute-valet.outputs.command == 'dry-run'
needs: execute-valet
steps:
- uses: actions/download-artifact@v2
if: always()
with:
name: output
- uses: actions/github-script@v5
with:
script: |
const fs = require('fs')
const directory = "${{ github.workspace }}/"
const globber = await glob.create(`${directory}**/*.yml`)
const workflows = []
for await (const file of globber.globGenerator()) {
const content = fs.readFileSync(file, 'utf8')
workflows.push(
"<details>",
` <summary>${file.substring(directory.length)}</summary>`,
"",
"```yaml",
content,
"```",
"</details>",
""
);
}
const body = `Dry run was successful :boom:
Transformed workflows:
${workflows.join("\n")}
`
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
migrate:
runs-on: ubuntu-latest
if: needs.execute-valet.outputs.command == 'migrate'
needs: execute-valet
steps:
- uses: actions/download-artifact@v2
if: always()
with:
name: logs
- id: pull-request-url
run: |
pullRequest=$(grep "${{ env.pullRequestPattern }}" ${{ needs.execute-valet.outputs.log-filename }} | sed -rn "s/^.*${{ env.pullRequestPattern }}'(.+)'.*$/\1/p")
echo $pullRequest
echo ::set-output name=output::$pullRequest
env:
pullRequestPattern: "Pull request: "
- uses: actions/github-script@v5
env:
PULL_REQUEST_URL: "${{ steps.pull-request-url.outputs.output }}"
with:
script: |
const body = `Migration was successful :sparkles:
Continue to the [pull request](${process.env.PULL_REQUEST_URL}) to complete the migration.
`
try {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
} catch(e) {
console.log(e)
}
cleanup:
runs-on: ubuntu-latest
needs: [execute-valet, audit, migrate, dry-run]
if: always()
steps:
- uses: actions/download-artifact@v2
if: always() && needs.execute-valet.result == 'failure'
with:
name: logs
- uses: actions/github-script@v5
if: always() && needs.execute-valet.result == 'failure'
with:
script: |
const fs = require('fs')
const body = `Something went wrong. Please check the logs for more information.
<details>
<summary>Logs :point_down:</summary>
\`\`\`
${fs.readFileSync("${{ needs.execute-valet.outputs.log-filename }}", "utf8")}
\`\`\`
</details>
`
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
- uses: actions-ecosystem/action-remove-labels@v1
if: always()
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: valet-running
+20
View File
@@ -0,0 +1,20 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/log/
/vendor/
.DS_Store
# rspec failure tracking
.rspec_status
.env*local
*.gem
.vscode/settings.json
+5
View File
@@ -0,0 +1,5 @@
--format progress
--color
--require spec_helper
--format RSpec::Github::Formatter
--pattern "**/*_spec.rb"
+37
View File
@@ -0,0 +1,37 @@
inherit_gem:
rubocop-github:
- config/default_edge.yml
Lint/AmbiguousBlockAssociation:
Exclude:
- "spec/**/*"
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/Documentation:
Enabled: false
Naming/MethodParameterName:
Enabled: false
Style/MultilineIfModifier:
Enabled: false
Performance/Detect:
Enabled: false
Layout/FirstArrayElementLineBreak:
Enabled: true
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/FirstHashElementLineBreak:
Enabled: true
Layout/FirstMethodArgumentLineBreak:
Enabled: true
Layout/HashAlignment:
EnforcedHashRocketStyle: table
EnforcedColonStyle: table
EnforcedLastArgumentHashStyle: always_inspect
Layout/MultilineHashKeyLineBreaks:
Enabled: true
Layout/MultilineMethodArgumentLineBreaks:
Enabled: true
+1
View File
@@ -0,0 +1 @@
2.7.1
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem "require_all", "~> 3.0.0"
group :development do
gem "dotenv", "~> 2.7.6"
gem "factory_bot", "~> 6.1"
gem "faker", "~> 2.17"
gem "pry-byebug", "~> 3.9"
gem "rspec", "~> 3.10"
gem "rspec-github", "~> 2.3", ">= 2.3.1"
gem "rubocop", "~> 0.80", "< 0.81"
gem "rubocop-github", "~> 0.14.0"
gem "rubocop-performance", "~> 1.6.1"
gem "ruby-debug-ide", "~> 0.7.2"
gem "shoulda-matchers", "~> 4.5.1"
end
+94
View File
@@ -0,0 +1,94 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
ast (2.4.2)
byebug (11.1.3)
coderay (1.1.3)
concurrent-ruby (1.1.9)
diff-lcs (1.4.4)
dotenv (2.7.6)
factory_bot (6.2.0)
activesupport (>= 5.0.0)
faker (2.19.0)
i18n (>= 1.6, < 2)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.4)
method_source (1.0.0)
minitest (5.14.4)
parallel (1.21.0)
parser (3.0.3.1)
ast (~> 2.4.1)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
rainbow (3.0.0)
rake (13.0.6)
require_all (3.0.0)
rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-github (2.3.1)
rspec-core (~> 3.0)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.3)
rubocop (0.80.1)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-github (0.14.0)
rubocop (~> 0.59)
rubocop-performance (1.6.1)
rubocop (>= 0.71.0)
ruby-debug-ide (0.7.3)
rake (>= 0.8.1)
ruby-progressbar (1.11.0)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
unicode-display_width (1.6.1)
zeitwerk (2.5.1)
PLATFORMS
ruby
x86_64-darwin-19
DEPENDENCIES
dotenv (~> 2.7.6)
factory_bot (~> 6.1)
faker (~> 2.17)
pry-byebug (~> 3.9)
require_all (~> 3.0.0)
rspec (~> 3.10)
rspec-github (~> 2.3, >= 2.3.1)
rubocop (~> 0.80, < 0.81)
rubocop-github (~> 0.14.0)
rubocop-performance (~> 1.6.1)
ruby-debug-ide (~> 0.7.2)
shoulda-matchers (~> 4.5.1)
BUNDLED WITH
2.2.33
+82 -1
View File
@@ -1 +1,82 @@
# issue-ops
# Valet Issue Ops
Valet can be orchestrated using GitHub Actions and Issues. This template repository can be used to enable this workflow to migrate pipelines from an existing CI/CD instance to GitHub Actions.
## Getting started
To get started create a new repository using this repository as the template by clicking [here](https://github.com/github/valet-issue-ops/generate). Next, add the repository secrets relevant to the CI/CD providers being migrated:
### All CI/CD providers
The following secrets are required:
- `VALET_GHCR_USERNAME`: The username to access the `valet` container.
- `VALET_GHCR_PASSWORD`: The password to access the `valet` container (requires `read:packages` scope).
- `GH_ACCESS_TOKEN`: GitHub personal access token to create pull requests (requires `repo` and `workflow` scopes).
Optionally, the following environment variables can be set:
- `GITHUB_INSTANCE_URL`: The base URL of your GitHub instance (only required if it is **not** <https://github.com>).
### Azure DevOps
The following secrets are required:
- `AZURE_DEVOPS_ACCESS_TOKEN`: The personal access token to access the Azure DevOps instance. This token requires the following scopes:
- Build (Read & Execute)
- Code (Read)
- Release (Read)
- Service Connections (Read)
- Variable Groups (Read)
Optionally, the following environment variables can be set:
- `AZURE_DEVOPS_INSTANCE_URL`: The base URL of your Azure DevOps instance (only required if it is **not** <https://dev.azure.com>).
### Circle CI
The following secrets are required:
- `CIRCLE_CI_ACCESS_TOKEN`: The personal access token to access the Circle CI instance.
- `CIRCLE_CI_SOURCE_GITHUB_ACCESS_TOKEN`: The personal access token to access pipeline files stored in GitHub.
Optionally, the following environment variables can be set:
- `CIRCLE_CI_INSTANCE_URL`: The base URL of your Circle CI instance (only required if it is **not** <https://circleci.com>).
### GitLab CI
The following secrets are required:
- `GITLAB_ACCESS_TOKEN`: The personal access token to access the GitLab CI instance (requires `read_api` scope).
Optionally, the following environment variables can be set:
- `GITLAB_INSTANCE_URL`: The base URL of your GitLab CI instance (only required if it is **not** <https://gitlab.com>).
### Jenkins
The following secrets are required:
- `JENKINSFILE_ACCESS_TOKEN`: The personal access token used to retrieve the contents of a `Jenkinsfile` stored in the build repository (requires `repo` scope).
- `JENKINS_ACCESS_TOKEN`: The access token used to view Jenkins resources.
- `JENKINS_USERNAME`: The username of the user's access token.
The following environment variables are required:
- `JENKINS_INSTANCE_URL`: The base URL of your Jenkins instance.
### Travis CI
The following secrets are required:
- `TRAVIS_CI_ACCESS_TOKEN`: The personal access token to access the Travis CI instance.
- `TRAVIS_CI_SOURCE_GITHUB_ACCESS_TOKEN`: The personal access token to access pipeline files stored in GitHub.
Optionally, the following environment variables can be set:
- `TRAVIS_CI_INSTANCE_URL`: The base URL of your Travis CI instance (only required if it is **not** <https://travis-ci.com>).
## Pipeline migration
Once configured, pipelines can be migrated to GitHub Actions by opening an issue with the relevant issue template and following the instructions.
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "./cli"
Pry.start
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "yaml"
require "open3"
require_relative "./../cli"
issue_content, comment_body, labels = ARGV
puts labels
raise "No issue content provided" if issue_content.nil?
raise "No comment provided" if comment_body.nil?
raise "No labels provided" if labels.nil?
provider = Provider.new(labels)
command = Command.new(comment_body)
arguments = Arguments.new(provider, command, issue_content)
return unless command.valid?
provider.to_output
command.to_output
arguments.to_output
Executable
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rspec' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path(
"../../Gemfile",
Pathname.new(__FILE__).realpath
)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rspec-core", "rspec")
Executable
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rubocop' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path(
"../../Gemfile",
Pathname.new(__FILE__).realpath
)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rubocop", "rubocop")
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
require "require_all"
require "pry" if ENV["VALET_CONTAINER"].nil?
require "json"
require_all "lib"
+10
View File
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module IssueParser
def parameter_from_issue(name, text)
match = text.match(/#{name}: ([^\n]+)/)
return if match.nil?
match[1].strip
end
end
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
module OutputWriter
def set_output(name, value)
return if value.nil?
puts "::set-output name=#{name}::#{value}"
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
require_relative "../concerns/output_writer"
class Arguments
include OutputWriter
def initialize(provider, command, issue_content)
@args = argument_class(provider, command, issue_content)
end
def argument_class(provider, command, issue_content)
provider.module.const_get(command.classify).new(issue_content, command)
end
def to_output
arguments = @args.to_a
return if arguments.blank?
set_output(
"args",
arguments.map do |a|
next a unless a.include?(" ")
a.inspect
end.join(" ")
)
end
end
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module AzureDevops
class Audit
include IssueParser
def initialize(issue_content, _)
@organization = parameter_from_issue("Organization", issue_content)
@project = parameter_from_issue("Project", issue_content)
end
def to_a
args = []
args.concat(["--azure-devops-organization", @organization]) unless @organization.nil?
args.concat(["--azure-devops-project", @project]) unless @project.nil?
return args unless args.empty?
end
end
end
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module AzureDevops
class DryRun
include IssueParser
def initialize(issue_content, command)
@organization = parameter_from_issue("Organization", issue_content)
@project = parameter_from_issue("Project", issue_content)
@pipeline_type = command.options.fetch("pipeline-type", "pipeline")
@pipeline_id = command.options["pipeline-id"]
end
def to_a
args = [@pipeline_type]
args.concat(["--azure-devops-organization", @organization]) unless @organization.nil?
args.concat(["--azure-devops-project", @project]) unless @project.nil?
args.concat(["--pipeline-id", @pipeline_id]) unless @pipeline_id.nil?
return args unless args.empty?
end
end
end
+26
View File
@@ -0,0 +1,26 @@
# frozen_string_literal: true
module AzureDevops
class Migrate
include IssueParser
def initialize(issue_content, command)
@organization = parameter_from_issue("Organization", issue_content)
@project = parameter_from_issue("Project", issue_content)
@pipeline_type = command.options.fetch("pipeline-type", "pipeline")
@pipeline_id = command.options["pipeline-id"]
@target_url = command.options["target-url"]
end
def to_a
args = [@pipeline_type]
args.concat(["--azure-devops-organization", @organization]) unless @organization.nil?
args.concat(["--azure-devops-project", @project]) unless @project.nil?
args.concat(["--pipeline-id", @pipeline_id]) unless @pipeline_id.nil?
args.concat(["--target-url", @target_url]) unless @target_url.nil?
return args unless args.empty?
end
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module CircleCI
class Audit
include IssueParser
def initialize(issue_content, _)
@organization = parameter_from_issue("Organization", issue_content)
end
def to_a
return if @organization.nil?
["--circle-ci-organization", @organization]
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module CircleCI
class DryRun
include IssueParser
def initialize(issue_content, command)
# TODO: manually test this
@organization = parameter_from_issue("Organization", issue_content)
@project = command.options["project"]
end
def to_a
args = []
args.concat(["--circle-ci-organization", @organization]) unless @organization.nil?
args.concat(["--circle-ci-project", @project]) unless @project.nil?
return args unless args.empty?
end
end
end
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module CircleCI
class Migrate
include IssueParser
def initialize(issue_content, command)
@organization = parameter_from_issue("Organization", issue_content)
@project = command.options["project"]
@target_url = command.options["target-url"]
end
def to_a
args = []
args.concat(["--circle-ci-organization", @organization]) unless @organization.nil?
args.concat(["--circle-ci-project", @project]) unless @project.nil?
args.concat(["--target-url", @target_url]) unless @target_url.nil?
return args unless args.empty?
end
end
end
+50
View File
@@ -0,0 +1,50 @@
# frozen_string_literal: true
require "active_support/core_ext/string"
require_relative "../concerns/output_writer"
class Command
include OutputWriter
VALID_COMMANDS = %w[audit migrate dry-run].freeze
def initialize(comment_body)
@comment_body = comment_body
end
def command
@command ||= begin
command = @comment_body&.match(%r{/([^\s\\]+)})
command[1] unless command.nil? || !VALID_COMMANDS.include?(command[1])
end
end
def valid?
!command.nil?
end
def classify
return unless valid?
command.tr("-", "_").classify
end
def options
return unless valid?
@options ||= begin
command_text = "/#{command}"
command_index = @comment_body.index(command_text)
options_text = @comment_body[command_index + command_text.length..-1]
options_text.split(" ")
.each_slice(2)
.to_h
.transform_keys { |key| key.delete_prefix("--") }
end
end
def to_output
set_output("command", command)
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module GitlabCI
class Audit
include IssueParser
def initialize(issue_content, _)
@namespace = parameter_from_issue("Namespace", issue_content)
end
def to_a
return if @namespace.nil?
["--namespace", @namespace]
end
end
end
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module GitlabCI
class DryRun
include IssueParser
def initialize(issue_content, command)
@namespace = parameter_from_issue("Namespace", issue_content)
@project = command.options["project"]
end
def to_a
args = []
args.concat(["--namespace", @namespace]) unless @namespace.nil?
args.concat(["--project", @project]) unless @project.nil?
return args unless args.empty?
end
end
end
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module GitlabCI
class Migrate
include IssueParser
def initialize(issue_content, command)
@namespace = parameter_from_issue("Namespace", issue_content)
@project = command.options["project"]
@target_url = command.options["target-url"]
end
def to_a
args = []
args.concat(["--namespace", @namespace]) unless @namespace.nil?
args.concat(["--project", @project]) unless @project.nil?
args.concat(["--target-url", @target_url]) unless @target_url.nil?
return args unless args.empty?
end
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module Jenkins
class Audit
include IssueParser
def initialize(issue_content, _)
@folders = parameter_from_issue("Folders", issue_content)
end
def to_a
return if @folders.nil?
["--folders", @folders]
end
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module Jenkins
class DryRun
include IssueParser
def initialize(_, command)
@source_url = command.options["source-url"]
end
def to_a
return if @source_url.nil?
["--source-url", @source_url]
end
end
end
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Jenkins
class Migrate
include IssueParser
def initialize(_, command)
@source_url = command.options["source-url"]
@target_url = command.options["target-url"]
end
def to_a
args = []
args.concat(["--source-url", @source_url]) unless @source_url.nil?
args.concat(["--target-url", @target_url]) unless @target_url.nil?
return args unless args.empty?
end
end
end
+41
View File
@@ -0,0 +1,41 @@
# frozen_string_literal: true
require_rel "./azure_devops/**/*.rb"
require_rel "./circle_ci/**/*.rb"
require_rel "./gitlab_ci/**/*.rb"
require_rel "./jenkins/**/*.rb"
require_rel "./travis_ci/**/*.rb"
class Provider
include OutputWriter
PROVIDER_MAP = {
"azure-devops" => ::AzureDevops,
"circle-ci" => ::CircleCI,
"gitlab" => ::GitlabCI,
"jenkins" => ::Jenkins,
"travis-ci" => ::TravisCI
}.freeze
def initialize(labels)
labels = labels.tr("\n", "").delete_prefix("[").delete_suffix("]").split(",").map(&:strip)
providers = labels.select { |label| PROVIDER_MAP.key?(label) }
raise "One provider must be selected" if providers.empty?
raise "Only one provider can be selected" unless providers.one?
@provider = providers.first
end
def cli_command
@provider
end
def module
PROVIDER_MAP[@provider]
end
def to_output
set_output("provider", cli_command)
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module TravisCI
class Audit
include IssueParser
def initialize(issue_content, _)
@organization = parameter_from_issue("Organization", issue_content)
end
def to_a
return if @organization.nil?
["--travis-ci-organization", @organization]
end
end
end
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module TravisCI
class DryRun
include IssueParser
def initialize(issue_content, command)
@organization = parameter_from_issue("Organization", issue_content)
@repository = command.options["repository"]
end
def to_a
args = []
args.concat(["--travis-ci-organization", @organization]) unless @organization.nil?
args.concat(["--travis-ci-repository", @repository]) unless @repository.nil?
return args unless args.empty?
end
end
end
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module TravisCI
class Migrate
include IssueParser
def initialize(issue_content, command)
@organization = parameter_from_issue("Organization", issue_content)
@repository = command.options["repository"]
@target_url = command.options["target-url"]
end
def to_a
args = []
args.concat(["--travis-ci-organization", @organization]) unless @organization.nil?
args.concat(["--travis-ci-repository", @repository]) unless @repository.nil?
args.concat(["--target-url", @target_url]) unless @target_url.nil?
return args unless args.empty?
end
end
end
+26
View File
@@ -0,0 +1,26 @@
# frozen_string_literal: true
RSpec.describe OutputWriter do
let(:test_class) do
class TestClass
include OutputWriter
end
TestClass.new
end
describe "#set_output" do
let(:name) { "var_name" }
let(:value) { "var_value" }
subject { test_class.set_output(name, value) }
it { expect { subject }.to output(/::set-output name=#{name}::#{value}/).to_stdout }
context "when value is nil" do
let(:value) { nil }
it { expect { subject }.not_to output(/::set-output name=#{name}::#{value}/).to_stdout }
end
end
end
+67
View File
@@ -0,0 +1,67 @@
# frozen_string_literal: true
RSpec.describe Arguments do
let(:arguments) { Arguments.new(provider, command, issue_content) }
let(:provider) { instance_double(Provider) }
let(:command) { instance_double(Command) }
let(:issue_content) { "some issue content" }
describe "#argument_class" do
subject { arguments.argument_class(provider, command, issue_content) }
context "when the command is found" do
before do
expect(provider).to receive(:module).and_return(::Jenkins).at_least(:once)
expect(command).to receive(:classify).and_return("Audit").at_least(:once)
end
it { is_expected.to be_a(::Jenkins::Audit) }
end
context "when the command is not found" do
before do
expect(provider).to receive(:module).and_return(::Jenkins).at_least(:once)
expect(command).to receive(:classify).and_return("Whoopsie").at_least(:once)
end
it { expect { subject }.to raise_error(NameError) }
end
end
describe "#to_output" do
subject { arguments.to_output }
before do
expect(provider).to receive(:module).and_return(::AzureDevops).at_least(:once)
expect(command).to receive(:classify).and_return("Audit").at_least(:once)
expect_any_instance_of(::AzureDevops::Audit).to receive(:to_a).and_return(output)
end
context "when the output is nil" do
let(:output) { nil }
it "does not write any output variable" do
expect(arguments).not_to receive(:set_output)
subject
end
end
context "when the output is not nil" do
let(:output) { ["--option", "value"] }
it "writes an output variable" do
expect(arguments).to receive(:set_output).with("args", "--option value")
subject
end
end
context "when the output contains a space" do
let(:output) { ["--option", "some value"] }
it "writes an output variable" do
expect(arguments).to receive(:set_output).with("args", "--option \"some value\"")
subject
end
end
end
end
+42
View File
@@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.describe AzureDevops::Audit do
let(:audit) { described_class.new(issue_content, nil) }
describe "#to_a" do
subject { audit.to_a }
context "when issue_content contains no args" do
let(:issue_content) do
<<~ISSUE
Organization:
Project:
ISSUE
end
it { is_expected.to be_nil }
end
context "when issue_content contains an organization" do
let(:issue_content) do
<<~ISSUE
Organization: my-organization
Project:
ISSUE
end
it { is_expected.to eq(["--azure-devops-organization", "my-organization"]) }
end
context "when issue_content contains a project" do
let(:issue_content) do
<<~ISSUE
Organization: my-organization
Project: my-project
ISSUE
end
it { is_expected.to eq(["--azure-devops-organization", "my-organization", "--azure-devops-project", "my-project"]) }
end
end
end
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.describe AzureDevops::DryRun do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: my-organization
Project: my-project
ISSUE
end
context "when the comment body does not contain a pipeline type" do
let(:comment_body) { "/dry-run" }
it { is_expected.to eq(["pipeline", "--azure-devops-organization", "my-organization", "--azure-devops-project", "my-project"]) }
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/dry-run --pipeline-id 42" }
it { is_expected.to eq(["pipeline", "--azure-devops-organization", "my-organization", "--azure-devops-project", "my-project", "--pipeline-id", "42"]) }
end
end
end
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.describe AzureDevops::Migrate do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: my-organization
Project: my-project
ISSUE
end
context "when the comment body does not contain a pipeline type" do
let(:comment_body) { "/migrate" }
it { is_expected.to eq(["pipeline", "--azure-devops-organization", "my-organization", "--azure-devops-project", "my-project"]) }
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/migrate --pipeline-id 42 --target-url https://github.com/org/repo" }
it { is_expected.to eq(["pipeline", "--azure-devops-organization", "my-organization", "--azure-devops-project", "my-project", "--pipeline-id", "42", "--target-url", "https://github.com/org/repo"]) }
end
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe CircleCI::Audit do
let(:audit) { described_class.new(issue_content, nil) }
describe "#to_a" do
subject { audit.to_a }
context "when issue_content contains no args" do
let(:issue_content) do
<<~ISSUE
Organization:
ISSUE
end
it { is_expected.to be_nil }
end
context "when issue_content contains a organization" do
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
it { is_expected.to eq(["--circle-ci-organization", "testing"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe CircleCI::DryRun do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
context "when the comment body contains a project" do
let(:comment_body) { "/dry-run --project repo" }
it { is_expected.to eq(["--circle-ci-organization", "testing", "--circle-ci-project", "repo"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe CircleCI::Migrate do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
context "when the comment body contains a project" do
let(:comment_body) { "/migrate --project repo --target-url https://github.com/org/repo" }
it { is_expected.to eq(["--circle-ci-organization", "testing", "--circle-ci-project", "repo", "--target-url", "https://github.com/org/repo"]) }
end
end
end
+109
View File
@@ -0,0 +1,109 @@
# frozen_string_literal: true
RSpec.describe Command do
let(:command) { described_class.new(comment_body) }
describe "#command" do
subject { command.command }
context "when no command is present" do
let(:comment_body) do
<<~COMMENT
This is just a comment.
COMMENT
end
it { is_expected.to be_nil }
end
context "when an unsupported comment is present" do
let(:comment_body) do
<<~COMMENT
/do-something-unsupported
COMMENT
end
it { is_expected.to be_nil }
end
described_class::VALID_COMMANDS.each do |c|
context "when a supported command is present" do
let(:comment_body) do
<<~COMMENT
/#{c}
COMMENT
end
it { is_expected.to eq(c) }
end
end
end
describe "#options" do
subject { command.options }
context "when the command is not valid" do
let(:comment_body) { "/do-something-unsupported --option-one value" }
it { is_expected.to be_nil }
end
context "when no options are present" do
let(:comment_body) { "/dry-run" }
it { is_expected.to be_blank }
end
context "when options are present" do
let(:comment_body) { "/dry-run --option-one value-one --option-two value-two" }
it { is_expected.to eq({ "option-one" => "value-one", "option-two" => "value-two" }) }
end
end
describe "#valid?" do
subject { command.valid? }
context "when the command is supported" do
let(:comment_body) { "/dry-run" }
it { is_expected.to be_truthy }
end
context "when the command is not supported" do
let(:comment_body) { "/run-something-else" }
it { is_expected.to be_falsey }
end
end
describe "#classify" do
subject { command.classify }
[%w[audit Audit], %w[dry-run DryRun], %w[migrate Migrate]].each do |command, klass|
context "when the command is #{command}" do
let(:comment_body) { "/#{command}" }
it { is_expected.to eq(klass) }
end
end
context "when the command is invalid" do
let(:comment_body) { "/do-something-unsupported" }
it { is_expected.to be_nil }
end
end
describe "to_output" do
subject { command.to_output }
let(:comment_body) do
<<~COMMENT
/audit
COMMENT
end
it { expect { subject }.to output(/::set-output name=command::audit/).to_stdout }
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe GitlabCI::Audit do
let(:audit) { described_class.new(issue_content, nil) }
describe "#to_a" do
subject { audit.to_a }
context "when issue_content contains no args" do
let(:issue_content) do
<<~ISSUE
Namespace:
ISSUE
end
it { is_expected.to be_nil }
end
context "when issue_content contains a namespace" do
let(:issue_content) do
<<~ISSUE
Namespace: testing
ISSUE
end
it { is_expected.to eq(["--namespace", "testing"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe GitlabCI::DryRun do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Namespace: testing
ISSUE
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/dry-run --project project" }
it { is_expected.to eq(["--namespace", "testing", "--project", "project"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe GitlabCI::Migrate do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Namespace: testing
ISSUE
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/migrate --project project --target-url https://github.com/org/repo" }
it { is_expected.to eq(["--namespace", "testing", "--project", "project", "--target-url", "https://github.com/org/repo"]) }
end
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe Jenkins::Audit do
let(:audit) { described_class.new(issue_content, nil) }
describe "#to_a" do
subject { audit.to_a }
context "when issue_content contains no args" do
let(:issue_content) do
<<~ISSUE
Folders:
ISSUE
end
it { is_expected.to be_nil }
end
context "when issue_content contains a folder" do
let(:issue_content) do
<<~ISSUE
Folders: test, prod
ISSUE
end
it { is_expected.to eq(["--folders", "test, prod"]) }
end
end
end
+16
View File
@@ -0,0 +1,16 @@
# frozen_string_literal: true
RSpec.describe Jenkins::DryRun do
let(:dry_run) { described_class.new(nil, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/dry-run --source-url https://jenkins.company.com/job/pipeline" }
it { is_expected.to eq(["--source-url", "https://jenkins.company.com/job/pipeline"]) }
end
end
end
+16
View File
@@ -0,0 +1,16 @@
# frozen_string_literal: true
RSpec.describe Jenkins::Migrate do
let(:dry_run) { described_class.new(nil, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/migrate --source-url https://jenkins.company.com/job/pipeline --target-url https://github.com/org/repo" }
it { is_expected.to eq(["--source-url", "https://jenkins.company.com/job/pipeline", "--target-url", "https://github.com/org/repo"]) }
end
end
end
+97
View File
@@ -0,0 +1,97 @@
# frozen_string_literal: true
RSpec.describe Provider do
let(:provider) { described_class.new(labels) }
describe "#initialize" do
subject { provider }
context "when no providers are selected" do
let(:labels) do
<<~LABELS
[]
LABELS
end
it { expect { subject }.to raise_error("One provider must be selected") }
end
context "when multiple providers are selected" do
let(:labels) do
<<~LABELS
[
jenkins,
azure-devops
]
LABELS
end
it { expect { subject }.to raise_error("Only one provider can be selected") }
end
context "when an unsupported provider is selected" do
let(:labels) do
<<~LABELS
[
something-unsupported
]
LABELS
end
it { expect { subject }.to raise_error("One provider must be selected") }
end
context "when a single provider is selected" do
let(:labels) do
<<~LABELS
[
jenkins
]
LABELS
end
it { expect { subject }.not_to raise_error }
end
end
describe "#cli_command" do
subject { provider.cli_command }
let(:labels) do
<<~LABELS
[
gitlab
]
LABELS
end
it { is_expected.to eq("gitlab") }
end
describe "#module" do
subject { provider.module }
let(:labels) do
<<~LABELS
[
travis-ci
]
LABELS
end
it { is_expected.to eq(::TravisCI) }
end
describe "to_output" do
subject { provider.to_output }
let(:labels) do
<<~LABELS
[
azure-devops
]
LABELS
end
it { expect { subject }.to output(/::set-output name=provider::azure-devops/).to_stdout }
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe TravisCI::Audit do
let(:audit) { described_class.new(issue_content, nil) }
describe "#to_a" do
subject { audit.to_a }
context "when issue_content contains no args" do
let(:issue_content) do
<<~ISSUE
Organization:
ISSUE
end
it { is_expected.to be_nil }
end
context "when issue_content contains an organization" do
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
it { is_expected.to eq(["--travis-ci-organization", "testing"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe TravisCI::DryRun do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/dry-run --repository repo" }
it { is_expected.to eq(["--travis-ci-organization", "testing", "--travis-ci-repository", "repo"]) }
end
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe TravisCI::Migrate do
let(:dry_run) { described_class.new(issue_content, command) }
let(:command) { Command.new(comment_body) }
describe "#to_a" do
subject { dry_run.to_a }
let(:issue_content) do
<<~ISSUE
Organization: testing
ISSUE
end
context "when the comment body contains a pipeline id" do
let(:comment_body) { "/migrate --repository repo --target-url https://github.com/org/repo" }
it { is_expected.to eq(["--travis-ci-organization", "testing", "--travis-ci-repository", "repo", "--target-url", "https://github.com/org/repo"]) }
end
end
end
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
require "bundler/setup"
require "factory_bot"
require "faker"
require_relative "./../cli"
Dir[File.join(__dir__, "support/**/*.rb")].sort.each { |f| require f }
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
# Override truncation of expected/got output
c.max_formatted_output_length = 1000
c.syntax = :expect
end
config.include FactoryBot::Syntax::Methods
config.before(:suite) do
FactoryBot.find_definitions
end
end
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
require "shoulda-matchers"
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
end
end