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

-[![Latest Release](https://img.shields.io/github/v/release/{organization}/{repo}?style=flat-square)][releases-page] -[![License](https://img.shields.io/github/license/{organization}/{repo}?style=flat-square)](LICENSE) +[![License](https://img.shields.io/github/license/wippyai/template-module?style=flat-square)](LICENSE) [![Documentation](https://img.shields.io/badge/Wippy-Documentation-brightgreen.svg?style=flat-square)][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:*