Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the CLA GitHub Actions Scripts #44

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/test_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ jobs:
cla-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Check if CLA signed
uses: ./
2 changes: 1 addition & 1 deletion .github/workflows/test_pull_request_target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ jobs:
cla-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Check if CLA signed
uses: ./
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ inputs:
accept-existing-contributors:
description: 'Pass CLA check for existing project contributors'
required: false
default: false
default: 'false'
github-token:
description: 'The GitHub access token (e.g. secrets.GITHUB_TOKEN) used to create a CLA comment on the pull request (default: {{ github.token }}).'
default: '${{ github.token }}'
Expand All @@ -18,8 +18,8 @@ inputs:
default: 'Apache-2.0'
required: false
runs:
using: 'node16'
main: 'index.js'
using: 'node20'
main: 'dist/index.js'
branding:
icon: 'user-check'
color: 'purple'
75 changes: 75 additions & 0 deletions dist/index.js

Large diffs are not rendered by default.

172 changes: 75 additions & 97 deletions index.js → index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
const core = require('@actions/core');
const exec = require('@actions/exec');
const github = require('@actions/github');
const axios = require('axios');
const path = require('path');
import * as core from '@actions/core';
import * as github from '@actions/github';

import { lp_email_check } from './lp_cla_check';

const githubToken = core.getInput('github-token', {required: true})
const exemptedBots = core.getInput('exempted-bots', {required: true}).split(',').map(input => input.trim());
const implicitLicenses = core.getInput('implicit-approval-from-licenses', {required: true}).split(',').map(input => input.trim());

// Returns the license that grants implicit CLA if found in the commit message.
// Otherwise, returns an empty string.
function hasImplicitLicense(commit_message) {
interface ContributorData {
username: string;
email: string;
signed: boolean;
};

/** Returns the license that grants implicit CLA if found in the commit message.
* Otherwise, returns an empty string.
*/
function hasImplicitLicense(commit_message: string): string {
const lines = commit_message.split('\n');

// Skip the commit subject (first line)
for (var i = 1; i < lines.length; i++) {
for (let i = 1; i < lines.length; i++) {
// Remove any trailing `\r` char
const line = lines[i].replace(/\r$/,'');
const line = lines[i].trim();
const license = line.match(/^License: ?(.+)$/);
if (license && implicitLicenses.includes(license[1])) {
return license[1];
Expand All @@ -26,187 +32,159 @@ function hasImplicitLicense(commit_message) {
}

async function run() {
// Install dependencies
core.startGroup('Installing python3-launchpadlib')
await exec.exec('sudo apt-get update');
await exec.exec('sudo apt-get install python3-launchpadlib');
core.endGroup()

console.log();

// Get existing contributors
const ghRepo = github.getOctokit(githubToken);
const accept_existing_contributors = (core.getInput('accept-existing-contributors') == "true");
let contributors_list = new Array<string>();

if (accept_existing_contributors) {
const contributors_url = github.context.payload['pull_request']['base']['repo']['contributors_url'];
const contributors_url: string | undefined = github.context.payload.pull_request?.base.repo.contributors_url;
const contributors = await ghRepo.request('GET ' + contributors_url);

var contributors_list = []
for (const i in contributors.data) {
contributors_list.push(contributors.data[i]['login']);
}
contributors_list = (contributors.data as { login: string; }[]).map(contributor => contributor.login);
}

// Get commit authors
const commits_url = github.context.payload['pull_request']['commits_url'];
const commits_url: string | undefined = github.context.payload.pull_request?.commits_url;
if (!commits_url)
throw new Error('commits_url is undefined');
const commits = await ghRepo.request('GET ' + commits_url);

var commit_authors = []
for (const i in commits.data) {
let commit_authors = new Map<string, ContributorData>();
for (const data of commits.data) {
// Check if the commit message contains a license header that matches
// one of the licenses granting implicit CLA approval
if (commits.data[i]['commit']['message']) {
const goodLicense = hasImplicitLicense(commits.data[i]['commit']['message']);
if (data.commit.message) {
const goodLicense = hasImplicitLicense(data.commit.message);
if (goodLicense) {
console.log('- commit ' + commits.data[i]['sha'] + ' ✓ (' + goodLicense + ' license)');
console.log(`- commit ${data.sha} ✓ (${goodLicense} license)`);
continue;
}
}

var username;
if (commits.data[i]['author']) {
username = commits.data[i]['author']['login'];
let username: string | undefined = data.author?.login;
if (!username) {
core.error(`author is undefined for commit ${data.sha}`);
continue;
}
const email = commits.data[i]['commit']['author']['email'];
commit_authors[username] = {
'username': username,
'email': email,
'signed': false
};
const email = data.commit.author.email;
commit_authors.set(username, {
'username': username,
'email': email,
'signed': false
});
}

// Check GitHub
console.log('Checking the following users on GitHub:');
for (const i in commit_authors) {
const username = commit_authors[i]['username'];
const email = commit_authors[i]['email'];
const nodeFetch = (await import('node-fetch')).default;
for (const [_, commit_author] of commit_authors) {
const { username, email } = commit_author;

if (!username) {
continue;
}
if (username.endsWith('[bot]') && exemptedBots.includes(username.slice(0, -5))) {
console.log('- ' + username + ' ✓ (Bot exempted from CLA)');
commit_authors[i]['signed'] = true;
console.log(`- ${username} ✓ (Bot exempted from CLA)`);
commit_author.signed = true;
continue
}
if (email.endsWith('@canonical.com')) {
console.log('- ' + username + ' ✓ (@canonical.com account)');
commit_authors[i]['signed'] = true;
console.log(`- ${username} ✓ (@canonical.com account)`);
commit_author.signed = true;
continue
}
if (email.endsWith('@mozilla.com')) {
console.log('- ' + username + ' ✓ (@mozilla.com account)');
commit_authors[i]['signed'] = true;
console.log(`- ${username} ✓ (@mozilla.com account)`);
commit_author.signed = true;
continue
}
if (email.endsWith('@ocadogroup.com') || email.endsWith('@ocado.com')) {
console.log('- ' + username + ' ✓ (@ocado{,group}.com account)');
commit_authors[i]['signed'] = true;
console.log(`- ${username} ✓ (@ocado{,group}.com account)`);
commit_author.signed = true;
continue
}
if (accept_existing_contributors && contributors_list.includes(username)) {
console.log('- ' + username + ' ✓ (already a contributor)');
commit_authors[i]['signed'] = true;
console.log(`- ${username} ✓ (already a contributor)`);
commit_author.signed = true;
continue
}

try {
console.log('Check in the signed list service');
const response = await axios.get(
'https://cla-checker.canonical.com/check_user/' + username
const response = await nodeFetch(
`https://cla-checker.canonical.com/check_user/${username}`,
);
if (response.status === 200) {
console.log('- ' + username + ' ✓ (has signed the CLA)');
commit_authors[i]['signed'] = true;
}
} catch (error) {
if (error.response && error.response.status === 404) {
console.log('- ' + username + ' ✕ (has not signed the CLA)');
console.log(`- ${username} ✓ (has signed the CLA)`);
commit_author.signed = true;
} else {
console.error('Error occurred while checking user:', error.message);
console.log(`- ${username} ✕ (has not signed the CLA)`);
}
} catch (error: any) {
const message = `'Error occurred while checking user: ${error.message}`;
core.error(message);
}
}

console.log();

// Check Launchpad
for (const i in commit_authors) {
if (commit_authors[i]['signed'] == false) {
for (const [_, commit_author] of commit_authors) {
if (commit_author.signed == false) {
console.log('Checking the following user on Launchpad:');
const email = commit_authors[i]['email'];

await exec.exec('python3', [path.join(__dirname, 'lp_cla_check.py'), email], options = {
silent: true,
listeners: {
stdout: (data) => {
process.stdout.write(data.toString());
},
stderr: (data) => {
process.stdout.write(data.toString());
}
}
})
.then((result) => {
commit_authors[i]['signed'] = true;
}).catch((error) => {
commit_authors[i]['signed'] = false;
});
commit_author.signed = await lp_email_check(commit_author.email);
}
}

console.log();

// Determine Result
passed = true
var non_signers = []
for (const i in commit_authors) {
if (commit_authors[i]['signed'] == false) {
let passed = true;
let non_signers = new Array<string>();
for (const [username, commit_author] of commit_authors) {
if (!commit_author.signed) {
passed = false;
non_signers.push(i)
non_signers.push(username);
break;
}
}

if (passed) {
console.log('CLA Check - PASSED');
console.info('CLA Check - PASSED');
}
else {
core.setFailed('CLA Check - FAILED');
}

// We can comment on the PR only in the target context
if (github.context.eventName != "pull_request_target") {
if (github.context.eventName !== "pull_request_target") {
return;
}

// Find previous CLA comment if any
const cla_header = '<!-- CLA signature is needed -->';
const pull_request_number = github.context.payload.pull_request.number;
const pull_request_number = github.context.payload.pull_request?.number;
const owner = github.context.repo.owner;
const repo = github.context.repo.repo;

const {data: comments} = await ghRepo.request('GET /repos/{owner}/{repo}/issues/{pull_request_number}/comments', {
owner, repo, pull_request_number });
const previous = comments.find(comment => comment.body.includes(cla_header));
const previous = comments.find((comment: { body: string; }) => comment.body.includes(cla_header));

// Write a new updated comment on PR if CLA is not signed for some users
if (!passed) {
console.log("Posting or updating a comment on the PR")

var authors_content;
var cla_content=`not signed the Canonical CLA which is required to get this contribution merged on this project.
let authors_content = '';
const cla_content=`not signed the Canonical CLA which is required to get this contribution merged on this project.
Please head over to https://ubuntu.com/legal/contributors to read more about it.`
non_signers.forEach(function (author, i) {
if (i == 0) {
authors_content=author;
return;
} else if (i == non_signers.length-1) {
authors_content=' and ' + author;
authors_content=`and ${author}`;
return;
}
authors_content=', ' + author;
authors_content=`, ${author}`;
});

if (non_signers.length > 1) {
Expand All @@ -215,7 +193,7 @@ Please head over to https://ubuntu.com/legal/contributors to read more about it.
authors_content+=' has ';
}

var body = `${cla_header}Hey! ${authors_content} ${cla_content}`
const body = `${cla_header}Hey! ${authors_content} ${cla_content}`
// Create new comments
if (!previous) {
await ghRepo.request('POST /repos/{owner}/{repo}/issues/{pull_request_number}/comments', {
Expand Down
33 changes: 0 additions & 33 deletions lp_cla_check.py

This file was deleted.

22 changes: 22 additions & 0 deletions lp_cla_check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const API_ENDPOINT = 'https://api.launchpad.net/devel';
const MEMBERSHIP_QUERY_LINK = `${API_ENDPOINT}/~contributor-agreement-canonical/+member`;

export async function lp_email_check(email: string): Promise<boolean> {
const url = new URL(`${API_ENDPOINT}/people?ws.op=getByEmail`);
url.searchParams.append('email', email);
const lp_account = await fetch(url);
if (!lp_account.ok) {
console.log(`- ${email} ✕ (has no Launchpad account)`);
return false;
}
const json = (await lp_account.json()) as { name: string };
const membership_query_url = new URL(`${MEMBERSHIP_QUERY_LINK}/${encodeURIComponent(json.name)}`);
console.log(membership_query_url);
const membership_query = await fetch(membership_query_url);
if (membership_query.ok) {
console.log(`- ${email} ✓ (has signed the CLA)`);
return true;
}
console.log(`- ${email} ✕ (has not signed the CLA)`);
return false;
}
1 change: 0 additions & 1 deletion node_modules/.bin/uuid

This file was deleted.

Loading
Loading