@biffo/cli 0.162.0 → 0.162.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ # Publishes this plugin's entry to the Biffo plugin registry.
2
+ #
3
+ # Without this, a plugin is invisible. The portal's plugin store reads a static
4
+ # `plugins.json` from the public `keiranholloway/biffo-plugins-registry` repo,
5
+ # and adding an entry used to be a manual step that nothing performed — so the
6
+ # registry sat empty from the day it was created while real plugins ran in
7
+ # production, and every instance's store read "No plugins available yet".
8
+ # Nobody had forgotten a step; there was no step. This is the step.
9
+ #
10
+ # It runs when `biffo.plugin.json` changes on `dev`, and republishes the entry
11
+ # from that manifest via `scripts/upsert_plugin.py` in the registry repo.
12
+ #
13
+ # ## What it does and does not overwrite
14
+ #
15
+ # `description`, `tags` and `ui_components` in the registry are **marketplace
16
+ # copy** — written to sell the plugin to a human browsing the store, which is a
17
+ # different job from this manifest's `description`, written for engineers. The
18
+ # registry preserves those three once set, seeding them from the manifest only
19
+ # on a plugin's first publish. `version`, `repo`, `required_core_version` and
20
+ # `api_routes` track this manifest on every run. `status` is an operator
21
+ # decision and is never changed by a publish.
22
+ #
23
+ # ## It needs a token
24
+ #
25
+ # The registry is a different repo, so the job-scoped GITHUB_TOKEN cannot write
26
+ # to it. Until `REGISTRY_PUBLISH_TOKEN` is set on this repository the job warns
27
+ # and skips — deliberately not a failure, so a missing token never reds an
28
+ # otherwise good merge, but deliberately not silent either, because silence is
29
+ # how the registry went stale in the first place.
30
+ #
31
+ # Create a fine-grained PAT with `contents: write` on the registry repo, then:
32
+ # gh secret set REGISTRY_PUBLISH_TOKEN
33
+ #
34
+ # If your plugin's repo is **public**, you may not need this at all: the
35
+ # registry also runs a pull-based sync that re-derives every entry listed in its
36
+ # `sources.json` from the source manifest, with no credential anywhere. Add your
37
+ # plugin there and the store converges within a week of any version bump. The
38
+ # token only upgrades that from eventually-correct to immediately-correct.
39
+ name: Publish to plugin registry
40
+
41
+ on:
42
+ push:
43
+ branches: [dev]
44
+ paths:
45
+ - biffo.plugin.json
46
+ workflow_dispatch:
47
+
48
+ jobs:
49
+ publish:
50
+ name: Upsert registry entry
51
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
52
+ steps:
53
+ - name: Check out this plugin
54
+ uses: actions/checkout@v4
55
+ with:
56
+ path: plugin
57
+
58
+ - name: Check for the registry token
59
+ id: token
60
+ env:
61
+ REGISTRY_PUBLISH_TOKEN: ${{ secrets.REGISTRY_PUBLISH_TOKEN }}
62
+ run: |
63
+ if [ -z "${REGISTRY_PUBLISH_TOKEN}" ]; then
64
+ echo "::warning title=Registry not updated::REGISTRY_PUBLISH_TOKEN is not set on this repository, so this plugin's entry in the Biffo plugin registry was NOT updated. The portal marketplace will keep showing the previously published version. See this workflow's header comment."
65
+ echo "present=false" >> "$GITHUB_OUTPUT"
66
+ else
67
+ echo "present=true" >> "$GITHUB_OUTPUT"
68
+ fi
69
+
70
+ - name: Check out the registry
71
+ if: steps.token.outputs.present == 'true'
72
+ uses: actions/checkout@v4
73
+ with:
74
+ repository: keiranholloway/biffo-plugins-registry
75
+ token: ${{ secrets.REGISTRY_PUBLISH_TOKEN }}
76
+ path: registry
77
+
78
+ - name: Set up Python
79
+ if: steps.token.outputs.present == 'true'
80
+ uses: actions/setup-python@v5
81
+ with:
82
+ python-version: '3.13'
83
+
84
+ - name: Install jsonschema
85
+ if: steps.token.outputs.present == 'true'
86
+ run: pip install jsonschema
87
+
88
+ - name: Upsert this plugin's entry
89
+ if: steps.token.outputs.present == 'true'
90
+ working-directory: registry
91
+ run: |
92
+ set -euo pipefail
93
+ python3 scripts/upsert_plugin.py \
94
+ --manifest "../plugin/biffo.plugin.json" \
95
+ --repo "${{ github.server_url }}/${{ github.repository }}"
96
+
97
+ - name: Commit and push
98
+ if: steps.token.outputs.present == 'true'
99
+ working-directory: registry
100
+ run: |
101
+ set -euo pipefail
102
+ if git diff --quiet -- plugins.json; then
103
+ echo "Registry entry already current — nothing to publish."
104
+ exit 0
105
+ fi
106
+ version="$(python3 -c "import json;print(json.load(open('../plugin/biffo.plugin.json'))['version'])")"
107
+ name="$(python3 -c "import json;print(json.load(open('../plugin/biffo.plugin.json'))['name'])")"
108
+ git config user.name "github-actions[bot]"
109
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
110
+ git add plugins.json
111
+ git commit -m "feat(registry): publish ${name}@${version}"
112
+ # Another plugin may have published between checkout and now.
113
+ git pull --rebase origin main
114
+ git push origin HEAD:main
@@ -1,15 +1,40 @@
1
1
  name: Release
2
2
 
3
- # Runs on every push of a `v*` tag: bumps/verifies the version via
4
- # python-semantic-release, builds the sdist/wheel, and publishes to PyPI.
3
+ # Runs on every push of a `v*` tag: checks that every version this plugin
4
+ # declares agrees with the tag, builds the sdist/wheel, and cuts a GitHub
5
+ # Release. Publishing to PyPI is opt-in — see below.
5
6
  #
6
- # ASPIRATIONAL PYPI PUBLISH: this repo (and every plugin repo forked from
7
- # this template) has no real PyPI project or publishing credential yet, so
8
- # the publish job below runs and fails at the
9
- # `pypa/gh-action-pypi-publish` step — that's expected, not a bug in this
10
- # template. Configure a publisher for *your* plugin's own PyPI project and
11
- # it starts publishing on the next tag push with no further changes. See
12
- # README.md's "PyPI publishing" section.
7
+ # ## Three versions have to agree
8
+ #
9
+ # A plugin declares its version twice, and the tag is a third:
10
+ #
11
+ # - `pyproject.toml` -> the Python package
12
+ # - `biffo.plugin.json` -> what the plugin registry publishes, and therefore
13
+ # the version the portal's plugin store shows
14
+ # - the `v*` tag itself
15
+ #
16
+ # The manifest one is the easy one to forget, and it fails in the worst way
17
+ # available: silently. Tagging v0.2.0 with a stale manifest gives you a v0.2.0
18
+ # release, a 0.2.0 package, and a plugin store still advertising 0.1.0 —
19
+ # indefinitely, with nothing anywhere reporting a problem. The registry sync
20
+ # faithfully re-derives whatever the manifest says, so it propagates the stale
21
+ # value rather than catching it. Both files are therefore checked against the
22
+ # tag below, and a mismatch fails the release rather than shipping a lie to the
23
+ # store.
24
+ #
25
+ # ## PyPI publishing is opt-in
26
+ #
27
+ # No plugin scaffolded from this skeleton has a PyPI project or publishing
28
+ # credential. Rather than ship a workflow whose last step always fails — which
29
+ # only teaches people that a red release means nothing, and is how a real
30
+ # failure later goes unnoticed — the publish step is gated on the
31
+ # `PYPI_PUBLISH` repository variable and skips with a notice.
32
+ #
33
+ # To enable for YOUR plugin: create the PyPI project, configure a trusted
34
+ # publisher (OIDC — no stored secret to rotate or leak; the `id-token: write`
35
+ # permission below is already in place for it), then
36
+ # `gh variable set PYPI_PUBLISH --body true`. See README.md's
37
+ # "Releasing" section.
13
38
  #
14
39
  # Note this is about *this plugin's* own distribution. The SDK it depends
15
40
  # on, biffo-plugin-sdk, has its own release path upstream —
@@ -18,6 +43,11 @@ name: Release
18
43
  # "biffo-plugin-sdk dependency" section for what that means for `uv sync`
19
44
  # before the SDK's first release lands.
20
45
  #
46
+ # Registry publishing is NOT done here. `publish-registry.yml` owns that,
47
+ # triggered by `biffo.plugin.json` changing on `dev` — i.e. when the version is
48
+ # merged, not when it is tagged. The marketplace should reflect `dev`, and
49
+ # tagging is an independent decision.
50
+ #
21
51
  # Prefer PyPI Trusted Publishing (OIDC) over a long-lived PYPI_API_TOKEN
22
52
  # when you set this up: it needs no stored secret to rotate or leak. The
23
53
  # `id-token: write` permission below is already in place for it, and
@@ -44,11 +74,12 @@ env:
44
74
 
45
75
  jobs:
46
76
  release:
47
- name: Build, Version & Publish
77
+ name: Verify, Build & Release
48
78
  runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
49
- environment:
50
- name: pypi
51
- url: https://pypi.org/project/biffo-plugin-example/
79
+ # No `environment: pypi` here: a job-level environment gates the WHOLE job,
80
+ # and this job now verifies and releases whether or not it publishes. If you
81
+ # enable PyPI and want environment protection on it, split publishing into
82
+ # its own job (passing dist/ as an artifact) and put the environment there.
52
83
  steps:
53
84
  - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
54
85
  with:
@@ -72,13 +103,24 @@ jobs:
72
103
  id: version
73
104
  run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
74
105
 
75
- - name: Verify pyproject.toml version matches tag
106
+ - name: Verify pyproject.toml and biffo.plugin.json match the tag
107
+ env:
108
+ TAG_VERSION: ${{ steps.version.outputs.version }}
76
109
  run: |
77
- project_version=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
78
- if [ "$project_version" != "${{ steps.version.outputs.version }}" ]; then
79
- echo "::error::pyproject.toml version ($project_version) does not match tag ${GITHUB_REF_NAME}"
80
- exit 1
110
+ set -euo pipefail
111
+ project_version="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
112
+ manifest_version="$(python3 -c "import json; print(json.load(open('biffo.plugin.json'))['version'])")"
113
+ failed=0
114
+ if [ "$project_version" != "$TAG_VERSION" ]; then
115
+ echo "::error file=pyproject.toml::pyproject.toml version ($project_version) does not match tag $GITHUB_REF_NAME"
116
+ failed=1
117
+ fi
118
+ if [ "$manifest_version" != "$TAG_VERSION" ]; then
119
+ echo "::error file=biffo.plugin.json::biffo.plugin.json version ($manifest_version) does not match tag $GITHUB_REF_NAME. The plugin registry publishes this field, so releasing with it stale would leave the portal's plugin store advertising $manifest_version."
120
+ failed=1
81
121
  fi
122
+ [ "$failed" -eq 0 ] || exit 1
123
+ echo "pyproject.toml, biffo.plugin.json and the tag all agree on $TAG_VERSION"
82
124
 
83
125
  - name: Install python-semantic-release
84
126
  run: uv tool install python-semantic-release
@@ -89,13 +131,12 @@ jobs:
89
131
  - name: Build distribution
90
132
  run: uv build
91
133
 
92
- # PYPI PUBLISH — see the file-level comment above. This step is real
93
- # and will run on every `v*` tag push, but has no credentials
94
- # configured yet anywhere in the plugin ecosystem (this template, the
95
- # SDK, or the RBAC reference plugin) and is expected to fail here
96
- # until a PyPI project + trusted publisher (or PYPI_API_TOKEN) is set
97
- # up for this specific plugin package.
134
+ # PYPI PUBLISH — opt-in; see the file-level comment above. Off until
135
+ # `PYPI_PUBLISH` is set, so a plugin with no PyPI project (which is every
136
+ # plugin, on the day it is scaffolded) gets a green release rather than
137
+ # one that always fails at the last step.
98
138
  - name: Publish to PyPI
139
+ if: vars.PYPI_PUBLISH == 'true'
99
140
  uses: pypa/gh-action-pypi-publish@release/v1
100
141
  with:
101
142
  # Prefer PyPI's trusted publishing (OIDC, via the `id-token: write`
@@ -107,6 +148,11 @@ jobs:
107
148
  # up for this project yet.
108
149
  skip-existing: true
109
150
 
151
+ - name: Note that PyPI publishing is not enabled
152
+ if: vars.PYPI_PUBLISH != 'true'
153
+ run: |
154
+ echo "::notice title=PyPI publish skipped::The PYPI_PUBLISH repository variable is not set to 'true', so the built distribution was not uploaded to PyPI. The GitHub Release below still carries it. See this workflow's header comment to enable."
155
+
110
156
  - name: Create GitHub Release
111
157
  uses: softprops/action-gh-release@v2
112
158
  with:
@@ -219,14 +219,59 @@ equivalent `github_branch_protection` resource. If you're setting this up
219
219
  by hand via the GitHub UI instead: **Settings → Branches → Add branch
220
220
  protection rule**, pattern `main`, and set each field above to match.
221
221
 
222
- ## PyPI publishing
223
-
224
- `release.yml` triggers on `v*` tag pushes, re-runs the full CI gate against
225
- the tagged commit, verifies the tag matches `pyproject.toml`'s
226
- `project.version`, builds the sdist/wheel, and publishes to PyPI via
227
- `pypa/gh-action-pypi-publish`. **This publish step will not succeed until a
228
- real PyPI project exists for this plugin and either a trusted publisher (OIDC)
229
- or a `PYPI_API_TOKEN` secret is configured** — see `release.yml`'s header
230
- comment for the full rationale (this is intentional, per issue #26's
231
- decision to build the release workflow as if publishing were real even
232
- though no plugin in the ecosystem is on PyPI yet).
222
+ ## Releasing
223
+
224
+ `release.yml` triggers on `v*` tag pushes: it re-runs the full CI gate against
225
+ the tagged commit, verifies the versions agree, builds the sdist/wheel, and
226
+ cuts a GitHub Release.
227
+
228
+ ### Three versions have to agree
229
+
230
+ Your plugin declares its version twice, and the tag is a third:
231
+
232
+ | where | what reads it |
233
+ | --- | --- |
234
+ | `pyproject.toml` `project.version` | the Python package |
235
+ | `biffo.plugin.json` `version` | the plugin registry — **and therefore the version the portal's plugin store shows** |
236
+ | the `v*` tag | the GitHub Release |
237
+
238
+ `release.yml` checks all three and fails the release on any mismatch.
239
+
240
+ The manifest one is the one to watch. It is easy to forget and it fails
241
+ silently: tag `v0.2.0` with a stale manifest and you get a `v0.2.0` release, a
242
+ `0.2.0` package, and a plugin store still advertising `0.1.0` — indefinitely,
243
+ with nothing anywhere reporting a problem. The registry sync re-derives
244
+ whatever the manifest says, so it propagates the stale value rather than
245
+ catching it. Bump both.
246
+
247
+ Note that the registry entry is published by `publish-registry.yml` when
248
+ `biffo.plugin.json` changes on `dev`, not by this workflow on a tag — the
249
+ marketplace reflects `dev`, and tagging is an independent decision.
250
+
251
+ ### Getting into the plugin store
252
+
253
+ `publish-registry.yml` publishes your entry to the Biffo plugin registry, which
254
+ is what the portal's plugin store reads. It needs a `REGISTRY_PUBLISH_TOKEN`
255
+ secret (a fine-grained PAT with `contents: write` on the registry repo) and
256
+ warns-and-skips without one, so check its header comment before assuming your
257
+ plugin will appear.
258
+
259
+ If your repo is public you may not need the token: the registry also runs a
260
+ credential-free pull-based sync over the repos listed in its `sources.json`.
261
+ Adding yours there is enough.
262
+
263
+ ### PyPI publishing is opt-in
264
+
265
+ The publish step is gated on a `PYPI_PUBLISH` repository variable and skips
266
+ with a notice until you set it, so a freshly-scaffolded plugin gets a green
267
+ release instead of one that always fails at the last step. The built
268
+ distribution still ships on the GitHub Release either way.
269
+
270
+ To enable it for your plugin:
271
+
272
+ 1. Create the PyPI project.
273
+ 2. Configure a **trusted publisher** (OIDC) for this repo at
274
+ `https://pypi.org/manage/project/<name>/settings/publishing/` — preferred
275
+ over a long-lived `PYPI_API_TOKEN`, since there is no stored secret to
276
+ rotate or leak. The `id-token: write` permission is already in place.
277
+ 3. `gh variable set PYPI_PUBLISH --body true`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.162.0",
3
+ "version": "0.162.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",