diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index e69de29..48188c7 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,21 @@
+# Contributing
+
+Pull requests are squash-merged, so the pull request title becomes the commit on `main` and drives
+the next version. The title must be a valid [Conventional Commit][conventional-commits]; CI rejects
+it otherwise.
+
+Allowed types: `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `perf`, `build`, `ci`, `style`,
+`revert`.
+
+- `fix:` triggers a patch release.
+- `feat:` triggers a minor release.
+- A `feat!:` prefix or a `BREAKING CHANGE:` footer triggers a major release.
+
+Before opening a pull request, run the checks locally from the module directory:
+
+```sh
+wippy lint --level error
+wippy test
+```
+
+[conventional-commits]: https://www.conventionalcommits.org
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f7e26b0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,12 @@
+name: CI
+
+on:
+ pull_request:
+ types: [opened, edited, synchronize, reopened]
+ push:
+ branches: [main]
+
+jobs:
+ ci:
+ if: ${{ github.repository != 'wippyai/template-module' }}
+ uses: wippyai/template-module/.github/workflows/reusable-ci.yml@v1
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index e2f935e..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: Publish in the Wippy Registry
-
-on:
- push:
- tags:
- - 'v*'
-
-concurrency:
- group: publish
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- steps:
- - name: Check out repository
- uses: actions/checkout@v5
-
- - name: Publishing
- uses: wippyai/action-module-release@main
- with:
- directory: .
- tag: ${{ github.ref_name }}
- username: ${{ secrets.WIPPY_USERNAME }}
- password: ${{ secrets.WIPPY_PASSWORD }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..793e5f8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,15 @@
+name: Release
+
+on:
+ push:
+ branches: [main]
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ release:
+ if: ${{ github.repository != 'wippyai/template-module' }}
+ uses: wippyai/template-module/.github/workflows/reusable-release.yml@v1
+ secrets: inherit
diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml
new file mode 100644
index 0000000..495efc9
--- /dev/null
+++ b/.github/workflows/reusable-ci.yml
@@ -0,0 +1,76 @@
+name: Reusable CI
+
+on:
+ workflow_call:
+ inputs:
+ wippy-version:
+ description: Tag of the wippyai/runtime release providing the wippy binary
+ type: string
+ default: v0.3.12a
+ module-dir:
+ description: Directory containing wippy.yaml
+ type: string
+ default: .
+ lint-level:
+ description: Minimum lint severity that fails the build (error, warning, hint)
+ type: string
+ default: error
+
+permissions:
+ contents: read
+
+jobs:
+ lint-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v7
+
+ - name: Install wippy runtime
+ env:
+ GH_TOKEN: ${{ github.token }}
+ WIPPY_VERSION: ${{ inputs.wippy-version }}
+ run: |
+ set -euo pipefail
+ tmp="$(mktemp -d)"
+ gh release download "$WIPPY_VERSION" --repo wippyai/runtime \
+ --pattern wippy-linux-amd64 --pattern SHA256SUMS --dir "$tmp"
+ ( cd "$tmp" && sha256sum --ignore-missing -c SHA256SUMS )
+ mkdir -p "$HOME/.wippy/bin"
+ install -m 0755 "$tmp/wippy-linux-amd64" "$HOME/.wippy/bin/wippy"
+ echo "$HOME/.wippy/bin" >> "$GITHUB_PATH"
+ "$HOME/.wippy/bin/wippy" version
+
+ - name: Lint
+ working-directory: ${{ inputs.module-dir }}
+ env:
+ LINT_LEVEL: ${{ inputs.lint-level }}
+ run: wippy lint --level "$LINT_LEVEL" --no-color
+
+ - name: Test
+ working-directory: ${{ inputs.module-dir }}
+ run: wippy test --silent
+
+ pr-title:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ steps:
+ - name: Enforce Conventional Commit pull request title
+ uses: amannn/action-semantic-pull-request@v6
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ with:
+ types: |
+ feat
+ fix
+ chore
+ refactor
+ docs
+ test
+ perf
+ build
+ ci
+ style
+ revert
diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml
new file mode 100644
index 0000000..36d3eab
--- /dev/null
+++ b/.github/workflows/reusable-release.yml
@@ -0,0 +1,104 @@
+name: Reusable Release
+
+on:
+ workflow_call:
+ inputs:
+ wippy-version:
+ description: Tag of the wippyai/runtime release providing the wippy binary
+ type: string
+ default: v0.3.12a
+ module-dir:
+ description: Directory containing wippy.yaml
+ type: string
+ default: .
+ publish:
+ description: Publish to the registry when a release is cut
+ type: boolean
+ default: true
+ secrets:
+ RELEASE_PLEASE_APP_ID:
+ description: GitHub App ID for release-please (preferred over RELEASE_PLEASE_TOKEN)
+ required: false
+ RELEASE_PLEASE_APP_PRIVATE_KEY:
+ description: GitHub App private key paired with RELEASE_PLEASE_APP_ID
+ required: false
+ RELEASE_PLEASE_TOKEN:
+ description: PAT fallback for release-please when no GitHub App is configured
+ required: false
+ WIPPY_TOKEN:
+ description: Registry token used by wippy publish
+ required: false
+ WIPPY_REGISTRY:
+ description: Registry URL override (defaults to the wippy built-in)
+ required: false
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Detect GitHub App configuration
+ id: cfg
+ env:
+ APP_ID: ${{ secrets.RELEASE_PLEASE_APP_ID }}
+ run: |
+ set -euo pipefail
+ if [ -n "${APP_ID:-}" ]; then
+ echo "use_app=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "use_app=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Mint GitHub App token
+ id: app-token
+ if: ${{ steps.cfg.outputs.use_app == 'true' }}
+ uses: actions/create-github-app-token@v3
+ with:
+ app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
+
+ - name: Run release-please
+ id: release
+ uses: googleapis/release-please-action@v5
+ with:
+ token: ${{ steps.app-token.outputs.token || secrets.RELEASE_PLEASE_TOKEN || github.token }}
+ config-file: release-please-config.json
+ manifest-file: .release-please-manifest.json
+
+ - name: Check out the released revision
+ if: ${{ steps.release.outputs.release_created && inputs.publish }}
+ uses: actions/checkout@v7
+
+ - name: Install wippy runtime
+ if: ${{ steps.release.outputs.release_created && inputs.publish }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ WIPPY_VERSION: ${{ inputs.wippy-version }}
+ run: |
+ set -euo pipefail
+ tmp="$(mktemp -d)"
+ gh release download "$WIPPY_VERSION" --repo wippyai/runtime \
+ --pattern wippy-linux-amd64 --pattern SHA256SUMS --dir "$tmp"
+ ( cd "$tmp" && sha256sum --ignore-missing -c SHA256SUMS )
+ mkdir -p "$HOME/.wippy/bin"
+ install -m 0755 "$tmp/wippy-linux-amd64" "$HOME/.wippy/bin/wippy"
+ echo "$HOME/.wippy/bin" >> "$GITHUB_PATH"
+
+ - name: Publish to the Wippy registry
+ if: ${{ steps.release.outputs.release_created && inputs.publish }}
+ working-directory: ${{ inputs.module-dir }}
+ env:
+ WIPPY_TOKEN: ${{ secrets.WIPPY_TOKEN }}
+ WIPPY_REGISTRY: ${{ secrets.WIPPY_REGISTRY }}
+ VERSION: ${{ steps.release.outputs.version }}
+ run: |
+ set -euo pipefail
+ if [ -z "${WIPPY_TOKEN:-}" ]; then
+ echo "WIPPY_TOKEN secret is required to publish" >&2
+ exit 1
+ fi
+ [ -z "${WIPPY_REGISTRY:-}" ] && unset WIPPY_REGISTRY
+ wippy publish --version "$VERSION" --create
diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml
new file mode 100644
index 0000000..19a3fdd
--- /dev/null
+++ b/.github/workflows/self-test.yml
@@ -0,0 +1,12 @@
+name: Self Test
+
+on:
+ pull_request:
+ types: [opened, edited, synchronize, reopened]
+ push:
+ branches: [main]
+
+jobs:
+ ci:
+ if: ${{ github.repository == 'wippyai/template-module' }}
+ uses: ./.github/workflows/reusable-ci.yml
diff --git a/.github/workflows/tag-major.yml b/.github/workflows/tag-major.yml
new file mode 100644
index 0000000..54fa597
--- /dev/null
+++ b/.github/workflows/tag-major.yml
@@ -0,0 +1,26 @@
+name: Update major tag
+
+on:
+ push:
+ tags:
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+
+permissions:
+ contents: write
+
+jobs:
+ move-major:
+ if: ${{ github.repository == 'wippyai/template-module' }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v7
+
+ - name: Move the major tag to this release
+ env:
+ TAG: ${{ github.ref_name }}
+ run: |
+ set -euo pipefail
+ major="${TAG%%.*}"
+ git tag -f "$major"
+ git push -f origin "refs/tags/$major"
diff --git a/.gitignore b/.gitignore
index dc9ef66..9e4011e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
/runtime
/dist
/build
+/.wippy
+*.wapp
# Dependency directories
/node_modules
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..e18ee07
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.0.0"
+}
diff --git a/README.md b/README.md
index 7dcc9c1..74724c4 100644
--- a/README.md
+++ b/README.md
@@ -6,51 +6,147 @@
-{Package Name}
+Wippy Module Template
-[][releases-page]
-[](LICENSE)
+[](LICENSE)
[][wippy-documentation]
-{description}
+A template for building, testing, and publishing a [Wippy][wippy] module. It ships GitHub Actions
+that lint and test the module on every pull request, then automatically version and publish it to
+the [Wippy registry][modules-registry] on every merge to `main`, driven by
+[Conventional Commits][conventional-commits].
----
+The repository is itself a working example module (a `greeting` library plus a test) so its own CI
+is green out of the box. Replace the example with your code.
-## TODO
+## What you get
-### Prepare the repository
+- Lint and test on every pull request, using the `wippy` CLI from the [runtime][runtime].
+- The pull request title is enforced to be a valid Conventional Commit.
+- Automated semantic versioning and registry publishing via [release-please][release-please].
+- A single source of truth: the heavy lifting lives in reusable workflows hosted here; modules
+ call them by reference, so the pipeline is updated in one place for every module.
-- Replace all `{repo}` with the actual repository name.
-- Replace all `{organization}` with the actual organization name (e.g., `wippyai`).
-- Replace all `{vendor}` with the actual module namespace (e.g., `wippy`).
-- Replace all `{package}` with the actual package name (e.g., `llm`).
-- Replace all `{Package Name}` with the actual human-readable package name.
-- Replace the `{description}` with the actual package short description.
-- Fill the `.github/CODEOWNERS` with the actual owners.
- - Configure access to the repository for the owners in https://github.com/{organization}/{repo}/settings/access
-- Check that the `LICENSE` file is present and contains the correct license information.
-- Customize the `CONTRIBUTING.md` file with the actual contribution guidelines or remove it if not needed.
-- Fill the blocks below with the actual information or remove them if not needed.
+## Versioning
-### Register the module in the Wippy registry
+The version lives in `wippy.yaml` and is bumped automatically from the Conventional Commit
+messages merged since the last release:
-The repository uses the [Github Action](https://github.com/wippyai/action-module-release)
-to automatically register the module in the [Wippy registry][modules-registry] on every release.
-To trigger the registration, you need to create a new tag in the format `vX.Y.Z` (e.g., `v1.0.0`).
+| Commit | Bump | Example |
+| --- | --- | --- |
+| `fix:` | patch | `1.2.3` to `1.2.4` |
+| `feat:` | minor | `1.2.3` to `1.3.0` |
+| `feat!:` or a `BREAKING CHANGE:` footer | major | `1.2.3` to `2.0.0` |
+| `chore:`, `docs:`, `refactor:`, `test:`, `ci:`, ... | none on their own | no release |
-Before you can use the action, you need [to set up](https://github.com/{organization}/{repo}/settings/secrets/actions) the following repository secrets.
+Because merges use squash, the pull request title becomes the commit message, which is why a valid
+Conventional Commit title is required.
-- `WIPPY_USERNAME` - the UUID of the user in the Wippy registry.
-- `WIPPY_PASSWORD` - the password for the user in the Wippy registry.
+Pre-1.0 note: by default a breaking change on a `0.x` module bumps to `1.0.0`. To remain in `0.x`,
+add `"bump-minor-pre-major": true` to `release-please-config.json`.
-Log in [Wippy registry][modules-registry] and use [PackCli tool][packcli] to register the new module.
+## How a release happens
----
+1. Merge pull requests to `main` as usual.
+2. release-please opens and maintains a release pull request that bumps `version` in `wippy.yaml`,
+ updates `CHANGELOG.md`, and updates `.release-please-manifest.json`.
+3. Merge the release pull request. release-please creates the git tag and GitHub release, and the
+ same workflow run then executes `wippy publish`, pushing the new immutable version to the
+ registry. (Publishing happens in the same run, so it does not depend on the tag event.)
+## Use this template for a new module
+
+1. Click "Use this template".
+2. In `wippy.yaml`, set `organization`, `module`, and `description`. Keep `version` at `0.0.0` and
+ keep `.release-please-manifest.json` at `0.0.0`; release-please takes over from there.
+3. Rename the namespace in `src/_index.yaml` and `src/test/_index.yaml`, and update the `exclude`
+ entry pattern in `wippy.yaml` so it matches your test namespace (it keeps test-only entries out
+ of the published artifact).
+4. Set `package-name` in `release-please-config.json`.
+5. Replace the example `greeting` library and `src/test` with your own entries and tests. Update
+ `src/README.md`, which is the registry-facing README referenced by the `ns.definition` entry.
+6. Add a `WIPPY_TOKEN` repository secret (a registry token) and the release-please App secrets
+ `RELEASE_PLEASE_APP_ID` / `RELEASE_PLEASE_APP_PRIVATE_KEY` (or a `RELEASE_PLEASE_TOKEN` PAT) —
+ see Notes. Optionally add `WIPPY_REGISTRY` to target a non-default registry.
+7. `.github/workflows/ci.yml` and `release.yml` already call the central reusable workflows at
+ `wippyai/template-module@v1`. You may delete `self-test.yml` and the two `reusable-*.yml`
+ files; they only run inside this template repository.
+
+## Migrate an existing module
+
+Add the following to your module repository and open a pull request:
+
+- `wippy.yaml` with `organization`, `module`, `version`, the `ns.definition` README reference, and
+ any `exclude` patterns for test-only entries.
+- `release-please-config.json` (set `package-name` to your module) and
+ `.release-please-manifest.json`, seeding the manifest with the module's current published version.
+- `.github/workflows/ci.yml` and `.github/workflows/release.yml` copied from this template (they
+ reference `wippyai/template-module@v1`).
+- `WIPPY_TOKEN` and the release-please App secrets `RELEASE_PLEASE_APP_ID` /
+ `RELEASE_PLEASE_APP_PRIVATE_KEY` (or a `RELEASE_PLEASE_TOKEN` PAT) — see Notes.
+
+## Notes
+
+- The `wippyai` organization blocks GitHub Actions from creating pull requests, so release-please
+ cannot open the release pull request with the default `GITHUB_TOKEN`. Configure a GitHub App
+ (recommended): create an App with `contents: write` and `pull requests: write`, install it on the
+ repository, and set `RELEASE_PLEASE_APP_ID` and `RELEASE_PLEASE_APP_PRIVATE_KEY` as
+ org/repository secrets. The reusable release workflow mints a short-lived token from the App. A
+ `RELEASE_PLEASE_TOKEN` PAT is supported as a fallback, and it falls back to `GITHUB_TOKEN` if
+ neither is set (which only works if the org enables "Allow GitHub Actions to create and approve
+ pull requests").
+- A release pull request opened with a GitHub App or PAT token triggers `pull_request` workflows;
+ one opened with `GITHUB_TOKEN` does not. Configure the App or PAT if you want the CI and PR-title
+ checks to run on the release pull request.
+- CI downloads the `wippy` binary from public `wippyai/runtime` releases using the repository
+ `GITHUB_TOKEN`.
+
+## Maintaining this template
+
+The reusable workflows are consumed at `wippyai/template-module@v1`. Releasing the template means
+pushing a `vX.Y.Z` tag on `main`; `tag-major.yml` then moves the floating `v1` tag to it, so
+consumers pinned to `@v1` pick up the change. Create the first `v1` line with:
+
+```sh
+git tag v1.0.0 && git push origin v1.0.0
+```
+
+## Local development
+
+Install the `wippy` CLI by downloading a release binary from [runtime releases][runtime-releases],
+then from the module directory:
+
+```sh
+wippy lint --level error
+wippy test
+wippy publish --dry-run --version 0.1.0
+```
+
+## Layout
+
+```
+.
+├── .github/workflows/
+│ ├── ci.yml # calls the reusable CI on pull requests and pushes
+│ ├── release.yml # calls the reusable release on pushes to main
+│ ├── reusable-ci.yml # lint, test, and PR-title check (reusable)
+│ ├── reusable-release.yml # release-please and publish (reusable)
+│ ├── self-test.yml # runs the reusable CI against this template only
+│ └── tag-major.yml # moves the v1 tag when a vX.Y.Z tag is pushed (template only)
+├── release-please-config.json
+├── .release-please-manifest.json
+├── wippy.yaml # module manifest (organization, module, version)
+├── wippy.lock # dependency lock
+└── src/ # module source and tests
+```
+
+[wippy]: https://wippy.ai
[wippy-documentation]: https://docs.wippy.ai
-[releases-page]: https://github.com/{organization}/{repo}/releases
-[packcli]: https://github.com/wippyai/wippy-releases/releases
[modules-registry]: https://modules.wippy.ai
+[conventional-commits]: https://www.conventionalcommits.org
+[release-please]: https://github.com/googleapis/release-please
+[runtime]: https://github.com/wippyai/runtime
+[runtime-releases]: https://github.com/wippyai/runtime/releases
diff --git a/packcli.yaml b/packcli.yaml
deleted file mode 100644
index 74701ea..0000000
--- a/packcli.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-name: {vendor}/{package}
-dir: src
-description: {description}
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..171639e
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+ "release-type": "simple",
+ "packages": {
+ ".": {
+ "package-name": "example",
+ "changelog-path": "CHANGELOG.md",
+ "include-component-in-tag": false,
+ "extra-files": [
+ {
+ "type": "yaml",
+ "path": "wippy.yaml",
+ "jsonpath": "$.version"
+ }
+ ]
+ }
+ }
+}
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..48feae6
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,8 @@
+# Example Module
+
+This is the registry-facing README for the published Wippy module. It is referenced by the
+`ns.definition` entry in `src/_index.yaml` (`readme: file://README.md`) and rendered on the
+module page in the Wippy registry.
+
+Replace this text with documentation for your module: what it provides, the entries it exposes,
+and how to depend on it from another Wippy application.
diff --git a/src/_index.yaml b/src/_index.yaml
index 8b33e64..b45d414 100644
--- a/src/_index.yaml
+++ b/src/_index.yaml
@@ -1,2 +1,16 @@
-version: "1.0.0"
-namespace: {vendor}.{package}
+version: "1.0"
+namespace: wippy.example
+
+entries:
+ - name: definition
+ kind: ns.definition
+ readme: file://README.md
+ meta:
+ title: Example Module
+ description: Example Wippy module scaffolded from the template
+
+ - name: greeting
+ kind: library.lua
+ meta:
+ comment: Example greeting library shipped by the module
+ source: file://greeting.lua
diff --git a/src/greeting.lua b/src/greeting.lua
new file mode 100644
index 0000000..ddecff9
--- /dev/null
+++ b/src/greeting.lua
@@ -0,0 +1,7 @@
+local greeting = {}
+
+function greeting.greet(name)
+ return "Hello, " .. tostring(name) .. "!"
+end
+
+return greeting
diff --git a/src/test/_index.yaml b/src/test/_index.yaml
new file mode 100644
index 0000000..0251aac
--- /dev/null
+++ b/src/test/_index.yaml
@@ -0,0 +1,46 @@
+version: "1.0"
+namespace: wippy.example.test
+
+entries:
+ - name: processes
+ kind: process.host
+ meta:
+ comment: Process host used to run the test entrypoint
+ host:
+ max_processes: 1000
+ workers: 8
+ lifecycle:
+ auto_start: true
+
+ - name: terminal
+ kind: terminal.host
+ meta:
+ comment: Terminal host used to exec the test runner
+ lifecycle:
+ auto_start: true
+
+ - name: runner
+ kind: process.lua
+ meta:
+ comment: Discovers and runs every entry with meta.type=test
+ command:
+ name: test
+ short: Run module tests
+ use_case: test
+ source: file://runner.lua
+ method: main
+ modules:
+ - io
+ - registry
+ - funcs
+
+ - name: greeting
+ kind: function.lua
+ meta:
+ type: test
+ suite: greeting
+ description: greeting.greet returns a formatted greeting
+ source: file://greeting_test.lua
+ method: main
+ imports:
+ greeting: wippy.example:greeting
diff --git a/src/test/greeting_test.lua b/src/test/greeting_test.lua
new file mode 100644
index 0000000..a4f7ed8
--- /dev/null
+++ b/src/test/greeting_test.lua
@@ -0,0 +1,12 @@
+local greeting = require("greeting")
+
+local function main()
+ local result = greeting.greet("Wippy")
+ if result ~= "Hello, Wippy!" then
+ error("expected 'Hello, Wippy!', got '" .. tostring(result) .. "'")
+ end
+
+ return true
+end
+
+return { main = main }
diff --git a/src/test/runner.lua b/src/test/runner.lua
new file mode 100644
index 0000000..b4d7f98
--- /dev/null
+++ b/src/test/runner.lua
@@ -0,0 +1,61 @@
+local io = require("io")
+local registry = require("registry")
+local funcs = require("funcs")
+
+local function run_one(id)
+ local ok, result, err = pcall(function()
+ return funcs.call(id)
+ end)
+
+ if not ok then
+ return false, tostring(result)
+ end
+
+ if err then
+ return false, tostring(err)
+ end
+
+ if result == false then
+ return false, "test returned false"
+ end
+
+ return true, nil
+end
+
+local function main()
+ local entries, err = registry.find({ ["meta.type"] = "test" })
+ if err then
+ io.print("registry error: " .. tostring(err))
+ return 1
+ end
+
+ if not entries or #entries == 0 then
+ io.print("no tests found")
+ return 0
+ end
+
+ local passed = 0
+ local failed = 0
+
+ for _, entry in ipairs(entries) do
+ local ok, message = run_one(entry.id)
+ if ok then
+ passed = passed + 1
+ io.print("PASS " .. entry.id)
+ else
+ failed = failed + 1
+ io.print("FAIL " .. entry.id .. ": " .. tostring(message))
+ end
+ end
+
+ io.print("")
+ io.print(tostring(passed) .. " passed, " .. tostring(failed) .. " failed")
+
+ if failed > 0 then
+ return 1
+ end
+
+ return 0
+end
+
+return { main = main }
diff --git a/wippy.lock b/wippy.lock
new file mode 100644
index 0000000..0f48e24
--- /dev/null
+++ b/wippy.lock
@@ -0,0 +1,3 @@
+directories:
+ modules: .wippy
+ src: ./src
diff --git a/wippy.yaml b/wippy.yaml
new file mode 100644
index 0000000..3f1b9e3
--- /dev/null
+++ b/wippy.yaml
@@ -0,0 +1,12 @@
+organization: wippy
+module: example
+version: 0.0.0
+description: Example Wippy module scaffolded from the template
+license: MPL-2.0
+repository: https://github.com/wippyai/template-module
+homepage: https://wippy.ai
+keywords:
+ - example
+ - template
+exclude:
+ - wippy.example.test:*