Skip to content

Turn this repo into the first subscribable Takaro module registry - #41

Closed
niekcandaele wants to merge 1 commit into
mainfrom
feature/subscribable-module-registry
Closed

niekcandaele wants to merge 1 commit into
mainfrom
feature/subscribable-module-registry

Conversation

@niekcandaele

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 4 of Takaro PR #2723 (subscribable module registries): reshape gettakaro/ai-module-writer into the first registry. A new build script generates a registry.json manifest plus per-version JSON files for each of the 11 modules into dist/registry/, and a GitHub Actions workflow publishes that tree to a registry branch on every push to main. End state: a Takaro user in any domain can subscribe to https://raw.githubusercontent.com/gettakaro/ai-module-writer/registry/registry.json and install/update modules from a clean UI flow.

The published registry expects modules to use real semver versions, names matching the PR's manifest schema (/^[a-zA-Z0-9_-]+$/), and to not collide with Takaro built-in module names — which drove a meaningful chunk of this branch's work.

Architecture

                                                    
push to main           CI workflow (publish-registry.yml)
─────────────►   ┌──────────────────────────────────────┐
                 │ npm ci → npm run build               │
                 │ npm run build:registry               │
                 │   ├── reads registry.config.json     │
                 │   ├── reads modules/*/module.json    │
                 │   ├── validates name/semver/length   │
                 │   └── writes dist/registry/{...}     │
                 │ peaceiris/actions-gh-pages@v4        │
                 │   force_orphan: true                 │
                 └──────┬───────────────────────────────┘
                        │
                        ▼
              registry branch (orphan)
              ├── registry.json
              ├── jackpot/1.0.0.json
              ├── server-toolkit/1.0.0.json
              └── … 9 more

raw.githubusercontent.com serves these files
                        │
                        ▼
             ┌─────────────────────────┐
             │ Takaro app-api          │
             │ RegistryService         │
             │  ├── fetchManifest()    │
             │  └── installFromRegistry│
             └─────────────────────────┘

What Changed

  • Module identity (all 11 modules): name field normalized to match directory name (drop test- prefix), version bumped from "latest" to "1.0.0" (registry requires valid semver). Module descriptions cleaned up for end-user clarity in the install dialog.
  • Module renames to avoid collisions with Takaro built-ins:
    • modules/lottery/ → modules/jackpot/ (Takaro ships a built-in lottery). Cascaded through commands (/jackpotinfo), permissions (JACKPOT_*), function file (jackpot-helpers.js), storage keys, log prefixes, and test assertions.
    • modules/utils/ → modules/server-toolkit/ (built-in utils). Cascaded through permissions (TOOLKIT_*), function files (server-toolkit-{helpers,pure}.js), log prefixes, and test assertions.
  • Registry build pipeline:
    • src/scripts/build-registry.ts (new): reads registry.config.json + each module, validates against the PR's RegistryManifestDTO constraints (name regex, semver, length limits), writes dist/registry/registry.json and dist/registry/{name}/{version}.json for each module. Wipes dist/registry/ before each build (with path-shape invariant) so renamed/removed modules don't leave stale artifacts.
    • src/scripts/module-to-json.ts: refactored to expose buildModuleExport() as a reusable function; CLI is a thin wrapper. safeRealpath() helper unifies broken-symlink errors.
    • src/utils/validate.ts: validateEntityName now throws (was process.exit(1)) so it's safe to call from long-running processes.
    • registry.config.json (new): registry-level metadata (name, description) shown in Takaro's "Add Registry" UI.
  • Publish workflow (.github/workflows/publish-registry.yml, new): triggered on push to main + workflow_dispatch. Logs into AWS CodeArtifact (mirrors ci.yml pattern for @takaro/* private deps), builds the registry, force-pushes the output to the registry branch via peaceiris/actions-gh-pages@v4. Concurrency guard prevents overlapping runs from racing.
  • Test infrastructure (test/helpers/modules.ts):
    • assertTestSafeHost() — refuses to run pushModule/cleanupTestModules against production-looking hosts. Substring matches localhost, *.takaro.dev, .staging./.dev./.test./.ci., plus single-label Docker hostnames and .test/.localhost TLDs (RFC 6761 reserved). Override env vars: TAKARO_TEST_HOST_ALLOWLIST (preferred) and TAKARO_TEST_ALLOW_ANY_HOST=1 (last resort).
    • getKnownModuleNames() — discovers modules from the filesystem at runtime instead of hand-maintained list.
    • pushModule/cleanupTestModules switched from search: (partial-match) to filters: (exact-match) for deterministic queries.
  • Tests: test/helpers/modules-guard.test.ts (new) — 12 unit tests for the safety guard. package.json test glob expanded to '{modules/*/test,test/helpers}/*.test.ts' so CI picks up the new file. pretest now runs build:registry so a broken module.json fails fast before integration tests start.
  • Documentation: AGENTS.md gained a "Module Conventions" section (semver bump on change, name=directory-name) and a "Safety guard for test helpers" reference (full error template, numbered fix list, auto-accepted hosts list). README.md added "Published Registry" section with the public URL, the known content-type blocker (see below), and a dev local-serve workflow.

Reviewer Guide

  • Start here: src/scripts/build-registry.ts — the new artifact pipeline. Then read the workflow at .github/workflows/publish-registry.yml.
  • Pay attention to:
    • test/helpers/modules.ts assertTestSafeHost() — the substring/TLD pattern logic is intentionally narrow; any change to the safe-host criteria should add a unit test in modules-guard.test.ts first.
    • The two module renames cascade through many files. The grep invariant should hold: grep -ri 'lottery\|utils' modules/jackpot modules/server-toolkit --include='*.json' --include='*.js' --include='*.ts' returns zero hits.
  • Design decisions:
    • Registry hosting: the registry branch served via raw.githubusercontent.com is the lowest-friction publishing story for third-party registry maintainers (just push files to a branch). However, raw.githubusercontent.com returns Content-Type: text/plain for .json files, and Takaro PR #2723's strict contentType.includes('json') check rejects this. Coordinated work: a small Takaro-side PR is needed to either drop that check (DTO validation already rejects malformed bodies) or accept text/plain when the body parses as JSON. Tracked in the README status note.
    • Single version per module for v1.0.0: registry manifest lists versions: ["1.0.0"] for each module. Multi-version history (walking git tags) is intentionally deferred to Phase 6. This means bumping a module to 1.0.1 will replace the 1.0.0 artifact on the registry branch — acceptable for v1.0.0 release; revisit before any module bumps.
    • Module renames lottery → jackpot, utils → server-toolkit: chosen over the alternative of keeping the names + adding BUILTIN_MODULE_NAMES guards in pushModule/cleanupTestModules. The rename is cleaner: no asymmetric guards, no surprises for registry consumers, no risk of clobbering Takaro built-ins.
    • Cleanup blast radius on shared dev hosts: cleanupTestModules will delete any module on TAKARO_HOST matching a modules/ subdir name. Mitigated by assertTestSafeHost. Codex flagged this as sev 7 across multiple review passes; accepted as a documented tradeoff.

Implementation Journey

Built via the player-coach loop in 8 turns (severity threshold 3).

Turn Phase Outcome
1 Verify Initial implementation: 11 module renames, build-registry script, publish workflow → 14 issues sev 3-8
2 Verify Validate-throws fix, host safety guard, search→filters, descriptions → 12 issues, exerciser PASSED
3 Verify Host pattern broadened too far; BUILTIN guard added → EXERCISER FAILED (sev 9 lottery/utils collision)
4 Verify Strategic rename to jackpot/server-toolkit → tests 144/144, only naming polish remained
5 Verify Comprehensive internal rename (commands, permissions, helpers, AGENTS.md guard rewrite) → 5 sev≥3
6 Verify Final stale-string sweep (log prefixes, test assertions) → 5 cosmetic items
7 Verify Directory rename + describe blocks + comments + capitalized log prefix → 2 doc/config items
8 Verify Skill-doc + npm test glob fixes → APPROVED

Friction Log

  • Module-name collisions with Takaro built-ins (turns 1-4): renaming modules to drop the test- prefix made modules/lottery/ and modules/utils/ collide with Takaro built-ins of the same name. Tests for those modules failed cryptically with "moduleId undefined" — pushModule couldn't delete the protected built-ins, so import never completed, and teardown crashed on the missing id. Strategic fix: rename to modules/jackpot/ and modules/server-toolkit/ rather than guard-hack around the collision. Reviewers should confirm no third Takaro built-in collides with our remaining 9 module names.

  • Host safety guard pattern matching (turns 2-3): the guard for assertTestSafeHost oscillated — turn 2 was too strict (rejected next.takaro.dev despite being the user's actual staging environment), turn 3 was too loose (accepted any .dev TLD including prod.dev). Final design: substring .takaro.dev for staging, single-label hostnames for Docker, RFC 6761 reserved TLDs (.test, .localhost). The 12 unit tests in modules-guard.test.ts lock this in.

  • Module rename completeness (turns 4-7): the lottery→jackpot/utils→server-toolkit rename took 4 incremental turns to fully propagate. Each pass missed a layer: turn 4 kept old internal identifiers (commands, permissions); turn 5 missed test trigger string + log assertion in one specific test file; turn 6 missed directory paths + describe blocks; turn 7 finally renamed source/test directories + describe blocks + comments + capitalized log prefix. Future similar refactors: grep -ri 'old-name' module-dir/ after each rename pass.

  • AGENTS.md guard documentation (turns 2-5): re-edited each turn with new minor issues until turn 5 took the one-shot rewrite approach (concise structure: error block + numbered fixes + auto-accepted hosts list). The pattern of "incrementally polishing prose introduces new mistakes" suggests doing one well-structured rewrite is faster than three rounds of edits.

Below-Threshold Issues (not blocking — for follow-up if useful)

  • (sev 2) AGENTS.md "CI:" line in safety guard section sits slightly detached from the auto-accepted hosts list — could prefix with "Note:" for cleaner reading.
  • (sev 2) assertTestSafeHost substring .takaro.dev would technically match attacker.takaro.dev.evil.com — dev-time guard, not an auth boundary; flagged for awareness only.
  • (sev 2) build-registry.ts rmSync invariant uses POSIX-style endsWith('dist/registry') — Windows would fail; acceptable since the README recommends WSL2 for Windows.
  • (sev 7, accepted) Codex repeatedly flagged that cleanupTestModules on a shared dev/staging tenant can delete modules other developers installed. Mitigated by assertTestSafeHost and documented in AGENTS.md. The tradeoff is real: getting strict isolation back would require a per-developer namespace or test-instance-per-developer.
  • (sev 8, accepted) Codex flagged that force_orphan: true + keep_files: false means published versions disappear from the registry branch when a newer version ships. Acceptable for v1.0.0 (no module has bumped yet) but revisit before any module hits 1.0.1 — the multi-version history work in Phase 6 of the plan addresses this.

Testing Plan

Happy Path

  • Pull this branch; run npm install --legacy-peer-deps then npm run build && npm run build:registry. Confirm dist/registry/registry.json exists and contains exactly 11 module entries (afk-kick, community-fund, daily-rewards, hello-world, jackpot, kill-tracker, mini-games, referral-program, server-messages, server-toolkit, vote-restart).
  • Inspect dist/registry/jackpot/1.0.0.json — verify command triggers include jackpotinfo (NOT lotteryinfo) and permissions are JACKPOT_*. Same for dist/registry/server-toolkit/1.0.0.json → TOOLKIT_* permissions.
  • Locally serve: python3 -m http.server 8765 --directory dist/registry. From a Takaro dev stack built off feature/module-registry-system with TAKARO_REGISTRY_ALLOWED_PRIVATE_HOSTS set to include host.docker.internal, subscribe to http://host.docker.internal:8765. Confirm 11 modules appear.
  • Install hello-world v1.0.0 from the registry. Confirm the module appears with the "Registry" chip, Edit/Delete are disabled, Copy is available.

Edge Cases

  • Run npm test against your local Takaro dev/staging — verify the host guard accepts your host (use TAKARO_TEST_HOST_ALLOWLIST if you have a non-standard hostname). The 12 unit tests in test/helpers/modules-guard.test.ts should all pass alongside the integration tests.
  • Run npm test with TAKARO_HOST=https://api.takaro.io — expect a clean refusal error pointing to AGENTS.md.
  • Temporarily corrupt modules/hello-world/module.json (set version: "latest" or name: "Hello World!") and run npm run build:registry — expect a clean exit-1 with a clear error message naming the offending module.
  • Bump modules/hello-world/module.json version to 1.0.1, change a command, re-run npm run build:registry — verify the new file appears at dist/registry/hello-world/1.0.1.json and the registry manifest's versions for hello-world updates.

Regression Checks

  • All 11 modules' integration tests still pass (especially modules/jackpot/test/* and modules/server-toolkit/test/server-toolkit.test.ts — those went through the most rename churn).
  • grep -ri 'lottery\|utils' modules/jackpot modules/server-toolkit --include='*.json' --include='*.js' --include='*.ts' returns ZERO hits (verifies the rename is comprehensive).
  • npm run module:to-json -- modules/hello-world still works (CLI wrapper around the refactored buildModuleExport).
  • npm run module:from-json and npm run module:migrate unaffected.

Coordinated Work Before Launch

  • Open a Takaro-side PR to relax the contentType.includes('json') check in packages/app-api/src/service/Registry/index.ts — raw.githubusercontent.com returns Content-Type: text/plain for .json files, so the strict check rejects the official URL. Either drop the check (DTO validation already rejects malformed bodies) or accept text/plain when the body parses as JSON. Without this, the published registry URL is unsubscribable from production Takaro.

Implements Phase 4 of Takaro PR #2723 (subscribable module registries).
Generates a registry.json manifest plus per-version JSON for each module
into dist/registry/ via build-registry.ts, and publishes the tree to a
`registry` branch on every push to main via a new GitHub Actions workflow.

Module changes:
- All 11 module.json files: name normalized to match directory (no test- prefix);
  version bumped from "latest" to "1.0.0" (registry requires semver)
- modules/lottery/ -> modules/jackpot/ (collided with Takaro built-in lottery);
  internal renames cascade to commands, permissions (LOTTERY_* -> JACKPOT_*),
  function file (lottery-helpers.js -> jackpot-helpers.js), storage keys,
  log prefixes, test assertions
- modules/utils/ -> modules/server-toolkit/ (collided with built-in utils);
  same cascade: TOOLKIT_* permissions, server-toolkit-{helpers,pure}.js,
  log prefixes, test assertions

Infrastructure:
- src/scripts/build-registry.ts (new): validates module names against the
  PR's manifest schema (regex/semver/length), writes dist/registry/ tree,
  clears stale artifacts before each build
- src/scripts/module-to-json.ts: refactored to expose buildModuleExport()
  function for reuse; safeRealpath() helper unifies broken-symlink errors
- src/utils/validate.ts: validateEntityName now throws (was process.exit)
- .github/workflows/publish-registry.yml (new): publishes to `registry`
  branch via peaceiris/actions-gh-pages; concurrency guard prevents
  overlapping pushes; force_orphan documented
- test/helpers/modules.ts: assertTestSafeHost() guard refuses to run
  pushModule/cleanupTestModules against production-looking hosts;
  cleanup discovers module names from filesystem (no manual list)
- test/helpers/modules-guard.test.ts (new): 12 unit tests for the guard;
  package.json test glob expanded to pick up test/helpers/*.test.ts

Documentation:
- AGENTS.md: module conventions (semver bump on change, name=directory,
  Takaro UI shows name verbatim), safety guard reference with full error
  template + numbered fix list + auto-accepted hosts list
- README.md: Published Registry section with public URL, status note
  about pending Takaro-side content-type fix, and dev local-serve
  workflow; warning that npm test refuses production hosts
- testing-methodology.md skill ref: updated to reflect new naming rule
@niekcandaele niekcandaele added the enhancement New feature or request label Apr 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4be685a82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/helpers/modules.ts
'.dev.', '.test.', '.ci.', '.staging.', '.local',
'-dev-', '-test-', '-ci-', '-staging-',
];
if (safePatterns.some((p) => host.includes(p))) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Match safety patterns against hostname only

assertTestSafeHost currently checks TAKARO_HOST with raw substring matching on the full URL string, so a production host can be misclassified as safe when test-like text appears outside the hostname (for example in userinfo/path/query, e.g. https://localhost@api.takaro.io). In that scenario the new guard is bypassed and pushModule/cleanupTestModules can still delete real modules on production.

Useful? React with 👍 / 👎.


// Safety invariant: confirm the path ends with 'dist/registry' before wiping it.
// This prevents accidental data loss if REPO_ROOT is misconfigured.
if (!DIST_REGISTRY_DIR.endsWith('dist/registry')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use path separator-safe check before rmSync

The safety invariant uses DIST_REGISTRY_DIR.endsWith('dist/registry'), which only matches POSIX separators. On Windows this path ends with dist\registry, so build:registry will always throw before writing output. This makes the new registry build script unusable for Windows-based development workflows.

Useful? React with 👍 / 👎.

@niekcandaele

Copy link
Copy Markdown
Contributor Author

Superseded by #71 (registry build, validator and GitHub Pages publish), part of gettakaro/takaro#4616. Posted by the epic-runner agent on behalf of @niekcandaele.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant