Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions .github/scripts/check-deploy-permissions.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import type { AsyncFunctionArguments } from 'github-script';
import type { PullRequestEvent, PullRequestReviewEvent } from '@octokit/webhooks-types';
import type { PullRequestEvent, IssueCommentEvent } from '@octokit/webhooks-types';

export default async function checkDeployPermissions({ core, context }: AsyncFunctionArguments) {
if (context.eventName === 'pull_request_review') {
const event = context.payload as PullRequestReviewEvent;
const reviewerAssociation = event.review.author_association;
export default async function checkDeployPermissions({ core, context, github }: AsyncFunctionArguments) {
if (context.eventName === 'issue_comment') {
const event = context.payload as IssueCommentEvent;

if (!isAllowedAuthor(reviewerAssociation)) {
await skipDeployment(core, 'Not authorized to trigger deployments.');
if (!event.issue.pull_request) {
core.setOutput('should-deploy', 'false');
core.info('Comment is not on a pull request');
return;
}

if (event.comment.body?.trim() !== 'ok-to-deploy') {
core.setOutput('should-deploy', 'false');
core.info('Comment is not the deployment command');
return;
}

if (event.review.body === 'ok-to-deploy') {
core.setOutput('should-deploy', 'true');
core.info('Deployment allowed: Triggered by maintainer review comment');
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: event.comment.user.login
});

const isAuthorized = ['admin', 'maintain', 'write'].includes(permission.permission);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use isAllowedAuthor somehow? Maybe with a different API call?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we fetch the comment by ID, can get author_association
https://api.github.com/repos/interledger/publisher-tools/issues/comments/3799339719


if (!isAuthorized) {
await skipDeployment(core, 'Not authorized to trigger deployments.');
return;
}

core.setOutput('should-deploy', 'false');
core.info('No deployment command found in review');
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: event.issue.number
});

core.setOutput('should-deploy', 'true');
core.setOutput('pr-number', pr.number);
core.setOutput('pr-head-sha', pr.head.sha);
core.setOutput('pr-base-sha', pr.base.sha);
core.info(`Deployment allowed: "ok-to-deploy" comment from authorized user @${event.comment.user.login}`);
return;
}

Expand All @@ -29,7 +51,7 @@ export default async function checkDeployPermissions({ core, context }: AsyncFun
if (!isAllowedAuthor(authorAssociation)) {
await skipDeployment(
core,
'The PR author is not authorized to run deployments. Maintainers can trigger a deployment by submitting a review with "pull-request-review" in the comment.'
'The PR author is not authorized to run deployments. Maintainers can trigger a deployment by commenting "ok-to-deploy" on the pull request.'
);
return;
}
Expand All @@ -39,6 +61,12 @@ export default async function checkDeployPermissions({ core, context }: AsyncFun
return;
}

if (context.eventName === 'push') {
core.setOutput('should-deploy', 'true');
core.info('Deployment allowed: Push to protected branch');
return;
}

// no deployment for other events
core.setOutput('should-deploy', 'false');
core.info('Deployment not triggered for this event type');
Expand All @@ -61,7 +89,7 @@ async function skipDeployment(coreApi: AsyncFunctionArguments['core'], reason: s
'Security Notice',
`Deployments are restricted to organization members, collaborators, and repository owners.
External contributors can still run builds and tests.
Maintainers can trigger deployments by reviewing the PR with "pull-request-review" in the comment.`
Maintainers can trigger deployments by adding a regular PR comment with "ok-to-deploy" exactly.`
)
.write();
}
68 changes: 44 additions & 24 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,57 @@ on:
pull_request:
branches: [main]
paths-ignore: ['**.md']
pull_request_review:
types: [submitted]
branches: [main]
issue_comment:
types: [created]

defaults:
run:
shell: bash

permissions:
pull-requests: write
issues: read
contents: read

jobs:
deploy:
name: Deploy
check-permissions:
name: Check Permissions
runs-on: ubuntu-latest
if: |
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request)
outputs:
should-deploy: ${{ steps.check.outputs.should-deploy }}
pr-number: ${{ steps.check.outputs.pr-number }}
pr-head-sha: ${{ steps.check.outputs.pr-head-sha }}
pr-base-sha: ${{ steps.check.outputs.pr-base-sha }}
steps:
- uses: actions/checkout@v6
- name: Checkout base branch for security check
uses: actions/checkout@v6
with:
# Ensures we checkout the PR head commit for both pull_request and pull_request_review events
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# Fetches the full git history, both base and head SHAs are available
fetch-depth: 0
fetch-depth: 1
Comment on lines 37 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use defaults?


- name: Check deployment permissions
id: deploy-check
id: check
uses: actions/github-script@v8
with:
script: |
const script = await import('${{ github.workspace }}/.github/scripts/check-deploy-permissions.ts');
await script.default({ core, context });

await script.default({ core, context, github });

deploy:
name: Deploy
runs-on: ubuntu-latest
needs: check-permissions
if: needs.check-permissions.outputs.should-deploy == 'true'
steps:
# warning: untrusted code, checkout the PR head only after auth check passed
- uses: actions/checkout@v6
with:
ref: ${{ needs.check-permissions.outputs.pr-head-sha || github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0

- uses: ./.github/workflows/setup

- name: Install wrangler globally
Expand All @@ -48,7 +68,7 @@ jobs:

# https://github.com/dorny/paths-filter/issues/232
- uses: dorny/paths-filter@v3
if: github.event_name != 'pull_request_review'
if: github.event_name != 'issue_comment'
id: changes
with:
filters: |
Expand All @@ -70,13 +90,13 @@ jobs:
- *cdn
- 'frontend/**'

- name: Detect changes for review-triggered deployment
if: github.event_name == 'pull_request_review'
- name: Detect changes for issue_comment deployment
if: github.event_name == 'issue_comment'
id: changes-review
run: |
# Get the PR base and head SHAs
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
# Get the PR base and head SHAs from the check-permissions job outputs
BASE_SHA=${{ needs.check-permissions.outputs.pr-base-sha }}
HEAD_SHA=${{ needs.check-permissions.outputs.pr-head-sha }}

# Get list of changed files in the PR
CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)
Expand Down Expand Up @@ -135,7 +155,7 @@ jobs:
name: 'api'
changed: ${{ steps.changes.outputs.api || steps.changes-review.outputs.api }}
mode: ${{ steps.deploy-mode.outputs.mode }}
skip-deploy: ${{ steps.deploy-check.outputs.should-deploy == 'false' }}
skip-deploy: 'false'
BUILD_AWS_PREFIX: ${{ vars.AWS_PREFIX }}
stagingUrl: ${{ vars.API_STAGING_URL }}
productionUrl: ${{ vars.API_PRODUCTION_URL }}
Expand All @@ -149,7 +169,7 @@ jobs:
name: 'cdn'
changed: ${{ steps.changes.outputs.cdn || steps.changes-review.outputs.cdn }}
mode: ${{ steps.deploy-mode.outputs.mode }}
skip-deploy: ${{ steps.deploy-check.outputs.should-deploy == 'false' }}
skip-deploy: 'false'
build: 'pnpm -C cdn run build'
BUILD_API_URL: ${{ steps.api.outputs.url }}
BUILD_AWS_PREFIX: ${{ vars.AWS_PREFIX }}
Expand All @@ -165,7 +185,7 @@ jobs:
name: 'frontend'
changed: ${{ steps.changes.outputs.frontend || steps.changes-review.outputs.frontend }}
mode: ${{ steps.deploy-mode.outputs.mode }}
skip-deploy: ${{ steps.deploy-check.outputs.should-deploy == 'false' }}
skip-deploy: 'false'
build: 'pnpm -C frontend run build'
BUILD_API_URL: ${{ steps.api.outputs.url }}
BUILD_CDN_URL: ${{ steps.cdn.outputs.url }}
Expand Down Expand Up @@ -222,11 +242,11 @@ jobs:
}

- name: Create deploy comment
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'issue_comment'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.pull_request.number || needs.check-permissions.outputs.pr-number }}
body-includes: '<!-- worker-deploy-summary -->'
comment-author: 'github-actions[bot]'
body: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pnpm -C localenv/s3 dev

### How to Run Preview Changes

For a pull request, **external contributors** (those without write access to the repository), deployment previews are not automatically. However, user with write access to repository can trigger the workflow **preview deployments** by adding a review-comment with body `ok-to-deploy` exactly.
For a pull request, **external contributors** (those without write access to the repository), deployment previews are not triggered automatically. However, users with write access to the repository can trigger **preview deployments** by adding a regular PR comment with `ok-to-deploy` exactly.
This will trigger the deploy workflow and create preview environments for the PR.

## Technology Stack
Expand Down