Skip to content

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.

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

{{ ["1.2.1", "1.2.3"] | compareSemver  == "patch" }}

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
 * @param {boolean} [lexicographical=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".
 * @param {boolean} [zeroExtend=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.
 * @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.1", "1.2.3"] | compareSemver  == "patch" }}
 * @license MIT
**/


module.exports = (v1, v2, options = {}) => {
  console.log("SEMVER", {v1, v2, options});

  // 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
  }

  const { lexicographical = false, zeroExtend = true } = options;
  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';
}

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.1", "1.2.3"] | compareSemver }}

Download Source Code

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

{{ pr.description | extractDependabotVersionBump | compareSemver }}

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 }}

Download Source Code

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

{{ pr.description | extractSnykVersionBump | compareSemver }}

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 }}

Download Source Code

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

{{ pr | extractOrcaFindings }}

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'

Download Source Code

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

{{ files | getCodeowners(pr, env.CODEOWNERS_TOKEN) }}

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@v1
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) }}

Download Source Code

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

{{ pr.author | isFlaggedUser }}

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.

Download Source Code