Skip to content
Merged
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
46 changes: 42 additions & 4 deletions ctfcli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from ctfcli.core.plugins import load_plugins
from ctfcli.utils.git import check_if_dir_is_inside_git_repo
from ctfcli.utils.integrations import INTEGRATIONS

# Init logging
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO").upper())
Expand All @@ -35,8 +36,12 @@ def init(
directory: str | os.PathLike | None = None,
no_git: bool = False,
no_commit: bool = False,
github: bool = False,
gitlab: bool = False,
):
log.debug(f"init: (directory={directory}, no_git={no_git}, no_commit={no_commit})")
log.debug(
f"init: (directory={directory}, no_git={no_git}, no_commit={no_commit}, github={github}, gitlab={gitlab})"
)
project_path = Path.cwd()

# Create our project directory if requested
Expand All @@ -62,7 +67,26 @@ def init(

ctf_token = click.prompt("Please enter CTFd Admin Access Token", default="", show_default=False)

# Confirm information with user
# Collect CI integrations chosen via switches
selected = []
if github:
selected.append("github")
if gitlab:
selected.append("gitlab")

# If no integration switch was passed, ask once for a comma-separated list
if not selected:
available = ", ".join(INTEGRATIONS)
response = click.prompt(
f"Any integrations to add: (comma-separated, leave blank for none)\n"
f"Available: {available} \n"
"Integrations",
default="",
show_default=False,
)
selected = [name.strip().lower() for name in response.split(",") if name.strip()]

# Confirm information with user before creating anything
if not click.confirm(f"Do you want to continue with {ctf_url} and {ctf_token}", default=True):
click.echo("Aborted!")
return
Expand All @@ -74,6 +98,20 @@ def init(
with (project_path / ".ctf" / "config").open(mode="a+") as config_file:
config.write(config_file)

# Generate integration files, collecting the paths to stage in the initial commit
paths_to_add = [".ctf/config"]
for name in selected:
entry = INTEGRATIONS.get(name)
if entry is None:
click.secho(
f"Unknown integration '{name}'. Available: {', '.join(INTEGRATIONS)}",
fg="yellow",
)
continue

_label, creator = entry
paths_to_add.extend(creator(project_path))

# if git init is to be skipped we can return
if no_git:
click.secho("Skipping git init.", fg="yellow")
Expand All @@ -88,7 +126,7 @@ def init(
click.secho("Skipping git commit.", fg="yellow")
return

subprocess.call(["git", "add", ".ctf/config"], cwd=project_path)
subprocess.call(["git", "add", *paths_to_add], cwd=project_path)
subprocess.call(["git", "commit", "-m", "init ctfcli project"], cwd=project_path)
return

Expand All @@ -100,7 +138,7 @@ def init(
click.secho("Skipping git commit.", fg="yellow")
return

subprocess.call(["git", "add", ".ctf/config"], cwd=project_path)
subprocess.call(["git", "add", *paths_to_add], cwd=project_path)
subprocess.call(["git", "commit", "-m", "init ctfcli project"], cwd=project_path)

def config(self):
Expand Down
8 changes: 8 additions & 0 deletions ctfcli/utils/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from ctfcli.utils.integrations.github import create_github_integration
from ctfcli.utils.integrations.gitlab import create_gitlab_integration

# name -> (human label, creator). Add a future provider here + one switch in init().
INTEGRATIONS = {
"github": ("GitHub Actions", create_github_integration),
"gitlab": ("GitLab CI/CD", create_gitlab_integration),
}
41 changes: 41 additions & 0 deletions ctfcli/utils/integrations/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from pathlib import Path

import click

GITHUB_ACTIONS_SYNC_WORKFLOW = """\
name: Sync challenges to CTFd

on:
push:
branches:
- main
- master

jobs:
sync:
# Only run on the repository's default branch
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install ctfcli
run: pip install ctfcli

- name: Install challenges to CTFd
run: ctf challenge install --force
"""


def create_github_integration(project_path: Path) -> list[str]:
workflows_dir = project_path / ".github" / "workflows"
workflows_dir.mkdir(parents=True, exist_ok=True)
(workflows_dir / "sync.yml").write_text(GITHUB_ACTIONS_SYNC_WORKFLOW)
click.secho("Created .github/workflows/sync.yml", fg="green")
return [".github/workflows/sync.yml"]
20 changes: 20 additions & 0 deletions ctfcli/utils/integrations/gitlab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pathlib import Path

import click

GITLAB_CI_SYNC_PIPELINE = """\
sync-challenges:
image: python:3
# Only run on the repository's default branch
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- pip install ctfcli
- ctf challenge install --force
"""


def create_gitlab_integration(project_path: Path) -> list[str]:
(project_path / ".gitlab-ci.yml").write_text(GITLAB_CI_SYNC_PIPELINE)
click.secho("Created .gitlab-ci.yml", fg="green")
return [".gitlab-ci.yml"]