Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
650e6a2
fix(ci): correct luacheck globals for plugin scenes and test spies
davidp57 Jul 17, 2026
0601272
feat(template): reference _template plugin scene exercising all exten…
davidp57 Jul 17, 2026
bbfc38e
Merge pull request #1 from VEAF/feature/template-scene
davidp57 Jul 17, 2026
c707381
chore(vendor): refresh CTLD.lua baseline (Metal FARP removed from core)
davidp57 Jul 17, 2026
87aa842
Merge pull request #2 from VEAF/feature/revendor-ctld-no-metalfarp
davidp57 Jul 17, 2026
f673688
fix(gate): validate GROUND descriptor unit types (unitType per coalit…
davidp57 Jul 17, 2026
e9c98a2
Merge pull request #3 from VEAF/feature/gate-ground-types
davidp57 Jul 17, 2026
e286063
docs(adr): add grilling context and architecture decision records
FullGas1 Jul 18, 2026
9de0580
feat(catalogue): add download buttons in catalogue and plugin templat…
FullGas1 Jul 18, 2026
494db41
feat(template): add plugins/_template/README.md for plugin authors
FullGas1 Jul 18, 2026
7dcf8e8
feat(metal-farp): add README, align mod name, regenerate doc pages
FullGas1 Jul 18, 2026
454d01b
feat(ci): add validate-docs job for README/generated-doc coherence
FullGas1 Jul 18, 2026
3890e6b
feat(docs): versioned site with mike (latest/dev)
FullGas1 Jul 18, 2026
2b69d5c
fix: align mod name FG_small_Helipad in catalogue + clearer CI error
FullGas1 Jul 18, 2026
f977181
fix(test): align modTypes assertion with renamed mod FG_small_Helipad
FullGas1 Jul 18, 2026
18ca51e
docs(adr): add versioned site URLs table to ADR-0002
FullGas1 Jul 18, 2026
c97b1c2
docs(readme): add versioned documentation site URLs
FullGas1 Jul 18, 2026
64475ee
Merge pull request #10 from VEAF/feature/lot4-catalogue-docs-mike
FullGas1 Jul 18, 2026
f95abd1
fix(metal-farp): correct spelling apparaitre -> apparaitre in README
FullGas1 Jul 18, 2026
ba8a829
fix(docs): fix download buttons and migrate all deliverables to English
FullGas1 Jul 19, 2026
51b64cc
fix(docs): fix download filename and external link new-tab behaviour
FullGas1 Jul 19, 2026
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
48 changes: 48 additions & 0 deletions .claude/skills/generate-plugin-doc/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: generate-plugin-doc
description: Generates docs/plugins/{name}.md (EN) and docs/plugins/{name}.fr.md (FR) for a CTLD plugin, from its plugins/{name}/README.md (optional) and metadata extracted from its .lua source. Use when a plugin author wants to publish or refresh the documentation pages for their plugin.
---

<what-to-do>

Generate the documentation pages for a CTLD plugin.

## Inputs to gather

1. **Plugin name** — infer from context or ask.
2. **`plugins/{name}/README.md`** — read if it exists (front-matter YAML + French prose). Optional.
3. **`plugins/{name}/src/*.lua`** — extract `requiresCtld`, `modTypes`, `requiresMod`, and the scene `name`.
4. **`plugins/_template/README.md`** — reference for front-matter structure.

## What to produce

Two files, both beginning with the `do not edit` comment:

```
<!-- generated by skill generate-plugin-doc — do not edit -->
```

### `docs/plugins/{name}.fr.md`

- Title: `# {scene name}` (from `.lua`)
- Download button (primary): `[⬇ {name}.lua](../downloads/{lua_file}){ download="{name}.lua" .md-button .md-button--primary }`
- Sections: Description, Prérequis, Installation, Remarques (omit if empty)
- Content: the French prose from `README.md` body (if present), otherwise minimal generated text
- Mod links: for each entry in `modUrls`, add a link in the Prérequis section:
`[⬇ {mod}]({url}){ .md-button }`
- External prose links: write them as plain markdown links — the MkDocs hook (`hooks.py`) automatically adds `target` and `rel="noopener"` to all external links at build time

### `docs/plugins/{name}.md`

- Same structure as the FR file, fully translated to English
- Translate all prose, section headings, and UI labels
- Keep code literals, type names, version numbers, and URLs untouched

## Rules

- Never edit `docs/plugins/` files by hand after generation — they are owned by this skill.
- If `README.md` is absent: generate minimal pages from `.lua` metadata only (no prose, no modUrls).
- Download buttons always use a relative path (`../downloads/{lua_file}`) — never an absolute URL.
- After writing both files, remind the author to add the plugin to `docs/index.md` and `docs/index.fr.md` if not already present. In those index files, the download button path is `downloads/{lua_file}` (no leading `../`).

</what-to-do>
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ jobs:
sudo apt-get update && sudo apt-get install -y lua5.1 liblua5.1-dev luarocks
sudo luarocks install busted
- name: Run busted
run: busted tests/
run: busted tests/ plugins/

# Documentation coherence gate: README front-matter + generated doc pages.
validate-docs:
name: Validate plugin docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PyYAML
run: pip install pyyaml
- name: Check README / generated docs coherence
run: python tools/ci/validate_docs.py

# Build every plugin into dist/<plugin>.lua and upload as artifacts.
build:
Expand Down
40 changes: 30 additions & 10 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ name: Docs

on:
push:
branches: [develop]
paths: ['docs/**', 'mkdocs.yml', '.github/workflows/docs.yml']
branches: [develop, master]
paths: ['docs/**', 'plugins/**/src/*.lua', 'mkdocs.yml', '.github/workflows/docs.yml']
workflow_dispatch:

permissions:
contents: write

jobs:
build-deploy:
name: Build & deploy catalogue
deploy:
name: Deploy versioned docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -20,10 +20,30 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install mkdocs-material + i18n
run: pip install mkdocs-material mkdocs-static-i18n
- name: Build
run: mkdocs build --strict
- name: Deploy to gh-pages
- name: Install dependencies
run: pip install mkdocs-material mkdocs-static-i18n mike
- name: Configure git for mike
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Copy plugin .lua files to docs/downloads/
run: |
mkdir -p docs/downloads
cp plugins/*/src/*.lua docs/downloads/
Comment on lines +29 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Copy step will fail if no plugin src trees or no .lua files exist

This cp plugins/*/src/*.lua docs/downloads/ pattern will fail if any plugin lacks a src directory or if there are no .lua files (e.g. new plugin skeletons, partial checkouts), causing the docs job to error unnecessarily. Please make this step resilient, e.g. by using find to locate .lua files or by tolerating a non-zero cp exit:

mkdir -p docs/downloads
find plugins -mindepth 2 -maxdepth 2 -type d -name src -exec find {} -name '*.lua' -exec cp {} docs/downloads/ \;
# or
cp plugins/*/src/*.lua docs/downloads/ 2>/dev/null || true

- name: Inject branch into download URLs
# Replace /master/ with the actual branch in raw download links,
# so the dev docs point to develop-branch sources.
run: |
branch="${GITHUB_REF_NAME}"
if [ "$branch" != "master" ]; then
find docs -name "*.md" -exec sed -i \
"s|/CTLD_plugins/master/|/CTLD_plugins/${branch}/|g" {} \;
fi
- name: Deploy latest (master → latest)
if: github.ref == 'refs/heads/master'
run: |
mike deploy --push --update-aliases master latest
mike set-default --push latest
- name: Deploy dev (develop → dev)
if: github.ref == 'refs/heads/develop'
run: mkdocs gh-deploy --force
run: mike deploy --push --update-aliases develop dev
13 changes: 9 additions & 4 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ self = false

exclude_files = {
"tools/**",
"vendor/**", -- vendored CTLD.lua build (not ours to lint)
"tests/data/**", -- generated datamine type set
"vendor/**", -- vendored CTLD.lua build (not ours to lint)
"tests/data/**", -- generated datamine type set
"tests/helpers/dcs_stubs.lua", -- vendored DCS API stubs (defines the DCS globals)
}

-- `ctld` is writable: plugin scenes populate ctld.i18n[...] at load.
globals = { "ctld" }

read_globals = {
-- DCS World API
"env", "world", "coalition", "country", "timer", "trigger",
Expand All @@ -23,7 +27,6 @@ read_globals = {
"Weapon", "Runway", "Warehouse",
"require", "dofile", "loadfile", "loadstring", "io", "os",
-- CTLD runtime (provided by CTLD, consumed by plugins)
"ctld",
"CTLDObjectRegistry", "CTLDSceneManager", "CtldScene",
"CTLDCrateManager", "CTLDCrateAssemblyManager",
"CTLDPlayerManager", "CTLDPlayer",
Expand All @@ -33,7 +36,9 @@ read_globals = {
}

files["tests/"] = {
-- busted globals + the CTLD singletons a test may spy on (mutate).
globals = { "describe", "it", "setup", "teardown",
"before_each", "after_each",
"assert", "spy", "mock", "stub" },
"assert", "spy", "mock", "stub",
"CTLDObjectRegistry", "CTLDSceneManager" },
}
57 changes: 57 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# CONTEXT.md — CTLD_plugins

Glossary and domain decisions. Contains no implementation details.

---

## Glossary

**Plugin**
A single `.lua` file that self-registers in CTLD when loaded. One plugin = one scene. Plugins live in `plugins/{name}/src/`.

**Scene**
What a plugin builds in-game (e.g. a metal FARP). Runtime DCS/CTLD term.

**Mission maker**
End user of a plugin: downloads the `.lua`, loads it in the DCS Mission Editor, and installs any required mods on their machine.

**DCS mod**
Third-party extension for DCS World providing non-stock object types. A plugin that requires a mod declares it in `modTypes` (runtime) and in `modUrls` (documentation).

**modUrls**
Table of `{mod, url}` pairs in the YAML front-matter of a plugin's `README.md`. Authoritative source for links to required mods. Distinct from `modTypes` (Lua runtime only).

**Plugin README**
File `plugins/{name}/README.md`. Single source of truth for a plugin's documentation. Contains a YAML front-matter (structured metadata including `modUrls`) and French prose. Follows the template at `plugins/_template/README.md`.

**Generated doc**
The files `docs/plugins/{name}.md` (EN) and `docs/plugins/{name}.fr.md` (FR) generated by an interactive Claude skill from the plugin README. Never edited by hand.

**Skill generate-plugin-doc**
Claude skill that reads `plugins/{name}/README.md` and the doc template, assembles and translates FR→EN, and produces the two `docs/plugins/` files.

**Catalogue**
Index page (`docs/index.md` / `docs/index.fr.md`) listing all available plugins with their download button.

**Download button**
Link to the `.lua` file served from `docs/downloads/` (same origin as the docs site).
Relative URL: `downloads/{file}.lua` (from the index) or `../downloads/{file}.lua` (from a plugin page).
Carries the `download` attribute to trigger a native browser file download.
Present in both the catalogue table and the plugin description page.

**Mod button**
Link in the Prerequisites section of a plugin page, pointing to the external mod page.
Carries `target="ext-{mod}" rel="noopener"`: multiple clicks reuse the same named browser tab rather than opening a new one each time.

**External prose link**
Hyperlink in generated prose pointing to an external site (e.g. a mod's GitHub repository).
Carries `target="_blank" rel="noopener"` to open an anonymous new tab.

**{branch}**
Variable injected at doc generation time. Value is `master` (site `latest`) or `develop` (site `dev`). Not applicable to download buttons (which point to `docs/downloads/`, copied from the current branch at CI build time).

**Versioned docs site**
MkDocs Material site managed by **mike**. Two versions coexist on GitHub Pages:
- `latest` → branch `master` (stable)
- `dev` → branch `develop` (staging)
Each version has its own download links coherent with its branch.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,28 @@ docs/ mkdocs bilingual (EN + FR) catalogue

The vendored `vendor/CTLD.lua` and `tests/data/dcs_types.lua` are pinned to a CTLD baseline; refresh
them together when bumping the supported CTLD version.

- **Document a plugin:** create `plugins/<name>/README.md` (use `plugins/_template/README.md` as
model), then run the `generate-plugin-doc` skill in Claude to produce `docs/plugins/<name>.md`
and `docs/plugins/<name>.fr.md`. Finally add the plugin row to `docs/index.md` and
`docs/index.fr.md`.

### CI gate — `validate-docs`

The `validate-docs` job runs `tools/ci/validate_docs.py` on every push and PR. It:

- **Warns** (exit 0) when a plugin has no `README.md` (docs will have no prose, but the build is not blocked).
- **Errors** (exit 1) when:
- `README.md` is present but its YAML front-matter is absent or malformed.
- `modUrls` exists but is not a list of `{mod, url}` pairs.
- `docs/plugins/<name>.md` or `docs/plugins/<name>.fr.md` is absent.

Run it locally before pushing: `python tools/ci/validate_docs.py` (requires `pyyaml`).

## Documentation site

| Version | URL |
|---------|-----|
| Production (`latest`, from `master`) | <https://veaf.github.io/CTLD_plugins/latest/> |
| Recette (`dev`, from `develop`) | <https://veaf.github.io/CTLD_plugins/dev/> |
| Racine (redirects to `latest`) | <https://veaf.github.io/CTLD_plugins/> |
14 changes: 14 additions & 0 deletions docs/adr/0001-readme-as-doc-source-of-truth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Plugin README as documentation source of truth

Each plugin has a `plugins/{name}/README.md` (YAML front-matter + French prose) that is the single source of truth for its documentation. The files `docs/plugins/{name}.md` and `{name}.fr.md` are **generated** by an interactive Claude skill — never edited by hand.

## Alternatives considered

- **Separate `meta.yaml` + prose in `docs/`**: two files to keep in sync, risk of divergence.
- **Metadata embedded in the `.lua`**: the `.lua` runs inside DCS — embedding GitHub URLs there pollutes runtime code with pure documentary metadata that has no value at execution time.

## Consequences

- `docs/plugins/` files must be regenerated after every change to a plugin's `README.md`.
- `plugins/_template/README.md` is the authoritative template for plugin authors.
- The FR→EN translation is produced by Claude on demand from the designer (interactive), not by CI.
22 changes: 22 additions & 0 deletions docs/adr/0002-mike-versioned-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Versioned docs site with mike (latest / dev)

The MkDocs Material site is managed by **mike**, with two versions coexisting on GitHub Pages: `latest` (branch `master`) and `dev` (branch `develop`). Each version injects its own `{branch}` value into download buttons, ensuring that a tester on `dev` downloads the `.lua` from `develop` and not from `master`.

## Alternatives considered

- **Single site (master only)**: impossible to test the docs and download links in staging without workarounds — the tester would have clicked "download" and received the stable version, not the one under test.
- **Two separate sites (`/` and `/dev/`)**: works but loses the built-in version selector, already validated in the VEAF org (`veaf.github.io/documentation/`).

## Site URLs

| Version | URL | Trigger |
| ------- | --- | ------- |
| Production (`latest`) | <https://veaf.github.io/CTLD_plugins/latest/> | push to `master` |
| Staging (`dev`) | <https://veaf.github.io/CTLD_plugins/dev/> | push to `develop` |
| Root | <https://veaf.github.io/CTLD_plugins/> | redirects to `latest` |

## Consequences

- The `docs.yml` workflow calls `mike deploy dev` on push to `develop` and `mike deploy latest` on push to `master`.
- `{branch}` is a variable injected at doc generation time (Claude skill) — value is `develop` or `master`.
- The `versions.json` managed by mike must stay on `gh-pages`, not committed to `develop`/`master`.
19 changes: 19 additions & 0 deletions docs/adr/0003-serve-lua-from-docs-downloads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Serve plugin `.lua` files from `docs/downloads/` instead of `raw.githubusercontent.com`

Plugin `.lua` files are copied into `docs/downloads/` by CI before the MkDocs build, so they are served from the same origin as the docs site (`veaf.github.io`). Download buttons point to this relative path and carry the `download` attribute, guaranteeing a native browser file download in all browsers.

## Problem

The HTML `download` attribute is silently ignored by Chrome (and Chromium-based browsers) when the link points to a cross-origin URL, even if CORS headers are present. `raw.githubusercontent.com` is a different origin from `veaf.github.io`: clicking the button was opening the file in the browser instead of downloading it.

## Alternatives considered

- **`download` attribute on the raw URL**: ignored by Chrome for cross-origin links — the problem persists.
- **Link to the GitHub blob page (`/blob/`)**: requires two clicks; the mission maker lands on the GitHub UI before being able to download.
- **Package `.lua` as a GitHub Release asset**: requires a dedicated release workflow and adds disproportionate operational complexity.

## Consequences

- `.lua` files are duplicated in the built site (negligible size).
- Branch coherence is automatic: CI copies the files from the branch currently being built.
- The existing `sed` branch-injection step in `docs.yml` no longer applies to `.lua` download URLs (harmless: these URLs are now relative paths).
6 changes: 3 additions & 3 deletions docs/index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ mission ne les embarque que si elle le décide.

## Plugins disponibles

| Plugin | Ce qu'il construit | Requiert |
|--------|--------------------|----------|
| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `Farp_FG_Petit_Helipad` |
| Plugin | Ce qu'il construit | Requiert | Télécharger |
|--------|--------------------|----------|-------------|
| [Metal FARP](plugins/metal-farp.md) | Un FARP à hélisurface métallique | Mod DCS `FG_small_Helipad` | [⬇ metal-farp.lua](downloads/CTLD_metalFarpScene.lua){ download .md-button } |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The download button attribute syntax looks inconsistent with other examples and may not be parsed as intended.

This link uses { download .md-button }, while other examples use download="metal-farp.lua" .md-button .md-button--primary. For consistent behavior and reliable parsing, consider adding an explicit download="metal-farp.lua" attribute and matching the class list to the existing button examples.

6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ only pays for them if it opts in.

## Available plugins

| Plugin | What it builds | Requires |
|--------|----------------|----------|
| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `Farp_FG_Petit_Helipad` |
| Plugin | What it builds | Requires | Download |
|--------|----------------|----------|----------|
| [Metal FARP](plugins/metal-farp.md) | A metal-helipad FARP | DCS mod `FG_small_Helipad` | [⬇ metal-farp.lua](downloads/CTLD_metalFarpScene.lua){ download .md-button } |
29 changes: 14 additions & 15 deletions docs/plugins/metal-farp.fr.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
<!-- generated by skill generate-plugin-doc — do not edit -->

# Metal FARP

Construit un FARP autour d'une hélisurface métallique du mod DCS **`Farp_FG_Petit_Helipad`**, avec
le mobilier FARP habituel (camion carburant, camion de réparation, tente, munitions, éclairage,
manche à air).
[⬇ metal-farp.lua](../downloads/CTLD_metalFarpScene.lua){ download="metal-farp.lua" .md-button .md-button--primary }

Ce plugin permet via CTLD de faire apparaître un héliport métallique de façon progressive pour
simuler sa construction, en y ajoutant des objets pour obtenir un héliport "décoré" plus réaliste.

## Prérequis

- **CTLD** ≥ 2.0.0 chargé en premier (le plugin prévient en jeu si CTLD est plus ancien).
- Le mod DCS fournissant le type statique **`Farp_FG_Petit_Helipad`**, installé sur **tous** les
clients. Sans lui, l'hélisurface ne peut pas apparaître.
- **CTLD** ≥ 2.0.0 chargé en premier.
- Le mod DCS **`FG_small_Helipad`** installé sur **tous** les clients.
Sans lui, l'héliport ne peut pas apparaître.

[⬇ FG_small_Helipad](https://github.com/FullGas1/DCS-mods/tree/main/FG_small_Helipad){ .md-button }

## Installation

1. Téléchargez `metal-farp.lua`.
1. Téléchargez le fichier `.lua` du plugin (bouton ci-dessus).
2. Dans l'éditeur de mission, ajoutez un déclencheur `DO SCRIPT FILE` au **démarrage de la
mission**, **après** le déclencheur qui charge `CTLD.lua`.
3. La scène ajoute une caisse dans le menu *Request Equipment* de CTLD ; déployez-la comme n'importe
quelle caisse de scène FARP.

## Remarques

La validation au design-time ne peut pas vérifier que le client a réellement le mod installé —
seulement que le nom de type est un mod déclaré. S'assurer que le mod est présent sur tous les
clients relève de la responsabilité du créateur de mission.
3. La scène ajoute une caisse dans le menu *Request Equipment* de CTLD ; déployez-la comme
n'importe quelle caisse de scène FARP.
Loading
Loading