From 7e4f458a05e92b824046b4ead8ea861b09690d17 Mon Sep 17 00:00:00 2001 From: joao10lima Date: Fri, 5 Nov 2021 12:39:47 -0300 Subject: [PATCH] feat: add ruby stack first implementation --- cli/tests/default_test.ts | 38 ++++++ .../workflows/pipelinit.ruby.lint.yaml | 18 +++ .../ruby/rubocop-lint/project/Gemfile | 5 + .../ruby/rubocop-lint/project/Gemfile.lock | 39 ++++++ .../ruby/rubocop-lint/project/README.md | 1 + .../ruby/rubocop-lint/project/lib/main.rb | 6 + .../ruby/rubocop-lint/project/main.gemspec | 15 ++ core/plugins/stack/mod.ts | 5 + core/plugins/stack/ruby/dependencies.ts | 31 +++++ core/plugins/stack/ruby/linters.ts | 29 ++++ core/plugins/stack/ruby/mod.ts | 43 ++++++ core/plugins/stack/ruby/rubocop.ts | 31 +++++ core/plugins/stack/ruby/version.ts | 128 ++++++++++++++++++ core/templates/github/ruby/lint.yaml | 26 ++++ 14 files changed, 415 insertions(+) create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/expected/.github/workflows/pipelinit.ruby.lint.yaml create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile.lock create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/project/README.md create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/project/lib/main.rb create mode 100644 cli/tests/fixtures/ruby/rubocop-lint/project/main.gemspec create mode 100644 core/plugins/stack/ruby/dependencies.ts create mode 100644 core/plugins/stack/ruby/linters.ts create mode 100644 core/plugins/stack/ruby/mod.ts create mode 100644 core/plugins/stack/ruby/rubocop.ts create mode 100644 core/plugins/stack/ruby/version.ts create mode 100644 core/templates/github/ruby/lint.yaml diff --git a/cli/tests/default_test.ts b/cli/tests/default_test.ts index caaf29c..823283b 100644 --- a/cli/tests/default_test.ts +++ b/cli/tests/default_test.ts @@ -111,3 +111,41 @@ test( await cleanGitHubFiles("python/setup-flake8"); }, ); + +test( + { fixture: "python/setup-flake8", args: ["--no-strict"] }, + async (proc) => { + const [stdout, _stderr, { code }] = await output(proc); + assertStringIncludes(stdout, "Detected stack: python"); + assertStringIncludes( + stdout, + "Couldn't detect the Python version, using the latest available: 3.10", + ); + assertStringIncludes( + stdout, + "No linters for python were identified in the project, creating default pipeline with 'flake8' WITHOUT any specific configuration", + ); + assertStringIncludes( + stdout, + "No formatters for python were identified in the project, creating default pipeline with 'black' WITHOUT any specific configuration", + ); + assertStringIncludes( + stdout, + "No formatters for python were identified in the project, creating default pipeline with 'isort' WITHOUT any specific configuration", + ); + assertEquals(code, 0); + await assertExpectedFiles("python/setup-flake8"); + await cleanGitHubFiles("python/setup-flake8"); + }, +); + +test( + { fixture: "ruby/rubocop-lint", args: ["--no-strict"] }, + async (proc) => { + const [stdout, _stderr, { code }] = await output(proc); + assertStringIncludes(stdout, "Detected stack: ruby"); + assertEquals(code, 0); + await assertExpectedFiles("ruby/rubocop-lint"); + await cleanGitHubFiles("ruby/rubocop-lint"); + }, +); diff --git a/cli/tests/fixtures/ruby/rubocop-lint/expected/.github/workflows/pipelinit.ruby.lint.yaml b/cli/tests/fixtures/ruby/rubocop-lint/expected/.github/workflows/pipelinit.ruby.lint.yaml new file mode 100644 index 0000000..021c35b --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/expected/.github/workflows/pipelinit.ruby.lint.yaml @@ -0,0 +1,18 @@ +# Generated with pipelinit 0.2.0 +# https://pipelinit.com/ +name: Lint Ruby +on: + pull_request: + paths: + - "**.rb" +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "2.5.0" + bundler-cache: true + - run: bundle exec rubocop --lint . diff --git a/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile b/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile new file mode 100644 index 0000000..3cbd81d --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +gemspec + +gem 'rubocop', '~> 0.57.2', require: false diff --git a/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile.lock b/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile.lock new file mode 100644 index 0000000..6427da0 --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/project/Gemfile.lock @@ -0,0 +1,39 @@ +PATH + remote: . + specs: + main (0.3.1) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + jaro_winkler (1.5.4) + parallel (1.21.0) + parser (3.0.2.0) + ast (~> 2.4.1) + powerpack (0.1.3) + rainbow (2.2.2) + rake + rake (10.5.0) + rubocop (0.57.2) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + ruby-progressbar (1.11.0) + unicode-display_width (1.8.0) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + bundler (>= 1.16) + main! + rubocop (~> 0.57.2) + +BUNDLED WITH + 2.2.30 diff --git a/cli/tests/fixtures/ruby/rubocop-lint/project/README.md b/cli/tests/fixtures/ruby/rubocop-lint/project/README.md new file mode 100644 index 0000000..b2eef3d --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/project/README.md @@ -0,0 +1 @@ +# test-ruby-pipelinit \ No newline at end of file diff --git a/cli/tests/fixtures/ruby/rubocop-lint/project/lib/main.rb b/cli/tests/fixtures/ruby/rubocop-lint/project/lib/main.rb new file mode 100644 index 0000000..ea29454 --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/project/lib/main.rb @@ -0,0 +1,6 @@ +require 'main/version' +require 'main/hash' +require 'main/script' +require 'main/report' +require 'main/config' +require 'main/cli' diff --git a/cli/tests/fixtures/ruby/rubocop-lint/project/main.gemspec b/cli/tests/fixtures/ruby/rubocop-lint/project/main.gemspec new file mode 100644 index 0000000..2f8b444 --- /dev/null +++ b/cli/tests/fixtures/ruby/rubocop-lint/project/main.gemspec @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +Gem::Specification.new do |spec| + spec.name = 'main' + spec.version = '0.3.1' + spec.author = 'pipelinit' + + spec.required_ruby_version = '>= 2.5.0' + + spec.summary = 'Aplicação para gerar' + spec.description = 'Aplicação para gerar' + + spec.add_development_dependency 'bundler', '>= 1.16' + spec.add_development_dependency 'rubocop', '~> 1.22' +end diff --git a/core/plugins/stack/mod.ts b/core/plugins/stack/mod.ts index 5000092..ea77d77 100644 --- a/core/plugins/stack/mod.ts +++ b/core/plugins/stack/mod.ts @@ -4,6 +4,7 @@ import { introspector as HtmlIntrospector } from "./html/mod.ts"; import { introspector as JavaIntrospector } from "./java/mod.ts"; import { introspector as JavaScriptIntrospector } from "./javascript/mod.ts"; import { introspector as PythonIntrospector } from "./python/mod.ts"; +import { introspector as RubyIntrospector } from "./ruby/mod.ts"; import { introspector as ShellIntrospector } from "./shell/mod.ts"; import type CSSProject from "./css/mod.ts"; @@ -12,6 +13,7 @@ import type HtmlProject from "./html/mod.ts"; import type JavaScriptProject from "./javascript/mod.ts"; import type JavaProject from "./java/mod.ts"; import type PythonProject from "./python/mod.ts"; +import type RubyProject from "./ruby/mod.ts"; import type ShellProject from "./shell/mod.ts"; // Keep it in alphabetical order @@ -22,6 +24,7 @@ export type ProjectData = | JavaScriptProject | JavaProject | PythonProject + | RubyProject | ShellProject; export type { @@ -31,6 +34,7 @@ export type { JavaProject, JavaScriptProject, PythonProject, + RubyProject, ShellProject, }; @@ -41,5 +45,6 @@ export const introspectors = [ { name: "javascript", ...JavaScriptIntrospector }, { name: "java", ...JavaIntrospector }, { name: "python", ...PythonIntrospector }, + { name: "ruby", ...RubyIntrospector }, { name: "shell", ...ShellIntrospector }, ]; diff --git a/core/plugins/stack/ruby/dependencies.ts b/core/plugins/stack/ruby/dependencies.ts new file mode 100644 index 0000000..77a091f --- /dev/null +++ b/core/plugins/stack/ruby/dependencies.ts @@ -0,0 +1,31 @@ +import { Context } from "../../../types.ts"; + +const rubyDepRegex = /(?:(["'])(?[a-zA-Z\-_\.]+)["'])/gm; + +function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +const readDependencyFile = async (context: Context) => { + for await (const file of await context.files.each("**/Gemfile")) { + const rubyDepsText = await context.files.readText(file.path); + + const depsRuby: string[] = Array.from( + rubyDepsText.matchAll(rubyDepRegex), + (match) => !match.groups ? null : match.groups.DependencyName, + ).filter(notEmpty); + + if (depsRuby) { + return depsRuby; + } + } + return []; +}; + +export const hasRubyDependency = async ( + context: Context, + dependencyName: string, +): Promise => { + const dependencies = await readDependencyFile(context); + return dependencies.some((dep) => dep === dependencyName); +}; diff --git a/core/plugins/stack/ruby/linters.ts b/core/plugins/stack/ruby/linters.ts new file mode 100644 index 0000000..ee8f4a7 --- /dev/null +++ b/core/plugins/stack/ruby/linters.ts @@ -0,0 +1,29 @@ +import { IntrospectFn } from "../../../types.ts"; +import { introspect as introspectRubocop, Rubocop } from "./rubocop.ts"; + +export type Linters = { + rubocop?: Rubocop; +}; + +export const introspect: IntrospectFn = async (context) => { + const linters: Linters = {}; + const logger = context.getLogger("ruby"); + + const rubocopInfo = await introspectRubocop(context); + if (rubocopInfo) { + logger.debug("detected Rubocop"); + linters.rubocop = rubocopInfo; + } else { + if (context.suggestDefault) { + logger.warning( + "No linters for ruby were identified in the project, creating default pipeline with 'Rubocop' WITHOUT any specific configuration", + ); + linters.rubocop = { + isDependency: false, + name: "rubocop", + }; + } + } + + return linters; +}; diff --git a/core/plugins/stack/ruby/mod.ts b/core/plugins/stack/ruby/mod.ts new file mode 100644 index 0000000..ec2acf4 --- /dev/null +++ b/core/plugins/stack/ruby/mod.ts @@ -0,0 +1,43 @@ +import { Introspector } from "../../../types.ts"; +import { introspect as introspectVersion } from "./version.ts"; +import { introspect as introspectLinters, Linters } from "./linters.ts"; + +/** + * Introspected information about a project with Ruby + */ +export default interface RubyProject { + /** + * Ruby version + */ + version?: string; + /** + * Which linter the project uses, if any + */ + linters: Linters; +} + +export const introspector: Introspector = { + detect: async (context) => { + return await context.files.includes("**/*.rb"); + }, + introspect: async (context) => { + const logger = context.getLogger("ruby"); + + // Version + logger.debug("detecting version"); + const version = await introspectVersion(context); + if (version === undefined) { + logger.debug("didn't detect the version"); + return undefined; + } + logger.debug(`detected version ${version}`); + + // Linters and Formatters + const linters = await introspectLinters(context); + + return { + version: version, + linters: linters, + }; + }, +}; diff --git a/core/plugins/stack/ruby/rubocop.ts b/core/plugins/stack/ruby/rubocop.ts new file mode 100644 index 0000000..711fd9e --- /dev/null +++ b/core/plugins/stack/ruby/rubocop.ts @@ -0,0 +1,31 @@ +import { IntrospectFn } from "../../../types.ts"; +import { hasRubyDependency } from "./dependencies.ts"; + +export interface Rubocop { + name: "rubocop"; + isDependency: boolean; +} + +export const introspect: IntrospectFn = async (context) => { + const isDependency = await hasRubyDependency(context, "rubocop"); + // Search for any of the following files: + // .rubocop.yml + // Reference: https://docs.rubocop.org/rubocop/configuration.html#config-file-locations + const hasRubocopConfig = await context.files.includes( + "**/.rubocop.yml", + ); + + if (hasRubocopConfig) { + return { + name: "rubocop", + isDependency: isDependency, + }; + } else if (isDependency) { + return { + name: "rubocop", + isDependency: true, + }; + } + + return null; +}; diff --git a/core/plugins/stack/ruby/version.ts b/core/plugins/stack/ruby/version.ts new file mode 100644 index 0000000..0ccc257 --- /dev/null +++ b/core/plugins/stack/ruby/version.ts @@ -0,0 +1,128 @@ +import { IntrospectFn } from "../../../types.ts"; + +// Find the latest stable version here: +// https://www.ruby-lang.org/en/downloads/ +const LATEST = "3.0.2"; + +const WARN_USING_LATEST = + `Couldn't detect the Ruby version, using the latest available: ${LATEST}`; + +const ERR_UNDETECTABLE_TITLE = + "Couldn't detect which Ruby version this project uses."; +const ERR_UNDETECTABLE_INSTRUCTIONS = ` +To fix this issue, consider one of the following suggestions: + +1. Add the 'required_ruby_version' to your Gemfile + +The Gemfile permits the specification of the adequate ruby version for this package. + +See https://guides.rubygems.org/specification-reference/#required_ruby_version= + +2. Create a .ruby-version file + +The .ruby-version file can be used by the RVM(Ruby Version Manager) to choose a specific Ruby version +for a project. + +Its the easiest option, all you have to do is create a .ruby-version text +file with a version inside, like "3.0.1". + +See https://rvm.io/workflow/projects#project-file-ruby-version +`; + +function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +/** + * Search for application specific `.ruby-version` file from RVM + * + * @see https://rvm.io/workflow/examples#rvm-info + */ +const rvm: IntrospectFn = async (context) => { + for await (const file of context.files.each("**/.ruby-version")) { + return await context.files.readText(file.path); + } + throw Error("Can't find ruby version at .ruby-version"); +}; + +/** + * Search a Gemfile for the required ruby version key + */ +const gemfile: IntrospectFn = async (context) => { + for await (const file of context.files.each("./Gemfile")) { + const gemfileText = await context.files.readText(file.path); + + const rubyVersion: string[] = Array.from( + gemfileText.matchAll(/ruby ["'](?.*)["']/), + // required_ruby_version.*(?[0-9]+\.[0-9]+\.[0-9]) + (match) => !match.groups ? null : match.groups.RubyVersion, + ).filter(notEmpty); + + if (rubyVersion.length > 0) { + return rubyVersion[0]; + } + } + throw Error("Can't find ruby version at Gemfile"); +}; + +/** + * Search a .gemspec file looking for the key required_ruby_version + * + * @see https://guides.rubygems.org/specification-reference/#required_ruby_version= + */ +const gemspec: IntrospectFn = async (context) => { + for await (const file of context.files.each("./*.gemspec")) { + const gemspecText = await context.files.readText(file.path); + + const rubyVersion: string[] = Array.from( + gemspecText.matchAll( + /required_ruby_version.*(?[0-9]+\.[0-9]+\.[0-9])/gm, + ), + (match) => !match.groups ? null : match.groups.RubyVersion, + ).filter(notEmpty); + + if (rubyVersion.length > 0) { + return rubyVersion[0]; + } + } + throw Error("Can't find ruby version at RubyVersion"); +}; + +/** + * Searches for the project Python version in multiple places, such as: + * - .ruby-version + * - Gemfile + * - .gemspec + * + * If it fails to find a version definition anywhere, the next step depends + * wheter Pipelinit is running in the strict mode. It emits an error if running + * in the strict mode, otherwise it emits an warning and fallback to the latest + * stable version. + */ +export const introspect: IntrospectFn = async (context) => { + const logger = context.getLogger("ruby"); + + try { + return await Promise.any([ + rvm(context), + gemfile(context), + gemspec(context), + ]); + } catch (error: unknown) { + if (error instanceof AggregateError) { + logger.debug(error.errors.map((e) => e.message).join("\n")); + + if (!context.strict) { + logger.warning(WARN_USING_LATEST); + return LATEST; + } + + context.errors.add({ + title: ERR_UNDETECTABLE_TITLE, + message: ERR_UNDETECTABLE_INSTRUCTIONS, + }); + } else { + throw error; + } + } +}; diff --git a/core/templates/github/ruby/lint.yaml b/core/templates/github/ruby/lint.yaml new file mode 100644 index 0000000..fcf476d --- /dev/null +++ b/core/templates/github/ruby/lint.yaml @@ -0,0 +1,26 @@ +<% if (it.linters) { -%> +name: Lint Ruby +on: + pull_request: + paths: + - "**.rb" +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "<%= it.version %>" + bundler-cache: true +<% if (!it.linters.rubocop.isDependency) { -%> + - run: gem install +<% } -%> +<% if (!it.linters.rubocop.isDependency) { -%> + - run: gem install rubocop +<% } -%> +<% if (it.linters.rubocop) { -%> + - run: bundle exec rubocop --lint . +<% } -%> +<%- } -%> \ No newline at end of file