Skip to content

Commit b5fbdb6

Browse files
ci: add scripts for trusting PRs from forks to allow CI to run (#1512)
## Please verify the following: - [x] `yarn build-and-test:local` passes - [x] I have added tests for any new features, if relevant - [x] `README.md` (or relevant documentation) has been updated with your changes ## Describe your PR ### Summary * adds the `ci:trust` script and related docs. to the local repo * CircleCI Changes: * add the "trust-check" step, to ensure CI doesn't run on untrusted forks * Adds github workflows to: * push PR changes to a trusted branch then clean up automatically after 2h * delete all temp trusted branches with matching branch names ### Notes: * Based on [this article](https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/), this config prevents the build-docs action from running on untrusted forks. * CI will check if a branch is trusted before running CI * Once a branch is trusted, then CI will run * The github workflow to create a temp branch will automatically clean up the branch after a couple hours, and a separate action is provided to manually delete all temp branches in case any get left behind. * the `ci:trust` script will need to be cleaned up manually for now -- can maybe automate that with a cron job in github workflows --------- Co-authored-by: Joshua Yoes <37849890+joshuayoes@users.noreply.github.com>
1 parent 9312864 commit b5fbdb6

File tree

9 files changed

+292
-10
lines changed

9 files changed

+292
-10
lines changed

.circleci/config.yml

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ parameters:
1212
# Anchors define reusable sections of YAML
1313
# See: https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/
1414
anchors:
15-
# Defaults passed to all jobs
15+
16+
### Executors
17+
node_executor: &node_executor
18+
name: node/default
19+
resource_class: large
20+
# Default Settings
1621
defaults: &defaults
1722
working_directory: ~/repo
1823
# Branch names used to trigger releases
@@ -198,9 +203,7 @@ commands:
198203
jobs:
199204
build_and_test:
200205
<<: *defaults
201-
executor: &node_executor
202-
name: node/default
203-
resource_class: large
206+
executor: *node_executor
204207
steps:
205208
- checkout
206209
- install-packages
@@ -222,6 +225,24 @@ jobs:
222225
- run:
223226
name: Typecheck
224227
command: yarn typecheck
228+
trust_check:
229+
executor: *node_executor
230+
steps:
231+
- run:
232+
name: Check if branch is trusted
233+
command: |
234+
if [ -n "$CIRCLE_PR_NUMBER" ]; then
235+
if [[ $CIRCLE_BRANCH == temp-ci-trusted-fork-* ]]; then
236+
echo "Branch is trusted"
237+
exit 0
238+
else
239+
echo "Branch is not trusted. Please use the trust process before merging."
240+
exit 1
241+
fi
242+
else
243+
echo "Not a PR from a fork, skipping trust check"
244+
exit 0
245+
fi
225246
226247
release_tags:
227248
<<: *defaults
@@ -259,6 +280,7 @@ jobs:
259280
yarn release:artifacts $CIRCLE_TAG
260281
261282
build_app_windows:
283+
executor: *node_executor
262284
<<: *defaults
263285
docker:
264286
- image: electronuserland/builder:20-wine
@@ -329,24 +351,30 @@ jobs:
329351
command: yarn workspace reactotron-app release:artifacts $CIRCLE_TAG
330352

331353
workflows:
332-
pull_request:
333-
# prevents the workflow from running when `force-publish-docs` is true
354+
pull_request: # prevents the workflow from running when `force-publish-docs` is true
334355
when:
335356
and:
336357
- not: << pipeline.parameters.force-publish-docs >>
337358
- true # Placeholder for correct YAML structure
338359
jobs:
360+
- trust_check:
361+
filters:
362+
branches:
363+
ignore: *release_branch_names
339364
- build_and_test:
365+
requires:
366+
- trust_check
340367
filters:
341368
branches:
342369
ignore: *release_branch_names
343370
- publish-docs/build_docs:
371+
requires:
372+
- trust_check
344373
<<: *ir_docs_config
345374
filters:
346375
branches:
347376
ignore: *release_branch_names
348377

349-
350378
# Allows for manual publishing of docs independent of deployment
351379
# Use the 'trigger pipeline' button in circle ci and set 'force-publish-docs' to 'true'
352380
force_publish_docs:
@@ -355,8 +383,7 @@ workflows:
355383
- publish-docs/publish_docs:
356384
<<: *ir_docs_config
357385

358-
release:
359-
# prevents the workflow from running when `force-publish-docs` is true
386+
release: # prevents the workflow from running when `force-publish-docs` is true
360387
when:
361388
and:
362389
- not: << pipeline.parameters.force-publish-docs >>
@@ -374,11 +401,19 @@ workflows:
374401
only: *release_branch_names
375402
requires:
376403
- build_and_test
404+
- publish-docs/build_docs:
405+
<<: *ir_docs_config
406+
filters:
407+
branches:
408+
only:
409+
- beta
410+
- alpha
377411
- publish-docs/publish_docs:
378412
<<: *ir_docs_config
379413
filters:
380414
branches:
381-
only: *release_branch_names
415+
only:
416+
- master
382417
- release_package:
383418
context:
384419
- infinitered-npm-package
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Cleanup All Temp Trusted Branches
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
delete-branches:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Checkout repository
11+
uses: actions/checkout@v2
12+
with:
13+
fetch-depth: 0
14+
15+
- name: Setup Git
16+
run: |
17+
git config user.name github-actions
18+
git config user.email github-actions@github.com
19+
20+
- name: Find branches to delete
21+
run: |
22+
# Fetch all branches
23+
git fetch --all
24+
25+
# Find branches starting with 'temp-ci-trusted-fork-'
26+
branches_to_delete=$(git branch -r | grep 'origin/temp-ci-trusted-fork-' | sed 's|origin/||')
27+
28+
if [ -z "$branches_to_delete" ]; then
29+
echo "No branches found to delete."
30+
exit 0
31+
fi
32+
33+
echo "Branches to delete:"
34+
echo "$branches_to_delete"
35+
36+
# Delete each branch
37+
for branch in $branches_to_delete; do
38+
echo "Deleting branch: $branch"
39+
git push origin --delete "$branch"
40+
done
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Cleanup Trusted Branch
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
branch_to_delete:
7+
description: 'Branch to delete'
8+
required: true
9+
delete_after:
10+
description: 'Delete after (ISO 8601 date)'
11+
required: true
12+
13+
jobs:
14+
cleanup:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Check if it's time to delete
18+
id: check_time
19+
run: |
20+
current_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
21+
if [[ "$current_time" > "${{ github.event.inputs.delete_after }}" ]]; then
22+
echo "::set-output name=should_delete::true"
23+
else
24+
echo "::set-output name=should_delete::false"
25+
fi
26+
27+
- name: Delete branch
28+
if: steps.check_time.outputs.should_delete == 'true'
29+
uses: actions/github-script@v6
30+
with:
31+
github-token: ${{secrets.GITHUB_TOKEN}}
32+
script: |
33+
github.rest.git.deleteRef({
34+
owner: context.repo.owner,
35+
repo: context.repo.repo,
36+
ref: 'heads/${{ github.event.inputs.branch_to_delete }}'
37+
});

.github/workflows/trust-fork-pr.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Trust Fork PR
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
pr_number:
7+
description: 'PR number to trust'
8+
required: true
9+
10+
jobs:
11+
trust-fork:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Generate unique identifier
15+
id: gen_uid
16+
run: echo "::set-output name=uid::$(date +%s)-${{ github.run_id }}"
17+
18+
- name: Checkout repository
19+
uses: actions/checkout@v2
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Setup Git
24+
run: |
25+
git config user.name github-actions
26+
git config user.email github-actions@github.com
27+
28+
- name: Fetch PR details
29+
id: pr
30+
uses: actions/github-script@v6
31+
with:
32+
github-token: ${{ secrets.GITHUB_TOKEN }}
33+
script: |
34+
const pr = await github.rest.pulls.get({
35+
owner: context.repo.owner,
36+
repo: context.repo.repo,
37+
pull_number: parseInt(context.payload.inputs.pr_number)
38+
});
39+
core.setOutput('head_repo', pr.data.head.repo.full_name);
40+
core.setOutput('head_branch', pr.data.head.ref);
41+
42+
- name: Fetch and push to trusted branch
43+
env:
44+
FORK_REPO: ${{ steps.pr.outputs.head_repo }}
45+
FORK_BRANCH: ${{ steps.pr.outputs.head_branch }}
46+
UNIQUE_ID: ${{ steps.gen_uid.outputs.uid }}
47+
run: |
48+
git remote add fork https://github.com/${FORK_REPO}.git
49+
git fetch fork ${FORK_BRANCH}
50+
git push origin fork/${FORK_BRANCH}:temp-ci-trusted-fork-${UNIQUE_ID} --force
51+
52+
- name: Cleanup
53+
if: always()
54+
run: |
55+
git remote remove fork
56+
57+
- name: Schedule branch deletion
58+
if: success()
59+
env:
60+
UNIQUE_ID: ${{ steps.gen_uid.outputs.uid }}
61+
uses: actions/github-script@v6
62+
with:
63+
github-token: ${{ secrets.GITHUB_TOKEN }}
64+
script: |
65+
const deleteDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 hours from now
66+
github.rest.actions.createWorkflowDispatch({
67+
owner: context.repo.owner,
68+
repo: context.repo.repo,
69+
workflow_id: 'trust-fork-pr-cleanup.yml',
70+
ref: 'main',
71+
inputs: {
72+
branch_to_delete: `temp-ci-trusted-fork-${process.env.UNIQUE_ID}`,
73+
delete_after: deleteDate
74+
}
75+
});

docs/contributing/ci.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
name: Running Tests on Untrusted Forks
3+
sidebar_position: 99
4+
---
5+
6+
# Running CI Scripts on Untrusted Forks
7+
8+
Untrusted forks could contain malicious code to mine cryptocurrency, steal secrets, or otherwise harm the CI server.
9+
10+
For PRs from untrusted forks, to run the CI scripts, we need to:
11+
12+
1. Review the code to ensure that it is safe to run on the CI server.
13+
2. If the code is safe, run the `ci:trust` script to push the commits to a branch on the main repository, where the CI scripts can be run.
14+
3. Once the tests have run, the status of the PR will be updated automatically (because the commits are the same).
15+
16+
## How to run the CI scripts on untrusted forks:
17+
18+
1. Copy the name of the branch from the PR.
19+
<img src="./images/ci-copy-fork-branch.png" alt="ci-copy-fork-branch" width="400"/>
20+
2. From your local clone of the main repository, run the `ci:trust` script.
21+
```bash
22+
yarn ci:trust <branch-name>
23+
```
24+
3. The branch will be pushed and the tests will run
25+
<img src="./images/ci-tests-running.png" alt="ci-tests-running" width="400"/>
26+
27+
## What does ci:trust do?
28+
29+
The `ci:trust` script does the following:
30+
31+
1. Adds and fetches the untrusted fork as a temporary remote in your local repository.
32+
2. Pushes the specific branch from the untrusted fork to a designated temporary branch in your original repository.
33+
3. Pushing to a local branch triggers the continuous integration (CI) tests on the commits of the branch.
34+
4. Because the commits are the same, the status of the PR will be updated automatically.
35+
36+
### Notes
37+
38+
1. The ci:trust script will only work if you have write access to the main repository. This prevents malicious users from running the script on the main repository.
39+
2. The ci:trust script pushes the commits to a branch called `temp-ci-trusted-fork`.
40+
41+
::: warning
42+
43+
The `temp-ci-trusted-fork` branch will be deleted and recreated if it already exists. This allows the script to
44+
clean up its own temporary branches.
45+
46+
:::
Loading
43.4 KB
Loading

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"lint": "npx nx run-many --target lint",
6767
"test": "npx nx run-many --target test",
6868
"ci:test": "npx nx run-many --target ci:test",
69+
"ci:trust": "sh ./scripts/git-clone-fork-to-trusted-branch.sh",
6970
"typecheck": "npx nx run-many --target typecheck",
7071
"format:write": "npx nx run-many --target format:write",
7172
"format:check": "npx nx run-many --target format:check",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
3+
################################################################################
4+
# This script is used to clone a forked repository and push a branch to a
5+
# trusted branch in the reactotron repository, so the CI scripts can.run
6+
# tests on the forked branch. This prevents unreviewed code from being run
7+
# in the CI pipeline.
8+
#
9+
# Usage: ./scripts/git-clone-fork-to-trusted-branch.sh <fork_username>:<fork_branchname>
10+
#
11+
# Example: ./scripts/git-clone-fork-to-trusted-branch.sh "infinitered:temp-branch-to-test-fork"
12+
#
13+
###############################################################################
14+
15+
set -eo pipefail
16+
17+
: "${GPF_REACTOTRON_BRANCH:=temp-ci-trusted-fork}"
18+
19+
REACTOTRON_REPO="git@github.com:infinitered/reactotron.git"
20+
BRANCH_SPEC=$1
21+
NUM_COLONS=$(echo "$BRANCH_SPEC" | awk -F: '{print NF-1}')
22+
23+
if [ "$#" -ne 1 ] || [ "$NUM_COLONS" -ne 1 ] ; then
24+
echo "Usage: <fork_username>:<fork_branchname>"
25+
exit 1
26+
fi
27+
28+
SOURCE_GH_USER=$(echo "$BRANCH_SPEC" | awk -F: '{print $1}')
29+
SOURCE_BRANCH=$(echo "$BRANCH_SPEC" | awk -F: '{print $2}')
30+
REPO_NAME=$(git remote get-url --push origin | awk -F/ '{print $NF}' | sed 's/\.git$//')
31+
32+
# Check if 'fork-to-test' remote exists and then remove it
33+
if git config --get "remote.fork-to-test.url" > /dev/null; then
34+
git remote remove fork-to-test
35+
echo "Removed remote fork-to-test"
36+
else
37+
echo "Remote fork-to-test does not exist, no need to remove it"
38+
fi
39+
40+
git remote add fork-to-test "git@github.com:$SOURCE_GH_USER/$REPO_NAME.git"
41+
42+
git fetch --all
43+
git push --force "$REACTOTRON_REPO" "refs/remotes/fork-to-test/$SOURCE_BRANCH:refs/heads/$GPF_REACTOTRON_BRANCH"
44+
git remote remove fork-to-test || echo "Removed new remote fork-to-test"
45+
46+
cat <<EOF
47+
Forked branch '$BRANCH_SPEC' has been pushed to branch '$GPF_REACTOTRON_BRANCH'
48+
EOF

0 commit comments

Comments
 (0)