diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e0aeef..99da2c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,6 +82,7 @@ jobs: "name": p["name"], "repo": p["repo"], "tag": p.get("tag"), "source_commit": p["source_commit"], "version": version, "bin": p["plugin_bin"], + "cargo_package": p.get("cargo_package"), "triple": t["triple"], "os": t["os"], "cross": t["cross"], }) print("plugins=" + json.dumps(plugins)) @@ -208,6 +209,7 @@ jobs: env: MATRIX_TRIPLE: ${{ matrix.triple }} MATRIX_BIN: ${{ matrix.bin }} + MATRIX_CARGO_PACKAGE: ${{ matrix.cargo_package }} run: | set -euo pipefail @@ -231,11 +233,17 @@ jobs: go build -ldflags="-s -w" -o "$outdir/$binexe" . else errfile="$(mktemp)" - if ! cargo build --release --locked --target "$MATRIX_TRIPLE" 2>"$errfile"; then + build_cmd=(cargo build --release --target "$MATRIX_TRIPLE") + if [ -n "${MATRIX_CARGO_PACKAGE:-}" ]; then + # Workspace-member plugin: the repo root builds something else + # by default, so select the plugin crate explicitly. + build_cmd+=(--package "$MATRIX_CARGO_PACKAGE") + fi + if ! "${build_cmd[@]}" --locked 2>"$errfile"; then if grep -q -- "--locked" "$errfile"; then echo "Locked build failed due to lockfile update; retrying without --locked" >&2 cat "$errfile" >&2 - cargo build --release --target "$MATRIX_TRIPLE" + "${build_cmd[@]}" else cat "$errfile" >&2 exit 1 diff --git a/README.md b/README.md index bec787f..fa72f28 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ plugin protocol handshake and corrected after a successful Nu 0.101.0 lifecycle. These plugins are in `manifest.json` but have not yet been built by CI. They will move to "Currently active" once the build succeeds and `verified_with` is populated. +- `wangjiawen2013/charton` @ `v0.9.0` → `nu_plugin_charton` 0.9.0 _(workspace member crate; built via cargo_package)_ - `dead10ck/nu_plugin_dns` @ `v4.0.11` → `nu_plugin_dns` 4.0.11 _(bumped from v4.0.10/Nu 0.113)_ - `tonythethompson/nu_plugin_qr_maker` @ `v1.2.0` → `nu_plugin_qr_maker` 1.2.0 _(fork: bumped from Nu 0.94)_ - `tonythethompson/nu_plugin_explore` @ `v0.114.1` → `nu_plugin_explore` 0.114.1 _(fork: bumped from Nu 0.102)_ diff --git a/docs/backlog.json b/docs/backlog.json index 21a064b..763715b 100644 --- a/docs/backlog.json +++ b/docs/backlog.json @@ -1089,6 +1089,104 @@ ], "backfill_targets": [], "c1_note": "2026-08-05: wave2 0.6.2 in official registry; lifecycle-prove OK Linux+Windows/Nu 0.114.1." + }, + { + "repo": "wangjiawen2013/charton", + "name": "nu_plugin_charton", + "plugin_bin": "nu_plugin_charton", + "stars": 448, + "status": "INTAKE_PENDING", + "versions": [ + { + "tag": "v0.9.0", + "nu_plugin": "0.116", + "nu_protocol": "0.116", + "has_assets": false, + "note": "GitHub release exists with no assets; CI-build from source (plugin crate in nu_plugin_charton/ workspace member)" + } + ], + "backfill_targets": [], + "c1_note": "2026-10-31: found via awesome-nu audit (was untracked). Plugin crate 0.9.0 pins nu-plugin =0.116, current Nu minor. Apache-2.0. High demand signal for a plugin (448 stars): renders pipeline tables as terminal/SVG/PNG charts. Promotion started 2026-10-31: manifest active[] entry added on the promote/nu_plugin_charton-0.9.0 branch with cargo_package selection (the plugin is a workspace member; the repo root is the charton library). Local locked build of the plugin crate on Linux x86_64 and a Nu 0.116.1 command smoke both pass; CI cross-platform build is the next gate." + }, + { + "repo": "kazu/seekzstdsep", + "name": "nu_plugin_zstdsep", + "plugin_bin": "nu_plugin_zstdsep", + "stars": 2, + "status": "SOURCE_ONLY", + "versions": [ + { + "tag": "v0.9.0", + "nu_plugin": "0.116", + "nu_protocol": "0.116", + "has_assets": false, + "note": "tag only, no GitHub release object; plugin crate at tag is 0.7.16 (repo versions the parent tool separately); crate at HEAD is 0.9.1 on nu-plugin 0.116.1" + } + ], + "backfill_targets": [], + "c1_note": "2026-10-31: found via awesome-nu audit (was untracked). Seekable separator-aware zstd reader plugin (jsonl/csv/logfmt). MIT. CI-build from source." + }, + { + "repo": "gtnebel/nu_plugin_nuplot", + "name": "nu_plugin_nuplot", + "plugin_bin": "nu_plugin_nuplot", + "stars": 0, + "status": "ELIGIBLE", + "versions": [ + { + "tag": "v0.2.8", + "nu_plugin": "0.116", + "nu_protocol": "0.116", + "has_assets": true, + "note": "8 release assets; upstream README compat matrix v0.2.8 -> Nu 0.116.0; implemented in Go on the ainvaltin/nu-plugin library (maintainer fork, Oct 2026), so no Cargo nu-plugin pin to read" + }, + { + "tag": "v0.2.7", + "nu_plugin": "0.114", + "nu_protocol": "0.114", + "has_assets": true, + "note": "8 release assets; upstream README compat matrix v0.2.7 -> Nu 0.114.0" + }, + { + "tag": "v0.2.6", + "nu_plugin": "0.113", + "nu_protocol": "0.113", + "has_assets": true, + "note": "8 release assets; upstream README compat matrix v0.2.6 -> Nu 0.113.0" + } + ], + "backfill_targets": ["0.114", "0.113"], + "c1_note": "2026-10-31: found via awesome-nu audit (was untracked). BSD-2-Clause. Caveat: Go implementation does not fit the numan-plugins Cargo CI-build path; intake would ship upstream release assets or need a Go build lane. Upstream README documents the compat matrix (v0.2.8 -> Nu 0.116, v0.2.7 -> Nu 0.114, v0.2.6 -> Nu 0.113); historical releases recorded for backfill planning. Protocol compat still needs a live check against current Nu before promotion." + }, + { + "repo": "nushell/nu_plugin_gstat", + "name": "nu_plugin_gstat", + "plugin_bin": "nu_plugin_gstat", + "stars": 0, + "status": "BUILTIN", + "versions": [], + "backfill_targets": [], + "c1_note": "Official Nushell plugin (nushell/nushell crates/nu_plugin_gstat). Ships with Nu releases; not a numan candidate (see official/bundled policy). Recorded from awesome-nu so the audit stops flagging it untracked." + }, + { + "repo": "nushell/nu_plugin_inc", + "name": "nu_plugin_inc", + "plugin_bin": "nu_plugin_inc", + "stars": 0, + "status": "BUILTIN", + "versions": [], + "backfill_targets": [], + "c1_note": "Official Nushell plugin (nushell/nushell crates/nu_plugin_inc). Ships with Nu releases; not a numan candidate (see official/bundled policy). Recorded from awesome-nu so the audit stops flagging it untracked." + }, + { + "repo": "nushell/nu_plugin_query", + "name": "nu_plugin_query", + "plugin_bin": "nu_plugin_query", + "stars": 0, + "status": "BUILTIN", + "versions": [], + "backfill_targets": [], + "c1_note": "Official Nushell plugin (nushell/nushell crates/nu_plugin_query). Ships with Nu releases; not a numan candidate (see official/bundled policy). Distinct from fdncred/nu_plugin_query_git, which is in the registry. Recorded from awesome-nu so the audit stops flagging it untracked." } ] } diff --git a/docs/roadmap.md b/docs/roadmap.md index 1ff5a45..ddc219c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -49,7 +49,7 @@ contract pins in this repo alone. - Wave 1, Wave 2, and Wave 3 CI-built plugins are on **`main`**, published, and intaken into the official registry. Wave 3 targets Nu 0.116; Wave 2 targets Nu 0.114; Wave 1 includes Nu 0.113.1 and Nu 0.112.2. `manifest.json` `active[]` currently - holds 33 plugins. + holds 34 plugins. - Release upload uses claim-ID upload (upload-by-id) to avoid softprops creating a second draft. diff --git a/manifest.json b/manifest.json index c285d53..3cde0f8 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,8 @@ "note": "Build matrix for the numan#30 CI build+sign pipeline. Each active plugin is cross-compiled from its immutable upstream source_commit after its human-facing tag is verified to resolve to that commit, packaged per target (.tar.gz on Unix, .zip on Windows), and published once as a GitHub release asset on THIS repo. numan-registry then pins those asset URLs via scripts/add-package.py and signs the index with the trust root. This repo never holds signing keys.", "schema_notes": { "intake_mode": "Optional per-entry field, 'tagged' (default) or 'commit-snapshot'. 'tagged' entries must provide a non-empty 'tag' field and the tag is verified via git ls-remote against source_commit. 'commit-snapshot' entries may omit 'tag' or set it to null; the tag-to-commit ls-remote check is skipped and source_commit is confirmed fetchable upstream via a scoped shallow fetch and verified to be a commit object, not a tree/blob/tag. The workflow derives the version for commit-snapshot entries as 0.0.0-snapshot..<7-char-sha> once per run (date = intake date) and uses it consistently for packaging, the release tag, and the generated spec, instead of the manifest 'version' field. gen_spec.py emits 'provenance': 'commit-snapshot' at the top level of the generated spec.", - "upstream_repo": "Optional per-entry field, 'owner/name' of the ORIGINAL upstream repo. Only set on numan-maintained fork entries: 'repo'/'owner'/'source_commit' point at the fork (owner must be exactly 'numan-maintained'), 'upstream_repo' preserves the original author's identity. gen_spec.py emits it as source.upstream in the generated spec and appends a fork-attribution note to the description. See https://github.com/numan-cli/numan/blob/master/docs/adr/0001-ecosystem-trust-upstream-contribution-fork-stewardship.md for the full fork-eligibility and stewardship requirements that must be satisfied before an entry like this is added -- this field only records identity, it doesn't grant eligibility." + "upstream_repo": "Optional per-entry field, 'owner/name' of the ORIGINAL upstream repo. Only set on numan-maintained fork entries: 'repo'/'owner'/'source_commit' point at the fork (owner must be exactly 'numan-maintained'), 'upstream_repo' preserves the original author's identity. gen_spec.py emits it as source.upstream in the generated spec and appends a fork-attribution note to the description. See https://github.com/numan-cli/numan/blob/master/docs/adr/0001-ecosystem-trust-upstream-contribution-fork-stewardship.md for the full fork-eligibility and stewardship requirements that must be satisfied before an entry like this is added -- this field only records identity, it doesn't grant eligibility.", + "cargo_package": "Optional per-entry field naming the Cargo package to build when the plugin is a workspace member rather than the repo's default package. The build workflow passes it to cargo build --package; entries that omit it build the repo's default package. Required in practice for workspace repos whose root package is a library or a different binary (for example wangjiawen2013/charton, whose root is the charton library and whose plugin crate is nu_plugin_charton)." }, "default_targets": [ "x86_64-unknown-linux-gnu", @@ -513,6 +514,22 @@ "verified_with": [], "description": "A plugin with various encoding schemes, from Crockford's base-32 to HTML entity escaping.", "tags": ["plugin", "endecode", "encoding", "ci-built"] + }, + { + "repo": "wangjiawen2013/charton", + "name": "nu_plugin_charton", + "owner": "wangjiawen2013", + "plugin_bin": "nu_plugin_charton", + "cargo_package": "nu_plugin_charton", + "tag": "v0.9.0", + "source_commit": "c36e794a3c5b03c53b033352b432daf0ad80e962", + "version": "0.9.0", + "exclude_targets": ["x86_64-apple-darwin"], + "exclude_reason": "Intel Mac EOL; not supported", + "nu_version": ">=0.116.0 <0.117.0", + "verified_with": [], + "description": "Turn Nushell pipeline tables into charts, rendered inline in the terminal or saved as SVG/PNG.", + "tags": ["plugin", "charts", "visualization", "ci-built"] } ], "backlog_note": "Remaining demand-ranked source-only plugins live in docs/backlog.json. Wave 1 and wave 2 (jwalk, strutils, query_git, ulid, nutext) are in the official registry.", diff --git a/scripts/test_validate_manifest.py b/scripts/test_validate_manifest.py index 833b11c..dfec9c4 100644 --- a/scripts/test_validate_manifest.py +++ b/scripts/test_validate_manifest.py @@ -175,6 +175,17 @@ def test_rejects_unknown_intake_mode(self): with self.assertRaisesRegex(ValueError, "intake_mode must be one of"): self.mod.validate_manifest(self.manifest([self.entry(intake_mode="bogus")])) + def test_accepts_cargo_package_selector(self): + entries = self.mod.validate_manifest( + self.manifest([self.entry(cargo_package="nu_plugin_widget")]) + ) + self.assertEqual(entries[0]["cargo_package"], "nu_plugin_widget") + + def test_rejects_invalid_cargo_package(self): + for bad in ("", "not a package", "foo/bar", 42): + with self.assertRaisesRegex(ValueError, "cargo_package must be a Cargo package name"): + self.mod.validate_manifest(self.manifest([self.entry(cargo_package=bad)])) + def test_verify_upstream_skips_tag_check_for_commit_snapshot(self): entry = self.entry(intake_mode="commit-snapshot") with mock.patch.object(self.mod, "resolve_tag") as resolve_tag: diff --git a/scripts/test_workflow_safety.py b/scripts/test_workflow_safety.py index 27b314e..37280a6 100644 --- a/scripts/test_workflow_safety.py +++ b/scripts/test_workflow_safety.py @@ -193,6 +193,18 @@ def test_archive_intake_publishes_through_the_release_transaction(self): self.assertIn("release_transaction.py cleanup", INTAKE) self.assertNotIn("gh release create", INTAKE) + def test_build_plumbs_cargo_package_selection(self): + """Workspace-member plugins need their crate selected in the build. + + The setup job must carry the manifest's cargo_package into the + matrix, the build step must receive it through env (never shell + interpolation), and the cargo invocation must add --package only + from that env value. + """ + self.assertIn('"cargo_package": p.get("cargo_package")', BUILD) + self.assertIn("MATRIX_CARGO_PACKAGE: ${{ matrix.cargo_package }}", BUILD) + self.assertIn('build_cmd+=(--package "$MATRIX_CARGO_PACKAGE")', BUILD) + def test_macos_uses_supported_runners(self): """Keep the executable matrix and manifest metadata on current macOS runners.""" targets = workflow_targets(BUILD) diff --git a/scripts/validate_manifest.py b/scripts/validate_manifest.py index 9f220ec..5cf4067 100644 --- a/scripts/validate_manifest.py +++ b/scripts/validate_manifest.py @@ -13,6 +13,7 @@ SHA_RE = re.compile(r"^[0-9a-f]{40}$") GITHUB_REPO_SLUG_RE = re.compile(r"^[^/\s]+/[^/\s]+$") +CARGO_PACKAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$") COMMAND_TIMEOUT_SECONDS = 30 REQUIRED_FIELDS = { "repo", @@ -85,6 +86,25 @@ def validate_intake_fields(name: str, entry: dict) -> None: raise ValueError(f"{name}: tag must be a string or null") +def validate_cargo_package(name: str, entry: dict) -> None: + """Validate the optional cargo_package workspace selector. + + Most plugins are their repo's default Cargo package and omit the field. + A plugin that lives in a workspace member crate (the repo root builds a + library or a different binary by default) sets cargo_package to that + crate's package name so the build workflow compiles it with + ``cargo build --package``. + """ + cargo_package = entry.get("cargo_package") + if cargo_package is None: + return + if not isinstance(cargo_package, str) or not CARGO_PACKAGE_RE.fullmatch(cargo_package): + raise ValueError( + f"{name}: cargo_package must be a Cargo package name " + f"(letters, digits, '-' or '_'), got {cargo_package!r}" + ) + + def validate_upstream_repo(entry: dict) -> None: """Enforce ADR 0001 fork-identity invariants: only 'numan-maintained' may set upstream_repo, it must be required (not optional) for that owner, and @@ -130,6 +150,7 @@ def validate_active_entry(manifest: dict, entry: dict, names: set[str]) -> str: if not isinstance(source_commit, str) or not SHA_RE.fullmatch(source_commit): raise ValueError(f"{name}: source_commit must be 40 lowercase hex characters") validate_intake_fields(name, entry) + validate_cargo_package(name, entry) validate_upstream_repo(entry) expected_targets(manifest, entry) return name