From 66d1e64ff5ebe65be432950b88c469a6c528be07 Mon Sep 17 00:00:00 2001 From: Michael Costello Date: Mon, 9 Nov 2026 19:20:02 -0500 Subject: [PATCH] feat: Run the catalog on the shared workflows The index this catalog publishes stopped updating, because its rebuild pushed straight to main and main only accepts reviewed pull requests. Users still saw only the removed inventory plugin and an old ipam. The catalog now uses the shared plugin catalog workflows from datum-cloud/actions v1.27.1. The rebuild and the new daily release scan open pull requests with the milo-os GitHub App, so CI runs on them. Its own scripts and schema copy go, and the index is rebuilt here so the current versions and the assistant plugin are published when this merges. Fixes #53 Fixes #50 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/catalog-health.yaml | 54 ++----- .github/workflows/ci.yaml | 5 +- .github/workflows/generate-index.yaml | 52 ------- .github/workflows/rebuild-index.yaml | 23 +++ .github/workflows/scan-releases.yaml | 28 ++++ README.md | 10 +- index.yaml | 196 ++++++++++++++++++++++---- plugins/assistant.yaml | 45 ++++++ schema/plugin-v1alpha1.json | 100 ------------- scripts/generate_index.py | 81 ----------- scripts/sign-commit | 132 ----------------- scripts/verify_manifests.py | 94 ------------ 12 files changed, 284 insertions(+), 536 deletions(-) delete mode 100644 .github/workflows/generate-index.yaml create mode 100644 .github/workflows/rebuild-index.yaml create mode 100644 .github/workflows/scan-releases.yaml create mode 100644 plugins/assistant.yaml delete mode 100644 schema/plugin-v1alpha1.json delete mode 100755 scripts/generate_index.py delete mode 100755 scripts/sign-commit delete mode 100755 scripts/verify_manifests.py diff --git a/.github/workflows/catalog-health.yaml b/.github/workflows/catalog-health.yaml index cc84489..ecfef96 100644 --- a/.github/workflows/catalog-health.yaml +++ b/.github/workflows/catalog-health.yaml @@ -1,53 +1,21 @@ name: Catalog health -# The catalog points at release assets that live in other repositories, which -# can be deleted, retagged, or re-cut after a plugin lands here. PR validation -# only catches breakage at merge time, so this re-verifies every plugin's -# download links and checksums on a schedule and opens an issue if the catalog -# has rotted — before a user hits it during `datumctl plugin install`. on: schedule: - - cron: "0 12 * * 1" # Mondays, 12:00 UTC + - cron: "0 12 * * 1" workflow_dispatch: permissions: contents: read - issues: write jobs: - verify-assets: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-python@v7 - with: - python-version: "3.x" - - - name: Install tooling - run: pip install --quiet "jsonschema[format-nongpl]>=4" "PyYAML>=6" - - - name: Verify every plugin's download links and checksums - run: python3 scripts/verify_manifests.py - - - name: Open an issue if the catalog is unhealthy - if: failure() - env: - GH_TOKEN: ${{ github.token }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - gh label create catalog-health \ - --color b60205 \ - --description "A plugin download link or checksum is broken" 2>/dev/null || true - - body=$(printf 'The scheduled catalog health check failed: one or more plugin download URLs no longer resolve, or their checksums no longer match the published release assets.\n\nUsers may be unable to install or verify the affected plugin(s) with datumctl until this is fixed. See the run log for the specific plugin and platform:\n\n%s\n' "$RUN_URL") - - number=$(gh issue list --state open --label catalog-health --json number --jq '.[0].number // empty') - if [ -n "$number" ]; then - gh issue comment "$number" --body "$body" - else - gh issue create \ - --title "Catalog health check is failing" \ - --label catalog-health \ - --body "$body" - fi + health: + uses: datum-cloud/actions/.github/workflows/plugin-catalog-health.yaml@v1.27.1 + permissions: + contents: read + issues: write + with: + name: milo-os + description: Portable CLI plugins for the Milo platform + owner: milo-os + homepage: https://github.com/milo-os/cli-plugins diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1839244..5c0aa3a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,7 @@ permissions: jobs: ci: - uses: datum-cloud/actions/.github/workflows/ci-basic.yaml@v1.26.0 + uses: datum-cloud/actions/.github/workflows/plugin-catalog-validate.yaml@v1.27.1 with: - run: python3 -m venv "$RUNNER_TEMP/venv" && "$RUNNER_TEMP/venv/bin/pip" install --quiet 'jsonschema[format-nongpl]==4.26.0' 'PyYAML==6.0.3' && "$RUNNER_TEMP/venv/bin/python" scripts/verify_manifests.py + # The branch rules require the "ci / Build and test" check. + check-name: Build and test diff --git a/.github/workflows/generate-index.yaml b/.github/workflows/generate-index.yaml deleted file mode 100644 index 358ee7a..0000000 --- a/.github/workflows/generate-index.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: Regenerate index.yaml - -# index.yaml is what datumctl reads, and it is fully derived from plugins/*.yaml. -# Rather than asking every contributor (and the release bot) to regenerate and -# commit it in their PR, it is reconciled here on merge: when plugins/ changes on -# main, regenerate the index and commit it straight back to main. -# -# main is not a protected branch, so the built-in GITHUB_TOKEN can push directly -# — no app token needed. The commit touches only index.yaml (not plugins/**) and -# GITHUB_TOKEN pushes don't retrigger workflows, so this never loops. -on: - push: - branches: [main] - paths: - - "plugins/**" - workflow_dispatch: - -# Serialize so two quick merges can't race to push index.yaml. -concurrency: - group: regenerate-index - cancel-in-progress: false - -permissions: - contents: write - -jobs: - regenerate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-python@v7 - with: - python-version: "3.x" - - - name: Install tooling - run: pip install --quiet "PyYAML>=6" - - - name: Regenerate index.yaml - run: python3 scripts/generate_index.py - - - name: Commit if index.yaml changed - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if git diff --quiet -- index.yaml; then - echo "index.yaml already in sync; nothing to do." - exit 0 - fi - EXPECTED_HEAD=$(git rev-parse HEAD) - export EXPECTED_HEAD - scripts/sign-commit main "chore: regenerate index.yaml" index.yaml diff --git a/.github/workflows/rebuild-index.yaml b/.github/workflows/rebuild-index.yaml new file mode 100644 index 0000000..95a46b6 --- /dev/null +++ b/.github/workflows/rebuild-index.yaml @@ -0,0 +1,23 @@ +name: Rebuild index + +on: + push: + branches: [main] + paths: + - "plugins/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + rebuild: + uses: datum-cloud/actions/.github/workflows/plugin-catalog-index.yaml@v1.27.1 + with: + name: milo-os + description: Portable CLI plugins for the Milo platform + owner: milo-os + homepage: https://github.com/milo-os/cli-plugins + secrets: + app-id: ${{ secrets.PLUGIN_INDEX_APP_ID }} + app-private-key: ${{ secrets.PLUGIN_INDEX_APP_PRIVATE_KEY }} diff --git a/.github/workflows/scan-releases.yaml b/.github/workflows/scan-releases.yaml new file mode 100644 index 0000000..61d3f95 --- /dev/null +++ b/.github/workflows/scan-releases.yaml @@ -0,0 +1,28 @@ +name: Scan releases + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + inputs: + dry-run: + description: Report which plugins are behind without opening pull requests + type: boolean + default: false + plugin: + description: Scan only this plugin, by name + type: string + default: "" + +permissions: + contents: read + +jobs: + scan: + uses: datum-cloud/actions/.github/workflows/plugin-catalog-scan.yaml@v1.27.1 + with: + dry-run: ${{ inputs.dry-run || false }} + plugin: ${{ inputs.plugin || '' }} + secrets: + app-id: ${{ secrets.PLUGIN_INDEX_APP_ID }} + app-private-key: ${{ secrets.PLUGIN_INDEX_APP_PRIVATE_KEY }} diff --git a/README.md b/README.md index 8f112a7..68cbfbc 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ Milo services ship their own command-line experiences as plugins. This catalog i | Name | Description | |------|-------------| +| [assistant](plugins/assistant.yaml) | Chat with Patch, the Datum Cloud assistant, and browse your conversation history | | [billing](plugins/billing.yaml) | View metered usage and spend for your organization or project | | [ipam](plugins/ipam.yaml) | Manage IP address space (pools and prefixes) across the platform | +| [search](plugins/search.yaml) | Search for resources across the platform by kind, name, and project scope | | [telemetry](plugins/telemetry.yaml) | Query logs for your Datum Cloud projects | ## Using this catalog @@ -39,9 +41,9 @@ datumctl ipam pool list ## Submitting a plugin 1. Add the `datumctl-plugin` topic to your plugin's GitHub repository. -2. Open a pull request adding `plugins/.yaml`, following the [schema](schema/plugin-v1alpha1.json). That's the only file you edit. +2. Open a pull request adding `plugins/.yaml`, following the [schema](https://github.com/datum-cloud/actions/blob/v1.27.1/scripts/plugin-catalog/schema/plugin-v1alpha1.json). That's the only file you edit. 3. CI validates every manifest: schema conformance, that each download URL resolves, and that each SHA256 matches the published archive. A weekly [health check](.github/workflows/catalog-health.yaml) re-verifies the whole catalog so a plugin's links can't rot unnoticed. -4. On merge, [`index.yaml`](index.yaml) — the file `datumctl` actually reads — is regenerated from `plugins/*.yaml` and committed automatically. You never edit it by hand. (To preview locally: `python3 scripts/generate_index.py`.) +4. On merge, [`index.yaml`](index.yaml) — the file `datumctl` actually reads — is regenerated from `plugins/*.yaml` and committed automatically. You never edit it by hand. ## Plugin manifest format @@ -66,3 +68,7 @@ spec: ### Binary naming Plugins in this catalog use the portable **`milo-`** binary convention (for example `milo-ipam`). `datumctl` recognizes both `milo-` and `datumctl-` prefixed plugins, so a single `milo-` binary works as a `datumctl` subcommand and as a standalone tool on your `PATH`. The binary inside each archive must be named `milo-` (or `milo-.exe` on Windows). + +## How this catalog runs + +Validation, the index rebuild, a daily scan for newer plugin releases, and the weekly health check all come from the shared [plugin catalog workflows](https://github.com/datum-cloud/actions/blob/main/docs/plugin-catalog/README.md) in `datum-cloud/actions`. diff --git a/index.yaml b/index.yaml index 73a9e2f..7a9adce 100644 --- a/index.yaml +++ b/index.yaml @@ -8,49 +8,94 @@ items: - apiVersion: datumctl.datum.net/v1alpha1 kind: Plugin metadata: - name: inventory + name: assistant spec: - shortDescription: Browse and populate the Datum Cloud inventory graph (typed nodes - and edges) - homepage: https://github.com/datum-cloud/inventory - version: v0.1.0 + shortDescription: Chat with Patch, the Datum Cloud assistant, and browse your + conversation history + homepage: https://github.com/milo-os/assistant + version: v0.0.14 platforms: - selector: matchLabels: os: linux arch: amd64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Linux_x86_64.tar.gz - sha256: 9c392c1bc6368d00a4496f4449e8de06c84a1d29da5d02999df4ccf19acc778d + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Linux_x86_64.tar.gz + sha256: 5d347d3e747dfbcb27261885194aaea5a286e01eb9ba12d3a7045d006b92dd2c - selector: matchLabels: os: linux arch: arm64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Linux_arm64.tar.gz - sha256: c1a1698faf072091c1fd11ed62bbc451e5d1be55b872014042241dc10061fdbc + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Linux_arm64.tar.gz + sha256: e5f2f057cec9573b569d024a330c03e13feff0f165db1a31b0e297778037ddf2 - selector: matchLabels: os: darwin arch: amd64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Darwin_x86_64.tar.gz - sha256: 4a8b958a7e91454729da493d7f1abf90f0303df7c35328fdaf181f141c98f1c2 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Darwin_x86_64.tar.gz + sha256: 740a1c0cbbdfa5c2090127345746c4f43ef48a5ccb050b2cc25f8b14e894c1ca - selector: matchLabels: os: darwin arch: arm64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Darwin_arm64.tar.gz - sha256: 5966496965bf8bec2675af3bdff9a71ae2429473e5b2b55ee4137571f0f8b0a5 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Darwin_arm64.tar.gz + sha256: ab0b9b211986673fa8cd6175c9a11eceef2194ff1974a66631713dfaa67a8978 - selector: matchLabels: os: windows arch: amd64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Windows_x86_64.zip - sha256: 03507d79ac88360bf2c6eab187a383542933e7686d8826c36df43df9bb91d902 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Windows_x86_64.zip + sha256: 99364427984b12aa42afa63becb18f44b785fd970e0204f299fa2762fb2881ec - selector: matchLabels: os: windows arch: arm64 - uri: https://github.com/datum-cloud/inventory/releases/download/v0.1.0/datumctl-inventory_Windows_arm64.zip - sha256: 86688afbb1968ceff1714562dea79176bdaabfb3c45cb61ce4a7a1c14f6a391a + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Windows_arm64.zip + sha256: 8afbc9f94fc4b2f7ae51818d2f7986ba9d9c8c523105a730efbb279711f87f24 +- apiVersion: datumctl.datum.net/v1alpha1 + kind: Plugin + metadata: + name: billing + spec: + shortDescription: View metered usage and spend for your organization or project + homepage: https://github.com/milo-os/billing + version: v0.8.3 + platforms: + - selector: + matchLabels: + os: linux + arch: amd64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Linux_x86_64.tar.gz + sha256: eaf21dc968ec8e27aa939e579311b9871f1dc99f968504f88ff67f3039f23c3c + - selector: + matchLabels: + os: linux + arch: arm64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Linux_arm64.tar.gz + sha256: 4d419693b47046191eb9ebaa2b60a9d891aa8d0837d7343a95bb496319c8f620 + - selector: + matchLabels: + os: darwin + arch: amd64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Darwin_x86_64.tar.gz + sha256: 083e6b37435b7c4c4e24effca61f45276f2dcda5fdea0d786fdf9f5cc171a804 + - selector: + matchLabels: + os: darwin + arch: arm64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Darwin_arm64.tar.gz + sha256: 3f62c5baca7cba40c81c2b34bfa0f56939226611074fb64ec8e774efc08cff82 + - selector: + matchLabels: + os: windows + arch: amd64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Windows_x86_64.zip + sha256: e0dc21eec224b4f0a53ca72b5f16c4b3a79b9a67d7e9d48302b7358498295d43 + - selector: + matchLabels: + os: windows + arch: arm64 + uri: https://github.com/milo-os/billing/releases/download/v0.8.3/milo-billing_Windows_arm64.zip + sha256: 7c568d2a302b0e39e978be08a1536438c16f534f4c978893107da37cfdffecc9 - apiVersion: datumctl.datum.net/v1alpha1 kind: Plugin metadata: @@ -58,41 +103,132 @@ items: spec: shortDescription: Manage IP address space (pools and prefixes) across the platform homepage: https://github.com/milo-os/ipam - version: v0.3.1 + version: v0.5.6 + platforms: + - selector: + matchLabels: + os: linux + arch: amd64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Linux_x86_64.tar.gz + sha256: ddb2e3d054952e4ed061644a2b128b3879bffddb813c628fbdf5640647bb628f + - selector: + matchLabels: + os: linux + arch: arm64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Linux_arm64.tar.gz + sha256: 7826215647d498f45d650543d94110156a7961f73d34cbf2196e561528a82615 + - selector: + matchLabels: + os: darwin + arch: amd64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Darwin_x86_64.tar.gz + sha256: 4b63ca18620d8d51606cc24b5464a5a42184319bfc0172d61fab94e7bf4dc906 + - selector: + matchLabels: + os: darwin + arch: arm64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Darwin_arm64.tar.gz + sha256: 12aa81aec53ac45341e97ae7f946f802467037f3b9196d8708f1de9bdcc8be65 + - selector: + matchLabels: + os: windows + arch: amd64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Windows_x86_64.zip + sha256: 864f19aae407ca9c23c74329e680c584f2d2523e532a2b4c2c3684701bb5eedf + - selector: + matchLabels: + os: windows + arch: arm64 + uri: https://github.com/milo-os/ipam/releases/download/v0.5.6/milo-ipam_Windows_arm64.zip + sha256: 7ea5808537375cb46d62174f5799902cad39383bc9324386ef004afc6021d9ec +- apiVersion: datumctl.datum.net/v1alpha1 + kind: Plugin + metadata: + name: search + spec: + shortDescription: Search for resources across the platform by kind, name, and + project scope + homepage: https://github.com/milo-os/search + version: v0.8.11 + platforms: + - selector: + matchLabels: + os: linux + arch: amd64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Linux_x86_64.tar.gz + sha256: 5268f949cb3f25c49b8f9174031457936a82c1c917309fd8bfa95b196b1102c3 + - selector: + matchLabels: + os: linux + arch: arm64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Linux_arm64.tar.gz + sha256: 5069e1ecd714d40bfaf1051e0e62fc66142e6131d364c203e38700db0782cb5b + - selector: + matchLabels: + os: darwin + arch: amd64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Darwin_x86_64.tar.gz + sha256: d225d45d43db93475f619be52b9be433e52a434e688be33382bc8779581a620c + - selector: + matchLabels: + os: darwin + arch: arm64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Darwin_arm64.tar.gz + sha256: f36d1fa3159b49da0c6f97da52cd0116854b990ea08281491773645f4d18d3d3 + - selector: + matchLabels: + os: windows + arch: amd64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Windows_x86_64.zip + sha256: 421334f1a343aded412f617aa8409470069f533cf8f987fcbef262a9ba8a6674 + - selector: + matchLabels: + os: windows + arch: arm64 + uri: https://github.com/milo-os/search/releases/download/v0.8.11/milo-search_Windows_arm64.zip + sha256: df14c96ef66d5d1280b4c1ab068b268dfbe96d25d8f12e5302463cdfb25bb8ba +- apiVersion: datumctl.datum.net/v1alpha1 + kind: Plugin + metadata: + name: telemetry + spec: + shortDescription: Query logs for your Datum Cloud projects + homepage: https://github.com/milo-os/telemetry + version: v0.6.8 platforms: - selector: matchLabels: os: linux arch: amd64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Linux_x86_64.tar.gz - sha256: d4000e6d82f3406d20e01d1c2feae972c3878ff56ebf10b00bbb5bbaa02898f2 + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Linux_x86_64.tar.gz + sha256: aa990795eb6a6e47f3ed618d75f25f6784b15dad36778b5da548dc46f3862d3a - selector: matchLabels: os: linux arch: arm64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Linux_arm64.tar.gz - sha256: ce8e0d93648a93eb8caf2510ca8ba6158a158b0fb4f51f10a8b9fed9325726a0 + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Linux_arm64.tar.gz + sha256: 9e94481065b633fc87e9b2a232fe1d908f63fd34b8cb8a82aa5ec6b9e776a4c5 - selector: matchLabels: os: darwin arch: amd64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Darwin_x86_64.tar.gz - sha256: 06dab9f12cf22074e7a3c3b3ff0374adf0907f8792ab0c6c5e58a9307d4aac51 + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Darwin_x86_64.tar.gz + sha256: a6942db1710265157f43b6a85ed1db8605e6953b0895a52b5bfd9fadf2b29b64 - selector: matchLabels: os: darwin arch: arm64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Darwin_arm64.tar.gz - sha256: 875fa4874a38c813ae5c079d1b47a67ebcf3613b8d54aac8470e0451b93e917f + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Darwin_arm64.tar.gz + sha256: d98c83815422382d4edffda70988c1f6cdf7ee096b5e557bfdc32662e963a3fb - selector: matchLabels: os: windows arch: amd64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Windows_x86_64.zip - sha256: d3d57f419f2fbc04c91472fa01642a6c8a138385964afc96a21eb37eca5a8277 + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Windows_x86_64.zip + sha256: bbc42af23ee9bb2c7232840658a49a4e358d2054a33f0f843b28e3119e4374f9 - selector: matchLabels: os: windows arch: arm64 - uri: https://github.com/milo-os/ipam/releases/download/v0.3.1/milo-ipam_Windows_arm64.zip - sha256: 3f9b7525708fabdce9047a49c2fa541d4acb3a62dd5a668270edd76472bfc8e4 + uri: https://github.com/milo-os/telemetry/releases/download/v0.6.8/milo-telemetry_Windows_arm64.zip + sha256: 669fb6117d6a6ab8bfe6643dc0ab7f2c64a561dcb1644ff9eb851688edae72cd diff --git a/plugins/assistant.yaml b/plugins/assistant.yaml new file mode 100644 index 0000000..5cd0354 --- /dev/null +++ b/plugins/assistant.yaml @@ -0,0 +1,45 @@ +apiVersion: datumctl.datum.net/v1alpha1 +kind: Plugin +metadata: + name: assistant +spec: + shortDescription: Chat with Patch, the Datum Cloud assistant, and browse your conversation history + homepage: https://github.com/milo-os/assistant + version: v0.0.14 + platforms: + - selector: + matchLabels: + os: linux + arch: amd64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Linux_x86_64.tar.gz + sha256: 5d347d3e747dfbcb27261885194aaea5a286e01eb9ba12d3a7045d006b92dd2c + - selector: + matchLabels: + os: linux + arch: arm64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Linux_arm64.tar.gz + sha256: e5f2f057cec9573b569d024a330c03e13feff0f165db1a31b0e297778037ddf2 + - selector: + matchLabels: + os: darwin + arch: amd64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Darwin_x86_64.tar.gz + sha256: 740a1c0cbbdfa5c2090127345746c4f43ef48a5ccb050b2cc25f8b14e894c1ca + - selector: + matchLabels: + os: darwin + arch: arm64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Darwin_arm64.tar.gz + sha256: ab0b9b211986673fa8cd6175c9a11eceef2194ff1974a66631713dfaa67a8978 + - selector: + matchLabels: + os: windows + arch: amd64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Windows_x86_64.zip + sha256: 99364427984b12aa42afa63becb18f44b785fd970e0204f299fa2762fb2881ec + - selector: + matchLabels: + os: windows + arch: arm64 + uri: https://github.com/milo-os/assistant/releases/download/v0.0.14/milo-assistant_Windows_arm64.zip + sha256: 8afbc9f94fc4b2f7ae51818d2f7986ba9d9c8c523105a730efbb279711f87f24 diff --git a/schema/plugin-v1alpha1.json b/schema/plugin-v1alpha1.json deleted file mode 100644 index 753efa0..0000000 --- a/schema/plugin-v1alpha1.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plugin", - "description": "A datumctl plugin manifest (apiVersion: datumctl.datum.net/v1alpha1, kind: Plugin)", - "type": "object", - "required": ["apiVersion", "kind", "metadata", "spec"], - "properties": { - "apiVersion": { - "type": "string", - "const": "datumctl.datum.net/v1alpha1" - }, - "kind": { - "type": "string", - "const": "Plugin" - }, - "metadata": { - "type": "object", - "required": ["name"], - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "description": "Short plugin name, e.g. 'compute'. Must be lowercase alphanumeric with hyphens." - } - } - }, - "spec": { - "type": "object", - "required": ["shortDescription", "version", "platforms"], - "properties": { - "shortDescription": { - "type": "string", - "maxLength": 120, - "description": "One-line summary shown in datumctl plugin search." - }, - "description": { - "type": "string", - "description": "Optional longer help text." - }, - "homepage": { - "type": "string", - "format": "uri" - }, - "version": { - "type": "string", - "pattern": "^v[0-9]+\\.[0-9]+\\.[0-9]", - "description": "Semantic version string, e.g. v1.2.0" - }, - "platforms": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["uri", "sha256"], - "properties": { - "selector": { - "type": "object", - "properties": { - "matchLabels": { - "type": "object", - "properties": { - "os": { - "type": "string", - "enum": ["linux", "darwin", "windows"] - }, - "arch": { - "type": "string", - "enum": ["amd64", "arm64", "386"] - } - } - } - } - }, - "uri": { - "type": "string", - "format": "uri" - }, - "sha256": { - "type": "string", - "pattern": "^[a-f0-9]{64}$", - "description": "Lowercase hex SHA256 of the archive at uri." - }, - "files": { - "type": "array", - "items": { - "type": "object", - "required": ["from"], - "properties": { - "from": { "type": "string" }, - "to": { "type": "string" } - } - } - } - } - } - } - } - } - } -} diff --git a/scripts/generate_index.py b/scripts/generate_index.py deleted file mode 100755 index cd92fcf..0000000 --- a/scripts/generate_index.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -"""Generate index.yaml from the per-plugin manifests in plugins/. - -index.yaml is the single file datumctl reads to discover plugins, their -versions, and per-platform download locations. It is fully derived from -plugins/*.yaml, so it is regenerated deterministically here and checked for -drift in CI (`--check`) rather than hand-edited. - -Usage: - scripts/generate_index.py # write index.yaml - scripts/generate_index.py --check # fail if index.yaml is out of date -""" -import difflib -import os -import sys - -import yaml - -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -PLUGINS_DIR = os.path.join(REPO_ROOT, "plugins") -INDEX_PATH = os.path.join(REPO_ROOT, "index.yaml") - -# Catalog identity header, surfaced in `datumctl plugin index list` and -# `datumctl plugin browse`, followed by one entry per plugin manifest. -HEADER = { - "apiVersion": "datumctl.datum.net/v1alpha1", - "kind": "PluginList", - "name": "milo-os", - "description": "Portable CLI plugins for the Milo platform", - "owner": "milo-os", - "homepage": "https://github.com/milo-os/cli-plugins", -} - - -def render() -> tuple[str, int]: - index = dict(HEADER) - index["items"] = [] - for filename in sorted(os.listdir(PLUGINS_DIR)): - if not filename.endswith(".yaml"): - continue - with open(os.path.join(PLUGINS_DIR, filename)) as fh: - index["items"].append(yaml.safe_load(fh)) - text = yaml.dump( - index, default_flow_style=False, allow_unicode=True, sort_keys=False - ) - return text, len(index["items"]) - - -def main() -> None: - expected, count = render() - - if "--check" in sys.argv[1:]: - current = "" - if os.path.exists(INDEX_PATH): - with open(INDEX_PATH) as fh: - current = fh.read() - if current != expected: - print( - "index.yaml is out of sync with plugins/.\n" - "Run `python3 scripts/generate_index.py` and commit the result.\n", - file=sys.stderr, - ) - diff = difflib.unified_diff( - current.splitlines(), - expected.splitlines(), - fromfile="index.yaml (committed)", - tofile="index.yaml (expected)", - lineterm="", - ) - print("\n".join(diff), file=sys.stderr) - sys.exit(1) - print(f"index.yaml is in sync ({count} plugin(s)).") - return - - with open(INDEX_PATH, "w") as fh: - fh.write(expected) - print(f"Wrote index.yaml ({count} plugin(s)).") - - -if __name__ == "__main__": - main() diff --git a/scripts/sign-commit b/scripts/sign-commit deleted file mode 100755 index 2082bbe..0000000 --- a/scripts/sign-commit +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -# Commit files to a branch through the GitHub API so GitHub signs the commit. -# Usage: sign-commit [-z] BRANCH MESSAGE [PATH...] -# A PATH that is a file is committed with its current contents; one that does -# not exist at all is deleted from the branch. Directories and symlinks are -# refused. With -z, further PATHs are read from stdin, NUL-separated, so -# `git diff -z --name-only` output passes through without quoting or splitting. -# Exits 1 when there is nothing to commit, and 3 when the remote branch is not -# at the expected head, so a caller can refetch and try again. -# Set EXPECTED_HEAD to the commit the job checked out: the commit fails unless -# the remote branch is still there. Set BASE_SHA instead to create BRANCH at -# that commit (or reset it there) first. One of the two is required. -# Needs GH_TOKEN and GITHUB_REPOSITORY. -set -euo pipefail - -from_stdin=0 -if [ "${1:-}" = "-z" ]; then - from_stdin=1 - shift -fi -if [ $# -lt 2 ]; then - echo "Usage: sign-commit [-z] BRANCH MESSAGE [PATH...]" >&2 - exit 2 -fi - -branch=$1 -message=$2 -shift 2 - -repo=${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required} -if [ -z "${BASE_SHA:-}" ] && [ -z "${EXPECTED_HEAD:-}" ]; then - echo "sign-commit: set EXPECTED_HEAD (or BASE_SHA to create the branch)." >&2 - exit 2 -fi -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT - -: > "$work/additions" -: > "$work/deletions" -n=0 -adds=0 -dels=0 - -add_path() { - local path=$1 - if [ -L "$path" ]; then - echo "sign-commit: ${path} is a symlink; the API cannot commit one." >&2 - exit 1 - elif [ -d "$path" ]; then - echo "sign-commit: ${path} is a directory; pass the files in it." >&2 - exit 1 - elif [ -f "$path" ]; then - n=$((n + 1)) - adds=$((adds + 1)) - base64 < "$path" | tr -d '\n' > "$work/$n.b64" - jq -nc --arg path "$path" --rawfile contents "$work/$n.b64" '{path: $path, contents: $contents}' >> "$work/additions" - elif [ ! -e "$path" ]; then - dels=$((dels + 1)) - jq -nc --arg path "$path" '{path: $path}' >> "$work/deletions" - else - echo "sign-commit: ${path} is not a regular file." >&2 - exit 1 - fi -} - -for path in "$@"; do - add_path "$path" -done -if [ "$from_stdin" = 1 ]; then - while IFS= read -r -d '' path; do - add_path "$path" - done -fi - -if [ $((adds + dels)) -eq 0 ]; then - echo "sign-commit: nothing to commit." >&2 - exit 1 -fi - -remote_head() { - gh api "repos/${repo}/git/ref/heads/${branch}" --jq .object.sha -} - -if [ -n "${BASE_SHA:-}" ]; then - if ! gh api -X POST "repos/${repo}/git/refs" -f "ref=refs/heads/${branch}" -f "sha=${BASE_SHA}" >/dev/null 2>&1; then - gh api -X PATCH "repos/${repo}/git/refs/heads/${branch}" -f "sha=${BASE_SHA}" -F force=true >/dev/null - fi - expected=$BASE_SHA -else - expected=$EXPECTED_HEAD - head=$(remote_head) - if [ "$head" != "$expected" ]; then - echo "sign-commit: ${branch} is at ${head}, but this job checked out ${expected}." >&2 - exit 3 - fi -fi - -headline=${message%%$'\n'*} -body="" -if [ "$headline" != "$message" ]; then - body=${message#*$'\n'} - body=${body#$'\n'} -fi - -query='mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { url } } }' - -jq -n --arg query "$query" --arg repo "$repo" --arg branch "$branch" --arg head "$expected" \ - --arg headline "$headline" --arg body "$body" \ - --slurpfile additions "$work/additions" --slurpfile deletions "$work/deletions" '{ - query: $query, - variables: {input: { - branch: {repositoryNameWithOwner: $repo, branchName: $branch}, - expectedHeadOid: $head, - message: ({headline: $headline} + (if $body == "" then {} else {body: $body} end)), - fileChanges: {additions: $additions, deletions: $deletions} - }} - }' > "$work/request.json" - -for attempt in 1 2 3 4 5; do - if gh api graphql --input "$work/request.json" --jq .data.createCommitOnBranch.commit.url; then - exit 0 - fi - head=$(remote_head) - if [ "$head" != "$expected" ]; then - echo "sign-commit: ${branch} moved to ${head} while committing; not retrying." >&2 - exit 3 - fi - echo "Attempt ${attempt} failed, retrying." >&2 - sleep $((attempt * 2)) -done -echo "Could not commit to ${branch}." >&2 -exit 1 diff --git a/scripts/verify_manifests.py b/scripts/verify_manifests.py deleted file mode 100755 index 11614af..0000000 --- a/scripts/verify_manifests.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -"""Validate plugin manifests and verify their published release assets. - -For each manifest (all of plugins/*.yaml by default, or the paths passed as -arguments) this: - * validates it against schema/plugin-v1alpha1.json, - * confirms every platform download URL resolves, and - * downloads each archive and confirms its sha256 matches the manifest. - -This is the check that keeps the catalog installable: a wrong checksum or a -deleted/retagged release asset fails here, in CI, rather than in a user's -`datumctl plugin install`. - -Usage: - scripts/verify_manifests.py # every plugins/*.yaml - scripts/verify_manifests.py plugins/ipam.yaml ... -""" -import hashlib -import json -import os -import sys -import urllib.error -import urllib.request - -import yaml -from jsonschema import Draft7Validator, FormatChecker - -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SCHEMA_PATH = os.path.join(REPO_ROOT, "schema", "plugin-v1alpha1.json") -PLUGINS_DIR = os.path.join(REPO_ROOT, "plugins") - - -def verify(path: str, validator: Draft7Validator) -> bool: - print(f"== {os.path.relpath(path, REPO_ROOT)}") - with open(path) as fh: - manifest = yaml.safe_load(fh) - - errors = sorted(validator.iter_errors(manifest), key=lambda e: list(e.path)) - if errors: - for e in errors: - loc = "/".join(str(p) for p in e.path) or "(root)" - print(f" SCHEMA ERROR at {loc}: {e.message}", file=sys.stderr) - return False - print(" schema OK") - - ok = True - for platform in manifest["spec"]["platforms"]: - uri = platform["uri"] - expected = platform["sha256"] - print(f" {uri}") - try: - with urllib.request.urlopen(uri) as resp: - data = resp.read() - except urllib.error.HTTPError as exc: - print(f" ERROR: returned HTTP {exc.code}", file=sys.stderr) - ok = False - continue - except urllib.error.URLError as exc: - print(f" ERROR: unreachable: {exc.reason}", file=sys.stderr) - ok = False - continue - actual = hashlib.sha256(data).hexdigest() - if actual != expected: - print( - f" ERROR: sha256 mismatch\n" - f" expected {expected}\n" - f" actual {actual}", - file=sys.stderr, - ) - ok = False - else: - print(f" OK {actual}") - return ok - - -def main() -> None: - paths = sys.argv[1:] or [ - os.path.join(PLUGINS_DIR, f) - for f in sorted(os.listdir(PLUGINS_DIR)) - if f.endswith(".yaml") - ] - with open(SCHEMA_PATH) as fh: - validator = Draft7Validator(json.load(fh), format_checker=FormatChecker()) - - failed = [p for p in paths if not verify(p, validator)] - if failed: - rel = ", ".join(os.path.relpath(p, REPO_ROOT) for p in failed) - print(f"\nFAILED {len(failed)} manifest(s): {rel}", file=sys.stderr) - sys.exit(1) - print(f"\nAll {len(paths)} manifest(s) valid and verified.") - - -if __name__ == "__main__": - main()