Skip to content

Commit

Permalink
Created base action (#5)
Browse files Browse the repository at this point in the history
Created base action with the ability to update PRs and print a summary
of the results.

Resolves #2
  • Loading branch information
Bullrich authored Jan 9, 2024
1 parent 9f348fa commit 83c26a5
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 28 deletions.
Binary file added .github/update-branch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions .github/workflows/up-to-date.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Up to date

on:
push:
branches:
- 'main'

jobs:
updatePullRequests:
runs-on: ubuntu-latest
steps:
- name: Update all the PRs
uses: paritytech/up-to-date-action@main
with:
GITHUB_TOKEN: ${{ github.token }}
79 changes: 73 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,76 @@
# Parity GitHub Action template
# Up to date action

Template used to generate GitHub Actions.
Keep your pull request up to date when the target branch changes.

## To start
## Why?

- Remember to modify the `action.yml` file to have your required attributes and details.
- You can use [GitHub Action Brandings cheatsheet](https://github.com/haya14busa/github-action-brandings) to set the style of the action.
- Remember to modify the name in the `package.json`.
This action was created on the need to keep all the PRs up to date, so they would always be tested on latest.

When a repo has too many PRs, it can became a bit troublesome to keep all the PRs up to date.

It basically does the same thing as pressing the following button:

![update-button](./.github/update-branch.png)

## Configuration

```yml
name: Up to date

on:
push:
branches:
- 'main'

jobs:
updatePullRequests:
runs-on: ubuntu-latest
steps:
- name: Update all the PRs
uses: paritytech/up-to-date-action@main
with:
GITHUB_TOKEN: ${{ secret.PAT }}
```
### Inputs
#### `GITHUB_TOKEN`
- Required
- Has to be a [Personal Access Token](https://github.com/settings/tokens/) with `repo` permissions.
- It can not be GitHub's action token because a push made by an action secret does not trigger new actions (and the new tests would not be triggered)
- [Related reading](https://github.com/orgs/community/discussions/25702#discussioncomment-3248819)

##### Using a GitHub app instead of a PAT
In some cases, specially in big organizations, it is more organized to use a GitHub app to authenticate, as it allows us to give it permissions per repository and we can fine-grain them even better. If you wish to do that, you need to create a GitHub app with the following permissions:
- Repository permissions:
- Pull Requests
- [x] Write
- Contents
- [x] Write

Because this project is intended to be used with a token we need to do an extra step to generate one from the GitHub app:
- After you create the app, copy the *App ID* and the *private key* and set them as secrets.
- Then you need to modify the workflow file to have an extra step:
```yml
steps:
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Update all the PRs
uses: paritytech/up-to-date-action@main
with:
# The previous step generates a token which is used as the input for this action
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
```

## Development
To work on this app, you require
- `Node 18.x`
- `yarn`

Use `yarn install` to set up the project.

`yarn build` compiles the TypeScript code to JavaScript.
8 changes: 4 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: "Example Action"
description: "This values need to be changed"
name: "Up to Date PRs"
description: "Keep all your PRs up to date when a new commit is pushed to the main branch"
author: Bullrich
branding:
icon: copy
color: yellow
icon: git-merge
color: gray-dark
inputs:
GITHUB_TOKEN:
required: true
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "parity-action-template",
"name": "up-to-date-action",
"version": "0.0.1",
"description": "GitHub action template for Parity",
"description": "Keep all your PRs up to date when a new commit is pushed to the main branch",
"main": "src/index.ts",
"scripts": {
"start": "node dist",
Expand All @@ -12,14 +12,14 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Bullrich/parity-action-template.git"
"url": "git+https://github.com/paritytech/up-to-date-action.git"
},
"author": "Javier Bullrich <javier@bullrich.dev>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Bullrich/parity-action-template/issues"
"url": "https://github.com/paritytech/up-to-date-action/issues"
},
"homepage": "https://github.com/Bullrich/parity-action-template#readme",
"homepage": "https://github.com/paritytech/up-to-date-action#readme",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
Expand Down
32 changes: 26 additions & 6 deletions src/github/pullRequest.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import { PullRequest } from "@octokit/webhooks-types";

import { ActionLogger, GitHubClient } from "./types";
import { GitHubClient } from "./types";

/** API class that uses the default token to access the data from the pull request and the repository */
export class PullRequestApi {
constructor(
private readonly api: GitHubClient,
private readonly logger: ActionLogger,
private readonly repo: { owner: string; repo: string },
) {}

getPrAuthor(pr: PullRequest): string {
return pr.user.login;
async listPRs(
onlyAutoMerge: boolean,
): Promise<{ number: number; title: string }[]> {
const openPRs = await this.api.paginate(this.api.rest.pulls.list, {
...this.repo,
state: "open",
});

if (onlyAutoMerge) {
return openPRs
.filter((pr) => pr.auto_merge)
.map(({ number, title }) => ({ number, title }) as const);
} else {
return openPRs.map(({ number, title }) => ({ number, title }) as const);
}
}

async update(number: number): Promise<string | undefined> {
const { data } = await this.api.rest.pulls.updateBranch({
...this.repo,
pull_number: number,
});

return data.message;
}
}
52 changes: 45 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getInput, info, setOutput } from "@actions/core";
import { getInput, setFailed, setOutput, summary } from "@actions/core";
import { SummaryTableRow } from "@actions/core/lib/summary";
import { context, getOctokit } from "@actions/github";
import { Context } from "@actions/github/lib/context";
import { PullRequest } from "@octokit/webhooks-types";

import { PullRequestApi } from "./github/pullRequest";
import { generateCoreLogger } from "./util";
Expand All @@ -23,10 +23,48 @@ const getRepo = (ctx: Context) => {
const repo = getRepo(context);

setOutput("repo", `${repo.owner}/${repo.repo}`);
const logger = generateCoreLogger();

if (context.payload.pull_request) {
const action = async () => {
const token = getInput("GITHUB_TOKEN", { required: true });
const api = new PullRequestApi(getOctokit(token), generateCoreLogger());
const author = api.getPrAuthor(context.payload.pull_request as PullRequest);
info("Author of the PR is " + author);
}
const repoInfo = getRepo(context);
const api = new PullRequestApi(getOctokit(token), repoInfo);
const prs = await api.listPRs(false);
if (prs.length > 0) {
logger.info(`About to update ${prs.length} PRs 🗄️`);
const rows: SummaryTableRow[] = [
[
{ data: "PR Number", header: true },
{ data: "Title", header: true },
{ data: "Result", header: true },
],
];
for (const { number, title } of prs.sort((a, b) => a.number - b.number)) {
logger.info(`📡 - Updating '${title}' #${number}`);
const repoTxt = `${repoInfo.owner}/${repoInfo.repo}#${number}`;
try {
await api.update(number);
rows.push([repoTxt, title, "Pass ✅"]);
logger.info(`📥 - Updated #${number}`);
} catch (error) {
logger.error(error as string | Error);
rows.push([repoTxt, title, "Fail ❌"]);
}
}
logger.info("🪄 - Finished updating PRs");
await summary
.addHeading("Up to date", 1)
.addHeading("PRs updated", 3)
.addTable(rows)
.write();
} else {
logger.info("No matching PRs found. Aborting");
summary.addHeading("Up to date", 1).addHeading("No matching PRs found");
}
};

action()
.then(() => {
logger.info("Operation completed");
})
.catch(setFailed);

0 comments on commit 83c26a5

Please sign in to comment.