From 8f3536f0467c36931bf2e15161a5563402921faf Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 15:17:38 -0400 Subject: [PATCH 1/6] feat: add registry build scripts for static JSON index and versions - build-index.sh: reads all plugins/*/manifest.json, generates v1/index.json (sorted by name with summary fields) and copies manifests to v1/plugins/ - build-versions.sh: queries GitHub Releases API via gh CLI for each plugin with a repository field, generates v1/plugins//versions.json and latest.json with download URLs and sha256 checksums from checksums.txt Co-Authored-By: Claude Sonnet 4.6 --- scripts/build-index.sh | 70 +++++++++++++++ scripts/build-versions.sh | 176 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100755 scripts/build-index.sh create mode 100755 scripts/build-versions.sh diff --git a/scripts/build-index.sh b/scripts/build-index.sh new file mode 100755 index 0000000..5de050d --- /dev/null +++ b/scripts/build-index.sh @@ -0,0 +1,70 @@ +#!/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 + summary="$(jq '{ + name: (.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 // []) + } + }' "${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..9808734 --- /dev/null +++ b/scripts/build-versions.sh @@ -0,0 +1,176 @@ +#!/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 + +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 (https://github.com/owner/repo or github.com/owner/repo) + gh_repo="$(echo "${repository}" | sed 's|https://github.com/||; s|http://github.com/||; s|github.com/||')" + + echo " ${plugin_name}: fetching releases for ${gh_repo}..." + + # Query GitHub Releases API + releases_json="$(gh release list \ + --repo "${gh_repo}" \ + --limit 100 \ + --json tagName,publishedAt,assets \ + 2>/dev/null || echo "[]")" + + if [[ "${releases_json}" == "[]" ]]; then + echo " no releases found" + jq -n --arg name "${plugin_name}" '{"name": $name, "versions": []}' \ + > "${dest_dir}/versions.json" + continue + fi + + # Build versions array from releases, newest-first (gh release list already returns newest-first) + versions="$(echo "${releases_json}" | jq --arg minEng "${min_engine}" ' + [ + .[] | + . as $release | + ($release.tagName | ltrimstr("v")) as $ver | + { + version: $ver, + released: $release.publishedAt, + minEngineVersion: (if $minEng != "" then $minEng else null end), + downloads: ( + # Find checksums.txt asset content URL if available + ($release.assets | map(select(.name == "checksums.txt")) | first | .url // "") as $checksums_url | + [ + $release.assets[] | + select(.name | test("(linux|darwin|windows)-(amd64|arm64)\\.tar\\.gz$")) | + . as $asset | + ($asset.name | capture("(?Plinux|darwin|windows)-(?Pamd64|arm64)\\.tar\\.gz$")) as $parts | + { + os: $parts.os, + arch: $parts.arch, + url: $asset.url, + sha256: "" + } + ] + ) + } + ] + ')" + + # Resolve checksums for each version by fetching checksums.txt + versions_with_checksums="$(echo "${releases_json}" | jq --arg minEng "${min_engine}" ' + [ + .[] | + ($release = .) | + ($release.tagName | ltrimstr("v")) as $ver | + ($release.assets | map(select(.name == "checksums.txt")) | first) as $checksum_asset | + { + version: $ver, + released: $release.publishedAt, + minEngineVersion: (if $minEng != "" then $minEng else null end), + checksum_url: ($checksum_asset.url // ""), + downloads: [ + $release.assets[] | + select(.name | test("(linux|darwin|windows)-(amd64|arm64)\\.tar\\.gz$")) | + . as $asset | + ($asset.name | capture("(?Plinux|darwin|windows)-(?Pamd64|arm64)\\.tar\\.gz$")) as $parts | + { + os: $parts.os, + arch: $parts.arch, + url: $asset.url, + asset_name: $asset.name, + sha256: "" + } + ] + } + ] + ')" + + # Fetch checksums and populate sha256 fields + final_versions="[]" + while IFS= read -r version_entry; do + checksum_url="$(echo "${version_entry}" | jq -r '.checksum_url // ""')" + ver="$(echo "${version_entry}" | jq -r '.version')" + + if [[ -n "${checksum_url}" ]]; then + # Download checksums.txt via gh api + checksums_txt="$(gh api "${checksum_url}" 2>/dev/null || echo "")" + # gh api on a release asset URL redirects; try direct download approach + if [[ -z "${checksums_txt}" ]]; then + checksums_txt="$(curl -sf -L "${checksum_url}" 2>/dev/null || echo "")" + fi + else + checksums_txt="" + fi + + # Update sha256 for each download by matching asset name in checksums.txt + updated_entry="$(echo "${version_entry}" | jq \ + --arg checksums "${checksums_txt}" ' + del(.checksum_url) | + .downloads = [ + .downloads[] | + . as $dl | + ($checksums | split("\n")[] | select(contains($dl.asset_name)) | split(" ")[0] // "") as $sha | + del(.asset_name) | + .sha256 = $sha + ] + ')" + + final_versions="$(echo "${final_versions}" | jq --argjson v "${updated_entry}" '. + [$v]')" + done < <(echo "${versions_with_checksums}" | 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" + + # 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 " wrote ${final_versions_count:-$(echo "${final_versions}" | jq 'length')} versions, latest: $(echo "${latest}" | jq -r '.version')" + else + echo " no versions parsed" + fi + +done < <(find "${PLUGINS_DIR}" -name "manifest.json" | sort) + +echo "Done. Version data written to v1/plugins/*/versions.json" From c6d122e4dbc22e00116496db78e384d7c79c7242 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 15:17:53 -0400 Subject: [PATCH 2/6] feat: add GitHub Pages deployment workflow for static registry - build-pages.yml: builds and deploys v1/ static JSON API to GitHub Pages on push to main, repository_dispatch (plugin-release), daily schedule, and manual workflow_dispatch - .gitignore: excludes generated v1/ directory from source control Registry served at: https://gocodealone.github.io/workflow-registry/v1/ Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build-pages.yml | 55 +++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/build-pages.yml create mode 100644 .gitignore diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml new file mode 100644 index 0000000..4fc9e6b --- /dev/null +++ b/.github/workflows/build-pages.yml @@ -0,0 +1,55 @@ +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 install -y jq + + - name: Setup GitHub CLI + run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token + + - name: Build static index + run: bash scripts/build-index.sh + + - 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/ From 1e044c2a652a75f9a56163b26ecea1bde3cfb997 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 15:18:05 -0400 Subject: [PATCH 3/6] docs: add notify-registry Action template for plugin authors Provides a copy-paste GitHub Actions job snippet that plugin repos can add to their release workflow to trigger a registry rebuild via repository_dispatch when a new version is published. Co-Authored-By: Claude Sonnet 4.6 --- templates/notify-registry.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 templates/notify-registry.yml diff --git a/templates/notify-registry.yml b/templates/notify-registry.yml new file mode 100644 index 0000000..1f6b216 --- /dev/null +++ b/templates/notify-registry.yml @@ -0,0 +1,24 @@ +# 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 GitHub PAT with `repo` scope for GoCodeAlone/workflow-registry +# - 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 }}"} From 09a5dc0447a735db683a7b05217c48c86501b9c1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 15:19:19 -0400 Subject: [PATCH 4/6] docs: update README with contribution guide and registry URL - Add Build & Deploy badge alongside existing Validate badge - Add registry API URL (https://gocodealone.github.io/workflow-registry/v1/) - Add Usage via wfctl section with search/install/list/update commands - Expand Submitting a Plugin into step-by-step PR process with manifest requirements summary - Add Automatic Version Tracking section pointing to templates/notify-registry.yml - Add Registry Structure overview showing v1/ generated directory and static API endpoint table - Add Plugin Tiers table (core/community/premium) - Add link to Plugin Authoring Guide Co-Authored-By: Claude Sonnet 4.6 --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c79b42c..6354aa8 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 Authoring Guide](./docs/plugin-authoring.md) for a complete walkthrough of 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"], From 1c1e1241dfbcd0083b5ed52e548e74557311f4bb Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 15:53:46 -0400 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20build=20scripts=20=E2=80=94=20name?= =?UTF-8?q?=20alignment,=20gh=20CLI=20API,=20jq=20regex,=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-index.sh: - Use directory name as canonical plugin name in index entries (overrides manifest .name field) so that v1/index.json names align exactly with v1/plugins// API paths; fixes 11 mismatches (e.g. "datadog" vs "workflow-plugin-datadog", "pipelinesteps" vs "pipeline-steps") build-versions.sh: - Replace `gh release list --json assets` (assets not a valid list field) with `gh release view --json assets` per-tag; was crashing silently and producing empty release lists for all plugins with releases - Use `digest` field from gh release view assets (contains sha256 natively) instead of downloading checksums.txt, simplifying the approach significantly - Remove dead-code first `versions=` computation block that was immediately superseded by the `versions_with_checksums` block - Fix jq named capture groups: `(?P...)` → `(?...)` (Oniguruma syntax); `(?P<>...)` caused "Regex failure: undefined group option" errors - Fix asset name regex escaping: use `[.]` instead of `\\.` for literal dots build-pages.yml: - Remove redundant "Setup GitHub CLI" step (`gh auth login --with-token`); GH_TOKEN env var is sufficient for gh CLI authentication in Actions README.md: - Fix dead link to non-existent docs/plugin-authoring.md Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build-pages.yml | 5 +- README.md | 2 +- scripts/build-index.sh | 8 +- scripts/build-versions.sh | 130 ++++++++++-------------------- 4 files changed, 51 insertions(+), 94 deletions(-) diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml index 4fc9e6b..f9d728b 100644 --- a/.github/workflows/build-pages.yml +++ b/.github/workflows/build-pages.yml @@ -27,11 +27,10 @@ jobs: - name: Install dependencies run: sudo apt-get install -y jq - - name: Setup GitHub CLI - run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token - - 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 diff --git a/README.md b/README.md index 6354aa8..dacf42c 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ workflow-registry/ ## Plugin Authoring Guide -See the [Plugin Authoring Guide](./docs/plugin-authoring.md) for a complete walkthrough of building, testing, and publishing a workflow engine plugin. +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. --- diff --git a/scripts/build-index.sh b/scripts/build-index.sh index 5de050d..ceafbb1 100755 --- a/scripts/build-index.sh +++ b/scripts/build-index.sh @@ -34,9 +34,11 @@ while IFS= read -r manifest; do continue fi - # Extract summary fields - summary="$(jq '{ - name: (.name // ""), + # 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 // ""), diff --git a/scripts/build-versions.sh b/scripts/build-versions.sh index 9808734..443163a 100755 --- a/scripts/build-versions.sh +++ b/scripts/build-versions.sh @@ -5,6 +5,14 @@ # 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 @@ -49,111 +57,58 @@ while IFS= read -r manifest; do echo " ${plugin_name}: fetching releases for ${gh_repo}..." - # Query GitHub Releases API - releases_json="$(gh release list \ + # List releases (tagName + publishedAt only; assets not available in list output) + releases_list="$(gh release list \ --repo "${gh_repo}" \ --limit 100 \ - --json tagName,publishedAt,assets \ + --json tagName,publishedAt \ 2>/dev/null || echo "[]")" - if [[ "${releases_json}" == "[]" ]]; then + if [[ "${releases_list}" == "[]" ]]; then echo " no releases found" jq -n --arg name "${plugin_name}" '{"name": $name, "versions": []}' \ > "${dest_dir}/versions.json" continue fi - # Build versions array from releases, newest-first (gh release list already returns newest-first) - versions="$(echo "${releases_json}" | jq --arg minEng "${min_engine}" ' - [ - .[] | - . as $release | - ($release.tagName | ltrimstr("v")) as $ver | - { - version: $ver, - released: $release.publishedAt, - minEngineVersion: (if $minEng != "" then $minEng else null end), - downloads: ( - # Find checksums.txt asset content URL if available - ($release.assets | map(select(.name == "checksums.txt")) | first | .url // "") as $checksums_url | - [ - $release.assets[] | - select(.name | test("(linux|darwin|windows)-(amd64|arm64)\\.tar\\.gz$")) | - . as $asset | - ($asset.name | capture("(?Plinux|darwin|windows)-(?Pamd64|arm64)\\.tar\\.gz$")) as $parts | - { - os: $parts.os, - arch: $parts.arch, - url: $asset.url, - sha256: "" - } - ] - ) - } - ] - ')" - - # Resolve checksums for each version by fetching checksums.txt - versions_with_checksums="$(echo "${releases_json}" | jq --arg minEng "${min_engine}" ' - [ - .[] | - ($release = .) | - ($release.tagName | ltrimstr("v")) as $ver | - ($release.assets | map(select(.name == "checksums.txt")) | first) as $checksum_asset | + # 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) + release_detail="$(gh release view "${tag}" \ + --repo "${gh_repo}" \ + --json assets \ + 2>/dev/null || echo '{"assets":[]}')" + + version_entry="$(echo "${release_detail}" | jq \ + --arg ver "${ver}" \ + --arg published_at "${published_at}" \ + --arg minEng "${min_engine}" ' { version: $ver, - released: $release.publishedAt, + released: $published_at, minEngineVersion: (if $minEng != "" then $minEng else null end), - checksum_url: ($checksum_asset.url // ""), downloads: [ - $release.assets[] | - select(.name | test("(linux|darwin|windows)-(amd64|arm64)\\.tar\\.gz$")) | + .assets[] | + select(.name | test("(linux|darwin|windows)-(amd64|arm64)[.]tar[.]gz$")) | . as $asset | - ($asset.name | capture("(?Plinux|darwin|windows)-(?Pamd64|arm64)\\.tar\\.gz$")) as $parts | + ($asset.name | capture("(?linux|darwin|windows)-(?amd64|arm64)[.]tar[.]gz$")) as $parts | { - os: $parts.os, - arch: $parts.arch, - url: $asset.url, - asset_name: $asset.name, - sha256: "" + os: $parts.os, + arch: $parts.arch, + url: $asset.url, + sha256: ($asset.digest | if . then ltrimstr("sha256:") else "" end) } ] } - ] - ')" - - # Fetch checksums and populate sha256 fields - final_versions="[]" - while IFS= read -r version_entry; do - checksum_url="$(echo "${version_entry}" | jq -r '.checksum_url // ""')" - ver="$(echo "${version_entry}" | jq -r '.version')" - - if [[ -n "${checksum_url}" ]]; then - # Download checksums.txt via gh api - checksums_txt="$(gh api "${checksum_url}" 2>/dev/null || echo "")" - # gh api on a release asset URL redirects; try direct download approach - if [[ -z "${checksums_txt}" ]]; then - checksums_txt="$(curl -sf -L "${checksum_url}" 2>/dev/null || echo "")" - fi - else - checksums_txt="" - fi - - # Update sha256 for each download by matching asset name in checksums.txt - updated_entry="$(echo "${version_entry}" | jq \ - --arg checksums "${checksums_txt}" ' - del(.checksum_url) | - .downloads = [ - .downloads[] | - . as $dl | - ($checksums | split("\n")[] | select(contains($dl.asset_name)) | split(" ")[0] // "") as $sha | - del(.asset_name) | - .sha256 = $sha - ] ')" - final_versions="$(echo "${final_versions}" | jq --argjson v "${updated_entry}" '. + [$v]')" - done < <(echo "${versions_with_checksums}" | jq -c '.[]') + 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 \ @@ -162,13 +117,14 @@ while IFS= read -r manifest; do '{"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 " wrote ${final_versions_count:-$(echo "${final_versions}" | jq 'length')} versions, latest: $(echo "${latest}" | jq -r '.version')" - else - echo " no versions parsed" + echo " latest: $(echo "${latest}" | jq -r '.version')" fi done < <(find "${PLUGINS_DIR}" -name "manifest.json" | sort) From fe81ecd5072f3363ad4bc04ce5559de68e30588e Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 14 Mar 2026 18:44:03 -0400 Subject: [PATCH 6/6] fix: address Copilot review feedback on registry PR - build-versions.sh: log warnings instead of silently swallowing gh CLI errors - build-versions.sh: normalize repo URL (strip .git, trailing slash, extra paths) - build-pages.yml: add apt-get update before installing jq - build-index.sh: include workflowHandlers and wiringHooks in capabilities - templates/notify-registry.yml: recommend fine-grained PAT over full repo scope Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build-pages.yml | 2 +- scripts/build-index.sh | 8 +++++--- scripts/build-versions.sh | 20 +++++++++++++------- templates/notify-registry.yml | 4 +++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-pages.yml b/.github/workflows/build-pages.yml index f9d728b..1831622 100644 --- a/.github/workflows/build-pages.yml +++ b/.github/workflows/build-pages.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - name: Install dependencies - run: sudo apt-get install -y jq + run: sudo apt-get update && sudo apt-get install -y jq - name: Build static index run: bash scripts/build-index.sh diff --git a/scripts/build-index.sh b/scripts/build-index.sh index ceafbb1..790991d 100755 --- a/scripts/build-index.sh +++ b/scripts/build-index.sh @@ -50,9 +50,11 @@ while IFS= read -r manifest; do repository: (.repository // null), minEngineVersion: (.minEngineVersion // null), capabilities: { - moduleTypes: (.capabilities.moduleTypes // []), - stepTypes: (.capabilities.stepTypes // []), - triggerTypes: (.capabilities.triggerTypes // []) + moduleTypes: (.capabilities.moduleTypes // []), + stepTypes: (.capabilities.stepTypes // []), + triggerTypes: (.capabilities.triggerTypes // []), + workflowHandlers: (.capabilities.workflowHandlers // []), + wiringHooks: (.capabilities.wiringHooks // []) } }' "${manifest}")" diff --git a/scripts/build-versions.sh b/scripts/build-versions.sh index 443163a..c624d39 100755 --- a/scripts/build-versions.sh +++ b/scripts/build-versions.sh @@ -52,19 +52,22 @@ while IFS= read -r manifest; do continue fi - # Extract owner/repo from URL (https://github.com/owner/repo or github.com/owner/repo) - gh_repo="$(echo "${repository}" | sed 's|https://github.com/||; s|http://github.com/||; s|github.com/||')" + # 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) - releases_list="$(gh release list \ + if ! releases_list="$(gh release list \ --repo "${gh_repo}" \ --limit 100 \ --json tagName,publishedAt \ - 2>/dev/null || echo "[]")" + 2>&1)"; then + echo " WARNING: failed to list releases for ${gh_repo}: ${releases_list}" >&2 + releases_list="[]" + fi - if [[ "${releases_list}" == "[]" ]]; then + 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" @@ -79,10 +82,13 @@ while IFS= read -r manifest; do ver="$(echo "${tag}" | sed 's/^v//')" # gh release view returns assets with a `digest` field (sha256:... format) - release_detail="$(gh release view "${tag}" \ + if ! release_detail="$(gh release view "${tag}" \ --repo "${gh_repo}" \ --json assets \ - 2>/dev/null || echo '{"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}" \ diff --git a/templates/notify-registry.yml b/templates/notify-registry.yml index 1f6b216..8cf0e65 100644 --- a/templates/notify-registry.yml +++ b/templates/notify-registry.yml @@ -4,7 +4,9 @@ # to automatically notify the workflow-registry when you publish a release. # # Prerequisites: -# - Create a GitHub PAT with `repo` scope for GoCodeAlone/workflow-registry +# - 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.