diff --git a/.claude/skills/deploy-plugin/SKILL.md b/.claude/skills/deploy-plugin/SKILL.md index 7fe4ffe8..64ce57d1 100644 --- a/.claude/skills/deploy-plugin/SKILL.md +++ b/.claude/skills/deploy-plugin/SKILL.md @@ -84,7 +84,7 @@ New plugins start at `1.0.0`. Use semver: | New stream, new optional config field, new default content | MINOR (`1.x.0`) | | Deleted/renamed stream, breaking config change | MAJOR (`x.0.0`) | -Every PR that modifies plugin files must include a version bump in `metadata.json`. +Versions only matter at merge, so they are compared against `main`. A PR that modifies an existing plugin must end with a `version` higher than the one on `main` β€” that is **one bump for the whole PR, not one per commit**, so don't bump again on each review round. A brand-new plugin has nothing on `main` to compare against: leave it at `1.0.0` until it merges. **Breaking (MAJOR) changes β€” do not create a new major version without asking the user first.** It is often possible to avoid the break entirely. If a major version is genuinely needed: - Create a new versioned folder (e.g. `v2/`) rather than modifying `v1/` diff --git a/.github/PULL_REQUEST_TEMPLATE/Add a new plugin.md b/.github/PULL_REQUEST_TEMPLATE/Add a new plugin.md index 05430fed..599029d4 100644 --- a/.github/PULL_REQUEST_TEMPLATE/Add a new plugin.md +++ b/.github/PULL_REQUEST_TEMPLATE/Add a new plugin.md @@ -2,12 +2,10 @@ Thanks for contributing a new community-authored SquaredUp plugin! --> ## πŸ”Œ Plugin overview - - **Plugin name**: - **Purpose / problem solved**: - **Primary audience** (e.g. platform teams, SREs, product teams): - **Authentication method(s)** (e.g. OAuth, Username/Password, API Key): ---- ## πŸ–ΌοΈ Plugin screenshots ---- - ## ⚠️ Known limitations ---- - ## πŸ“š Checklist +- [ ] This PR adds a single plugin only - [ ] Plugin, datastream and UI naming follow SquaredUp guidelines - [ ] Logo added - [ ] One or more dashboards added diff --git a/.github/PULL_REQUEST_TEMPLATE/Change to an existing plugin.md b/.github/PULL_REQUEST_TEMPLATE/Change to an existing plugin.md index 4a953da2..5ac07a78 100644 --- a/.github/PULL_REQUEST_TEMPLATE/Change to an existing plugin.md +++ b/.github/PULL_REQUEST_TEMPLATE/Change to an existing plugin.md @@ -7,8 +7,6 @@ > Fixes an issue when authenticating with OAuth --> ---- - ## πŸ”— Related issue(s) ---- - ## 🧩 Plugin details - - **Plugin name**: - **Type of change**: - [ ] Bug fix @@ -30,12 +25,14 @@ - [ ] Documentation / metadata / logo - [ ] Other (please describe): ---- +## πŸ§ͺ Testing + ## ⚠️ Breaking changes - Does this PR introduce any breaking changes? - - [ ] No - [ ] Yes (please describe): @@ -44,17 +41,13 @@ If yes, describe: - Who is impacted - Any migration steps ---- - ## πŸ“š Documentation - - [ ] Documentation updated - [ ] No documentation changes needed ---- - ## βœ… Checklist +- [ ] This PR changes a single plugin only - [ ] No secrets or credentials included - [ ] Plugin, datastream and UI naming follow SquaredUp guidelines - [ ] I agree to the [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/.github/PULL_REQUEST_TEMPLATE/Miscellaneous change.md b/.github/PULL_REQUEST_TEMPLATE/Miscellaneous change.md index 1c99517f..93566ab1 100644 --- a/.github/PULL_REQUEST_TEMPLATE/Miscellaneous change.md +++ b/.github/PULL_REQUEST_TEMPLATE/Miscellaneous change.md @@ -7,8 +7,6 @@ Describe what this PR changes. --> ---- - ## πŸ” Scope of change - [ ] Documentation only @@ -16,8 +14,6 @@ - [ ] CI / automation - [ ] Other (please describe): ---- - ## πŸ“š Checklist - [ ] I agree to the [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/.github/workflows/pr-run.yaml b/.github/workflows/pr-run.yaml index bdbf94a4..a2de727d 100644 --- a/.github/workflows/pr-run.yaml +++ b/.github/workflows/pr-run.yaml @@ -23,17 +23,116 @@ jobs: if [ -z "$plugin_paths" ]; then echo "No plugins were modified in this PR." echo "plugins_modified=false" >> $GITHUB_OUTPUT - else - echo "Found modified plugin(s):" - echo "$plugin_paths" - echo "plugins_modified=true" >> $GITHUB_OUTPUT - echo "plugin_paths<> $GITHUB_OUTPUT - echo "$plugin_paths" >> $GITHUB_OUTPUT + echo "existing_modified=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Found modified plugin(s):" + echo "$plugin_paths" + + # A PR that deletes a plugin version folder still lists it as touched, but it + # cannot be validated or deployed - the CLI reports "Path not found". Split the + # paths that still exist so only those reach the version, validate and deploy + # steps; the full list stays for the one-plugin-per-PR check, which must still + # count a deleted plugin as touched. + existing_paths=$(while IFS= read -r p; do if [ -d "$p" ]; then printf '%s\n' "$p"; fi; done <<< "$plugin_paths") + removed_paths=$(while IFS= read -r p; do if [ ! -d "$p" ]; then printf '%s\n' "$p"; fi; done <<< "$plugin_paths") + + if [ -n "$removed_paths" ]; then + echo "Removed in this PR (skipped for validate and deploy):" + echo "$removed_paths" + fi + + echo "plugins_modified=true" >> $GITHUB_OUTPUT + echo "plugin_paths<> $GITHUB_OUTPUT + echo "$plugin_paths" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if [ -n "$existing_paths" ]; then + echo "existing_modified=true" >> $GITHUB_OUTPUT + echo "existing_paths<> $GITHUB_OUTPUT + echo "$existing_paths" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + else + echo "existing_modified=false" >> $GITHUB_OUTPUT fi - - name: Install & Configure SquaredUp CLI + # Plugin paths come from the contributor's branch, so they are untrusted input. + # They are passed through env rather than interpolated into the script: GitHub + # expands ${{ }} before Bash parses it, so a crafted directory name would + # otherwise run as shell code on the runner. + - name: Check PR scope & version bumps + id: checks if: steps.detect.outputs.plugins_modified == 'true' + env: + PLUGIN_PATHS: ${{ steps.detect.outputs.plugin_paths }} + EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} + run: | + checks_failed=false + + # One plugin per PR. Several version folders of the same plugin are fine, + # so that deprecating in v1 alongside adding v2 stays a single PR. + plugin_names=$(printf '%s\n' "$PLUGIN_PATHS" | cut -d/ -f2 | sort -u) + plugin_count=$(echo "$plugin_names" | wc -l | tr -d ' ') + + if [ "$plugin_count" -gt 1 ]; then + echo "[FAIL] This PR modifies ${plugin_count} plugins:" + echo "$plugin_names" | sed 's/^/ - /' + echo "Raise one pull request per plugin - see REVIEW.md." + checks_failed=true + else + echo "[PASS] Single plugin modified: ${plugin_names}" + fi + echo "" + + # Every change to an existing plugin must increase metadata.json version. + while IFS= read -r plugin_path; do + if [ -z "$plugin_path" ]; then + continue + fi + + if [ ! -f "${plugin_path}/metadata.json" ]; then + echo "[FAIL] ${plugin_path} has no metadata.json" + checks_failed=true + continue + fi + + new_version=$(jq -r '.version' "${plugin_path}/metadata.json") + + # The major version must match the version folder: v2/ holds 2.x.y + folder_major=$(basename "${plugin_path}") + folder_major=${folder_major#v} + if [ "${new_version%%.*}" != "$folder_major" ]; then + echo "[FAIL] ${plugin_path} is version ${new_version} - a plugin in v${folder_major}/ must be ${folder_major}.x.y" + checks_failed=true + fi + + if ! old_metadata=$(git show "origin/main:${plugin_path}/metadata.json" 2>/dev/null); then + echo "[SKIP] ${plugin_path} is new - no previous version to compare" + continue + fi + + old_version=$(echo "$old_metadata" | jq -r '.version') + + if [ "$new_version" = "$old_version" ]; then + echo "[FAIL] ${plugin_path} version is unchanged (${old_version}) - bump it in metadata.json" + checks_failed=true + elif [ "$(printf '%s\n%s\n' "$old_version" "$new_version" | sort -V | head -1)" != "$old_version" ]; then + echo "[FAIL] ${plugin_path} version decreased (${old_version} -> ${new_version}) - it can never decrease" + checks_failed=true + else + echo "[PASS] ${plugin_path} version bumped ${old_version} -> ${new_version}" + fi + done <<< "$EXISTING_PATHS" + + if [ "$checks_failed" = "true" ]; then + echo "" + echo "One or more repository checks failed." + exit 1 + fi + + - name: Install & Configure SquaredUp CLI + if: steps.detect.outputs.existing_modified == 'true' env: SQUAREDUP_API_KEY: ${{ secrets.SQUAREDUP_API_KEY }} run: | @@ -44,7 +143,9 @@ jobs: - name: Validate modified plugins id: validate - if: steps.detect.outputs.plugins_modified == 'true' + if: steps.detect.outputs.existing_modified == 'true' + env: + EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} run: | validation_failed=false @@ -66,7 +167,7 @@ jobs: validation_failed=true fi echo "" - done <<< "${{ steps.detect.outputs.plugin_paths }}" + done <<< "$EXISTING_PATHS" if [ "$validation_failed" = "true" ]; then echo "One or more plugins failed validation." @@ -75,17 +176,21 @@ jobs: - name: Deploy modified plugins id: deploy - if: steps.detect.outputs.plugins_modified == 'true' + if: steps.detect.outputs.existing_modified == 'true' + env: + EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} run: | while IFS= read -r plugin_path; do echo "Deploying ${plugin_path}..." squaredup deploy "${plugin_path}" --suffix "${{ github.event.pull_request.number }}" --force echo "Deployed ${plugin_path} successfully." echo "" - done <<< "${{ steps.detect.outputs.plugin_paths }}" + done <<< "$EXISTING_PATHS" - name: Summary if: always() + env: + PLUGIN_PATHS: ${{ steps.detect.outputs.plugin_paths }} run: | OUT=/tmp/summary.md @@ -101,20 +206,28 @@ jobs: echo "### πŸ“¦ Modified Plugins" >> $OUT while IFS= read -r plugin_path; do echo "- \`${plugin_path}\`" >> $OUT - done <<< "${{ steps.detect.outputs.plugin_paths }}" + done <<< "$PLUGIN_PATHS" echo "" >> $OUT echo "### πŸ“‹ Results" >> $OUT echo "| Step | Status |" >> $OUT echo "|------|--------|" >> $OUT - validate_conclusion="${{ steps.validate.conclusion }}" - deploy_conclusion="${{ steps.deploy.conclusion }}" + # A failed earlier step skips the later ones, so report skipped as skipped + # rather than folding it into a failure. + status_of() { + case "$1" in + success) echo "βœ… Passed" ;; + skipped|"") echo "⏭️ Skipped" ;; + *) echo "❌ Failed" ;; + esac + } - [ "$validate_conclusion" = "success" ] && v_status="βœ… Passed" || v_status="❌ Failed" - [ "$deploy_conclusion" = "success" ] && d_status="πŸš€ Deployed" || d_status="⏭️ Skipped" + deploy_conclusion="${{ steps.deploy.conclusion }}" + [ "$deploy_conclusion" = "success" ] && d_status="πŸš€ Deployed" || d_status="$(status_of "$deploy_conclusion")" - echo "| Validation | ${v_status} |" >> $OUT + echo "| Scope & version | $(status_of "${{ steps.checks.conclusion }}") |" >> $OUT + echo "| Validation | $(status_of "${{ steps.validate.conclusion }}") |" >> $OUT echo "| Deployment | ${d_status} |" >> $OUT echo "" >> $OUT diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..15328839 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# AGENTS.md + +Orientation for AI coding agents working in this repository. It tells you where things are and which document is authoritative for what β€” it deliberately does not restate the rules those documents contain. + +## What this repository is + +The community-authored plugins for [SquaredUp](https://squaredup.com), plus the AI skills used to build them. A plugin defines how SquaredUp connects to a third-party API, indexes objects from it, and queries data for dashboards. + +Plugins are **low-code**: JSON definitions plus small JavaScript expressions and scripts. There is no build step, no bundler and no local test suite. A plugin is proven by deploying it to a real SquaredUp organization with the `squaredup` CLI and querying live data β€” so most meaningful work here needs a logged-in CLI and an organization you can authenticate against. + +> [!NOTE] +> The code says `plugins`; the SquaredUp UI says **data sources**. Same thing. + +## Start with a skill + +Three skills live in `.claude/skills/`. They carry the actual procedures β€” read the relevant `SKILL.md` before improvising an approach. + +| Skill | Use it when | +| --- | --- | +| `build-plugin` | Building a new plugin, or adding streams to an existing one. Nine phases from API exploration to a deployed, tested plugin with dashboards. | +| `deploy-plugin` | Validating or deploying a plugin, or working out the right version bump. | +| `convert-dashboard` | Turning an exported platform dashboard JSON into plugin default content. | + +Claude Code discovers these automatically (`/build-plugin`). Other agents may need the `SKILL.md` read directly, or installed via `npx skills add squaredup/plugins` β€” see the [full guide](https://docs.squaredup.com/ai-features/building-plugins-with-ai). + +## Where the rules live + +Read the relevant file rather than inferring conventions from nearby plugins: + +| Document | Authoritative for | +| --- | --- | +| [`REVIEW.md`](REVIEW.md) | Conventions for every file in a plugin β€” naming, metadata, `ui.json` fields, data streams, dashboards, docs. **Read it before editing anything under `plugins/`.** It is phrased for reviewers, but the same rules govern authoring. | +| [`.claude/skills/deploy-plugin/SKILL.md`](.claude/skills/deploy-plugin/SKILL.md) | CLI commands, `--json` output shapes, and versioning. | +| [`.claude/skills/build-plugin/references/`](.claude/skills/build-plugin/references/) | Per-file depth: `metadata.md`, `ui.md`, `data-streams.md`, `index-defs.md`, `oob-content.md`, `readme.md`, `testing.md`. | +| [`.github/PULL_REQUEST_TEMPLATE/`](.github/PULL_REQUEST_TEMPLATE/) | What a pull request must cover. Three templates: new plugin, change to an existing plugin, miscellaneous. | +| [`CONTRIBUTING.md`](CONTRIBUTING.md) | The contributor signpost β€” where to go for each kind of change. | +| [`.github/CODEOWNERS`](.github/CODEOWNERS) | Who reviews which plugin. | +| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Expected conduct for contributions. | + +## Layout + +```text +plugins//v/ one folder per plugin, per major version +.claude/skills/ build-plugin, deploy-plugin, convert-dashboard +.github/workflows/ validate, deploy and clean up PR plugins +REVIEW.md plugin conventions +``` + +45 plugin version folders currently exist. Only one plugin has a `v2`, so `v1` is the norm. + +## Anatomy of a plugin folder + +What each file is, in rough order of how universal it is: + +| Path | What it is | +| --- | --- | +| `metadata.json` | Identity and configuration: name, version, author, category, base plugin, config template. Every plugin has one. | +| `dataStreams/*.json` | One file per queryable dataset β€” usually a wrapper around an API endpoint, with the columns it returns. `dataStreams/scripts/` holds JavaScript for streams that need it. | +| `ui.json` | The configuration form a user fills in when adding the data source. | +| `docs/README.md` | Setup documentation, rendered in-product while a user configures the plugin. | +| `defaultContent/**/*.dash.json` | Out-of-the-box dashboards, listed in a sibling `manifest.json`. | +| `indexDefinitions/*.json` | How objects are imported from the API into the SquaredUp graph. | +| `icon.svg` / `icon.png` | The plugin logo. | +| `configValidation.json` | The check run when a user saves their configuration, backed by a data stream. | +| `custom_types.json` | Display names and icons for the object types the plugin imports. | +| `cspell.json` | Per-plugin spellcheck word list, for product names the root list doesn't cover. | +| `correlationRules/`, `preRequest.js`, `screenshots/` | Rare extras, used by one or two plugins each. | + +## Tooling + +- **Node.js 22 or later**, and the CLI: `npm i -g @squaredup/cli`, then `squaredup login`. Check state with `squaredup status --json`. +- **JSON schemas** β€” [`.vscode/settings.json`](.vscode/settings.json) maps each plugin file type to its published schema at `schemas.squaredup.com`. Fetch one when you need ground truth on what a field accepts. +- **Formatting** is set by [`.editorconfig`](.editorconfig): UTF-8, LF, 4-space indent for JSON, Markdown and JavaScript, final newline. There is no linter or formatter to run. + +## What CI does to your pull request + +- **`pr-run.yaml`** diffs against `origin/main` to find changed `plugins/*/v*` folders, then in order: checks the PR touches a single plugin; checks each changed plugin's major version matches its folder, and β€” for plugins that already exist on `main` β€” that `metadata.json` version increased (a new plugin has nothing to compare against, so it keeps its initial `.0.0`); runs `squaredup validate --json` on each; and deploys each to a shared organization suffixed with the PR number. Any of those failing fails the check. Results land in a sticky PR comment. +- **`pr-cleanup.yaml`** deletes those PR-suffixed plugins when the PR closes. +- **`main-deploy.yaml`** dispatches to `squaredup/plugins-automation` to release on merge. + +So validate locally before you push β€” CI runs the same command and will block the PR on the same errors. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..45c047da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Contributing + +Thanks for contributing to the SquaredUp community plugins. This page is a signpost β€” each link below is the authoritative place for that topic. + +## Before you start + +| If you're… | Read | +| --- | --- | +| Building a new plugin | [Building plugins with AI](https://docs.squaredup.com/ai-features/building-plugins-with-ai) for the video introduction and full walkthrough, or the [quick start](README.md#how-do-i-build-my-own-plugin) in the README | +| Changing an existing plugin | [REVIEW.md](REVIEW.md) β€” the conventions every file in a plugin must follow, and what your PR is reviewed against | +| Working with an AI coding agent | [AGENTS.md](AGENTS.md) β€” orients an agent around the repository and points it at the right documents | + +## Raising a pull request + +See [How do I submit my plugin?](README.md#how-do-i-submit-my-plugin) in the README for what a PR needs, and [Pull requests](REVIEW.md#pull-requests) in REVIEW.md for the rules on PR scope and templates. Two of those catch people out most often: + +- One plugin per pull request +- Choose the matching template β€” *Add a new plugin*, *Change to an existing plugin*, or *Miscellaneous change* + +CI checks PR scope and plugin versions, then validates and deploys every changed plugin. Running `squaredup validate` locally first saves a round trip. + +## Reporting issues + +Bug reports and feature requests belong in [issues](https://github.com/squaredup/plugins/issues/new/choose) for community-authored plugins. For SquaredUp-authored plugins or the platform itself, use [Community Answers](https://community.squaredup.com) or contact support@squaredup.com β€” see [Reporting issues or feature requests](README.md#reporting-issues-or-feature-requests). + +## Conduct + +All contributions are covered by our [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index be939584..ff637ac1 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ claude Then ask for the skill: -``` +```text /build-plugin ``` @@ -83,6 +83,9 @@ Community-authored plugins are welcome β€” open a pull request against this repo - Never commit secrets, API keys or credentials - For changes to an existing plugin, bump `version` in `metadata.json`. Breaking changes need a new major version folder (e.g. `v2/`) alongside the old one, so existing users aren't broken - Add yourself to [`.github/CODEOWNERS`](.github/CODEOWNERS) so you're asked to review future changes to your plugin -- Open your PR using the **Add a new plugin** template; a community moderator will review it +- **One plugin per pull request** β€” if your work spans several plugins, raise a separate PR for each +- **Use the matching PR template** β€” *Add a new plugin*, *Change to an existing plugin*, or *Miscellaneous change*. PRs raised without the appropriate template may be closed + +A community moderator will review your PR. By contributing you agree to our [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/REVIEW.md b/REVIEW.md index 15fa9c9e..6eebbeff 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -1,10 +1,19 @@ When reviewing code, focus on: +## Pull requests + +- **One plugin per pull request.** A PR should add or modify a single plugin. If a change spans several plugins, ask the author to split it into one PR per plugin β€” this keeps review focused, routes CODEOWNERS correctly, and keeps the per-plugin validate/deploy checks meaningful. +- **The correct PR template must be used** β€” `Add a new plugin`, `Change to an existing plugin`, or `Miscellaneous change` (see `.github/PULL_REQUEST_TEMPLATE/`). PRs raised without the appropriate template may be closed. + ## Versioning -Any diff that touches files inside a plugin directory must include a corresponding change to metadata.json that increases the `version` field. If no version bump is present, assume the task is unfinished and prompt to add one. +Versions are compared against `main`, because merging is the only point at which a version matters. A PR that modifies an existing plugin must leave `metadata.json` with a `version` higher than the one on `main`. If it doesn't, assume the task is unfinished and prompt to add one. +- **One bump per PR, not per commit.** Once the version is above `main`, later commits and review rounds on the same PR do not each need another bump. Do not ask for one. +- **A new plugin stays at `1.0.0`.** There is no version on `main` to compare against, so it stays at `1.0.0` for the life of the PR however many review rounds it takes. Do not ask for a bump. +- **The major version must match the folder.** A plugin in `v1/` is `1.x.y`, a plugin in `v2/` is `2.x.y`. A new major version folder therefore starts at `.0.0`, not `1.0.0`. - Breaking changes (e.g. removing or renaming a data stream, significantly changing UI parameters) require a new major version **folder** (e.g. `plugins/MyPlugin/v2/`), not just a version bump within the existing folder. The old folder must remain to avoid breaking existing users. +- An earlier major version is not always present here. Some plugins' earlier versions are closed-source high-code plugins maintained outside this repository, so they start at `v2/` (e.g. `plugins/UptimeRobot/`). A missing `v1/` is not automatically a mistake. ## Security @@ -15,7 +24,7 @@ Any diff that touches files inside a plugin directory must include a correspondi - Follow existing formatting in the repo - Use consistent naming - Do not introduce formatting tools or config files unless explicitly requested. -- Do not commit editor or AI tool configuration files (e.g. `.claude/settings.json`). Personal tooling config belongs in the user's home directory. +- Do not commit **personal** editor or AI tool configuration (e.g. `.claude/settings.json`, anything matching `*.local.json`) β€” that belongs in the user's home directory. Shared instructions that the whole project relies on are a different thing and are committed deliberately: `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `.claude/skills/` and `.vscode/settings.json` all belong here. ## CODEOWNERS