Skip to content

Commit

Permalink
Create lock GitHub Action (#30)
Browse files Browse the repository at this point in the history
* Create `lock` GitHub Action

* Fix typo in README.md

* Do not lock already locked issues

* Update README.md
  • Loading branch information
mollyIV authored Apr 26, 2020
1 parent 38c20ad commit b26aaa5
Show file tree
Hide file tree
Showing 12 changed files with 454 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The Fastlane GitHub Actions provide a set of GitHub Actions to make maintaining

Adds a comment and a label to a pull request and referenced issue when it is released. Read more [here](communicate-on-pull-request-released).

- 🔒 [@github-actions/lock](lock)

Locks closed issues and pull requests that have not had recent interaction. Read more [here](lock).

## Versioning

All the actions are released in one batch. We do not support semantic versioning (yet). Reference a `latest` branch in your workflow:
Expand Down
3 changes: 3 additions & 0 deletions lock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package-lock.json
node_modules
__tests__/runner/*
11 changes: 11 additions & 0 deletions lock/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
}
7 changes: 7 additions & 0 deletions lock/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:slim

COPY . .

RUN npm install --production

ENTRYPOINT ["node", "/lib/main.js"]
26 changes: 26 additions & 0 deletions lock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Lock

An action for locking closed, inactive issues and pull requests.

# Usage

See [action.yml](action.yml)

```yaml
name: Lock closed, inactive issues and pull requests
on:
schedule:
- cron: "0 0 * * *"

jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: fastlane/github-actions/lock@latest
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
```
# License
The scripts and documentation in this project are released under the [MIT License](LICENSE)
91 changes: 91 additions & 0 deletions lock/__tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const path = require('path');
const nock = require('nock');

describe('action test suite', () => {
it(`It locks a closed issue when the issue was not updated in a given timespan`, async () => {
process.env['INPUT_REPO-TOKEN'] = 'token';
process.env['INPUT_DAYS-BEFORE-LOCK'] = '60';
process.env['INPUT_OPERATIONS-PER-RUN'] = '2';

process.env['GITHUB_REPOSITORY'] = 'foo/bar';

var date = new Date();
date.setDate(date.getDate() - 61);

const api = nock('https://api.github.com')
.persist()
.get('/repos/foo/bar/issues?state=closed&per_page=100&page=1')
.reply(
200,
JSON.parse(
`[{"id": 1, "title": "Issue to be locked", "number": 1347, "locked": false, "updated_at": "${date.toISOString()}"}]`
)
)
.put('/repos/foo/bar/issues/1347/lock')
.reply(204);

const main = require('../src/main');
await main.run();

expect(api.isDone()).toBeTruthy();
nock.cleanAll();
});

it(`It does not lock an issue, when the required time has not passed`, async () => {
process.env['INPUT_REPO-TOKEN'] = 'token';
process.env['INPUT_DAYS-BEFORE-LOCK'] = '60';
process.env['INPUT_OPERATIONS-PER-RUN'] = '2';

process.env['GITHUB_REPOSITORY'] = 'foo/bar';

var date = new Date();
date.setDate(date.getDate() - 1); // yesterday

const api = nock('https://api.github.com')
.persist()
.get('/repos/foo/bar/issues?state=closed&per_page=100&page=1')
.reply(
200,
JSON.parse(
`[{"id": 1, "title": "The issue cannot be locked yet", "number": 1347, "locked": false, "updated_at": "${date.toISOString()}"}]`
)
)
.put('/repos/foo/bar/issues/1347/lock')
.reply(204);

const main = require('../src/main');
await main.run();

expect(api.isDone()).not.toBeTruthy();
nock.cleanAll();
});

it(`It does not lock an issue, when the issue is already locked`, async () => {
process.env['INPUT_REPO-TOKEN'] = 'token';
process.env['INPUT_DAYS-BEFORE-LOCK'] = '60';
process.env['INPUT_OPERATIONS-PER-RUN'] = '2';

process.env['GITHUB_REPOSITORY'] = 'foo/bar';

var date = new Date();
date.setDate(date.getDate() - 61);

const api = nock('https://api.github.com')
.persist()
.get('/repos/foo/bar/issues?state=closed&per_page=100&page=1')
.reply(
200,
JSON.parse(
`[{"id": 1, "title": "Locked issue", "number": 1347, "locked": true, "updated_at": "${date.toISOString()}"}]`
)
)
.put('/repos/foo/bar/issues/1347/lock')
.reply(204);

const main = require('../src/main');
await main.run();

expect(api.isDone()).not.toBeTruthy();
nock.cleanAll();
});
});
16 changes: 16 additions & 0 deletions lock/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: 'Lock'
description: 'An action for locking closed, inactive issues and pull requests'
author: 'fastlane'
inputs:
repo-token:
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
required: true
days-before-lock:
description: 'The number of days to wait to lock an issue or pull request after it being closed'
default: 60
operations-per-run:
description: 'The maximum number of operations per run, used to control rate limiting'
default: 30
runs:
using: 'docker'
image: 'Dockerfile'
11 changes: 11 additions & 0 deletions lock/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
90 changes: 90 additions & 0 deletions lock/lib/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const github = __importStar(require("@actions/github"));
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const args = getAndValidateArgs();
const client = new github.GitHub(args.repoToken);
yield processIssues(client, args.daysBeforeLock, args.operationsPerRun);
}
catch (error) {
core.error(error);
core.setFailed(error.message);
}
});
}
exports.run = run;
function getAndValidateArgs() {
const args = {
repoToken: core.getInput('repo-token', { required: true }),
daysBeforeLock: parseInt(core.getInput('days-before-lock', { required: true })),
operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true }))
};
for (var numberInput of ['days-before-lock', 'operations-per-run']) {
if (isNaN(parseInt(core.getInput(numberInput)))) {
throw Error(`input ${numberInput} did not parse to a valid integer`);
}
}
return args;
}
function processIssues(client, daysBeforeLock, operationsLeft, page = 1) {
return __awaiter(this, void 0, void 0, function* () {
const issues = yield client.issues.listForRepo({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
state: 'closed',
per_page: 100,
page: page
});
operationsLeft -= 1;
if (issues.data.length === 0 || operationsLeft === 0) {
return operationsLeft;
}
for (var issue of issues.data.values()) {
core.debug(`Found issue: "${issue.title}", last updated ${issue.updated_at}`);
if (wasLastUpdatedBefore(issue, daysBeforeLock) && !issue.locked) {
operationsLeft -= yield lock(client, issue);
}
if (operationsLeft <= 0) {
return 0;
}
}
return yield processIssues(client, daysBeforeLock, operationsLeft, page + 1);
});
}
function wasLastUpdatedBefore(issue, days) {
const daysInMillis = 1000 * 60 * 60 * 24 * days;
const millisSinceLastUpdated = new Date().getTime() - new Date(issue.updated_at).getTime();
return millisSinceLastUpdated >= daysInMillis;
}
function lock(client, issue) {
return __awaiter(this, void 0, void 0, function* () {
core.debug(`Locking issue "${issue.title}"`);
yield client.issues.lock({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue.number,
headers: { 'Content-Length': 0 } // if you choose not to pass any parameters, you'll need to set Content-Length to zero when calling out to this endpoint. For more info see https://developer.github.com/v3/#http-verbs
});
return 1; // the number of API operations performed
});
}
run();
32 changes: 32 additions & 0 deletions lock/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "lock",
"version": "0.0.0",
"private": true,
"description": "An action for locking closed issues and pull requests",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
"format": "prettier --write **/*.ts",
"test": "jest"
},
"keywords": [
"actions",
"fastlane"
],
"author": "fastlane",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.0.0",
"@actions/github": "^1.0.0"
},
"devDependencies": {
"@types/jest": "^24.0.13",
"@types/node": "^12.0.4",
"jest": "^24.8.0",
"jest-circus": "^24.7.1",
"nock": "^10.0.6",
"prettier": "^1.17.1",
"ts-jest": "^24.0.2",
"typescript": "^3.5.1"
}
}
Loading

0 comments on commit b26aaa5

Please sign in to comment.