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
29 changes: 29 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Code Quality

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
pwnshop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: tools/pwnshop/**

- name: Install Ruff
run: |
uv tool install --force ruff
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Check pwnshop
working-directory: tools/pwnshop
run: |
ruff check
ruff format --check --diff
41 changes: 24 additions & 17 deletions .github/workflows/test-challenges.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: "{pwnshop,src/pwnshop/**}"
cache-dependency-glob: tools/pwnshop/**

- name: Get challenges to test
id: changed-challenges
Expand All @@ -39,8 +39,8 @@ jobs:

if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
echo "Collecting all challenges (triggered by ${{ github.event_name }})"
TABLE=$(./pwnshop list --format table)
MATRIX=$(./pwnshop list --format matrix)
TABLE=$(./pwnshop list ./challenges --format table)
MATRIX=$(./pwnshop list ./challenges --format matrix)
else
if [ "${{ github.event_name }}" = "pull_request" ]; then
REF="${{ github.event.pull_request.base.sha }}"
Expand All @@ -53,8 +53,8 @@ jobs:
fi

echo "Collecting challenges modified since $REF"
TABLE=$(./pwnshop list --modified-since "$REF" --format table)
MATRIX=$(./pwnshop list --modified-since "$REF" --format matrix)
TABLE=$(./pwnshop list ./challenges --modified-since "$REF" --format table)
MATRIX=$(./pwnshop list ./challenges --modified-since "$REF" --format matrix)
fi

echo "Challenges to test:"
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: build
cache-dependency-glob: tools/pwnshop/**

- name: Set up Docker storage
run: |
Expand Down Expand Up @@ -118,25 +118,32 @@ jobs:
import os
import sys

challenges = os.environ["GROUP_CHALLENGES"].splitlines()
group = os.environ["GROUP_NAME"]
raw_challenges = os.environ["GROUP_CHALLENGES"].splitlines()
def challenge_path(challenge: str) -> str:
parts = ["challenges"]
if group != "default":
parts.append(group)
parts.append(challenge)
return "/".join(parts)
challenges = [(challenge, challenge_path(challenge)) for challenge in raw_challenges]
concurrency = (os.cpu_count() or 1) * 2
bar_width = 20

print(f"::group::Test summary for {group}")
print(f"Group: {group}")
print(f"Total challenges: {len(challenges)}")
print("Challenges in this group:")
for challenge in challenges:
print(f"- {challenge}")
for challenge, path in challenges:
print(f"- {challenge} ({path})")
print("::endgroup::")

async def run_tests(index: int, challenge: str):
async def run_tests(index: int, challenge: str, path: str):
try:
process = await asyncio.create_subprocess_exec(
"./pwnshop",
"test",
f"{group}/{challenge}",
path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
Expand All @@ -156,19 +163,19 @@ jobs:
next_index = 1
pending = {}

for index, challenge in enumerate(challenges, start=1):
await task_queue.put((index, challenge))
for index, (challenge, path) in enumerate(challenges, start=1):
await task_queue.put((index, challenge, path))

for _ in range(concurrency):
await task_queue.put((None, None))
await task_queue.put((None, None, None))

async def worker():
while True:
index, challenge = await task_queue.get()
if index is None or challenge is None:
index, challenge, path = await task_queue.get()
if index is None or challenge is None or path is None:
task_queue.task_done()
break
result = await run_tests(index, challenge)
result = await run_tests(index, challenge, path)
await result_queue.put(result)
task_queue.task_done()

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is the pwn.college challenge monorepo containing cybersecurity CTF challeng

### Challenge CLI

All workflows run through the `./pwnshop` CLI (implemented with Click/Rich in `src/pwnshop/commands` and backed by shared helpers in `src/pwnshop/lib`). The older `./build` script has been retired; never call it or duplicate its behavior.
All workflows run through the `./pwnshop` CLI (implemented with Click/Rich in `tools/pwnshop/src/pwnshop/commands` and backed by shared helpers in `tools/pwnshop/src/pwnshop/lib`). The older `./build` script has been retired; never call it or duplicate its behavior.

Each subcommand accepts either a direct path or a challenge slug (e.g., `web-security/path-traversal-1`). Slugs must contain the module and challenge, and the CLI searches `./challenges` for that exact path.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ git crypt unlock

All developer workflows now run through the `./pwnshop` command. The legacy `./build` helper has been removed, so every rendered, build, run, or test action should use the new CLI instead.

The CLI is implemented with Click and Rich (`src/pwnshop/commands/*.py`) on top of the core helper library in `src/pwnshop/lib/__init__.py`. Keeping formatting/terminal logic in the commands and reusable challenge logic in `src/pwnshop/lib` makes it easy to add or modify commands without duplicating functionality.
The CLI is implemented with Click and Rich (`tools/pwnshop/src/pwnshop/commands/*.py`) on top of the core helper library in `tools/pwnshop/src/pwnshop/lib/__init__.py`. Keeping formatting/terminal logic in the commands and reusable challenge logic in `tools/pwnshop/src/pwnshop/lib` makes it easy to add or modify commands without duplicating functionality.

All CLI subcommands accept either a direct filesystem path or a challenge slug. Slugs must include the module (e.g., `web-security/path-traversal-1`); the tool searches under `./challenges` for that module/challenge pair and errors if nothing matches.

Expand Down
25 changes: 5 additions & 20 deletions pwnshop
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "black",
# "jinja2",
# "pyastyle",
# "click",
# "rich",
# ]
# ///
#!/bin/sh
set -eu

import pathlib
import sys
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UV_PROJECT="$SCRIPT_DIR/tools/pwnshop"

REPO_ROOT = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(REPO_ROOT / "src"))

from pwnshop import cli

if __name__ == "__main__":
cli()
exec uv run --project "$UV_PROJECT" --with-editable "$UV_PROJECT" pwnshop "$@"
24 changes: 0 additions & 24 deletions src/pwnshop/commands/build.py

This file was deleted.

43 changes: 0 additions & 43 deletions src/pwnshop/commands/list.py

This file was deleted.

110 changes: 0 additions & 110 deletions src/pwnshop/commands/render.py

This file was deleted.

Loading