diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml new file mode 100644 index 0000000..1831622 --- /dev/null +++ b/.github/workflows/build-pages.yml @@ -0,0 +1,54 @@ +name: Build & Deploy Registry + +on: + push: + branches: [main] + repository_dispatch: + types: [plugin-release] + schedule: + - cron: '0 6 * * *' + workflow_dispatch: {} + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Build static index + run: bash scripts/build-index.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build version data + run: bash scripts/build-versions.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: v1 + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3517b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +v1/ diff --git a/README.md b/README.md index c79b42c..dacf42c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,26 @@ # workflow-registry [![Validate Registry](https://github.com/GoCodeAlone/workflow-registry/actions/workflows/validate.yml/badge.svg)](https://github.com/GoCodeAlone/workflow-registry/actions/workflows/validate.yml) +[![Build & Deploy](https://github.com/GoCodeAlone/workflow-registry/actions/workflows/build-pages.yml/badge.svg)](https://github.com/GoCodeAlone/workflow-registry/actions/workflows/build-pages.yml) The official plugin and template registry for the [GoCodeAlone/workflow](https://github.com/GoCodeAlone/workflow) engine. +**Registry API**: `https://gocodealone.github.io/workflow-registry/v1/` + This registry catalogs all built-in plugins, community extensions, and reusable templates that can be used with the workflow engine. It serves as the source of truth for the `wfctl` CLI's marketplace and `wfctl publish` command. ## Table of Contents - [What is this?](#what-is-this) -- [Browsing Plugins](#browsing-plugins) +- [Usage via wfctl](#usage-via-wfctl) - [Plugin Tiers](#plugin-tiers) - [Built-in Plugins](#built-in-plugins) +- [External Plugins](#external-plugins) - [Templates](#templates) - [Schema](#schema) -- [Submitting a Community Plugin](#submitting-a-community-plugin) -- [Plugin Manifest Format](#plugin-manifest-format) +- [Submitting a Plugin](#submitting-a-plugin) +- [Automatic Version Tracking](#automatic-version-tracking) +- [Registry Structure](#registry-structure) --- @@ -30,21 +35,28 @@ The registry is consumed by: - `wfctl marketplace` — browse and search available plugins - `wfctl publish` — submit your plugin to the registry - The workflow UI Marketplace page +- The static JSON API at `https://gocodealone.github.io/workflow-registry/v1/` --- -## Browsing Plugins - -Plugins are organized under `plugins//manifest.json`. Each manifest describes the plugin's capabilities, version, tier, and source location. - -To search via CLI: +## Usage via wfctl ```bash +# Search for plugins by keyword wfctl marketplace search http -wfctl marketplace info http -``` -To browse manually, see the [`plugins/`](./plugins/) directory. +# Get details for a specific plugin +wfctl marketplace info payments + +# Install a plugin into your project +wfctl install payments + +# List all installed plugins +wfctl plugin list + +# Update all plugins to latest versions +wfctl plugin update +``` --- @@ -154,25 +166,37 @@ Every pull request and push to `main` triggers the [Validate Registry](.github/w 1. Validates all `plugins/*/manifest.json` files against `schema/registry-schema.json` (JSON Schema draft 2020-12 via `ajv-cli`) 2. Checks that every plugin referenced in `templates/*.yaml` has a corresponding manifest +The [Build & Deploy](.github/workflows/build-pages.yml) workflow runs on every push to `main`, on a daily schedule, and whenever a plugin sends a `plugin-release` dispatch event. It: + +1. Generates `v1/index.json` from all manifests +2. Queries GitHub Releases for each plugin to build `v1/plugins//versions.json` +3. Deploys the `v1/` directory to GitHub Pages + PRs that fail validation cannot be merged. --- -## Submitting a Community Plugin +## Submitting a Plugin + +### Step-by-step PR Process 1. **Fork** this repository 2. **Create** a directory under `plugins//` 3. **Add** a `manifest.json` that conforms to the [registry schema](./schema/registry-schema.json) -4. **Validate** your manifest against the schema -5. **Open a PR** with a description of your plugin +4. **Validate** your manifest locally: + ```bash + bash scripts/validate-manifests.sh + ``` +5. **Open a PR** with a description of your plugin, what it provides, and a link to the source repository ### Manifest Requirements - `name`, `version`, `author`, `description`, `type`, `tier`, `license` are required - `type` must be `"external"` for community plugins (only GoCodeAlone sets `"builtin"`) - `tier` must be `"community"` for third-party submissions -- `source` should point to the public repository where the plugin lives +- `repository` should point to the public GitHub repository where the plugin lives - `capabilities.moduleTypes`, `stepTypes`, `triggerTypes`, `workflowHandlers` must accurately reflect what the plugin registers +- `private: true` must be set for plugins that are not publicly installable ### Review Process @@ -184,6 +208,67 @@ PRs are reviewed by maintainers for: --- +## Automatic Version Tracking + +When you publish a new release of your plugin, you can automatically trigger a registry rebuild so that `v1/plugins//versions.json` and `v1/plugins//latest.json` are updated within minutes. + +See [`templates/notify-registry.yml`](./templates/notify-registry.yml) for the reusable workflow snippet to add to your plugin's release workflow. + +**Setup**: +1. Create a GitHub PAT with `repo` scope for `GoCodeAlone/workflow-registry` +2. Add it as a secret named `REGISTRY_PAT` in your plugin repo +3. Copy the `notify-registry` job from the template into your `.github/workflows/release.yml` + +The registry rebuilds daily at 06:00 UTC as a fallback even without dispatch events. + +--- + +## Registry Structure + +``` +workflow-registry/ +├── plugins/ # Source of truth — one directory per plugin +│ └── / +│ └── manifest.json # Plugin metadata and capabilities +├── templates/ # Reusable workflow config templates +│ ├── notify-registry.yml # Action snippet for plugin release notifications +│ └── *.yaml # Workflow starter templates +├── schema/ +│ └── registry-schema.json # JSON Schema for manifest validation +├── scripts/ +│ ├── build-index.sh # Generates v1/index.json +│ ├── build-versions.sh # Queries GitHub Releases → v1/plugins/*/versions.json +│ ├── validate-manifests.sh # CI manifest validation +│ └── validate-templates.sh # CI template validation +├── .github/workflows/ +│ ├── validate.yml # PR validation gate +│ └── build-pages.yml # Build and deploy static registry to GitHub Pages +└── v1/ # Generated — served via GitHub Pages (not committed) + ├── index.json # Array of all plugin summaries, sorted by name + └── plugins/ + └── / + ├── manifest.json # Copy of source manifest + ├── versions.json # Release history from GitHub + └── latest.json # Latest release entry only +``` + +### Static API Endpoints + +| Endpoint | Description | +|----------|-------------| +| `GET /v1/index.json` | All plugin summaries (name, description, version, capabilities, ...) | +| `GET /v1/plugins//manifest.json` | Full manifest for a specific plugin | +| `GET /v1/plugins//versions.json` | All release versions with download URLs | +| `GET /v1/plugins//latest.json` | Latest release version only | + +--- + +## Plugin Authoring Guide + +See the [Plugin Manifest Format](#plugin-manifest-format) section below and the [registry schema](./schema/registry-schema.json) for a complete reference on building, testing, and publishing a workflow engine plugin. + +--- + ## Plugin Manifest Format ```json @@ -198,6 +283,7 @@ PRs are reviewed by maintainers for: "tier": "community", "license": "MIT", "minEngineVersion": "0.1.0", + "repository": "https://github.com/yourorg/my-plugin", "keywords": ["tag1", "tag2"], "capabilities": { "moduleTypes": ["mymodule.type"], diff --git a/scripts/build-index.sh b/scripts/build-index.sh new file mode 100755 index 0000000..790991d --- /dev/null +++ b/scripts/build-index.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# scripts/build-index.sh +# +# Generates v1/index.json — an array of plugin summaries sorted by name. +# Also copies each manifest to v1/plugins//manifest.json. +# +# Requires: jq + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +PLUGINS_DIR="${REPO_ROOT}/plugins" +OUT_DIR="${REPO_ROOT}/v1" + +if ! command -v jq &>/dev/null; then + echo "error: jq is required but not found in PATH" >&2 + exit 1 +fi + +echo "Building registry index..." + +mkdir -p "${OUT_DIR}/plugins" + +# Collect summaries from all plugin manifests, sorted by name +summaries="[]" + +while IFS= read -r manifest; do + plugin_name="$(basename "$(dirname "${manifest}")")" + + # Validate it's readable JSON + if ! jq empty "${manifest}" 2>/dev/null; then + echo "warning: skipping invalid JSON at ${manifest}" >&2 + continue + fi + + # Extract summary fields. + # Use the directory name as the canonical "name" so that it matches the + # v1/plugins// API path, even if the manifest's "name" field differs. + summary="$(jq --arg dir_name "${plugin_name}" '{ + name: $dir_name, + description: (.description // ""), + version: (.version // ""), + type: (.type // ""), + tier: (.tier // ""), + license: (.license // ""), + author: (.author // ""), + keywords: (.keywords // []), + private: (.private // false), + repository: (.repository // null), + minEngineVersion: (.minEngineVersion // null), + capabilities: { + moduleTypes: (.capabilities.moduleTypes // []), + stepTypes: (.capabilities.stepTypes // []), + triggerTypes: (.capabilities.triggerTypes // []), + workflowHandlers: (.capabilities.workflowHandlers // []), + wiringHooks: (.capabilities.wiringHooks // []) + } + }' "${manifest}")" + + summaries="$(echo "${summaries}" | jq --argjson s "${summary}" '. + [$s]')" + + # Copy manifest to v1/plugins//manifest.json + dest_dir="${OUT_DIR}/plugins/${plugin_name}" + mkdir -p "${dest_dir}" + cp "${manifest}" "${dest_dir}/manifest.json" + echo " copied plugins/${plugin_name}/manifest.json" +done < <(find "${PLUGINS_DIR}" -name "manifest.json" | sort) + +# Sort summaries by name and write index +echo "${summaries}" | jq 'sort_by(.name)' > "${OUT_DIR}/index.json" + +plugin_count="$(echo "${summaries}" | jq 'length')" +echo "Done. Generated v1/index.json with ${plugin_count} plugins." diff --git a/scripts/build-versions.sh b/scripts/build-versions.sh new file mode 100755 index 0000000..c624d39 --- /dev/null +++ b/scripts/build-versions.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# scripts/build-versions.sh +# +# For each plugin with a GitHub repository field, queries GitHub Releases API +# and builds v1/plugins//versions.json and v1/plugins//latest.json. +# +# Requires: gh CLI (authenticated), jq +# +# Notes: +# - `gh release list` does NOT support --json assets; per-release assets are +# fetched via `gh release view --json assets`. +# - Asset digests (sha256) are read directly from the `digest` field returned +# by `gh release view`, so checksums.txt does not need to be downloaded. +# - The canonical plugin name used in versions.json is the directory name +# (same convention as build-index.sh), not the manifest's "name" field. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +PLUGINS_DIR="${REPO_ROOT}/plugins" +OUT_DIR="${REPO_ROOT}/v1" + +if ! command -v jq &>/dev/null; then + echo "error: jq is required but not found in PATH" >&2 + exit 1 +fi + +if ! command -v gh &>/dev/null; then + echo "error: gh CLI is required but not found in PATH" >&2 + exit 1 +fi + +echo "Building version data..." + +mkdir -p "${OUT_DIR}/plugins" + +while IFS= read -r manifest; do + plugin_name="$(basename "$(dirname "${manifest}")")" + dest_dir="${OUT_DIR}/plugins/${plugin_name}" + mkdir -p "${dest_dir}" + + # Read fields from manifest + repository="$(jq -r '.repository // empty' "${manifest}")" + min_engine="$(jq -r '.minEngineVersion // ""' "${manifest}")" + + # Plugins without a GitHub repository get an empty versions array + if [[ -z "${repository}" ]] || [[ "${repository}" != *"github.com"* ]]; then + jq -n --arg name "${plugin_name}" '{"name": $name, "versions": []}' \ + > "${dest_dir}/versions.json" + echo " ${plugin_name}: no GitHub repository, wrote empty versions" + continue + fi + + # Extract owner/repo from URL, normalizing trailing slashes, .git suffix, and extra path segments + gh_repo="$(echo "${repository}" | sed 's|https://github.com/||; s|http://github.com/||; s|github.com/||; s|\.git$||; s|/$||' | cut -d/ -f1,2)" + + echo " ${plugin_name}: fetching releases for ${gh_repo}..." + + # List releases (tagName + publishedAt only; assets not available in list output) + if ! releases_list="$(gh release list \ + --repo "${gh_repo}" \ + --limit 100 \ + --json tagName,publishedAt \ + 2>&1)"; then + echo " WARNING: failed to list releases for ${gh_repo}: ${releases_list}" >&2 + releases_list="[]" + fi + + if [[ "${releases_list}" == "[]" ]] || [[ "$(echo "${releases_list}" | jq 'length')" == "0" ]]; then + echo " no releases found" + jq -n --arg name "${plugin_name}" '{"name": $name, "versions": []}' \ + > "${dest_dir}/versions.json" + continue + fi + + # For each release tag, fetch full asset list (includes digest/sha256) + final_versions="[]" + while IFS= read -r release_entry; do + tag="$(echo "${release_entry}" | jq -r '.tagName')" + published_at="$(echo "${release_entry}" | jq -r '.publishedAt')" + ver="$(echo "${tag}" | sed 's/^v//')" + + # gh release view returns assets with a `digest` field (sha256:... format) + if ! release_detail="$(gh release view "${tag}" \ + --repo "${gh_repo}" \ + --json assets \ + 2>&1)"; then + echo " WARNING: failed to fetch assets for ${gh_repo}@${tag}: ${release_detail}" >&2 + release_detail='{"assets":[]}' + fi + + version_entry="$(echo "${release_detail}" | jq \ + --arg ver "${ver}" \ + --arg published_at "${published_at}" \ + --arg minEng "${min_engine}" ' + { + version: $ver, + released: $published_at, + minEngineVersion: (if $minEng != "" then $minEng else null end), + downloads: [ + .assets[] | + select(.name | test("(linux|darwin|windows)-(amd64|arm64)[.]tar[.]gz$")) | + . as $asset | + ($asset.name | capture("(?linux|darwin|windows)-(?amd64|arm64)[.]tar[.]gz$")) as $parts | + { + os: $parts.os, + arch: $parts.arch, + url: $asset.url, + sha256: ($asset.digest | if . then ltrimstr("sha256:") else "" end) + } + ] + } + ')" + + final_versions="$(echo "${final_versions}" | jq --argjson v "${version_entry}" '. + [$v]')" + done < <(echo "${releases_list}" | jq -c '.[]') + + # Write versions.json (newest-first order preserved from gh release list) + jq -n \ + --arg name "${plugin_name}" \ + --argjson versions "${final_versions}" \ + '{"name": $name, "versions": $versions}' \ + > "${dest_dir}/versions.json" + + version_count="$(echo "${final_versions}" | jq 'length')" + echo " wrote ${version_count} version(s)" + + # Write latest.json (first/newest version entry) + latest="$(echo "${final_versions}" | jq 'first // null')" + if [[ "${latest}" != "null" ]]; then + echo "${latest}" > "${dest_dir}/latest.json" + echo " latest: $(echo "${latest}" | jq -r '.version')" + fi + +done < <(find "${PLUGINS_DIR}" -name "manifest.json" | sort) + +echo "Done. Version data written to v1/plugins/*/versions.json" diff --git a/templates/notify-registry.yml b/templates/notify-registry.yml new file mode 100644 index 0000000..8cf0e65 --- /dev/null +++ b/templates/notify-registry.yml @@ -0,0 +1,26 @@ +# templates/notify-registry.yml +# +# Add this job to your plugin's .github/workflows/release.yml +# to automatically notify the workflow-registry when you publish a release. +# +# Prerequisites: +# - Create a fine-grained GitHub PAT scoped to GoCodeAlone/workflow-registry +# with "Contents: Read" permission (sufficient for repository_dispatch). +# Alternatively, a classic PAT with `public_repo` scope works. +# - Add it as a secret named REGISTRY_PAT in your plugin repo +# +# Usage: Copy the job below into your release workflow, after the GoReleaser job. + +# notify-registry: +# if: startsWith(github.ref, 'refs/tags/v') +# needs: [release] # adjust to match your release job name +# runs-on: ubuntu-latest +# steps: +# - name: Notify workflow-registry of new release +# uses: peter-evans/repository-dispatch@v3 +# with: +# token: ${{ secrets.REGISTRY_PAT }} +# repository: GoCodeAlone/workflow-registry +# event-type: plugin-release +# client-payload: >- +# {"plugin": "${{ github.repository }}", "tag": "${{ github.ref_name }}"}