Skip to content

Commit

Permalink
Merge pull request #10 from product-os/develop
Browse files Browse the repository at this point in the history
Post instructional comments for pending deployments
  • Loading branch information
klutchell authored Dec 3, 2024
2 parents d4591a6 + 4086bba commit 3a43053
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ LOG_LEVEL=debug

# Go to https://smee.io/new set this to the URL that you are redirected to.
WEBHOOK_PROXY_URL=

# A comma-separated list of GitHub user IDs to bypass the approval process.
# For users, you can find the ID by visiting `https://api.github.com/users/<username>`
# For apps, you can find the ID by visiting `https://api.github.com/users/<app-name>%5Bbot%5D`
BYPASS_ACTORS=
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ docker build -t deploynaut .
docker run -e APP_ID=<app-id> -e PRIVATE_KEY=<pem-value> deploynaut
```

## Environment Variables

- `BYPASS_ACTORS`: A comma-separated list of GitHub user IDs to bypass the approval process.

For users, you can find the ID by visiting `https://api.github.com/users/<username>`
For apps, you can find the ID by visiting `https://api.github.com/users/<app-name>%5Bbot%5D`

```shell
# https://api.github.com/users/flowzone-app%5Bbot%5D
# https://api.github.com/users/balena-renovate%5Bbot%5D
BYPASS_ACTORS=124931076,133977723
```

- `APP_ID`: The ID of the GitHub App.
- `PRIVATE_KEY`: The private key of the GitHub App.
- `WEBHOOK_SECRET`: The secret used to verify the authenticity of the webhook.
- `WEBHOOK_PROXY_URL`: The URL to proxy the webhook to.
- `LOG_LEVEL`: Defaults to `info`.

## Contributing

If you have suggestions for how deploynaut could be improved, or want to report a bug, open an issue! We'd love all and any contributions.
Expand Down
78 changes: 60 additions & 18 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,25 @@
// return deployments;
// }

export async function listPullRequestContributors(
context: any,
prNumber: number,
): Promise<string[]> {
const commits = await this.listPullRequestCommits(context, prNumber);
return commits.map((c: any) => c.author.id);
}
// export async function listPullRequestContributors(
// context: any,
// prNumber: number,
// ): Promise<string[]> {
// const commits = await this.listPullRequestCommits(context, prNumber);
// return commits.map((c: any) => c.author.id);
// }

export async function listPullRequestCommits(
context: any,
prNumber: number,
): Promise<any[]> {
const request = context.repo({
pull_number: prNumber,
});
const { data: commits } =
await context.octokit.rest.pulls.listPullRequestCommits(request);
return commits;
}
// export async function listPullRequestCommits(
// context: any,
// prNumber: number,
// ): Promise<any[]> {
// const request = context.repo({
// pull_number: prNumber,
// });
// const { data: commits } =
// await context.octokit.rest.pulls.listPullRequestCommits(request);
// return commits;
// }

// https://octokit.github.io/rest.js/v21/#pulls-list-reviews
// https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#list-reviews-for-a-pull-request
Expand Down Expand Up @@ -156,3 +156,45 @@ export async function reviewWorkflowRun(
await context.octokit.rest.actions.reviewCustomGatesForRun(request);
return review;
}

// https://octokit.github.io/rest.js/v18/#issues-list-comments
// https://docs.github.com/en/rest/issues/comments#list-issue-comments
export async function listIssueComments(
context: any,
issueNumber: number,
): Promise<any[]> {
const request = context.repo({
issue_number: issueNumber,
});
const { data: comments } =
await context.octokit.rest.issues.listComments(request);
return comments;
}

// https://octokit.github.io/rest.js/v18/#issues-create-comment
// https://docs.github.com/en/rest/issues/comments#create-an-issue-comment
export async function createIssueComment(
context: any,
issueNumber: number,
body: string,
): Promise<any> {
const request = context.repo({
issue_number: issueNumber,
body,
});
const { data: comment } =
await context.octokit.rest.issues.createComment(request);
return comment;
}

// // https://octokit.github.io/rest.js/v18/#issues-delete-comment
// // https://docs.github.com/en/rest/issues/comments#delete-an-issue-comment
// export async function deleteIssueComment(
// context: any,
// commentId: number,
// ): Promise<void> {
// const request = context.repo({
// comment_id: commentId,
// });
// await context.octokit.rest.issues.deleteComment(request);
// }
63 changes: 48 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type {
} from '@octokit/webhooks-types';
import * as GitHubClient from './client.js';

export const instructionalComment =
'One or more environments associated with this pull request require approval before deploying workflow runs.\n\n' +
'Maintainers can approve by submitting a [Review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/reviewing-proposed-changes-in-a-pull-request#submitting-your-review) with the comment `/deploy please`.';

export default (app: Probot) => {
app.on('deployment_protection_rule.requested', async (context: Context) => {
const {
Expand All @@ -31,25 +35,18 @@ export default (app: Probot) => {
},
};

context.log.info(
app.log.info(
'Received deployment protection rule event: %s',
JSON.stringify(eventDetails, null, 2),
);

if (!deployment || !event || !environment || !callbackUrl) {
context.log.error('Payload is missing required properties');
app.log.error('Payload is missing required properties');
return;
}

// app.log.info('Received Deployment Protection Rule with Deployment ID: %s', deployment.id);
// app.log.info(JSON.stringify(context.payload, null, 2));

// const client = await app.auth(); // Gets an authenticated Octokit client
// const { data: appDetails } = await client.apps.getAuthenticated(); // Retrieves details about the authenticated app
// // app.log.info(JSON.stringify(appDetails, null, 2)); // Logs details about the app

if (!['pull_request', 'pull_request_target', 'push'].includes(event)) {
context.log.info(
app.log.info(
'Ignoring unsupported deployment protection rule event: %s',
event,
);
Expand All @@ -65,11 +62,15 @@ export default (app: Probot) => {
});
}

context.log.debug(
app.log.debug(
'Actor is not included in bypass actors: %s',
deployment.creator.login,
);

const client = await app.auth(); // Gets an authenticated Octokit client
const { data: appDetails } = await client.apps.getAuthenticated(); // Retrieves details about the authenticated app
// app.log.info(JSON.stringify(appDetails, null, 2)); // Logs details about the app

if (pullRequests) {
for (const pull of pullRequests) {
// get all reviews for the pull request
Expand All @@ -93,6 +94,22 @@ export default (app: Probot) => {
comment: `Approved by ${deployReview.user.login} via [review](${deployReview.html_url})`,
});
}

const comments = await filterIssueComments(
context,
pull.number,
appDetails.id,
instructionalComment,
);

// Try to avoid creating duplicate comments but there will always be a race condition
if (comments.length === 0) {
await GitHubClient.createIssueComment(
context,
pull.number,
instructionalComment,
);
}
}
}
});
Expand All @@ -112,18 +129,18 @@ export default (app: Probot) => {
},
};

context.log.info(
app.log.info(
'Received pull request review event: %s',
JSON.stringify(eventDetails, null, 2),
);

if (review.user.type === 'Bot') {
context.log.info('Ignoring bot review');
app.log.info('Ignoring bot review');
return;
}

if (!review.body?.startsWith('/deploy')) {
context.log.info('Ignoring unsupported comment');
app.log.info('Ignoring unsupported comment');
return;
}

Expand All @@ -140,7 +157,7 @@ export default (app: Probot) => {
);

if (deployments.length === 0) {
context.log.info(
app.log.info(
'No pending deployments found for workflow run %s',
run.id,
);
Expand Down Expand Up @@ -169,3 +186,19 @@ export default (app: Probot) => {
// To get your app running against GitHub, see:
// https://probot.github.io/docs/development/
};

// Find existing comments that match the provided criteria
async function filterIssueComments(
context: any,
issueNumber: number,
appId: number,
startsWith: string,
): Promise<any> {
const comments = await GitHubClient.listIssueComments(context, issueNumber);
return comments.filter(
(c) =>
c.body.startsWith(startsWith) &&
c.performed_via_github_app.id === appId &&
c.created_at === c.updated_at,
);
}
Loading

0 comments on commit 3a43053

Please sign in to comment.