Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const prefix = await getCommandPrefix(client, gameServerId);
await uninstallModule(client, moduleId, gameServerId);
await deleteModule(client, moduleId);

// Safety net: delete orphaned test-* modules and game servers
// Safety net: delete orphaned modules (matching modules/ directory listing) and game servers
await cleanupTestModules(client);
await cleanupTestGameServers(client);
```
Expand All @@ -121,10 +121,10 @@ await cleanupTestGameServers(client);

When tests crash before `after()` runs, mock game servers and test modules are left behind in Takaro. Both cleanup functions should be called in `before()` hooks:

- `cleanupTestModules(client)` — deletes modules with names starting with `test-`
- `cleanupTestModules(client)` — deletes modules whose names match a directory under `modules/`
- `cleanupTestGameServers(client)` — deletes game servers with names starting with `test-`

Module names should start with `test-` (e.g., `test-hello-world` in `module.json`) so `cleanupTestModules` can find and remove them.
Module names must match their directory name (e.g., for `modules/hello-world/`, set `"name": "hello-world"` in module.json — this is enforced by both the test cleanup helper and the registry build).

### Writing Tests for a New Module

Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/publish-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Publish Registry

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: publish-registry
cancel-in-progress: true

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Login to CodeArtifact
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_CODEARTIFACT_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_CODEARTIFACT_SECRET_ACCESS_KEY }}
run: |
TOKEN=$(aws codeartifact get-authorization-token \
--domain takaro --domain-owner 967985385710 \
--region eu-west-3 --query authorizationToken --output text)
REGISTRY=takaro-967985385710.d.codeartifact.eu-west-3.amazonaws.com
npm config set //${REGISTRY}/npm/takaro-private/:_authToken=$TOKEN

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Build TypeScript
run: npm run build

- name: Build registry
run: npm run build:registry

- name: Publish to registry branch
# force_orphan: true — the registry branch carries no commit history.
# Each push to main produces a clean snapshot of the current registry state.
# We intentionally accept this loss of history pre-1.0.1: the registry branch
# is purely a distribution artifact, not a source branch, so history adds no value.
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist/registry
publish_branch: registry
force_orphan: true
keep_files: false
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,54 @@ Module code lives locally in `modules/` as editable files, then gets pushed to T
**Why**: Mocked tests pass while the real system fails. The whole point of this test infrastructure is to catch integration issues (wrong API shapes, missing permissions, runtime errors in the Takaro function sandbox). Unit tests with mocked helpers bypass all of that.


## Module Conventions

### Versioning

**Every change to a module requires a version bump in `module.json`.** Versions must be valid semver (e.g. `1.0.0`, `1.0.1`, `2.1.0`). The value `"latest"` is not accepted by the registry.

### Module naming

Module names must match their directory name under `modules/`. Names may only contain `[a-zA-Z0-9_-]` (no spaces, no dots, no slashes). Maximum 100 characters.

For example: the module in `modules/afk-kick/` must have `"name": "afk-kick"` in its `module.json`.

> **Known limitation**: The `name` field appears literally in the Takaro UI as the module identifier (there is no separate `displayName` field today). Choose names that are readable as-is — kebab-case slugs like `afk-kick` or `mini-games` display acceptably. This is a Takaro platform constraint that may be relaxed in a future version.

### Adding a new module

1. Create `modules/<name>/module.json` with `"name": "<name>"` and `"version": "1.0.0"`.
2. Run `npm run build && npm run build:registry` to verify the registry builds cleanly.
3. Commit and push — the publish workflow updates the `registry` branch automatically.

`cleanupTestModules` discovers module names from the `modules/` directory at runtime — no manual list to update when adding a new module.

See `src/scripts/build-registry.ts` for the registry build logic.

### Safety guard for test helpers

`pushModule` and `cleanupTestModules` refuse to touch a Takaro instance whose `TAKARO_HOST` doesn't look like a test/dev environment. This prevents accidentally wiping production modules when running tests.

**If you see this error:**
```
pushModule/cleanupTestModules refused: TAKARO_HOST='<HOST>' does not match any known test/dev host pattern. ...
```

**Common fixes (try in this order):**
1. Check `TAKARO_HOST` in `.env` is correct — pointing at production (`api.takaro.io`) is the most common cause.
2. If your dev/staging host has a non-standard hostname, add a substring to `TAKARO_TEST_HOST_ALLOWLIST` (comma-separated) in `.env`.
3. Last resort, for a single throwaway run: `TAKARO_TEST_ALLOW_ANY_HOST=1 npm test`. Use only when you're certain the target is not shared production state.

**Auto-accepted hosts:** `localhost`, `127.0.0.1`, `host.docker.internal`, single-label Docker hostnames (e.g. `takaro_api`), `*.takaro.dev`, `.staging.`/`.dev.`/`.test.`/`.ci.` substrings, `.test`/`.localhost` TLDs.

**CI:** uses `localhost:13000` and passes the guard automatically.

### Test pretest blocking

`npm test` runs `build:registry` as a `pretest` step. If any module fails registry validation (bad semver, invalid name, description too long), the entire test run is blocked — including unrelated modules. To unblock: fix the validation error in the offending module, or temporarily move the module directory out of `modules/` while debugging other tests.

Workaround: bypass the pretest gate with `node --test --import=ts-node-maintained/register/esm 'modules/<name>/test/*.test.ts'`.

## Available Tools

- **`scripts/takaro-auth.sh`** — Authenticate with the Takaro API
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Each clone also needs a unique `TAKARO_MC_IDENTITY_TOKEN` in `.env`.
You need:
- `TAKARO_USERNAME` and `TAKARO_PASSWORD` — Your Takaro account credentials
- `TAKARO_HOST` — The Takaro API URL (e.g., `https://api.takaro.io`)

Note: `npm test` will refuse to run against production hosts (e.g. `api.takaro.io`). Use a dev/staging host or see [AGENTS.md](AGENTS.md#safety-guard-for-test-helpers) for the bypass.
- `TAKARO_DOMAIN_ID` — Your Takaro domain ID

### Minecraft Server Setup
Expand Down Expand Up @@ -85,6 +87,41 @@ claude
> Write me a module that says 'hello' to every player when they join
```

## Published Registry

This repository is the **Takaro Official Module Registry**. Modules are automatically published to the `registry` branch on every push to `main`.

> **Status — end-to-end subscription currently blocked:** `raw.githubusercontent.com` serves `.json` files with `Content-Type: text/plain`, but the Takaro registry client requires `Content-Type` to contain `"json"`. This is a Takaro server-side limitation; subscriptions will work automatically once the next Takaro release ships the content-type fix (see [gettakaro/takaro#2723](https://github.com/gettakaro/takaro/pull/2723)). The `registry` branch is published and up to date in the meantime.

**Public registry URL** (usable once the Takaro-side fix ships):

```
https://raw.githubusercontent.com/gettakaro/ai-module-writer/registry
```

To subscribe in Takaro, navigate to **Modules → Registries → Add Registry** and enter the URL above. Note: subscription will return an error until the content-type fix ships.

### Local testing (developer workaround)

While the production URL is blocked, developers can test the registry locally:

```bash
npm run build && npm run build:registry
python3 -m http.server 8765 --directory dist/registry
```

Then subscribe to `http://host.docker.internal:8765` from a local Takaro dev stack with `TAKARO_REGISTRY_ALLOWED_PRIVATE_HOSTS` set to include `host.docker.internal`.

### Versioning convention

Any change to a module **requires a version bump** in that module's `module.json`. Module versions must be valid semver (e.g. `1.0.1`). Module names must match their directory name and may only contain `[a-zA-Z0-9_-]`.

### Adding a new module

1. Create `modules/<your-module-name>/module.json` with `"name": "<your-module-name>"` and `"version": "1.0.0"`.
2. Run `npm run build && npm run build:registry` to verify the registry builds.
3. Push to `main` — the publish workflow will update the `registry` branch automatically.

## How it Works

The Docker containers run:
Expand Down
6 changes: 3 additions & 3 deletions modules/afk-kick/module.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "test-afk-kick",
"description": "AFK Kick — periodically checks player positions and warns/kicks idle players. A permission exempts specific players from AFK detection.",
"version": "latest",
"name": "afk-kick",
"description": "AFK Kick \u2014 periodically checks player positions and warns/kicks idle players. A permission exempts specific players from AFK detection.",
"version": "1.0.0",
"supportedGames": [
"all"
],
Expand Down
6 changes: 3 additions & 3 deletions modules/community-fund/module.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "test-community-fund",
"description": "Community Fund — players contribute currency toward a shared goal. When the threshold is reached the fund resets and completion commands fire.",
"version": "latest",
"name": "community-fund",
"description": "Community Fund \u2014 players contribute currency toward a shared goal. When the threshold is reached the fund resets and completion commands fire.",
"version": "1.0.0",
"supportedGames": [
"all"
],
Expand Down
14 changes: 7 additions & 7 deletions modules/daily-rewards/module.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "test-daily-rewards",
"name": "daily-rewards",
"description": "Daily login reward system with streak tracking, milestone bonuses, and role-based multipliers.",
"version": "latest",
"version": "1.0.0",
"supportedGames": [
"all"
],
Expand All @@ -27,22 +27,22 @@
{
"days": 7,
"reward": 1000,
"message": "🎉 Week streak! Bonus: 1000 coins!"
"message": "\ud83c\udf89 Week streak! Bonus: 1000 coins!"
},
{
"days": 30,
"reward": 5000,
"message": "🏆 Monthly streak! Bonus: 5000 coins!"
"message": "\ud83c\udfc6 Monthly streak! Bonus: 5000 coins!"
},
{
"days": 100,
"reward": 20000,
"message": "🌟 100 day streak! Legendary Bonus: 20000 coins!"
"message": "\ud83c\udf1f 100 day streak! Legendary Bonus: 20000 coins!"
},
{
"days": 365,
"reward": 100000,
"message": "👑 365 day streak! You are a legend! Bonus: 100000 coins!"
"message": "\ud83d\udc51 365 day streak! You are a legend! Bonus: 100000 coins!"
}
],
"items": {
Expand Down Expand Up @@ -72,7 +72,7 @@
"type": "number",
"default": 48,
"minimum": 25,
"description": "Hours after which a streak expires (must be >24 so players can always reclaim before expiry). Setting this to 25 means players have only 1 hour after the 24h cooldown before their streak expires — effectively always at-risk. The default of 48h gives a comfortable window."
"description": "Hours after which a streak expires (must be >24 so players can always reclaim before expiry). Setting this to 25 means players have only 1 hour after the 24h cooldown before their streak expires \u2014 effectively always at-risk. The default of 48h gives a comfortable window."
},
"notifyOnLogin": {
"type": "boolean",
Expand Down
8 changes: 4 additions & 4 deletions modules/hello-world/module.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "test-hello-world",
"description": "A simple hello world module for testing",
"version": "latest",
"name": "hello-world",
"description": "Greets players in chat with a configurable hello message when they join or use /greet.",
"version": "1.0.0",
"supportedGames": [
"all"
],
"commands": {
"greet": {
"trigger": "greet",
"description": "Greet a player by name",
"helpText": "Usage: /greet [name] — Greets you or the specified name",
"helpText": "Usage: /greet [name] \u2014 Greets you or the specified name",
"function": "src/commands/greet/index.js",
"arguments": [
{
Expand Down
46 changes: 23 additions & 23 deletions modules/lottery/module.json → modules/jackpot/module.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "test-lottery",
"description": "Lottery — players buy tickets with in-game currency and a daily draw picks a weighted random winner. Prize pool grows with each ticket purchase.",
"version": "latest",
"name": "jackpot",
"description": "Players buy tickets with in-game currency and a daily draw picks a winner — more tickets = better odds. Prize pool grows with each ticket purchase.",
"version": "1.0.0",
"supportedGames": [
"all"
],
Expand Down Expand Up @@ -50,23 +50,23 @@
},
"permissions": [
{
"permission": "LOTTERY_BUY",
"friendlyName": "Buy Lottery Tickets",
"description": "Allows a player to buy lottery tickets",
"permission": "JACKPOT_BUY",
"friendlyName": "Buy Jackpot Tickets",
"description": "Allows a player to buy jackpot tickets",
"canHaveCount": false
},
{
"permission": "LOTTERY_VIEW_TICKETS",
"friendlyName": "View Lottery Tickets",
"description": "Allows a player to view their current ticket count",
"permission": "JACKPOT_VIEW_TICKETS",
"friendlyName": "View Jackpot Tickets",
"description": "Allows a player to view their current jackpot ticket count",
"canHaveCount": false
}
],
"commands": {
"buy-ticket": {
"trigger": "buyticket",
"description": "Buy lottery tickets",
"helpText": "Buy one or more lottery tickets for the current draw. Usage: buyticket <amount>",
"description": "Buy jackpot tickets",
"helpText": "Buy one or more jackpot tickets for the current draw. Usage: buyticket <amount>",
"function": "src/commands/buy-ticket/index.js",
"arguments": [
{
Expand All @@ -77,38 +77,38 @@
}
]
},
"lottery-info": {
"trigger": "lotteryinfo",
"description": "View current lottery status",
"jackpot-info": {
"trigger": "jackpotinfo",
"description": "View current jackpot status",
"helpText": "Shows the current pot, participant count, total tickets, and ticket price for the upcoming draw.",
"function": "src/commands/lottery-info/index.js",
"function": "src/commands/jackpot-info/index.js",
"arguments": []
},
"next-draw": {
"trigger": "nextdraw",
"description": "See when the next lottery draw is",
"helpText": "Shows how long until the next lottery draw at midnight UTC.",
"description": "See when the next jackpot draw is",
"helpText": "Shows how long until the next jackpot draw at midnight UTC.",
"function": "src/commands/next-draw/index.js",
"arguments": []
},
"view-tickets": {
"trigger": "viewtickets",
"description": "View your current lottery ticket count",
"description": "View your current jackpot ticket count",
"helpText": "Shows how many tickets you currently hold for the upcoming draw.",
"function": "src/commands/view-tickets/index.js",
"arguments": []
}
},
"cronJobs": {
"draw-lottery": {
"draw-jackpot": {
"temporalValue": "0 0 * * *",
"description": "Daily lottery draw at midnight UTC — picks a weighted random winner from ticket holders",
"function": "src/cronjobs/draw-lottery/index.js"
"description": "Daily jackpot draw at midnight UTC — picks a winner weighted by ticket count",
"function": "src/cronjobs/draw-jackpot/index.js"
}
},
"functions": {
"lottery-helpers": {
"function": "src/functions/lottery-helpers.js"
"jackpot-helpers": {
"function": "src/functions/jackpot-helpers.js"
}
}
}
Loading
Loading