用于开发一场 FloatCTF 比赛。
创建一个空的 Private Repository,例如:
git@github.com:fb0sh/freshcup-2027.git
git clone -b event/base --single-branch --filter=blob:none \
git@github.com:FloatCTF/floatctf-content.git \
freshcup-2027
cd freshcup-2027目录名即 Event ID,建议使用:
freshcup-2027
summer-2027
xzmu-2027
./scripts/init-event.sh git@github.com:fb0sh/freshcup-2027.git
git push -u origin main初始化后:
origin → 当前比赛私有仓库
upstream → FloatCTF/floatctf-content
并创建:
challenges/
gameboxes/
events/<event-id>.toml
将 Challenge 和 GameBox 分别放入:
challenges/
gameboxes/
每个子目录必须包含:
meta.toml
meta.toml 字段见 Content Metadata。
内容发生变化后运行:
./scripts/sync-event.sh脚本会自动:
- 扫描
challenges/ - 扫描
gameboxes/ - 更新
events/<event-id>.toml的[content] - 生成
docs/<event-id>.md - 最后校验
meta.toml(scripts/content.py validate)
先同步再校验:删除或重命名内容时不会被 Event 中的旧引用卡住。
然后正常提交:
git add .
git commit -m "..."
git push比赛结束并确认可以公开后:
./scripts/publish.sh脚本会发布到:
floatctf-content:event/freshcup-2027
并创建合并到:
floatctf-content:main
的 Pull Request。
比赛公开前,只向
origin推送,不要向upstream推送。
meta.toml 是 Challenge / GameBox 的唯一元数据来源,用于生成 catalog.json。
meta.toml
↓
scripts/content.py
↓
catalog.json
↓
FloatCTF Platform
本仓库只负责元数据校验与 Catalog 生成,不负责构建、发布或验证镜像。
三个概念要区分清楚:
| 概念 | 来源 | 用途 |
|---|---|---|
id |
目录名 | FloatCTF 内部稳定 ID,Event 引用、catalog 中的 id |
name |
meta.toml |
UI 显示名称 |
safe_name |
meta.toml(可选) |
Docker repository 名,用于 catalog 中的 image 引用 |
id 不需要在 meta.toml 中声明,Challenge 与 GameBox 允许使用相同 id
与相同 safe_name,因为 image tag(challenge-v* / gamebox-v*)不同。
name = "comment"
version = "1.0.0"
author = "fb0sh@outlook.com"
category = "web"
difficulty = "easy"
tags = ["php", "web"]
description = "注释里面有什么?"
# 可选;缺省由目录名派生
# safe_name = "comment"
[flag]
type = "dynamic"
[docker]
port = 80
[docker.recommended_resources]
cpu_millis = 500
memory_bytes = 268435456
pids_limit = 100必填字段:
name version author category difficulty tags description
字段说明:
| 字段 | 说明 |
|---|---|
difficulty |
unknown / beginner / easy / medium / hard / expert |
tags |
字符串数组,可以为空数组,每项必须是非空字符串 |
safe_name |
可选字段;未填写时由目录名自动派生。所有 Challenge / GameBox 都必须最终得到合法的 safe_name;自动派生失败时必须显式填写 |
version使用x.y.z(SemVer),例如1.0.0。category不限制取值,现有内容使用ai/crypto/misc/pwn/reverse/web。- 旧内容已统一补充
difficulty = "unknown"与tags = [];unknown仅用于兼容,新内容请填写真实难度。 events关系由events/*.toml自动反向生成,不要在meta.toml中手工维护。
所有 Challenge / GameBox 都必须拥有有效的 safe_name,无论它是 container
还是 static / attachment-only 内容。
meta.toml 中的 safe_name 字段本身可以省略,此时由目录名自动派生;
如果无法自动派生,则必须显式填写。
safe_name 是 Docker repository 名(必须匹配
^[a-z0-9]+(?:[._-][a-z0-9]+)*$),而 id 可以包含空格、大写、撇号甚至中文。
没有显式写 safe_name 时,由目录名自动派生:
comment → comment
Android_reverse → android_reverse
FloatCTF-qidong → floatctf-qidong
Cirno's perfect math class → cirnos-perfect-math-class
派生规则:小写 → Unicode NFKD 归一化 → 删除撇号(' 与 ’)→
非 a-z0-9._- 字符转成 - → 合并连续分隔符 → 去首尾 . _ -。
例如:
challenges/Cirno's perfect math class/meta.toml
name = "Cirno's perfect math class"
safe_name = "cirnos-perfect-math-class" # 可省略,自动派生结果相同
Catalog 中仍然是原始 id,只有 image 使用 safe_name:
{
"id": "Cirno's perfect math class",
"image": "floatctf/cirnos-perfect-math-class:challenge-v1.0.0"
}只有自动派生失败时才必须显式写 safe_name(例如全中文目录名):
name = "题目"
safe_name = "challenge-001"否则 validate 会报错:
error: challenges/题目/meta.toml: unable to derive Docker safe_name; set safe_name explicitly
同一类型下 safe_name 不允许冲突(challenges/Foo 与 challenges/foo
都会派生成 foo,必须改名或显式指定)。
校验整个仓库:
python3 scripts/content.py validateevents/*.toml 里除了由脚本生成的 [content],还必须包含:
id title description started_at ended_at
schema_version = 1
id = "freshcup-2027"
title = "2027 FloatCTF 新生赛"
description = "2027 FloatCTF 新生赛题目仓库"
started_at = "2027-10-19 14:30"
ended_at = "2027-10-19 18:30"
# BEGIN GENERATED CONTENT
[content]
challenges = []
gameboxes = []
# END GENERATED CONTENTid必须等于文件名:events/freshcup-2027.toml→id = "freshcup-2027"。title/description/started_at/ended_at必须是 strip 后非空的字符串 (暂不校验日期格式)。[content]由./scripts/sync-event.sh自动生成,不要手工维护。
./scripts/sync-event.sh 的执行顺序:
扫描 Challenge / GameBox
→ 更新 Event 的 [content] generated block
→ 生成 docs/<event-id>.md
→ 最后执行完整 metadata validation
先同步再校验,所以删除或重命名内容时不会被 Event 里的旧引用阻塞。
Catalog 中的 image 只是规范化的引用,由 scripts/content.py 生成,
供 FloatCTF 平台使用。本仓库不构建、不推送、不验证该镜像:
Challenge: floatctf/{safe_name}:challenge-v{version}
GameBox: floatctf/{safe_name}:gamebox-v{version}
例如:
floatctf/comment:challenge-v1.0.0
floatctf/cirnos-perfect-math-class:challenge-v1.0.0
src/Dockerfile 是否存在决定内容类型:
存在 → container content
Catalog 才可能包含 image 与 docker
不存在 → static / attachment content
仍然进入 Catalog,但既没有 image 也没有 docker
(即使 meta.toml 中写了 [docker] 也不会输出)
不检查 Docker Hub 是否已有该镜像、本地是否能构建、tag 是否存在,也不做 pull / push。
catalog.json 是自动生成的官方题库索引,由 GitHub Actions 在 main
分支上重新生成并提交。平台可以直接读取:
https://raw.githubusercontent.com/FloatCTF/floatctf-content/main/catalog.json
本地重新生成与校验:
python3 scripts/content.py catalog # 写入 ./catalog.json
python3 scripts/content.py catalog --output /tmp/c.json # 写到别处,不动工作树
python3 scripts/content.py catalog --check # 只检查是否最新catalog.json is generated. Do not edit it manually.
Catalog 只包含元数据,不包含 flag 值;只有带 src/Dockerfile 的内容才有
image 与 docker 字段。
提交到 main 的原因:Git 历史可追踪、raw.githubusercontent.com 直接访问、
不需要 GitHub Pages、本地开发也能查看。
Event private repo
└─ ./scripts/sync-event.sh # 扫描内容 → 更新 event manifest / docs → validate
└─ ./scripts/publish.sh # 推送到 upstream event/<event-id> 并创建 PR
└─ Pull Request # validate + catalog 生成测试 + unittest
└─ main # validate + unittest + 重新生成 catalog.json
└─ catalog.json # 有变化时由 github-actions[bot] 自动提交
└─ FloatCTF 平台读取 raw catalog.json
- Pull Request:
validate→catalog --output(不修改工作树)→ unittest。 不要求catalog.json已经是最新。 main(push 或workflow_dispatch):validate+ unittest 通过后重新生成catalog.json,有变化时用github-actions[bot]提交chore: update catalog [skip ci]。- 只有
main会提交 catalog;PR 与其他分支不会。 - Action 不登录任何 Registry、不构建镜像、不需要任何 Secret。