End-to-end walkthrough: author + register + sign + publish + assign a custom NodeModule. Companion seed: db/seeds/example_custom_module.rb (Phase 3).
Goal: demonstrate the full module supply chain — from blank git repo to assigned-to-Template — with concrete commands.
Audience: module authors, platform contributors, external developers consuming the system extension.
Prerequisites:
- Gitea account at
registry.example.comwith permission to create repos under your account docker+oras+cosignCLI installed locally- A NodePlatform you'll assign the module to (e.g., ubuntu-24.04-amd64)
git clone git@registry.example.com:powernode/templates/module-repo.git my-redis-module
cd my-redis-module
rm -rf .git
git init
git remote add origin git@registry.example.com:<account>/modules/my-redis-module.gitschema_version: 1
identity:
name: my-redis
category: userland
variety: subscription
description: Redis 7.4 with TLS + persistence
cosign_identity_regexp: '^https://git\.ipnode\.org/<account>/modules/my-redis-module@.*$'
cosign_issuer_regexp: '^https://gitea\.ipnode\.org$'
package_spec:
- redis-server
- redis-tools
file_spec:
include:
- "/etc/redis/**"
- "/var/lib/redis/.gitkeep"
exclude:
- "/etc/redis/sentinel.conf"
protected_spec:
- "/etc/redis/redis.conf" # this module owns the main config
dependency_spec:
- name: system-base
- name: security-hardeningmkdir -p rootfs/etc/redis rootfs/var/lib/redis
touch rootfs/var/lib/redis/.gitkeep # keeps the empty data dir in the artifact# rootfs/etc/redis/redis.conf
bind 0.0.0.0 ::
port 6379
protected-mode yes
tls-port 6380
tls-cert-file /etc/redis/tls/server.crt
tls-key-file /etc/redis/tls/server.key
tls-ca-cert-file /etc/redis/tls/ca.crt
appendonly yes
dir /var/lib/redisplatform.system_validate_module_manifest({ // ⚠️ aspirational — see project_system_mcp_gaps
manifest_yaml: <contents of manifest.yaml>,
category_slug: "userland"
})
// → { valid: true, warnings: [], conflicts: [] }Note: until
system_validate_module_manifestships, run a local syntax check via the builder image:docker run --rm -v $PWD:/work:ro ghcr.io/powernode/module-builder:latest --dry-run
git add manifest.yaml Containerfile rootfs/ .gitea/
git commit -m "feat: my-redis module v0.1.0"
git tag v0.1.0
git push origin develop --tagsThe .gitea/workflows/build.yaml triggers on tag push. Watch progress:
platform.list_gitea_workflow_runs({ owner: "<account>", repo: "modules/my-redis-module" })
// → { runs: [{ id, status: "in_progress", ... }] }The workflow:
- Runs the builder image with
manifest.yaml+rootfs/→ emits artifact tar atdist/module.tar - Pushes to OCI:
oras push registry.example.com/<account>/modules/my-redis-module:v0.1.0 ./dist/module.tar:application/vnd.powernode.module.v1+tar - Signs with Cosign (keyless via Sigstore Fulcio):
cosign sign --yes <artifact-ref>
After the workflow completes (~5 min), the platform's ModuleOciIngestService polls the registry and creates a NodeModuleVersion row in lifecycle_state: draft.
platform.system_list_module_versions({ module_name: "my-redis" })
// → { versions: [{
// id: "v-redis-0.1.0",
// version_string: "0.1.0",
// lifecycle_state: "draft",
// composefs_digest: "sha256:abc...",
// ...
// }] }platform.system_promote_module_version({ id: "v-redis-0.1.0", to: "staging" })
// Test on a non-prod NodeInstance...
platform.system_promote_module_version({ id: "v-redis-0.1.0", to: "blessed" })
// Operator review passed; module is recommendable
platform.system_promote_module_version({ id: "v-redis-0.1.0", to: "live" })
// Now eligible for fleet-wide rolloutplatform.system_assign_module_to_template({
template_id: "<your-template>",
module_name: "my-redis"
})
// → assignment created; instances built from this template will get my-redis on next reconcileplatform.system_get_instance({ id: "<instance-from-the-template>" })
// → { instance: {
// running_module_digests: { "my-redis": "sha256:abc...", ... },
// ...
// }}
platform.system_drift_report({ instance_id: "<id>" })
// → { drift: false }SSH (or system_execute_task) to the instance:
systemctl status redis-server.service
# → active (running)
redis-cli ping
# → PONG- Cosign identity regex must match the OIDC issuer of your Gitea Actions runs — if the regex doesn't match, ingestion rejects the artifact
protected_speccollisions — if a higher-priority module owns/etc/redis/redis.conf, your file_spec is silently dropped during composition. Usemaskin a config-variety override module if you need to carve out.- Module-Builder image version drift — pin to a specific tag (
module-builder:1.2.0) in your Containerfile for reproducibility - Promotion to
liveisrequire_approvalin many setups — checkmodule_promote_to_liveintervention policy
runbooks/module-authoring.md— full reference for manifest fields + variety typestemplates/module-repo/— canonical layout this example clones fromtemplates/example-modules/— 7 working examples (nginx, apache, chrony, security-hardening, system-base, rpi4-firmware)DISK_IMAGE_CI.md— companion pipeline for base disk images (different from modules)