Skip to content

Commit cdb4403

Browse files
authored
ci: git-crypt unlock on trusted runs (#122)
PR #83 changed many test cases, but CI can skip running them because `tests_private/` is encrypted and we remove encrypted files on untrusted runs. This PR adds a trust gate so CI can unlock git-crypt (and run private tests) only on trusted events, while still ensuring encrypted files are removed on untrusted PRs. It also runs `pwnshop test --silent-failures` in CI to avoid printing failing test output into the job log.
1 parent 7af76f1 commit cdb4403

4 files changed

Lines changed: 64 additions & 6 deletions

File tree

.github/actions/test-challenges/action.yml

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ inputs:
88
description: Base ref challenges have been modified since.
99
required: false
1010
default: ""
11+
gpg_private_key:
12+
description: GPG private key used to unlock git-crypt.
13+
required: false
14+
default: ""
1115
runs:
1216
using: composite
1317
steps:
@@ -16,10 +20,31 @@ runs:
1620
with:
1721
fetch-depth: 0
1822

19-
- name: Remove encrypted directories
23+
- name: Manage encrypted files
2024
shell: 'nix develop -c bash -euo pipefail {0}'
25+
env:
26+
GPG_PRIVATE_KEY: ${{ inputs.gpg_private_key }}
27+
CHALLENGES_DIR: challenges/${{ inputs.challenges }}
2128
run: |
22-
git crypt status -e | sed -e "s/ *encrypted: //" | xargs -r dirname | xargs -r rm -rf
29+
encrypted_paths() {
30+
git crypt status -e \
31+
| sed -n 's/^[[:space:]]*encrypted:[[:space:]]*//p' \
32+
| sort -u
33+
}
34+
35+
if [ -n "$GPG_PRIVATE_KEY" ]; then
36+
encrypted_paths | while IFS= read -r encrypted_path; do
37+
case "$encrypted_path" in
38+
"$CHALLENGES_DIR"/*) ;;
39+
*) rm -f "$encrypted_path" ;;
40+
esac
41+
done
42+
43+
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
44+
git crypt unlock
45+
else
46+
encrypted_paths | xargs -r rm -f
47+
fi
2348
2449
- name: Test challenges
2550
shell: 'nix develop -c bash -euo pipefail {0}'
@@ -36,6 +61,7 @@ runs:
3661
echo "::group::Challenges to be tested"
3762
./pwnshop list "$CHALLENGES_DIR" --modified-since="$MODIFIED_SINCE"
3863
echo "::endgroup::"
64+
3965
echo "::group::Testing challenges"
40-
./pwnshop test --jobs "$JOBS" --modified-since="$MODIFIED_SINCE" "$CHALLENGES_DIR"
66+
./pwnshop test --silent-failures --jobs "$JOBS" --modified-since="$MODIFIED_SINCE" "$CHALLENGES_DIR"
4167
echo "::endgroup::"

.github/workflows/publish-challenges.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ jobs:
4646
with:
4747
challenges: ${{ matrix.challenges }}
4848
modified_since_ref: ""
49+
gpg_private_key: ${{ secrets.GPG_ROOT_PRIVATE_KEY }}
4950

5051
- name: Install rclone
5152
run: sudo apt-get update && sudo apt-get install -y rclone

.github/workflows/test-challenges.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,39 @@ jobs:
3737

3838
steps:
3939
- uses: actions/checkout@v4
40+
41+
- name: Determine trust
42+
id: trust
43+
uses: actions/github-script@v7
44+
with:
45+
script: |
46+
// Trusted contexts:
47+
// - Non-PR events (workflow_dispatch/schedule) are trusted.
48+
// - PRs from the main repo (not a fork) are trusted; fork PRs are not.
49+
let trusted = false;
50+
if (context.eventName === "workflow_dispatch" || context.eventName === "schedule") {
51+
trusted = true;
52+
} else if (context.eventName === "pull_request") {
53+
const pr = context.payload.pull_request;
54+
const headRepo = pr?.head?.repo?.full_name;
55+
const baseRepo = pr?.base?.repo?.full_name;
56+
if (headRepo && baseRepo && headRepo === baseRepo) trusted = true;
57+
}
58+
core.setOutput("trusted", trusted ? "true" : "false");
59+
4060
- uses: ./.github/actions/setup-nix
4161
- uses: ./.github/actions/setup-challenge-runtime
4262

43-
- name: Test challenges
63+
- name: Test challenges (trusted)
64+
if: ${{ steps.trust.outputs.trusted == 'true' }}
65+
uses: ./.github/actions/test-challenges
66+
with:
67+
challenges: ${{ matrix.challenges }}
68+
modified_since_ref: ${{ needs.determine-modified-challenges.outputs.modified_since_ref }}
69+
gpg_private_key: ${{ secrets.GPG_ROOT_PRIVATE_KEY }}
70+
71+
- name: Test challenges (untrusted)
72+
if: ${{ steps.trust.outputs.trusted != 'true' }}
4473
uses: ./.github/actions/test-challenges
4574
with:
4675
challenges: ${{ matrix.challenges }}

tools/pwnshop/src/pwnshop/commands/test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
type=click.Path(path_type=pathlib.Path, file_okay=False, resolve_path=True),
4040
help="Write failure output to DIR/challenge/test.log files.",
4141
)
42+
@click.option("--silent-failures", is_flag=True, help="Do not print failing test output to stdout/stderr.")
4243
@click.argument(
4344
"targets",
4445
nargs=-1,
@@ -51,13 +52,14 @@
5152
resolve_path=False,
5253
),
5354
)
54-
def test_command(targets, modified_since, jobs, require_tests, test_timeout, log_failures):
55+
def test_command(targets, modified_since, jobs, require_tests, test_timeout, log_failures, silent_failures):
5556
"""Test one or more challenges."""
5657
if not (challenge_paths := lib.resolve_targets(targets, modified_since=modified_since)):
5758
if modified_since:
5859
console.print(f"[yellow]No challenges found since {modified_since}[/]")
5960
return
6061
raise click.ClickException("No challenges found in provided targets.")
62+
6163
jobs = jobs or os.cpu_count() or 1
6264
failed: dict[pathlib.Path, list] = {}
6365
passed_count = failed_count = total_tests = failed_tests = 0
@@ -145,7 +147,7 @@ def test_challenge(challenge_path):
145147
log_file = log_failures / challenge / f"{test_path}.log"
146148
log_file.parent.mkdir(parents=True, exist_ok=True)
147149
log_file.write_text(output)
148-
else:
150+
elif not silent_failures:
149151
console.print(output.rstrip("\n"), markup=False)
150152
if challenge in failed:
151153
failed_count += 1

0 commit comments

Comments
 (0)