Skip to content

Commit

Permalink
feat: add ruby stack first implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
joao10lima committed Nov 9, 2021
1 parent f257403 commit 3c21978
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 0 deletions.
38 changes: 38 additions & 0 deletions cli/tests/default_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
},
);
Original file line number Diff line number Diff line change
@@ -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 .
1 change: 1 addition & 0 deletions cli/tests/fixtures/ruby/rubocop-lint/project
Submodule project added at 665f3b
5 changes: 5 additions & 0 deletions core/plugins/stack/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -22,6 +24,7 @@ export type ProjectData =
| JavaScriptProject
| JavaProject
| PythonProject
| RubyProject
| ShellProject;

export type {
Expand All @@ -31,6 +34,7 @@ export type {
JavaProject,
JavaScriptProject,
PythonProject,
RubyProject,
ShellProject,
};

Expand All @@ -41,5 +45,6 @@ export const introspectors = [
{ name: "javascript", ...JavaScriptIntrospector },
{ name: "java", ...JavaIntrospector },
{ name: "python", ...PythonIntrospector },
{ name: "ruby", ...RubyIntrospector },
{ name: "shell", ...ShellIntrospector },
];
31 changes: 31 additions & 0 deletions core/plugins/stack/ruby/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Context } from "../../../types.ts";

const rubyDepRegex = /(?:(["'])(?<DependencyName>[a-zA-Z\-_\.]+)["'])/gm;

function notEmpty<TValue>(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<boolean> => {
const dependencies = await readDependencyFile(context);
return dependencies.some((dep) => dep === dependencyName);
};
29 changes: 29 additions & 0 deletions core/plugins/stack/ruby/linters.ts
Original file line number Diff line number Diff line change
@@ -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<Linters> = 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;
};
43 changes: 43 additions & 0 deletions core/plugins/stack/ruby/mod.ts
Original file line number Diff line number Diff line change
@@ -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<RubyProject | undefined> = {
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,
};
},
};
31 changes: 31 additions & 0 deletions core/plugins/stack/ruby/rubocop.ts
Original file line number Diff line number Diff line change
@@ -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<Rubocop | null> = 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;
};
128 changes: 128 additions & 0 deletions core/plugins/stack/ruby/version.ts
Original file line number Diff line number Diff line change
@@ -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<TValue>(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<string> = 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<string> = 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 ["'](?<RubyVersion>.*)["']/),
// required_ruby_version.*(?<RubyVersion>[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<string> = 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.*(?<RubyVersion>[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<string | undefined> = 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;
}
}
};
26 changes: 26 additions & 0 deletions core/templates/github/ruby/lint.yaml
Original file line number Diff line number Diff line change
@@ -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 .
<% } -%>
<%- } -%>

0 comments on commit 3c21978

Please sign in to comment.