Skip to content

Commit 0c15b9a

Browse files
authored
Merge pull request #331 from github/fork-deployment-safety
Fork Deployment Safety 🔒
2 parents 6b64b4a + 1cd6d59 commit 0c15b9a

File tree

9 files changed

+319
-15
lines changed

9 files changed

+319
-15
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -626,23 +626,27 @@ What to see live examples of this Action in use?
626626

627627
Check out some of the links below to see how others are using this Action in their projects:
628628

629-
- [github/entitlements-config](https://github.com/github/entitlements-config/blob/076a1f0f9e8cc1f5acb8a0b8e133b0a1300c8191/.github/workflows/branch-deploy.yml)
629+
- [github/entitlements-config](https://github.com/github/entitlements-config/blob/a0ae9820dceaf4542335e4668316d049e466841a/.github/workflows/branch-deploy.yml)
630630
- [the-hideout/cloudflare](https://github.com/the-hideout/cloudflare/blob/3f3adedb729b9aba0cc324a161ad8ddd6f56141b/.github/workflows/branch-deploy.yml)
631631
- [the-hideout/tarkov-api](https://github.com/the-hideout/tarkov-api/blob/1677543951d5f2a848c2650eb3400178b8f9a55b/.github/workflows/branch-deploy.yml)
632-
- [the-hideout/stash](https://github.com/the-hideout/stash/blob/bbcf12425c63122bf1ddb5a0dff6e0eb9ad9939d/.github/workflows/branch-deploy.yml)
632+
- [the-hideout/stash](https://github.com/the-hideout/stash/blob/aef5a5f16b4fa6946d2eba107e7b92c5f6583c0d/.github/workflows/branch-deploy.yml)
633633
- [GrantBirki/blog](https://github.com/GrantBirki/blog/blob/559b9be5cc3eac923be5d7923ec9a0b50429ced2/.github/workflows/branch-deploy.yml)
634634

635635
> Are you using this Action in a cool new way? Open a pull request to this repo to have your workflow added to the list above!
636636

637637
## Suggestions 🌟
638638

639-
This section will cover a few suggestions that will help you when using this Action
639+
This section will cover a few suggestions and best practices that will help you when using this Action.
640640

641641
1. Suggest Updating Pull Request Branches - You should absolutely use this option when using the `branch-deploy` Action. This option can be found in your repository's `/settings` page
642-
643-
![branch-setting](https://user-images.githubusercontent.com/23362539/172939811-a8816db8-8e7c-404a-b12a-11ec5bc6e93d.png)
644-
642+
![update-pr-branches](./docs//assets/update-branch-setting.png)
645643
2. Enable Branch Protection Settings - It is always a good idea to enable branch protection settings for your repo, especially when using this Action
644+
1. Require Pull Request Reviews - Enforce that pull requests have approvals, code owner approvals, and dismiss stale pull request approvals upon new commits
645+
![use-pr-reviews](./docs/assets/pr-reviews.png)
646+
2. Add Required Status Checks - Enforce that certain CI checks must pass before a pull request can be merged
647+
![use-status-checks](./docs/assets/required-ci-checks.png)
648+
3. If you don't need to deploy PR forks (perhaps your project is internal and not open source), you can set the `allow_forks` input to `"false"` to prevent deployments from running on forks.
649+
4. You should **always** (unless you have a certain restriction) use the `sha` output variable over the `ref` output variable when deploying. It is more reliable for deployments, and safer from a security perspective. More details about using commit SHAs for deployments can be found [here](./docs/deploying-commit-SHAs.md).
646650

647651
## Alternate Command Syntax
648652

__tests__/functions/prechecks.test.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ beforeEach(() => {
155155
jest.spyOn(isOutdated, 'isOutdated').mockImplementation(() => {
156156
return {outdated: false, branch: 'test-branch'}
157157
})
158+
159+
jest.spyOn(isAdmin, 'isAdmin').mockImplementation(() => {
160+
return false
161+
})
158162
})
159163

160164
test('runs prechecks and finds that the IssueOps command is valid for a branch deployment', async () => {
@@ -482,6 +486,242 @@ test('runs prechecks and finds that the IssueOps command is valid for a branch d
482486
})
483487
})
484488

489+
test('runs prechecks and finds that the IssueOps command is valid for a branch deployment and is from a forked repository and the PR is approved but CI is failing and it is a noop', async () => {
490+
octokit.graphql = jest.fn().mockReturnValue({
491+
repository: {
492+
pullRequest: {
493+
reviewDecision: 'APPROVED',
494+
reviews: {
495+
totalCount: 4
496+
},
497+
commits: {
498+
nodes: [
499+
{
500+
commit: {
501+
oid: 'abcde12345',
502+
checkSuites: {
503+
totalCount: 8
504+
},
505+
statusCheckRollup: {
506+
state: 'FAILURE'
507+
}
508+
}
509+
}
510+
]
511+
}
512+
}
513+
}
514+
})
515+
octokit.rest.pulls.get = jest.fn().mockReturnValue({
516+
data: {
517+
head: {
518+
sha: 'abcde12345',
519+
ref: 'test-ref',
520+
label: 'test-repo:test-ref',
521+
repo: {
522+
fork: true
523+
}
524+
},
525+
base: {
526+
ref: 'base-ref'
527+
}
528+
},
529+
status: 200
530+
})
531+
532+
data.environmentObj.noop = true
533+
534+
expect(await prechecks(context, octokit, data)).toStrictEqual({
535+
message:
536+
'### ⚠️ Cannot proceed with deployment\n\n- reviewDecision: `APPROVED`\n- commitStatus: `FAILURE`\n\n> Your pull request is approved but CI checks are failing',
537+
status: false
538+
})
539+
})
540+
541+
test('runs prechecks and finds that the IssueOps command is a fork and does not require reviews so it proceeds but with a warning', async () => {
542+
octokit.graphql = jest.fn().mockReturnValue({
543+
repository: {
544+
pullRequest: {
545+
reviewDecision: null,
546+
reviews: {
547+
totalCount: 0
548+
},
549+
commits: {
550+
nodes: [
551+
{
552+
commit: {
553+
oid: 'abcde12345',
554+
checkSuites: {
555+
totalCount: 8
556+
},
557+
statusCheckRollup: {
558+
state: 'SUCCESS'
559+
}
560+
}
561+
}
562+
]
563+
}
564+
}
565+
}
566+
})
567+
octokit.rest.pulls.get = jest.fn().mockReturnValue({
568+
data: {
569+
head: {
570+
sha: 'abcde12345',
571+
ref: 'test-ref',
572+
label: 'test-repo:test-ref',
573+
repo: {
574+
fork: true
575+
}
576+
},
577+
base: {
578+
ref: 'base-ref'
579+
}
580+
},
581+
status: 200
582+
})
583+
584+
expect(await prechecks(context, octokit, data)).toStrictEqual({
585+
message:
586+
'🎛️ CI checks have been defined but required reviewers have not been defined',
587+
status: true,
588+
noopMode: false,
589+
ref: 'abcde12345',
590+
sha: 'abcde12345'
591+
})
592+
593+
expect(warningMock).toHaveBeenCalledWith(
594+
'🚨 pull request reviews are not enforced by this repository and this operation is being performed on a fork - this operation is dangerous! You should require reviews via branch protection settings (or rulesets) to ensure that the changes being deployed are the changes that you reviewed.'
595+
)
596+
})
597+
598+
test('runs prechecks and rejects a pull request from a forked repository because it does not have completed reviews', async () => {
599+
octokit.graphql = jest.fn().mockReturnValue({
600+
repository: {
601+
pullRequest: {
602+
reviewDecision: 'REVIEW_REQUIRED',
603+
reviews: {
604+
totalCount: 0
605+
},
606+
commits: {
607+
nodes: [
608+
{
609+
commit: {
610+
oid: 'abcde12345',
611+
checkSuites: {
612+
totalCount: 8
613+
},
614+
statusCheckRollup: {
615+
state: 'SUCCESS'
616+
}
617+
}
618+
}
619+
]
620+
}
621+
}
622+
}
623+
})
624+
octokit.rest.pulls.get = jest.fn().mockReturnValue({
625+
data: {
626+
head: {
627+
sha: 'abcde12345',
628+
ref: 'test-ref',
629+
label: 'test-repo:test-ref',
630+
repo: {
631+
fork: true
632+
}
633+
},
634+
base: {
635+
ref: 'base-ref'
636+
}
637+
},
638+
status: 200
639+
})
640+
641+
// Even admins cannot deploy from a forked repository without reviews
642+
jest.spyOn(isAdmin, 'isAdmin').mockImplementation(() => {
643+
return true
644+
})
645+
646+
// Even with skipReviews set, the PR is from a forked repository and must have reviews out of pure safety
647+
data.environment = 'staging'
648+
data.inputs.skipReviews = 'staging'
649+
650+
expect(await prechecks(context, octokit, data)).toStrictEqual({
651+
message:
652+
'### ⚠️ Cannot proceed with deployment\n\n- reviewDecision: `REVIEW_REQUIRED`\n\n> All deployments from forks **must** have the required reviews before they can proceed. Please ensure this PR has been reviewed and approved before trying again.',
653+
status: false
654+
})
655+
656+
expect(debugMock).toHaveBeenCalledWith(
657+
'rejecting deployment from fork without required reviews - noopMode: false'
658+
)
659+
})
660+
661+
test('runs prechecks and rejects a pull request from a forked repository because it does not have completed reviews (noop)', async () => {
662+
octokit.graphql = jest.fn().mockReturnValue({
663+
repository: {
664+
pullRequest: {
665+
reviewDecision: 'REVIEW_REQUIRED',
666+
reviews: {
667+
totalCount: 0
668+
},
669+
commits: {
670+
nodes: [
671+
{
672+
commit: {
673+
oid: 'abcde12345',
674+
checkSuites: {
675+
totalCount: 8
676+
},
677+
statusCheckRollup: {
678+
state: 'SUCCESS'
679+
}
680+
}
681+
}
682+
]
683+
}
684+
}
685+
}
686+
})
687+
octokit.rest.pulls.get = jest.fn().mockReturnValue({
688+
data: {
689+
head: {
690+
sha: 'abcde12345',
691+
ref: 'test-ref',
692+
label: 'test-repo:test-ref',
693+
repo: {
694+
fork: true
695+
}
696+
},
697+
base: {
698+
ref: 'base-ref'
699+
}
700+
},
701+
status: 200
702+
})
703+
704+
// Even admins cannot deploy from a forked repository without reviews
705+
jest.spyOn(isAdmin, 'isAdmin').mockImplementation(() => {
706+
return true
707+
})
708+
709+
// Even with skipReviews set, the PR is from a forked repository and must have reviews out of pure safety
710+
data.environment = 'staging'
711+
data.inputs.skipReviews = 'staging'
712+
data.environmentObj.noop = true
713+
714+
expect(await prechecks(context, octokit, data)).toStrictEqual({
715+
message:
716+
'### ⚠️ Cannot proceed with deployment\n\n- reviewDecision: `REVIEW_REQUIRED`\n\n> All deployments from forks **must** have the required reviews before they can proceed. Please ensure this PR has been reviewed and approved before trying again.',
717+
status: false
718+
})
719+
720+
expect(debugMock).toHaveBeenCalledWith(
721+
'rejecting deployment from fork without required reviews - noopMode: true'
722+
)
723+
})
724+
485725
test('runs prechecks and finds that the IssueOps command is on a PR from a forked repo and is not allowed', async () => {
486726
octokit.graphql = jest.fn().mockReturnValue({
487727
repository: {
@@ -1593,6 +1833,10 @@ test('runs prechecks and finds that no CI checks exist but reviews are defined a
15931833
}
15941834
})
15951835

1836+
jest.spyOn(isAdmin, 'isAdmin').mockImplementation(() => {
1837+
return true
1838+
})
1839+
15961840
expect(await prechecks(context, octokit, data)).toStrictEqual({
15971841
message:
15981842
'✅ CI checks have not been defined and approval is bypassed due to admin rights',
@@ -1628,6 +1872,10 @@ test('runs prechecks and finds that no CI checks exist and the PR is not approve
16281872
}
16291873
})
16301874

1875+
jest.spyOn(isAdmin, 'isAdmin').mockImplementation(() => {
1876+
return true
1877+
})
1878+
16311879
expect(await prechecks(context, octokit, data)).toStrictEqual({
16321880
message:
16331881
'✅ CI checks have not been defined and approval is bypassed due to admin rights',

dist/index.js

Lines changed: 28 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/assets/pr-reviews.png

69.8 KB
Loading
26.9 KB
Loading

docs/deploying-commit-SHAs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Do this:
3232
3333
This ensures you are deploying the __exact__ commit SHA that branch-deploy has determined is safe to deploy. This is a best practice for security, reliability, and safety during deployments.
3434
35+
Don't worry, this is still a _branch deployment_, you are just telling your deployment process to use the __exact commit SHA__ that the branch points to rather than the branch name itself which is mutable.
36+
3537
## Introduction
3638
3739
Deploying commit SHAs (Secure Hash Algorithms) is a best practice in software development and deployment processes. This document explains the importance of deploying commit SHAs, focusing on aspects of security, reliability, and safety. It also provides an overview of how commit SHAs work under the hood in Git and how this contributes to the overall safety of the deployment process.

0 commit comments

Comments
 (0)