Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -208,6 +209,7 @@ jobs:
env:
MATRIX_TRIPLE: ${{ matrix.triple }}
MATRIX_BIN: ${{ matrix.bin }}
MATRIX_CARGO_PACKAGE: ${{ matrix.cargo_package }}
run: |
set -euo pipefail

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)_
Expand Down
98 changes: 98 additions & 0 deletions docs/backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
opencode-agent[bot] marked this conversation as resolved.
Resolved
"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."
}
]
}
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
19 changes: 18 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.<YYYYMMDD>.<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",
Expand Down Expand Up @@ -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.",
Expand Down
11 changes: 11 additions & 0 deletions scripts/test_validate_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions scripts/test_workflow_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions scripts/validate_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading