Filter Function Plugin Library
JavaScript plugins that enable custom filter functions for gitStream. To learn how to use these examples, read our guide on how to use gitStream plugins.
askAI
A gitStream plugin to interact with AI models. Currently works with ChatGPR-4o-mini
Returns: Object
- Returns the AI-generated response based on the provided context and prompt. License: MIT
Param | Type | Description |
---|---|---|
context | Object | The context that needs to be sent to the AI model for analysis. |
role | string | Free text. If not empty, Defines the role or persona for the AI to adopt while generating the response. |
prompt | string | The specific request or question you want the AI to respond to, after the context has been provided. |
token | Object | The token to the AI model |
Example
Encoding output
The output of AI models may be lengthy, which might cause issues when setting the comment. We recommend using the encode
filter function, as shown in the example, to ensure that the comment is passed fully. The add-comment
action automatically decodes encoded strings.
{{ source | askAI("QA tester", "Based on the given context, search for new functions without tests and suggest the tests to add. If all functions are covered completely, return 'no tests to suggest.'", env.OPEN_AI_TOKEN) | encode }}
Plugin Code: askAI
/**
* @module askAI
* @description A gitStream plugin to interact with AI models. Currently works with `ChatGPR-4o-mini`.
* @param {Object} context - The context that will be attached to the prompt .
* @param {string} role - Role instructions for the conversation.
* @param {string} prompt - The prompt string.
* @param {Object} token - The token to the AI model.
* @returns {Object} Returns the response from the AI model.
* @example {{ branch | generateDescription(pr, repo, source) }}
* @license MIT
* */
const MAX_TOKENS = 4096;
const OPEN_AI_ENDPOINT = 'https://api.openai.com/v1/chat/completions';
const LOCK_FILES = [
'package-lock.json',
'yarn.lock',
'npm-shrinkwrap.json',
'Pipfile.lock',
'poetry.lock',
'conda-lock.yml',
'Gemfile.lock',
'composer.lock',
'packages.lock.json',
'project.assets.json',
'pom.xml',
'Cargo.lock',
'mix.lock',
'pubspec.lock',
'go.sum',
'stack.yaml.lock',
'vcpkg.json',
'conan.lock',
'ivy.xml',
'project.clj',
'Podfile.lock',
'Cartfile.resolved',
'flake.lock',
'pnpm-lock.yaml'
];
const EXCLUDE_EXPRESSIONS_LIST = [
'.*\\.(ini|csv|xls|xlsx|xlr|doc|docx|txt|pps|ppt|pptx|dot|dotx|log|tar|rtf|dat|ipynb|po|profile|object|obj|dxf|twb|bcsymbolmap|tfstate|pdf|rbi|pem|crt|svg|png|jpeg|jpg|ttf)$',
'.*(package-lock|packages\\.lock|package)\\.json$',
'.*(yarn|gemfile|podfile|cargo|composer|pipfile|gopkg)\\.lock$',
'.*gradle\\.lockfile$',
'.*lock\\.sbt$',
'.*dist/.*\\.js',
'.*public/assets/.*\\.js',
'.*ci\\.yml$'
];
const IGNORE_FILES_REGEX_LIST = [
...LOCK_FILES.map(f => f.replace('.', '\\.')),
...EXCLUDE_EXPRESSIONS_LIST
];
const EXCLUDE_PATTERN = new RegExp(IGNORE_FILES_REGEX_LIST.join('|'));
/**
* @description Check if a file should be excluded from the context like "package-lock.json"
* @param {*} fileObject
* @returns returns true if the file should be excluded
*/
const shouldExcludeFile = fileObject => {
const shouldExludeByName = EXCLUDE_PATTERN.test(fileObject.original_file);
const shouldExludeBySize = (fileObject.diff?.split(' ').length ?? 0) > 1000;
return shouldExludeByName || shouldExludeBySize;
};
/**
* @description Check if a file should be included in the context
* @param {*} fileObject
* @returns returns true if the file should be included
*/
const shouldIncludeFile = fileObject => {
return !shouldExcludeFile(fileObject);
};
const buildContextForGPT = context => {
if (Array.isArray(context)) {
return context.filter(element =>
typeof element !== 'object' ? true : context.filter(shouldIncludeFile)
);
}
if (context?.diff?.files) {
const files = context.diff.files.filter(shouldIncludeFile);
return files;
}
return context;
};
const askAI = async (context, role, prompt, token, callback) => {
const formattedContext = buildContextForGPT(context);
if (!formattedContext?.length) {
const message = `There are no context files to analyze.\nAll ${context?.diff?.files?.length} files were excluded by pattern or size.`;
console.log(message);
return callback(null, message);
}
const response = await fetch(OPEN_AI_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'system',
content: `You are a ${role}. Answer only to the request, without any introductory or conclusion text.`
},
{
role: 'user',
content: JSON.stringify(formattedContext)
},
{ role: 'user', content: prompt }
],
max_tokens: MAX_TOKENS
})
});
const data = await response.json();
if (data?.error?.message) {
console.error(data.error.message);
return callback(null, data.error.message);
}
const suggestion =
data.choices?.[0]?.message?.content ??
'context was too big for api, try with smaller context object';
return callback(null, suggestion);
};
module.exports = {
async: true,
filter: askAI
};
gitStream CM Example: askAI
triggers:
exclude:
branch:
- r/dependabot/
automations:
generate_pr_desc_on_new_pr:
on:
- pr_created
if:
- true
run:
- action: add-comment@v1
args:
comment: |
{{ source | askAI("Experienced developer", "Summarize in simple terms the changes in this PR using bullet points.", env.OPEN_AI_TOKEN) | encode }}
generate_pr_desc_on_ask_ai_label:
on:
- label_added
if:
- {{ pr.labels | match(term="/ask-ai qa") | some }}
run:
- action: add-comment@v1
args:
comment: |
{{ source | askAI("qa tester", "Based on the given context, search for new functions without tests and suggest the tests to add. If all functions are covered completely, return 'no tests to suggest.'", env.OPEN_AI_TOKEN) | encode }}
checklist
Automatically check PRs against a checklist of conditions. This is useful if you want to ensure that PRs meet certain criteria before they can be merged.
Returns: string
- Returns a formatted GitHub comment with a checklist of conditions that the PR meets.
License: MIT
Param | Type | Description |
---|---|---|
Input | string | A blank string (no input variable is required) |
branch | object | The branch context variable. |
files | object | The files context variable. |
pr | object | The pr context variable. |
repo | object | The repo context variable. |
env | object | The env context variable. |
source | object | The source context variable. |
Example
With this plugin, you can easily customize the checklist using the object in the JavaScript code. To add a new check to the list, just add a new object with a descriptive title
for your own benefit, a label
that'll get posted in the comment, and the condition
that, if true, would cause the entry in the checklist to be checked off.
Plugin Code: checklist
/**
* @module checklist
* @description Automatically check PRs against a checklist of conditions.
* This is useful if you want to ensure that PRs meet certain criteria before they can be merged.
* @param {string} Input - A blank string (no input variable is required)
* @param {object} branch - The branch context variable.
* @param {object} files - The files context variable.
* @param {object} pr - The pr context variable.
* @param {object} repo - The repo context variable.
* @param {object} env - The env context variable.
* @param {object} source - The source context variable.
* @returns {string} Returns a formatted GitHub comment with a checklist of conditions that the PR meets.
* @example
* - action: add-comment@v1
args:
comment: {{ "" | checklist(branch, files, pr, repo, env, source) }}
* @license MIT
**/
const checklistFilter = async (empty, branch, files, pr, repo, env, source, callback) => { // made sync temporarily
const checks = [
{
title: "low-risk",
label: "The PR is a low-risk change",
// our sample definition of a low-risk change is a docs-only PR from designated docs writers
condition: files.every(file => /docs\//.test(file)) && pr.author_teams.includes("tech-writers")
},
{
title: "has-jira",
label: "The PR has a Jira reference in the title",
condition: /\b[A-Za-z]+-\d+\b/.test(pr.title)
},
{
title: "updates-tests",
label: "The PR includes updates to tests",
condition: files.some(file => /[^a-zA-Z0-9](spec|test|tests)[^a-zA-Z0-9]/.test(file))
},
{
title: "includes-docs",
label: "The PR includes changes to the documentation",
condition: files.some(file => /docs\//.test(file))
},
{
title: "first-time",
label: "The PR author is a first-time contributor",
condition: repo.author_age < 1 && repo.age > 0 // if the PR author made their first contirbution on the current day
},
{
title: "requires-opsec",
label: "The PR doesn't expose any secrets",
condition: source.diff.files
.map(file => file.new_content)
.every(file_content =>
[
"MY_SECRET_ENVIRONMENT_VARIABLE"
].every(env_var => !file_content.includes(env_var))
// nothing added to any file during this comment contains any of the secret environment variables in this array
)
}
];
const comment = await Promise.resolve(checks
.map(check => `- [${check.condition ? "x" : " "}] ${check.label}`)
.join("\n"));
return callback(
null,
JSON.stringify(comment)
);
};
module.exports = {
async: true,
filter: checklistFilter
}
gitStream CM Example: checklist
# -*- mode: yaml -*-
manifest:
version: 1.0
automations:
checklist:
if:
- true
run:
- action: add-comment@v1
args:
comment: {{ "" | checklist(branch, files, pr, repo, env, source) }}
compareMultiSemver
Processes a list of pairs of semantic version numbers and determines the most significant change among them.
Returns: string
- It returns a string of either: 'major' if any pair has a major version increment. 'minor' if no pair has a major version increment but has a minor version increment. 'patch' if no pair has major or minor version increments but has a patch version increment. 'downgrade' if no pairs have a higher version. 'equal' if all pairs are equal. 'error' if the comparison is abnormal or cannot be determined.
License: MIT
Param | Type | Default | Description |
---|---|---|---|
listOfPairs | Array.<Array> | An array of version pairs, where each pair is an array of two semantic version strings. |
Example
Plugin Code: compareMultiSemver
/**
* @module compareMultiSemver
* @description Processes a list of pairs of semantic version numbers and determines the most significant change among them.
* Each pair consists of two versions to be compared.
* @param {string[][]} listOfPairs - An array of version pairs, where each pair is an array of two semantic version strings.
* @returns {string} It returns a string of either:
* 'major' if any pair has a major version increment.
* 'minor' if no pair has a major version increment but has a minor version increment.
* 'patch' if no pair has major or minor version increments but has a patch version increment.
* 'downgrade' if no pairs have a higher version.
* 'equal' if all pairs are equal.
* 'error' if the comparison is abnormal or cannot be determined.
* @example {{ [["1.2.3", "1.2.1"], ["1.3.1", "1.2.3"]] | compareMultiSemver == "minor" }}
* @license MIT
*/
const compareSemver = require('../compareSemver/index.js');
module.exports = (listOfPairs) => {
const priority = {
'major': 3,
'minor': 2,
'patch': 1,
'downgrade': 0,
'equal': -1,
'error': -2
};
let mostSignificantChange = 'equal';
listOfPairs.forEach(pair => {
const result = compareSemver(pair);
if (priority[result] > priority[mostSignificantChange]) {
mostSignificantChange = result;
}
});
return mostSignificantChange;
}
const compareMultiSemver = require('./index.js');
console.assert(compareMultiSemver([["1.2.3", "1.2.1"], ["1.3.1", "1.2.3"]]) === 'minor', `compareSemver([["1.2.3", "1.2.1"], ["1.3.1", "1.2.3"]]) == 'minor'`);
console.assert(compareMultiSemver([["1.2.3", "0.2.1"], ["1.3.1", "1.2.3"]]) === 'major', `compareMultiSemver([["1.2.3", "0.2.1"], ["1.3.1", "1.2.3"]]) === 'major'`);
console.assert(compareMultiSemver([["2.2.3", "0.2.1"], ["1.3.1", "1.2.3"]]) === 'major', `compareMultiSemver([["2.2.3", "0.2.1"], ["1.3.1", "1.2.3"]]) === 'major'`);
console.assert(compareMultiSemver([["1.2.3", "1.2.1"], ["1.2.4", "1.2.3"]]) === 'patch', `compareMultiSemver([["1.2.3", "1.2.1"], ["1.2.4", "1.2.3"]]) === 'patch'`);
gitStream CM Example: compareMultiSemver
manifest:
version: 1.0
automations:
bump_minor:
if:
- {{ bump == 'minor' }}
run:
- action: approve@v1
- action: add-comment@v1
args:
comment: |
bot `minor` version bumps are approved automatically.
bump_patch:
if:
- {{ bump == 'patch' }}
run:
- action: approve@v1
- action: merge@v1
- action: add-comment@v1
args:
comment: |
bot `patch` version bumps are approved and merged automatically.
bump: {{ [["1.2.3", "1.2.1"], ["1.3.1", "1.2.3"]] | compareMultiSemver }}
compareSemver
Compares two software version numbers (e.g., "1.2.1" or "1.2b") and determines the type of version change. The first version to be compared, and the second are passed as argument 1 and 2 or as array of 2 items. When V1 > V2 the it means and upgrade.
Returns: string
- It returns a string of either: 'major' if the major version is incremented. 'minor' if the minor version is incremented. 'patch' if the patch version is incremented. 'downgrade' if the second version is lower than the first. 'equal' if both versions are equal. 'error' if the comparison is abnormal or cannot be determined.
License: MIT
Param | Type | Default | Description |
---|---|---|---|
versions | Array.<string> | V1 and V2 in Semver format | |
[lexicographical] | boolean | false | compares each part of the version strings lexicographically instead of naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than "1.2". |
[zeroExtend] | boolean | true | changes the result if one version string has less parts than the other. In this case the shorter string will be padded with "zero" parts instead of being considered smaller. |
Example
Plugin Code: compareSemver
/**
* @module compareSemver
* @description Compares two software version numbers (e.g., "1.2.1" or "1.2b") and determines the type of version change.
* The first version to be compared, and the second are passed as argument 1 and 2 or as array of 2 items.
* When V1 > V2 the it means and upgrade.
* @param {string[]} versions - V1 and V2 in Semver format
* @returns {string} It returns a string of either:
* 'major' if the major version is incremented.
* 'minor' if the minor version is incremented.
* 'patch' if the patch version is incremented.
* 'downgrade' if the second version is lower than the first.
* 'equal' if both versions are equal.
* 'error' if the comparison is abnormal or cannot be determined.
* @example {{ ["1.2.3", "1.2.1"] | compareSemver == "patch" }}
* @license MIT
**/
module.exports = (v1, v2) => {
const lexicographical = false;
const zeroExtend = true;
// support array as input
if (Array.isArray(v1) && v2 === undefined) {
[v1, v2] = v1; // Destructure the first two elements of the array into v1 and v2
}
let v1parts = (v1 || "0").split('.');
let v2parts = (v2 || "0").split('.');
const isValidPart = x => lexicographical ? /^\d+[A-Za-zαß]*$/.test(x) : /^\d+[A-Za-zαß]?$/.test(x);
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return 'error';
}
if (zeroExtend) {
const maxLength = Math.max(v1parts.length, v2parts.length);
v1parts = [...v1parts, ...Array(maxLength - v1parts.length).fill("0")];
v2parts = [...v2parts, ...Array(maxLength - v2parts.length).fill("0")];
}
const convertPart = x => {
const match = /[A-Za-zαß]/.exec(x);
return Number(match ? x.replace(match[0], "." + x.charCodeAt(match.index)) : x);
};
if (!lexicographical) {
v1parts = v1parts.map(convertPart);
v2parts = v2parts.map(convertPart);
}
for (let i = 0; i < v1parts.length; i++) {
if (v1parts[i] !== v2parts[i]) {
if (v1parts[i] < v2parts[i]) {
return 'downgrade';
}
switch (i) {
case 0: return 'major';
case 1: return 'minor';
case 2: return 'patch';
default: return 'error';
}
}
}
return 'equal';
}
const compareSemver = require('./index.js');
console.assert(compareSemver(["1.2.3", "1.2.1"]) === 'patch', `compareSemver(["1.2.3", "1.2.1"]) == 'patch'`);
console.assert(compareSemver(["1.2.0", "1.2.1"]) === 'downgrade', `compareSemver(["1.2.0", "1.2.1"]) === downgrade'`);
console.assert(compareSemver(["1.3.0", "1.2.1"]) === 'minor', `compareSemver(["1.3.0", "1.2.1"]) == 'minor'`);
console.assert(compareSemver(["2.0.0", "1.2.1"]) === 'major', `compareSemver(["2.0.0", "1.2.1"]) == 'major'`);
console.assert(compareSemver(["1.2.1", "1.2.1"]) === 'equal', `compareSemver(["1.2.1", "1.2.1"]) == 'equal'`);
console.assert(compareSemver(["1.2b", "1.2.1"]) === 'minor', `compareSemver(["1.2b", "1.2.1"]) == 'error'`);
console.assert(compareSemver(["1.2.0", "1.2"]) === 'equal', `compareSemver(["1.2.0", "1.2"]) == 'equal'`);
console.assert(compareSemver(["1.2.1.0", "1.2.1"]) === 'equal', `compareSemver(["1.2.1.0", "1.2.1"]) == 'equal'`);
console.assert(compareSemver(["1.2.1.0a", "1.2.1"]) === 'error', `compareSemver(["1.2.1.0a", "1.2.1"]) === downgrade'`);
gitStream CM Example: compareSemver
manifest:
version: 1.0
automations:
bump_minor:
if:
- {{ bump == 'minor' }}
run:
- action: approve@v1
- action: add-comment@v1
args:
comment: |
bot `minor` version bumps are approved automatically.
bump_patch:
if:
- {{ bump == 'patch' }}
run:
- action: approve@v1
- action: merge@v1
- action: add-comment@v1
args:
comment: |
bot `patch` version bumps are approved and merged automatically.
bump: {{ ["1.2.3", "1.2.1"] | compareSemver }}
extractDependabotVersionBump
Extract version bump information from Dependabot PRs description
Returns: Array.<string>
- V1 (to) and V2 (from)
License: MIT
Param | Type | Description |
---|---|---|
description | string | the PR description |
Example
Plugin Code: extractDependabotVersionBump
/**
* @module extractDependabotVersionBump
* @description Extract version bump information from Dependabot PRs description
* @param {string} description - the PR description
* @returns {string[]} V1 (to) and V2 (from)
* @example {{ pr.description | extractDependabotVersionBump | compareSemver }}
* @license MIT
**/
module.exports = (desc) => {
if (desc && desc !== '""' && desc !== "''" ) {
const matches = /Bumps.*from ([\d\.]+[A-Za-zαß]*) to ([\d\.]+[A-Za-zαß]*)/.exec(desc);
if (matches && matches.length == 3) {
var [_, from, to] = matches;
// remove trailing dot on to
if (to[to.length - 1] === ".") {
to = to.slice(0, -1);
}
return [to, from];
}
}
return null;
}
gitStream CM Example: extractDependabotVersionBump
manifest:
version: 1.0
automations:
bump_minor:
if:
- {{ bump == 'minor' }}
- {{ branch.name | includes(term="dependabot") }}
- {{ branch.author | includes(term="dependabot") }}
run:
- action: approve@v1
- action: add-comment@v1
args:
comment: |
Dependabot `minor` version bumps are approved automatically.
bump_patch:
if:
- {{ bump == 'patch' }}
- {{ branch.name | includes(term="dependabot") }}
- {{ branch.author | includes(term="dependabot") }}
run:
- action: approve@v1
- action: merge@v1
- action: add-comment@v1
args:
comment: |
Dependabot `patch` version bumps are approved and merged automatically.
bump: {{ pr.description | extractDependabotVersionBump | compareSemver }}
extractRenovateVersionBump
Extract version bump information from Renovate PRs description
Returns: Array.<string>
- V1 (to) and V2 (from) License: MIT
Param | Type | Description |
---|---|---|
description | string | the PR description |
Example
Plugin Code: extractRenovateVersionBump
/**
* @module extractRenovateVersionBump
* @description Extract version bump information from Renovate PRs description
* @param {string} description - the PR description
* @returns {string[]} V1 (to) and V2 (from)
* @example {{ pr.description | extractRenovateVersionBump | compareMultiSemver }}
* @license MIT
**/
module.exports = (desc) => {
const results = [];
if (desc && desc !== '""' && desc !== "''") {
const regex =
/\[[\\]*`([\d\.]+[A-Za-zαß]*)[\\]*` -> [\\]*`([\d\.]+[A-Za-zαß]*)[\\]*`\]/g;
let matches = null;
do {
matches = regex.exec(desc);
if (matches?.length === 3) {
let [_, from, to] = matches;
// remove trailing dot on to
if (to.at(-1) === ".") {
to = to.slice(0, -1);
}
results.push([to, from]);
}
} while (matches !== null);
}
return results;
}
gitStream CM Example: extractRenovateVersionBump
manifest:
version: 1.0
automations:
bump_minor:
if:
- {{ bump == 'minor' }}
- {{ branch.name | includes(term="renovate") }}
- {{ branch.author | includes(term="renovate") }}
run:
- action: approve@v1
- action: add-comment@v1
args:
comment: |
Renovate `minor` version bumps are approved automatically.
bump_patch:
if:
- {{ bump == 'patch' }}
- {{ branch.name | includes(term="renovate") }}
- {{ branch.author | includes(term="renovate") }}
run:
- action: approve@v1
- action: merge@v1
- action: add-comment@v1
args:
comment: |
Renovate `patch` version bumps are approved and merged automatically.
bump: {{ pr.description | extractRenovateVersionBump | compareMultiSemver }}
extractSnykVersionBump
Extract version bump information from Snyk PRs description
Returns: Array.<string>
- V1 (to) and V2 (from)
License: MIT
Param | Type | Description |
---|---|---|
description | string | the PR description |
Example
Plugin Code: extractSnykVersionBump
/**
* @module extractSnykVersionBump
* @description Extract version bump information from Snyk PRs description
* @param {string} description - the PR description
* @returns {string[]} V1 (to) and V2 (from)
* @example {{ pr.description | extractSnykVersionBump | compareSemver }}
* @license MIT
**/
module.exports = (desc) => {
if (desc && desc !== '""' && desc !== "''" ) {
const matches = /Upgrade.*from ([\d\.]+[A-Za-zαß]*) to ([\d\.]+[A-Za-zαß]*)/.exec(desc);
if (matches && matches.length == 3) {
var [_, from, to] = matches;
// remove trailing dot on to
if (to[to.length - 1] === ".") {
to = to.slice(0, -1);
}
return [to, from];
}
}
return null;
}
gitStream CM Example: extractSnykVersionBump
manifest:
version: 1.0
automations:
bump_minor:
if:
- {{ bump == 'minor' }}
- {{ branch.name | includes(term="snyk-update"") }}
- {{ branch.author | includes(term="snyk-update"") }}
run:
- action: approve@v1
- action: add-comment@v1
args:
comment: |
Snyk `minor` version bumps are approved automatically.
bump_patch:
if:
- {{ bump == 'patch' }}
- {{ branch.name | includes(term="snyk-update"") }}
- {{ branch.author | includes(term="snyk-update"") }}
run:
- action: approve@v1
- action: merge@v1
- action: add-comment@v1
args:
comment: |
Snyk `patch` version bumps are approved and merged automatically.
bump: {{ pr.description | extractSnykVersionBump | compareSemver }}
extractOrcaFindings
Extract security issues information from Orca PR reviews
Returns: Object
- Findings Findings.infrastructure_as_code: { count: null, priority: '' }, Findings.vulnerabilities: { count: null, priority: '' }, Findings.secrets: { count: null, priority: '' },
License: MIT
Param | Type | Description |
---|---|---|
PR | Object | the gitStream's PR context variable |
Example
Usage example, that adds lables based on Orca Secuirty findings.
Plugin Code: extractOrcaFindings
/**
* @module extractOrcaFindings
* @description Extract security issues information from Orca PR reviews
* @param {Object} pr - the gitStream's PR context variable
* @returns {Object} Findings
* Findings.infrastructure_as_code: { count: null, priority: '' },
* Findings.vulnerabilities: { count: null, priority: '' },
* Findings.secrets: { count: null, priority: '' },
* @example {{ pr | extractOrcaFindings }}
* @license MIT
**/
function getOrcaPropertyRating(lines, lineIdentifierRegex, findingsCellIndex) {
const matches = lines.filter(x => x.match(lineIdentifierRegex));
const [firstMatch] = matches;
const cells = firstMatch.split('|');
const [_, high, medium, low, info] = /"High"> ([\d]+).*"Medium"> ([\d]+).*"Low"> ([\d]+).*"Info"> ([\d]+)/
.exec(cells[findingsCellIndex])
.map(x => parseInt(x));
return {high, medium, low, info};
}
module.exports = (pr) => {
let orcaObject = {
infrastructure_as_code: { count: null, priority: '' },
vulnerabilities: { count: null, priority: '' },
secrets: { count: null, priority: '' },
};
// Orca comments are added as PR review
const orcaComment = pr.reviews.filter(x => x.commenter.includes('orca-security'));
if (orcaComment.length) {
const orcaCommentArray = orcaComment[orcaComment.length - 1].content.split('\n');
var priority = getOrcaPropertyRating(orcaCommentArray, /Infrastructure as Code/, 3);
orcaObject.infrastructure_as_code = {
count: priority.high + priority.medium + priority.low + priority.info,
priority,
};
var priority = getOrcaPropertyRating(orcaCommentArray, /Vulnerabilities/, 3);
orcaObject.vulnerabilities = {
count: priority.high + priority.medium + priority.low + priority.info,
priority,
};
var priority = getOrcaPropertyRating(orcaCommentArray, /Secrets/, 3);
orcaObject.secrets = {
count: priority.high + priority.medium + priority.low + priority.info,
priority,
};
}
return JSON.stringify(orcaObject);
}
gitStream CM Example: extractOrcaFindings
# -*- mode: yaml -*-
manifest:
version: 1.0
automations:
{% for item in reports %}
label_orca_{{ item.name }}:
if:
- {{ item.count > 0 }}
run:
- action: add-label@v1
args:
label: 'orca:{{ item.name }}'
{% endfor %}
orca: {{ pr | extractOrcaFindings }}
reports:
- name: introduced-cves
count: {{ orca.vulnerabilities.count }}
- name: iac-misconfigurations
count: {{ orca.infrastructure_as_code.count }}
- name: exposed-secrets
count: {{ orca.secrets.count }}
colors:
red: 'b60205'
generateDescription
A gitStream plugin to auto-generate pull request descriptions based on commit messages and other criteria.
Returns: Object
- Returns the generated PR description. License: MIT
Param | Type | Description |
---|---|---|
branch | Object | The current branch object. |
pr | Object | The pull request object. |
repo | Object | The repository object. |
source | Object | The source object containing diff information. |
callback | function | The callback function. |
Example
Plugin Code: generateDescription
/**
* @module generateDescription
* @description A gitStream plugin to auto-generate pull request descriptions based on commit messages and other criteria.
* @param {Object} branch - The current branch object.
* @param {Object} pr - The pull request object.
* @param {Object} repo - The repository object.
* @param {Object} source - The source object containing diff information.
* @param {function} callback - The callback function.
* @returns {Object} Returns the generated PR description.
* @example {{ branch | generateDescription(pr, repo, source) }}
* @license MIT
**/
// Parse commit messages
function parseCommitMessages(messages) {
const commitTypes = {
feat: [],
fix: [],
chore: [],
docs: [],
style: [],
refactor: [],
perf: [],
test: [],
build: [],
ci: [],
other: [],
};
messages
.filter((message) => !message.includes("Merge branch"))
.forEach((message) => {
const match = message.match(
/^(feat|fix|chore|docs|style|refactor|perf|test|build|ci):/,
);
if (match) {
commitTypes[match[1]].push(message.replace(`${match[1]}:`, "").trim());
} else {
commitTypes.other.push(message);
}
});
return commitTypes;
}
// Format commit section
function formatCommitSection(type, commits) {
return commits.length
? `> - **${type}:**\n${commits.map((msg) => `> - ${msg}`).join("\n")}\n`
: "";
}
function containsNewTests(files) {
const testPattern = /(test_|spec_|__tests__|_test|_tests|\.test|\.spec)/i;
const testDirectoryPattern = /[\\/]?(tests|test|__tests__)[\\/]/i;
const testKeywords = /describe\(|it\(|test\(|expect\(/i; // Common test keywords for JavaScript
for (const file of files) {
const { new_file, diff, new_content } = file;
// Check if the filename indicates it's a test file
if (testPattern.test(new_file) || testDirectoryPattern.test(new_file)) {
return true;
}
// Check if the diff or new content contains test-related code
if (testKeywords.test(diff) || testKeywords.test(new_content)) {
return true;
}
}
return false;
}
function extractUserAdditions(description) {
const match = description.match(
/<!--- user additions start --->([\s\S]*?)<!--- user additions end --->/,
);
return match ? match[1].trim() : description.trim();
}
// Generate PR description
async function generateDescription(branch, pr, repo, source, callback) {
if (process.env[__filename]) {
return callback(null, process.env[__filename]);
}
const commitTypes = parseCommitMessages(branch.commits.messages);
const addTests = containsNewTests(source.diff.files) ? "X" : " ";
const codeApproved = pr.approvals > 0 ? "X" : " ";
const changes = Object.entries(commitTypes)
.map(([type, commits]) => formatCommitSection(type, commits))
.join("");
const changesWithoutLastBr = changes.slice(0, -1);
const userAdditions = extractUserAdditions(pr.description);
const result = `
<!--- user additions start --->
${userAdditions}
<!--- user additions end --->
**PR description below is managed by gitStream**
<!--- Auto-generated by gitStream--->
> #### Commits Summary
> This pull request includes the following changes:
${changesWithoutLastBr}
> #### Checklist
> - [${addTests}] Add tests
> - [${codeApproved}] Code Reviewed and approved
<!--- Auto-generated by gitStream end --->
`;
process.env[__filename] = result.split("\n").join("\n ");
return callback(null, process.env[__filename]);
}
module.exports = { filter: generateDescription, async: true };
gitStream CM Example: generateDescription
triggers:
exclude:
branch:
- r/dependabot/
automations:
generate_pr_desc:
if:
- true
run:
- action: update-description@v1
args:
description: |
{{ branch | generateDescription(pr, repo, source) }}
getCodeowners
Resolves the PR's code owners based on the repository's CODEOWNERS file
Returns: Array.<string>
- user names
License: MIT
Param | Type | Description |
---|---|---|
files | Array.<string> | the gitStream's files context variable |
pr | Object | the gitStream's pr context variable |
token | string | access token with repo:read scope, used to read the CODEOWNERS file |
Example
When used, create a secret TOKEN, and add it to the workflow file, in GitHub:
jobs:
gitStream:
...
env:
CODEOWNERS: ${{ secrets.GITSTREAM_CODEOWNERS }}
steps:
- name: Evaluate Rules
uses: linear-b/gitstream-github-action@v2
Plugin Code: getCodeowners
/**
* @module getCodeowners
* @description Resolves the PR's code owners based on the repository's CODEOWNERS file
* @param {string[]} files - the gitStream's files context variable
* @param {Object} pr - the gitStream's pr context variable
* @param {string} token - access token with repo:read scope, used to read the CODEOWNERS file
* @returns {string[]} user names
* @example {{ files | getCodeowners(pr, env.CODEOWNERS_TOKEN) }}
* @license MIT
**/
const { Octokit } = require("@octokit/rest");
const ignore = require('./ignore/index.js');
async function loadCodeownersFile(owner, repo, auth) {
const octokit = new Octokit({
request: { fetch },
auth,
});
const res = await octokit.repos.getContent({
owner,
repo,
path: 'CODEOWNERS'
});
return Buffer.from(res.data.content, 'base64').toString()
}
function codeownersMapping(data) {
return data
.toString()
.split('\n')
.filter(x => x && !x.startsWith('#'))
.map(x => x.split("#")[0])
.map(x => {
const line = x.trim();
const [path, ...owners] = line.split(/\s+/);
return {path, owners};
});
}
function resolveCodeowner(mapping, file) {
const match = mapping
.slice()
.reverse()
.find(x =>
ignore()
.add(x.path)
.ignores(file)
);
if (!match) return false;
return match.owners;
}
module.exports = {
async: true,
filter: async (files, pr, token, callback) => {
const fileData = await loadCodeownersFile(pr.author, pr.repo, token);
const mapping = codeownersMapping(fileData);
const resolved = files
.map(f => resolveCodeowner(mapping, f))
.flat()
.filter(i => typeof i === 'string')
.map(u => u.replace(/^@/, ""));
const unique = [...new Set(resolved)];
return callback(null, unique);
},
}
gitStream CM Example: getCodeowners
# -*- mode: yaml -*-
manifest:
version: 1.0
automations:
senior_review:
if:
- true
run:
- action: explain-code-experts@v1
args:
gt: 10
- action: add-reviewers@v1
args:
reviewers: {{ experts | intersection(list=owners) }}
experts: {{ repo | codeExperts(gt=10) }}
owners: {{ files | codeowners(pr, env.CODEOWNERS) }}
hasJiraIssue
Check to see if the input string matches a specified field for one or more Jira issues.
Returns: boolean
- Returns true if the input string matches a Jira task title.
License: MIT
Param | Type | Description |
---|---|---|
input | string | The string to search for a Jira task title. |
password | string | Your Jira API token |
key | string | The Jira key to search for matches against the input string. |
jiraSpaceName | string | The name of the Jira space to search for tasks. |
string | The email address associated with the Jira API token. |
Example
{{ "https://github.com/{{ repo.owner }}/{{ repo.name }}/pull/{{ pr.number }}" | hasJiraIssue(password, key, jiraSpaceName, email) }}
Prerequisite Configuration
You will need to complete the following steps to use this plugin:
- Create an API token for your Jira account.
- Make the token available to gitStream via an environment variable.
Plugin Code: hasJiraIssue
/**
* @module hasJiraIssue
* @description Check to see if the input string matches a specified field for one or more Jira issues.
* @param {string} input - The string to search for a Jira task title.
* @param {string} password - Your Jira API token
* @param {string} key - The Jira key to search for matches against the input string.
* @param {string} jiraSpaceName - The name of the Jira space to search for tasks.
* @param {string} email - The email address associated with the Jira API token.
* @returns {boolean} Returns true if the input string matches a Jira task title.
* @example {{ "https://github.com/{{ repo.owner }}/{{ repo.name }}/pull/{{ pr.number }}" | hasJiraIssue(password, key, jiraSpaceName, email) }}
* @license MIT
*/
module.exports = {
async: true,
filter: async (inputString, password, key, jiraSpaceName, email, callback) => {
const jql = `"${key}" = "${inputString}"`;
const resp = await fetch(`https://${jiraSpaceName}.atlassian.net/rest/api/2/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${email}:${password}`)
},
body: JSON.stringify({
'jql': jql,
'maxResults': 1,
"fieldsByKeys": true,
'fields': [ 'assignee' ]
})
});
const results = await resp.json();
return callback(null, !!results.issues?.length);
}
}
gitStream CM Example: hasJiraIssue
# -*- mode: yaml -*-
manifest:
version: 1.0
###### ** Configure This Section ** ######
# Configure this for your Jira instance and the email associated with your API key.
# You can safely use these values because only your API key is sensitive.
jiraSpaceName: "my-company" # e.g. my-company.atlassian.net
email: "my.email@example.com"
# If you're concerned about exposing this information,
# we recommend using environment variables for your production environment.
# -----------
# Pass the API token associated with the email above to gitStream via an environment variable.
jiraAuth: {{ env.JIRA_API_TOKEN }}
# Learn more about env: https://docs.gitstream.cm/context-variables/#env
# -----------
# Change this to the Jira field you want to match the input string against.
jiraField: "myField"
# If you want to search a custom field, you should provide the ID like so:
# jiraField: "cf[XXXXX]"
# Replace XXXXX with the ID of the custom field you want to search.
# More information:
# Using JQL to search the Jira API: https://support.atlassian.com/jira-service-management-cloud/docs/jql-fields/
# How to find the ID of a custom field: https://confluence.atlassian.com/jirakb/how-to-find-any-custom-field-s-ids-744522503.html
# -----------
prUrl: "https://github.com/{{ repo.owner }}/{{ repo.name }}/pull/{{ pr.number }}"
has_jira_issue: {{ prUrl | hasJiraIssue(jiraAuth, jiraField, jiraSpaceName, email) }}
automations:
has_jira_issue:
if:
- {{ not has_jira_issue }}
run:
- action: add-comment@v1
args:
comment: "This PR is missing a related issue in Jira. Please create a Jira task."
isFlaggedUser
Returns true if the username that is passed to this function is specified in a predefined list of users. This is useful if you want gitStream automations to run only for specified users.
Returns: boolean
- Returns true if the user is specified in the flaggedUsers list, otherwise false.
License: MIT
Param | Type | Description |
---|---|---|
Input | string | The GitHub username to check. |
Example
Plugin Code: isFlaggedUser
// Add users who you want to add to the flag list.
const flaggedUsers = ["user1", "user2"];
/**
* @module isFlaggedUser
* @description Returns true if the username that is passed to this function is specified in a predefined list of users.
* This is useful if you want gitStream automations to run only for specified users.
* @param {string} Input - The GitHub username to check.
* @returns {boolean} Returns true if the user is specified in the flaggedUsers list, otherwise false.
* @example {{ pr.author | isFlaggedUser }}
* @license MIT
*/
function isFlaggedUser(username) {
if (flaggedUsers.includes(username)) {
return true;
} else {
return false;
}
};
function containsString(arr, str) {
return arr.includes(str);
};
module.exports = isFlaggedUser;
gitStream CM Example: isFlaggedUser
# -*- mode: yaml -*-
manifest:
version: 1.0
automations:
detect_flagged_user:
if:
- {{ pr.author | isFlaggedUser }}
run:
- action: add-comment@v1
args:
comment: {{ pr.author }} is a gitStream user.