From 2d5544e9b8be7c23d2f616e47da9f688a0108b43 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 05:47:12 -0700 Subject: [PATCH 01/11] Fix post-release WinGet automation --- .../active-plugin-update-acceptance.yml | 4 ++-- .github/workflows/ci.yml | 24 +++++++++---------- .github/workflows/claude-code-review.yml | 4 ++-- .github/workflows/claude.yml | 2 +- .github/workflows/cline-pr-review.yml | 2 +- .../official-registry-acceptance.yml | 4 ++-- .github/workflows/release.yml | 19 +++++++-------- .github/workflows/winget.yml | 13 +++++++--- docs/PACKAGING.md | 2 +- docs/RELEASING.md | 2 +- 10 files changed, 41 insertions(+), 35 deletions(-) diff --git a/.github/workflows/active-plugin-update-acceptance.yml b/.github/workflows/active-plugin-update-acceptance.yml index 5f410939..37b4cf36 100644 --- a/.github/workflows/active-plugin-update-acceptance.yml +++ b/.github/workflows/active-plugin-update-acceptance.yml @@ -18,13 +18,13 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Restore Rust cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@v2.9.1 - name: Install Nushell 0.113.1 uses: hustcer/setup-nu@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85d5e246..c2451252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - run: cargo test --verbose - run: cargo build --verbose @@ -32,18 +32,18 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - run: cargo clippy -- -D warnings fmt: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt @@ -53,25 +53,25 @@ jobs: name: MSRV (1.88) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@1.88 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - run: cargo +1.88 check --locked --all-targets package: name: Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - run: cargo package --locked deny: name: Deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: EmbarkStudios/cargo-deny-action@v2 real-nu-acceptance: @@ -81,9 +81,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - uses: hustcer/setup-nu@v3 with: version: "0.113" diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index d177acae..6093a41b 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: fetch-depth: 1 @@ -30,4 +30,4 @@ jobs: prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' allowed_bots: '*' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options \ No newline at end of file + # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6b15fac7..0078a62d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,7 +26,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/cline-pr-review.yml b/.github/workflows/cline-pr-review.yml index e8ac72e4..29da6f40 100644 --- a/.github/workflows/cline-pr-review.yml +++ b/.github/workflows/cline-pr-review.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/official-registry-acceptance.yml b/.github/workflows/official-registry-acceptance.yml index 259887ca..c4ef1934 100644 --- a/.github/workflows/official-registry-acceptance.yml +++ b/.github/workflows/official-registry-acceptance.yml @@ -14,13 +14,13 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Restore Rust cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@v2.9.1 - name: Install Nushell 0.113.1 uses: hustcer/setup-nu@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52e0be49..865d1ff6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,11 +52,10 @@ jobs: exit 1 - name: Wait for CI checks on tagged commit - uses: lewagon/wait-on-check-action@v1.3.4 + uses: lewagon/wait-on-check-action@v1.9.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.sha }} - repo: ${{ github.repository }} wait-interval: 30 allowed-conclusions: success check-regexp: ^(Test \(.*\)|Clippy|Format|MSRV \(1\.88\)|Package|Deny|Real-Nu acceptance \(.*\))$ @@ -67,11 +66,11 @@ jobs: if: ${{ !cancelled() && (needs.verify-ci.result == 'success' || needs.verify-ci.result == 'skipped') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 - name: Format check run: cargo fmt --all -- --check - name: Clippy @@ -103,7 +102,7 @@ jobs: bin_name: numan steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - name: Resolve release version id: meta @@ -122,7 +121,7 @@ jobs: with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@v2.9.1 with: key: release-${{ matrix.target }} @@ -154,7 +153,7 @@ jobs: Compress-Archive -Path $artifact -DestinationPath "$artifact.zip" -Force echo "artifact=$artifact.zip" >> $env:GITHUB_ENV - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7.0.1 with: name: ${{ matrix.target }} path: ${{ env.artifact }} @@ -165,7 +164,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - name: Resolve release tag id: meta @@ -177,7 +176,7 @@ jobs: echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" fi - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8.0.1 with: path: dist merge-multiple: true @@ -206,7 +205,7 @@ jobs: if: github.ref_type == 'tag' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - uses: dtolnay/rust-toolchain@stable - run: cargo publish --locked env: diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index f25a58a7..320eb496 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -1,8 +1,9 @@ name: Publish to WinGet on: - release: - types: [published] + workflow_run: + workflows: [Release] + types: [completed] workflow_dispatch: inputs: release_tag: @@ -16,6 +17,12 @@ permissions: jobs: publish: name: Submit manifest update + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest steps: - name: Publish manifest to WinGet @@ -24,5 +31,5 @@ jobs: identifier: tonythethompson.numan fork-user: tonythethompson installers-regex: '\.zip$' - release-tag: ${{ inputs.release_tag || github.event.release.tag_name }} + release-tag: ${{ inputs.release_tag || github.event.workflow_run.head_branch }} token: ${{ secrets.WINGET_TOKEN }} diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 9555aad9..e730f7fa 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -7,7 +7,7 @@ Third-party install manifests live under `packaging/`. They pin GitHub Release b After a GitHub Release is published (see [RELEASING.md](RELEASING.md)): 1. Download `SHA256SUMS` from the release assets. -2. The [`Publish to WinGet`](../.github/workflows/winget.yml) workflow generates and submits the update PR using the published Windows `.zip` asset. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. +2. After the tag-triggered Release workflow succeeds, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow generates and submits the update PR using the published Windows `.zip` asset. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. 3. **winget** — the generated PR contains `packaging/winget/manifests/t/tonythethompson/numan//` with three manifests (schema **1.12.0**): - `tonythethompson.numan.yaml` (version) - `tonythethompson.numan.installer.yaml` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 612d0590..7986e5cc 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -49,7 +49,7 @@ Then: 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. 7. Confirm platform archives and `SHA256SUMS` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires `CRATES_IO_TOKEN` repository secret). -9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow opens the update PR after the release is published. +9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow opens the update PR after the successful tag-triggered Release workflow completes. 10. After publication, update documentation only if it needs links that depend on newly created release pages or assets; do not use this step to repair README content already shipped in the crate or tag. **Do not tag until CI is green on `master`.** The release workflow gates on CI check results for tag pushes; pushing a tag on a failing commit blocks publication. From feb21b4ce6ed043bb9340d56a6fbe8cd871ea59a Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 05:53:56 -0700 Subject: [PATCH 02/11] Pin updated workflow actions --- .../active-plugin-update-acceptance.yml | 6 ++--- .github/workflows/ci.yml | 24 +++++++++---------- .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude.yml | 2 +- .github/workflows/cline-pr-review.yml | 2 +- .../official-registry-acceptance.yml | 6 ++--- .github/workflows/release.yml | 18 +++++++------- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/active-plugin-update-acceptance.yml b/.github/workflows/active-plugin-update-acceptance.yml index 37b4cf36..8da65dc2 100644 --- a/.github/workflows/active-plugin-update-acceptance.yml +++ b/.github/workflows/active-plugin-update-acceptance.yml @@ -18,13 +18,13 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Restore Rust cache - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Install Nushell 0.113.1 uses: hustcer/setup-nu@v3 @@ -48,7 +48,7 @@ jobs: - name: Upload acceptance evidence if: ${{ always() }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: active-plugin-update-evidence-${{ matrix.os }} path: target/acceptance/active-plugin-update-real-nu/*/evidence/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2451252..76f0aa66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test --verbose - run: cargo build --verbose @@ -32,18 +32,18 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: clippy - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo clippy -- -D warnings fmt: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt @@ -53,25 +53,25 @@ jobs: name: MSRV (1.88) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@1.88 - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo +1.88 check --locked --all-targets package: name: Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo package --locked deny: name: Deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: EmbarkStudios/cargo-deny-action@v2 real-nu-acceptance: @@ -81,9 +81,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - uses: hustcer/setup-nu@v3 with: version: "0.113" diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 6093a41b..0cc509de 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 0078a62d..0e7ae43b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,7 +26,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/cline-pr-review.yml b/.github/workflows/cline-pr-review.yml index 29da6f40..41bb9d58 100644 --- a/.github/workflows/cline-pr-review.yml +++ b/.github/workflows/cline-pr-review.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/official-registry-acceptance.yml b/.github/workflows/official-registry-acceptance.yml index c4ef1934..0c93a8fb 100644 --- a/.github/workflows/official-registry-acceptance.yml +++ b/.github/workflows/official-registry-acceptance.yml @@ -14,13 +14,13 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Restore Rust cache - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Install Nushell 0.113.1 uses: hustcer/setup-nu@v3 @@ -45,7 +45,7 @@ jobs: - name: Upload acceptance evidence if: ${{ always() }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: official-registry-stage1-evidence path: target/acceptance/official-registry-stage1/*/evidence/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 865d1ff6..823b98a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: exit 1 - name: Wait for CI checks on tagged commit - uses: lewagon/wait-on-check-action@v1.9.0 + uses: lewagon/wait-on-check-action@2271c86c146b96545b4e871b855e10ffa6f50773 # v1.9.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.sha }} @@ -66,11 +66,11 @@ jobs: if: ${{ !cancelled() && (needs.verify-ci.result == 'success' || needs.verify-ci.result == 'skipped') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Format check run: cargo fmt --all -- --check - name: Clippy @@ -102,7 +102,7 @@ jobs: bin_name: numan steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Resolve release version id: meta @@ -121,7 +121,7 @@ jobs: with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: key: release-${{ matrix.target }} @@ -153,7 +153,7 @@ jobs: Compress-Archive -Path $artifact -DestinationPath "$artifact.zip" -Force echo "artifact=$artifact.zip" >> $env:GITHUB_ENV - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.target }} path: ${{ env.artifact }} @@ -164,7 +164,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Resolve release tag id: meta @@ -176,7 +176,7 @@ jobs: echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" fi - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true @@ -205,7 +205,7 @@ jobs: if: github.ref_type == 'tag' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - run: cargo publish --locked env: From 65826f637d0e9526495defedbb079b0ea3e2e02e Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 05:57:33 -0700 Subject: [PATCH 03/11] Verify release assets before WinGet publish --- .github/workflows/winget.yml | 19 +++++++++++++++++-- docs/PACKAGING.md | 2 +- docs/RELEASING.md | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 320eb496..8d91bdc6 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -19,17 +19,32 @@ jobs: name: Submit manifest update if: >- github.event_name == 'workflow_dispatch' || - (github.event.workflow_run.conclusion == 'success' && + ((github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure') && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository && startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest + env: + RELEASE_TAG: ${{ inputs.release_tag || github.event.workflow_run.head_branch }} steps: + - name: Verify published Windows release asset + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + expected_asset="numan-${RELEASE_TAG#v}-x86_64-pc-windows-msvc.zip" + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ + | jq -e --arg expected_asset "$expected_asset" \ + '.isDraft == false and any(.assets[]; .name == $expected_asset and .state == "uploaded")' \ + >/dev/null + - name: Publish manifest to WinGet uses: vedantmgoyal9/winget-releaser@b3a5dae0047c6180023acba3f548c55fdf6b7193 with: identifier: tonythethompson.numan fork-user: tonythethompson installers-regex: '\.zip$' - release-tag: ${{ inputs.release_tag || github.event.workflow_run.head_branch }} + release-tag: ${{ env.RELEASE_TAG }} token: ${{ secrets.WINGET_TOKEN }} diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index e730f7fa..c0e40f41 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -7,7 +7,7 @@ Third-party install manifests live under `packaging/`. They pin GitHub Release b After a GitHub Release is published (see [RELEASING.md](RELEASING.md)): 1. Download `SHA256SUMS` from the release assets. -2. After the tag-triggered Release workflow succeeds, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow generates and submits the update PR using the published Windows `.zip` asset. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. +2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the non-draft GitHub Release and its published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. 3. **winget** — the generated PR contains `packaging/winget/manifests/t/tonythethompson/numan//` with three manifests (schema **1.12.0**): - `tonythethompson.numan.yaml` (version) - `tonythethompson.numan.installer.yaml` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 7986e5cc..0f19372c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -49,7 +49,7 @@ Then: 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. 7. Confirm platform archives and `SHA256SUMS` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires `CRATES_IO_TOKEN` repository secret). -9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow opens the update PR after the successful tag-triggered Release workflow completes. +9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the published Windows release asset and opens the update PR after the tag-triggered Release workflow completes. 10. After publication, update documentation only if it needs links that depend on newly created release pages or assets; do not use this step to repair README content already shipped in the crate or tag. **Do not tag until CI is green on `master`.** The release workflow gates on CI check results for tag pushes; pushing a tag on a failing commit blocks publication. From 048a989ddfa90d84ff1448e6d77ca6dd29305024 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 05:58:49 -0700 Subject: [PATCH 04/11] Restrict CI cache writes to trusted pushes --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76f0aa66..b1fef245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: ${{ github.event_name == 'push' }} - run: cargo test --verbose - run: cargo build --verbose @@ -37,6 +39,8 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: ${{ github.event_name == 'push' }} - run: cargo clippy -- -D warnings fmt: @@ -56,6 +60,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@1.88 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: ${{ github.event_name == 'push' }} - run: cargo +1.88 check --locked --all-targets package: @@ -65,6 +71,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: ${{ github.event_name == 'push' }} - run: cargo package --locked deny: @@ -84,6 +92,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: ${{ github.event_name == 'push' }} - uses: hustcer/setup-nu@v3 with: version: "0.113" From 5c4b40859898340c0f1bca3e162c321015960f4d Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 06:05:25 -0700 Subject: [PATCH 05/11] Complete workflow runtime hardening --- .../active-plugin-update-acceptance.yml | 4 +++- .github/workflows/ci.yml | 22 ++++++++++++++----- .github/workflows/cline-pr-review.yml | 2 +- .../official-registry-acceptance.yml | 4 +++- .github/workflows/release.yml | 10 ++++++--- .github/workflows/winget.yml | 2 ++ 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/workflows/active-plugin-update-acceptance.yml b/.github/workflows/active-plugin-update-acceptance.yml index 8da65dc2..fd3dd998 100644 --- a/.github/workflows/active-plugin-update-acceptance.yml +++ b/.github/workflows/active-plugin-update-acceptance.yml @@ -21,7 +21,9 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - name: Restore Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1fef245..03b88787 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,9 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.event_name == 'push' }} @@ -35,8 +37,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable with: + toolchain: stable components: clippy - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: @@ -48,8 +51,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable with: + toolchain: stable components: rustfmt - run: cargo fmt --all -- --check @@ -58,7 +62,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@1.88 + - uses: dtolnay/rust-toolchain@39b0b3842c7e8bbf6904c0bfc3d9006fdd4dc4e0 # 1.88 + with: + toolchain: "1.88" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.event_name == 'push' }} @@ -69,7 +75,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.event_name == 'push' }} @@ -90,7 +98,9 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/cline-pr-review.yml b/.github/workflows/cline-pr-review.yml index 41bb9d58..19aed5f4 100644 --- a/.github/workflows/cline-pr-review.yml +++ b/.github/workflows/cline-pr-review.yml @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 22 diff --git a/.github/workflows/official-registry-acceptance.yml b/.github/workflows/official-registry-acceptance.yml index 0c93a8fb..27b28b73 100644 --- a/.github/workflows/official-registry-acceptance.yml +++ b/.github/workflows/official-registry-acceptance.yml @@ -17,7 +17,9 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - name: Restore Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 823b98a6..bff6a565 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,8 +67,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable with: + toolchain: stable components: rustfmt, clippy - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Format check @@ -117,8 +118,9 @@ jobs: echo "version=${tag#v}" >> "$GITHUB_OUTPUT" fi - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable with: + toolchain: stable targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -206,7 +208,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + with: + toolchain: stable - run: cargo publish --locked env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 8d91bdc6..7d8b6a87 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -34,6 +34,8 @@ jobs: shell: bash run: | set -euo pipefail + command -v gh >/dev/null || { echo "::error::GitHub CLI is required"; exit 1; } + command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; } expected_asset="numan-${RELEASE_TAG#v}-x86_64-pc-windows-msvc.zip" gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ | jq -e --arg expected_asset "$expected_asset" \ From 4c2724ad0023c4242cf9153fb38d2c9247d2b8d9 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 06:07:28 -0700 Subject: [PATCH 06/11] Gate WinGet on the release publication job --- .github/workflows/winget.yml | 21 +++++++++++++++++++-- docs/PACKAGING.md | 2 +- docs/RELEASING.md | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 7d8b6a87..b11a306a 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -12,6 +12,7 @@ on: type: string permissions: + actions: read contents: read jobs: @@ -28,14 +29,30 @@ jobs: env: RELEASE_TAG: ${{ inputs.release_tag || github.event.workflow_run.head_branch }} steps: + - name: Verify required tools + shell: bash + run: | + command -v gh >/dev/null || { echo "::error::GitHub CLI is required"; exit 1; } + command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; } + + - name: Verify originating release job + if: github.event_name == 'workflow_run' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_RUN_ID: ${{ github.event.workflow_run.id }} + shell: bash + run: | + set -euo pipefail + gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}/jobs?per_page=100" \ + --jq 'any(.jobs[]; .name == "Publish GitHub Release" and .conclusion == "success")' \ + | grep -Fx true + - name: Verify published Windows release asset env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail - command -v gh >/dev/null || { echo "::error::GitHub CLI is required"; exit 1; } - command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; } expected_asset="numan-${RELEASE_TAG#v}-x86_64-pc-windows-msvc.zip" gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ | jq -e --arg expected_asset "$expected_asset" \ diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index c0e40f41..b9306d85 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -7,7 +7,7 @@ Third-party install manifests live under `packaging/`. They pin GitHub Release b After a GitHub Release is published (see [RELEASING.md](RELEASING.md)): 1. Download `SHA256SUMS` from the release assets. -2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the non-draft GitHub Release and its published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. +2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies that the GitHub Release job succeeded and that its non-draft release contains the published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. 3. **winget** — the generated PR contains `packaging/winget/manifests/t/tonythethompson/numan//` with three manifests (schema **1.12.0**): - `tonythethompson.numan.yaml` (version) - `tonythethompson.numan.installer.yaml` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 0f19372c..dd904107 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -49,7 +49,7 @@ Then: 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. 7. Confirm platform archives and `SHA256SUMS` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires `CRATES_IO_TOKEN` repository secret). -9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the published Windows release asset and opens the update PR after the tag-triggered Release workflow completes. +9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the successful GitHub Release job and published Windows release asset, then opens the update PR after the tag-triggered Release workflow completes. 10. After publication, update documentation only if it needs links that depend on newly created release pages or assets; do not use this step to repair README content already shipped in the crate or tag. **Do not tag until CI is green on `master`.** The release workflow gates on CI check results for tag pushes; pushing a tag on a failing commit blocks publication. From 836fea3ec88ce1d042394b94360dc0f5a66e3d3e Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 06:49:33 -0700 Subject: [PATCH 07/11] Add remaining roadmap docs --- README.md | 4 + docs/plans/2026-07-29-remaining-roadmap.md | 133 +++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 docs/plans/2026-07-29-remaining-roadmap.md diff --git a/README.md b/README.md index 41bd4056..acb10336 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,10 @@ PR reviewers should follow [`.github/instructions/review.instructions.md`](.gith **Releases:** see the [latest GitHub Release](https://github.com/tonythethompson/numan/releases/latest) — feature-complete core on **0.1.x** while dogfooding the official registry. +For the current cross-repository plan across `numan`, `numan-registry`, and +`numan-plugins`, see +[docs/plans/2026-07-29-remaining-roadmap.md](docs/plans/2026-07-29-remaining-roadmap.md). + | Phase | Scope | Status | |-------|--------|--------| | **1–2** | Types, platform, lockfile, signed registry, install transaction | ✅ | diff --git a/docs/plans/2026-07-29-remaining-roadmap.md b/docs/plans/2026-07-29-remaining-roadmap.md new file mode 100644 index 00000000..b3b68c62 --- /dev/null +++ b/docs/plans/2026-07-29-remaining-roadmap.md @@ -0,0 +1,133 @@ +# Remaining Numan Roadmap + +**Status date:** 2026-07-29 + +This is the cross-repository plan for the remaining work in the Numan product +line. It is intentionally grounded in the current repository split: + +- `numan` owns the client, user experience, local state, Nu integration, and + release packaging. +- `numan-plugins` owns CI-built plugin binaries for upstreams that do not ship + compliant release artifacts. +- `numan-registry` owns the signed official catalog, package intake evidence, + staging, production signing, and publication. + +The operating rule remains: new plugin catalog depth flows +`numan-plugins -> numan-registry -> numan`. Client work should not paper over a +missing registry artifact, and registry work should not trust a plugin build +until the hardened plugin pipeline has produced immutable assets and specs. + +## Current Baseline + +- Numan client core is feature-complete for the current 0.1.x line: + signed registries, inert installs, plugin/module activation, update/remove/gc, + snapshots, doctor, completions, nupm import/diff, release packaging, crates.io, + and winget automation are in place. +- The official registry is live and has moved past the original seed catalog. + Current package truth lives in + [`numan-registry/docs/intake-candidates.md`](https://github.com/tonythethompson/numan-registry/blob/main/docs/intake-candidates.md). +- The plugin-build pipeline has an open catalog expansion PR: + `numan-plugins` PR #4, branch `feature/catalog-expansion-wave-1`, commit + `88151d8`, adding `FMotalleb/nu_plugin_port_extension` and + `FMotalleb/nu_plugin_image` plus updated macOS runner coverage. +- No PR #4 assets have been published yet. Registry intake for those plugins + must wait until that PR is merged and the manual build workflow is dispatched + with an explicit `only` list. + +## Release 0.1.x To 1.0 Priorities + +### 1. Finish catalog wave 1 through the hardened pipeline + +- [ ] Merge `numan-plugins` PR #4 after review and green checks. +- [ ] Dispatch `build-plugins` manually with only: + `nu_plugin_port_extension,nu_plugin_image`. +- [ ] Confirm every expected release asset exists and every generated spec + preserves `source.rev` as the immutable upstream commit. +- [ ] Intake the generated specs in `numan-registry` without hand-typed hashes. +- [ ] Run registry validation, manifest/index lint, staging, and lifecycle + evidence before production publication. +- [ ] Publish the signed registry only after the registry PR is reviewed and the + production workflow validation job passes. +- [ ] Run a fresh client smoke: + `init -> registry sync -> search -> info -> install -> activate -> doctor -> list -> deactivate -> remove -> gc`. + +### 2. Keep client compatibility UX honest as the catalog grows + +- [ ] Keep `numan search` filtered by detected Nu/platform by default and ensure + `--all` explains plugin ABI mismatch clearly. +- [ ] Keep `numan info` showing source provenance, verification metadata, package + type, supported targets, and Nu constraints without implying security approval. +- [ ] Keep `numan try` aligned with the live catalog. Starter packages must fail + clearly when no compatible starter exists; they must not silently switch Nu. +- [ ] Keep install errors explicit that nothing was installed when resolution + fails because of Nu or platform incompatibility. +- [ ] Add or refresh doctor checks when catalog growth exposes common local + setup failures: PATH Nu drift, managed Nu pin drift, official registry trust + drift, stale plugin activation records, and pending lifecycle journals. + +### 3. Decide when active plugin update can become default-on + +- [ ] Keep `NUMAN_ENABLE_ACTIVE_PLUGIN_MUTATION=1` as the only mutation opt-in + until real-Nu active-update evidence is boring on Ubuntu, Windows, and macOS. +- [ ] Keep `update` orchestration free of direct Nu registration ownership; Nu + integration remains owned by activate/deactivate lifecycle helpers. +- [ ] Before any default-on change, require: + exact failure-before-lifecycle guard coverage, deactivate failure coverage, + upgrade failure rollback coverage, activate failure recovery coverage, and + real-Nu matrix evidence. +- [ ] Update `docs/active-plugin-gate.md`, `AGENTS.md`, README, and changelog in + the same PR as any default semantics change. + +### 4. Grow install-only package usefulness without weakening activation rules + +- [ ] Keep scripts and completion packages install-only until their activation + contracts are designed and tested. +- [ ] For completions, define whether activation means managed vendor autoload, + shell-specific install hints, or a separate `numan completions` adjunct. +- [ ] For scripts, define execution/discovery boundaries before adding any Nu + config mutation. +- [ ] Add lifecycle evidence per package type before changing README support + tiers. + +### 5. Revisit Phase 5.2 source builds only after catalog intake is steady + +- [ ] Keep source builds deferred while `numan-plugins` can cover the highest + demand source-only plugins through controlled CI. +- [ ] When revived, source builds need explicit user consent, dependency + disclosure, deterministic install paths, failure cleanup, and no hidden Nu + activation. +- [ ] Do not mix source builds with registry catalog expansion PRs. + +### 6. Distribution and release polish + +- [ ] Keep winget automation monitored after each GitHub release. +- [ ] Keep macOS/Linux package manager work deferred until there is a verified, + maintained formula/tap/channel that will not advertise a broken path. +- [ ] Keep release docs version-agnostic where possible, because README ships in + crates.io package metadata and tagged source archives. +- [ ] For each release, run the existing dry-run gates before tagging: + `cargo test`, `cargo clippy -- -D warnings`, `cargo fmt --check`, package + checks, release-note extraction, and fresh archive smoke where practical. + +## 1.0 Gate + +Numan is ready for 1.0 when: + +- the official registry has enough packages to make first-use demos feel real on + Windows, macOS, and Linux; +- install, activate, update, deactivate, remove, gc, snapshots, and doctor have + green local and CI evidence across the supported OS matrix; +- registry intake no longer depends on ad hoc hand editing for routine packages; +- package compatibility failures are discoverable before install; +- release packaging and winget updates are routine; +- there are no open P0/P1 lifecycle, trust, or data-loss issues. + +## Explicitly Deferred + +- Silent side-by-side Nu profile switching. +- Source builds hidden inside registry intake. +- Publishing registry entries without review. +- Calling packages "approved" or "audited" solely because they are in the + official registry. +- Broad maintained forks of upstream plugins before the catalog pipeline has + exhausted ordinary CI-built upstream tags. From 117c9c26ef6af125fdf223f9edc46e891273a3b6 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:07:41 -0700 Subject: [PATCH 08/11] Gate WinGet on a stable release marker artifact Stop matching the mutable Publish GitHub Release job display name. Co-authored-by: Cursor --- .github/workflows/release.yml | 12 ++++++++++++ .github/workflows/winget.yml | 8 +++++--- README.md | 4 ++-- docs/PACKAGING.md | 2 +- docs/RELEASING.md | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bff6a565..03e2b4e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -201,6 +201,18 @@ jobs: body_path: release-notes.md files: dist/* + # Stable producer/consumer contract for winget.yml (do not rename this artifact). + - name: Emit WinGet release marker + run: | + mkdir -p winget-marker + printf '%s\n' "${{ steps.meta.outputs.tag }}" > winget-marker/release-tag.txt + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: winget-release-ready + path: winget-marker/ + retention-days: 3 + publish-crate: name: Publish to crates.io needs: [preflight, release] diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index b11a306a..ba1d19c4 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -35,7 +35,7 @@ jobs: command -v gh >/dev/null || { echo "::error::GitHub CLI is required"; exit 1; } command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; } - - name: Verify originating release job + - name: Verify originating release marker if: github.event_name == 'workflow_run' env: GH_TOKEN: ${{ github.token }} @@ -43,8 +43,10 @@ jobs: shell: bash run: | set -euo pipefail - gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}/jobs?per_page=100" \ - --jq 'any(.jobs[]; .name == "Publish GitHub Release" and .conclusion == "success")' \ + # Contract with release.yml: successful publication uploads artifact + # winget-release-ready (stable name; independent of job display names). + gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}/artifacts?per_page=100" \ + --jq 'any(.artifacts[]; .name == "winget-release-ready" and .expired == false)' \ | grep -Fx true - name: Verify published Windows release asset diff --git a/README.md b/README.md index acb10336..4ff9a94e 100644 --- a/README.md +++ b/README.md @@ -409,8 +409,8 @@ PR reviewers should follow [`.github/instructions/review.instructions.md`](.gith **Releases:** see the [latest GitHub Release](https://github.com/tonythethompson/numan/releases/latest) — feature-complete core on **0.1.x** while dogfooding the official registry. -For the current cross-repository plan across `numan`, `numan-registry`, and -`numan-plugins`, see +For the cross-repository plan snapshot (as of 2026-07-29) across `numan`, +`numan-registry`, and `numan-plugins`, see [docs/plans/2026-07-29-remaining-roadmap.md](docs/plans/2026-07-29-remaining-roadmap.md). | Phase | Scope | Status | diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index b9306d85..7577dbf5 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -7,7 +7,7 @@ Third-party install manifests live under `packaging/`. They pin GitHub Release b After a GitHub Release is published (see [RELEASING.md](RELEASING.md)): 1. Download `SHA256SUMS` from the release assets. -2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies that the GitHub Release job succeeded and that its non-draft release contains the published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. +2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the Release workflow's `winget-release-ready` artifact and that the non-draft release contains the published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. 3. **winget** — the generated PR contains `packaging/winget/manifests/t/tonythethompson/numan//` with three manifests (schema **1.12.0**): - `tonythethompson.numan.yaml` (version) - `tonythethompson.numan.installer.yaml` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index dd904107..71572d1c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -49,7 +49,7 @@ Then: 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. 7. Confirm platform archives and `SHA256SUMS` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires `CRATES_IO_TOKEN` repository secret). -9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the successful GitHub Release job and published Windows release asset, then opens the update PR after the tag-triggered Release workflow completes. +9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the `winget-release-ready` artifact and published Windows release asset, then opens the update PR after the tag-triggered Release workflow completes. 10. After publication, update documentation only if it needs links that depend on newly created release pages or assets; do not use this step to repair README content already shipped in the crate or tag. **Do not tag until CI is green on `master`.** The release workflow gates on CI check results for tag pushes; pushing a tag on a failing commit blocks publication. From 62b295a04d38c34e49ac16d1ca09dd46174cbca3 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:24:30 -0700 Subject: [PATCH 09/11] Document and enforce WinGet v-prefixed tag contract Make dispatch tag selection event-explicit and reject malformed tags. Co-authored-by: Cursor --- .github/workflows/winget.yml | 19 +++++++++++++++++-- docs/PACKAGING.md | 2 +- docs/RELEASING.md | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index ba1d19c4..03214f50 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -1,5 +1,9 @@ name: Publish to WinGet +# Tag contract (matches release.yml): only SemVer tags with a `v` prefix +# (`v*.*.*`). Auto-trigger uses workflow_run.head_branch as that tag name. +# Manual recovery requires release_tag (required on workflow_dispatch). + on: workflow_run: workflows: [Release] @@ -7,7 +11,7 @@ on: workflow_dispatch: inputs: release_tag: - description: Release tag to submit (for example, v0.1.5) + description: Release tag to submit (must be v*.*.*, for example v0.1.5) required: true type: string @@ -27,7 +31,9 @@ jobs: startsWith(github.event.workflow_run.head_branch, 'v')) runs-on: ubuntu-latest env: - RELEASE_TAG: ${{ inputs.release_tag || github.event.workflow_run.head_branch }} + # workflow_dispatch: required input. workflow_run: tag name from the + # Release push (head_branch is the tag ref for tag pushes). + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.event.workflow_run.head_branch }} steps: - name: Verify required tools shell: bash @@ -35,6 +41,15 @@ jobs: command -v gh >/dev/null || { echo "::error::GitHub CLI is required"; exit 1; } command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; } + - name: Verify release tag shape + shell: bash + run: | + set -euo pipefail + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$ ]]; then + echo "::error::RELEASE_TAG must be a v-prefixed SemVer tag (got: ${RELEASE_TAG:-})" + exit 1 + fi + - name: Verify originating release marker if: github.event_name == 'workflow_run' env: diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 7577dbf5..0d77074e 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -7,7 +7,7 @@ Third-party install manifests live under `packaging/`. They pin GitHub Release b After a GitHub Release is published (see [RELEASING.md](RELEASING.md)): 1. Download `SHA256SUMS` from the release assets. -2. After the tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the Release workflow's `winget-release-ready` artifact and that the non-draft release contains the published Windows `.zip` asset, then generates and submits the update PR. It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual dispatch remains available as a recovery path. +2. After a `v*.*.*` tag-triggered Release workflow completes, the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the Release workflow's `winget-release-ready` artifact and that the non-draft release contains the published Windows `.zip` asset, then generates and submits the update PR. Automatic runs only accept Release workflows whose head branch is a `v`-prefixed tag (same contract as [RELEASING.md](RELEASING.md)). It requires the repository's `WINGET_TOKEN` secret and the existing `tonythethompson/winget-pkgs` fork. Manual `workflow_dispatch` recovery requires an explicit `release_tag` input (also `v*.*.*`). 3. **winget** — the generated PR contains `packaging/winget/manifests/t/tonythethompson/numan//` with three manifests (schema **1.12.0**): - `tonythethompson.numan.yaml` (version) - `tonythethompson.numan.installer.yaml` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 71572d1c..a33441b7 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -49,7 +49,7 @@ Then: 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. 7. Confirm platform archives and `SHA256SUMS` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires `CRATES_IO_TOKEN` repository secret). -9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the `winget-release-ready` artifact and published Windows release asset, then opens the update PR after the tag-triggered Release workflow completes. +9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the `winget-release-ready` artifact and published Windows release asset, then opens the update PR after the `v*.*.*` tag-triggered Release workflow completes (manual recovery: dispatch with required `release_tag`). 10. After publication, update documentation only if it needs links that depend on newly created release pages or assets; do not use this step to repair README content already shipped in the crate or tag. **Do not tag until CI is green on `master`.** The release workflow gates on CI check results for tag pushes; pushing a tag on a failing commit blocks publication. From 8526e07df1a2792cdab8a10327ca2bd7067eca59 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 05:05:11 -0700 Subject: [PATCH 10/11] Add consolidated multi-repo roadmap for 1.0 work Fold plugins, registry, and client remaining plans into one ordered critical path, and point the prior draft at the new authority. Co-authored-by: Cursor --- docs/plans/2026-07-29-remaining-roadmap.md | 8 +- ...6-07-30-consolidated-multi-repo-roadmap.md | 301 ++++++++++++++++++ 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-07-30-consolidated-multi-repo-roadmap.md diff --git a/docs/plans/2026-07-29-remaining-roadmap.md b/docs/plans/2026-07-29-remaining-roadmap.md index b3b68c62..81399207 100644 --- a/docs/plans/2026-07-29-remaining-roadmap.md +++ b/docs/plans/2026-07-29-remaining-roadmap.md @@ -2,7 +2,13 @@ **Status date:** 2026-07-29 -This is the cross-repository plan for the remaining work in the Numan product +**Superseded:** Use the consolidated multi-repo plan +[`2026-07-30-consolidated-multi-repo-roadmap.md`](2026-07-30-consolidated-multi-repo-roadmap.md). +This file is retained as the prior client draft for history. + +--- + +This was the cross-repository plan for the remaining work in the Numan product line. It is intentionally grounded in the current repository split: - `numan` owns the client, user experience, local state, Nu integration, and diff --git a/docs/plans/2026-07-30-consolidated-multi-repo-roadmap.md b/docs/plans/2026-07-30-consolidated-multi-repo-roadmap.md new file mode 100644 index 00000000..de1e21ad --- /dev/null +++ b/docs/plans/2026-07-30-consolidated-multi-repo-roadmap.md @@ -0,0 +1,301 @@ +# Numan Consolidated Multi-Repo Roadmap + +**Status date:** 2026-07-30 + +**Authority:** This is the single cross-repo plan for remaining work toward Numan 1.0. +Repo-local roadmaps keep operational detail and should link here: + +- [`numan-plugins/docs/roadmap.md`](https://github.com/tonythethompson/numan-plugins/blob/master/docs/roadmap.md) +- [`numan-registry/docs/roadmap.md`](https://github.com/tonythethompson/numan-registry/blob/main/docs/roadmap.md) +- Prior client draft: [`2026-07-29-remaining-roadmap.md`](2026-07-29-remaining-roadmap.md) (superseded by this doc) +- Intake automation endgame: [`docs/registry-intake-roadmap.md`](../registry-intake-roadmap.md) + +## Repository Split + +| Repo | Owns | +|------|------| +| `numan` | Client, UX, local state, Nu integration, release packaging | +| `numan-plugins` | CI-built plugin binaries for upstreams without compliant release assets | +| `numan-registry` | Signed official catalog, intake evidence, staging, production signing | + +**Operating rule:** catalog depth flows `numan-plugins → numan-registry → numan`. + +- Client work must not paper over a missing registry artifact. +- Registry work must not trust a plugin build until the hardened pipeline has produced immutable assets and specs. +- Plugin builds never publish registry changes. + +--- + +## Current Baseline (2026-07-30) + +### Client (`numan`) + +- 0.1.x line is feature-complete for core product surface: signed registries, inert installs, plugin/module activation, update/remove/gc, snapshots, doctor, completions, nupm import/diff, crates.io, winget automation. +- Compat UX (`search` filtered by default, `try`, managed Nu pin offer) is in place; keep it honest as the catalog grows. +- Active-plugin update remains exact-`NUMAN_ENABLE_ACTIVE_PLUGIN_MUTATION=1` opt-in. + +### Registry (`numan-registry`) + +- Production publication is live via the protected `Production registry` workflow. +- Source-tree `registry/index.json.sig` remains a placeholder by design. +- Intake tooling: spec scaffold, SHA256 download, schema/validate, secrets scan, preflight, Numan parser check, manifest/index Nu-constraint lint. +- Stage 1 lifecycle evidence is mandatory for activatable package promotion. +- Live candidate truth: [`intake-candidates.md`](https://github.com/tonythethompson/numan-registry/blob/main/docs/intake-candidates.md) (synced from `docs/intake-state.json`). + +### Plugins (`numan-plugins`) + +- Hardened build pipeline requires manual dispatch with a non-empty package list. +- Manifest entries pin human-facing tags and immutable `source_commit`. +- Publication refuses existing release tags/assets; changed bytes need a new version or explicit build revision. +- Demand-ranked source-only queue: `docs/backlog.json`. +- **Blocking handoff:** [PR #4](https://github.com/tonythethompson/numan-plugins/pull/4) (`feature/catalog-expansion-wave-1`, `88151d8`) is open and green. Adds `FMotalleb/nu_plugin_port_extension` and `FMotalleb/nu_plugin_image`, plus macOS-15 runner labels. **No Wave 1 assets published yet.** + +--- + +## P0 Critical Path: Finish Catalog Wave 1 End-To-End + +Do these in order. Registry intake and client smoke wait on plugins publication. + +### A. `numan-plugins` — merge, build, verify + +- [ ] Merge PR #4 after review and green checks; pull merge commit into `master`. +- [ ] Dispatch `build-plugins` manually with only: + `nu_plugin_port_extension,nu_plugin_image`. +- [ ] Confirm workflow checks each upstream tag against recorded `source_commit`. +- [ ] Confirm all expected target assets exist; no pre-existing release/asset was replaced. +- [ ] Confirm generated specs preserve `source.rev` as the immutable upstream commit. +- [ ] Download generated `spec-*.json` artifacts for registry intake. +- [ ] Do not rebuild existing releases unless a new version or explicit build revision was chosen. +- [ ] Do not publish any registry changes from this repo. + +**Wave 1 packages:** + +| Package | Version | Notes | +|---------|---------|-------| +| `FMotalleb/nu_plugin_port_extension` | 0.113.1 | Prepared in PR #4 | +| `FMotalleb/nu_plugin_image` | 0.112.2 | Prepared in PR #4 | + +**Handoff contract to registry (every successful build wave):** + +- generated `spec-*.json` artifacts +- release URLs hosted by `numan-plugins` +- immutable upstream `source.rev` values +- upstream tags for human-facing provenance +- target list and exclusions +- Nu compatibility and `verified_with` values + +### B. `numan-registry` — intake, evidence, publish + +- [ ] Fetch `spec-*.json` from the successful plugins build run. +- [ ] Place specs under `specs/` on a focused registry branch (no unrelated catalog targets). +- [ ] Run `python scripts/add-package.py --spec specs/.json --write` for each package (script downloads + computes SHA256; never hand-type hashes). +- [ ] Run `python scripts/sync-intake-candidates.py` if intake-state/index changes need the human doc refreshed. +- [ ] Local checks: + - `python scripts/scan_for_secrets.py` + - `python scripts/preflight.py` + - `python scripts/validate.py --index registry/index.json --sig registry/index.json.sig --pub keys/official.pub --skip-artifacts` + - `cargo run --locked --manifest-path tools/numan-parser-check/Cargo.toml -- registry/index.json` + - `python scripts/lint-manifest-index.py --index registry/index.json --manifest ../numan-plugins/manifest.json` +- [ ] Open PR with specs, index diff, intake doc updates, and test evidence. +- [ ] Run staging after review if needed. +- [ ] Run `lifecycle-prove` against a real Nu matching each package constraint before production. +- [ ] Dispatch production only after validation is green and reviewer approval exists. + +### C. `numan` — client smoke after production sync + +- [ ] Fresh smoke on a clean root: + `init → registry sync → search → info → install → activate → doctor → list → deactivate → remove → gc` +- [ ] Confirm Wave 1 packages appear with honest Nu/platform filtering on the machine under test. +- [ ] Confirm `numan try` still fails clearly if no compatible starter exists (never silent Nu switch). + +--- + +## P1 Catalog Growth Loop + +Repeat this loop for each subsequent wave. Prefer one or two plugins at a time. + +### Promotion gates (`numan-plugins`) + +Move a candidate from `docs/backlog.json` → `manifest.json` `active[]` only when recorded: + +- [ ] Upstream reachable (or archive state explicitly accepted) +- [ ] Tag resolves to recorded 40-char lowercase `source_commit` +- [ ] `nu-plugin` / `nu-protocol` dependency versions known +- [ ] Nu compatibility range is minor-scoped and matches those deps +- [ ] `plugin_bin` confirmed +- [ ] Windows locked build succeeds, or Windows excluded with concrete reason +- [ ] Linux/macOS expected to work, or excluded with concrete reasons +- [ ] Exact-version Nu command-discovery smoke succeeds where practical +- [ ] No existing `numan-plugins` release tag/assets for that package version +- [ ] README active list and backlog notes updated in the same PR + +### Wave 2 research queue (`numan-plugins`) + +Source: `docs/backlog.json`. Research before promoting: + +- [ ] `devyn/nu_plugin_dbus` +- [ ] `PhotonBursted/nu_plugin_vec` +- [ ] `drbrain/nu_plugin_prometheus` +- [ ] `galuszkak/nu_plugin_bigquery` +- [ ] `jcornaz/nu_plugin_from_beancount` +- [ ] `dam4rus/nu_plugin_nuts` + +For each, record: supported Nu minor compatibility, native system deps, Windows buildability, simple command-discovery smoke. + +### Registry catalog maintenance + +- [ ] Keep `docs/intake-state.json` as editable candidate source; regenerate `docs/intake-candidates.md`. +- [ ] Keep every live entry tied to provenance: upstream URL, source revision, asset URL, hashes, Nu constraints, targets, package type. +- [ ] Preserve upstream-vs-mirror distinction; prefer upstream byte-stable archives when available. +- [ ] Track outreach in `docs/upstream-release-outreach.md`. +- [ ] Revisit blocked packages when upstreams add archives, Nu pins, or platforms (see intake-candidates "Blocked for now"). + +### Deferred plugin candidates (do not promote yet) + +- Pre-0.112 plugins (unless Numan re-supports older Nu minors) +- Repos with no release tag (unless commit-snapshot policy is explicitly adopted) +- Bare binary uploads / unsupported archive layouts +- Plugins needing heavy native services or credentials until lifecycle proof can be automated meaningfully + +--- + +## P1 Client Priorities (`numan`) + +### Compat UX stays honest as catalog grows + +- [ ] `search` filtered by detected Nu/platform by default; `--all` explains plugin ABI mismatch clearly +- [ ] `info` shows provenance, verification metadata, type, targets, Nu constraints without implying security approval +- [ ] `try` aligned with live catalog; clear failure when no compatible starter; never silent Nu switch +- [ ] Install errors explicit that nothing was installed on Nu/platform resolution failure +- [ ] Refresh doctor checks as catalog growth exposes setup failures: PATH Nu drift, managed Nu pin drift, official trust drift, stale plugin activation, pending lifecycle journals + +### Active plugin update default-on decision + +- [ ] Keep exact `NUMAN_ENABLE_ACTIVE_PLUGIN_MUTATION=1` until real-Nu active-update evidence is boring on Ubuntu, Windows, and macOS +- [ ] Keep `update` free of direct Nu registration ownership (activate/deactivate boundary owns Nu callbacks) +- [ ] Before default-on: failure-before-lifecycle, deactivate failure, upgrade rollback, activate recovery, and real-Nu matrix evidence +- [ ] Same PR updates `docs/active-plugin-gate.md`, `AGENTS.md`, README, changelog + +### Install-only package types + +- [ ] Scripts and completions stay install-only until activation contracts are designed and tested +- [ ] Completions: decide managed vendor autoload vs shell-specific hints vs `numan completions` adjunct +- [ ] Scripts: define execution/discovery boundaries before any Nu config mutation +- [ ] Lifecycle evidence per package type before changing README support tiers + +### Source builds (Phase 5.2) + +- [ ] Keep deferred while `numan-plugins` covers highest-demand source-only plugins via CI +- [ ] When revived: explicit consent, dependency disclosure, deterministic paths, failure cleanup, no hidden Nu activation +- [ ] Never mix source builds with registry catalog expansion PRs + +### Distribution polish + +- [ ] Monitor winget automation after each GitHub release +- [ ] Defer macOS/Linux package managers until a verified maintained formula/tap/channel exists +- [ ] Keep release docs version-agnostic where possible (README ships in crates.io + tagged archives) +- [ ] Per release dry-run: `cargo test`, `cargo clippy -- -D warnings`, `cargo fmt --check`, package checks, release-note extraction, archive smoke + +--- + +## P2 Intake Automation (`numan-registry` + `numan`) + +Stage 1 (lifecycle harness) is done. Remaining stages follow [`docs/registry-intake-roadmap.md`](../registry-intake-roadmap.md). Do not block Wave 1 on these. + +### Stage 2: Stronger local lint + +- [ ] Actionable errors: missing metadata, duplicate targets, unknown triples, unsupported archive suffixes, missing activation declarations, malformed Nu constraints, source provenance mismatches +- [ ] Deterministic lint output for before/after PR comparison +- [ ] PR template asks for lint, parser-check, and lifecycle evidence + +### Stage 3: Repo discovery + +- [ ] Read-only discovery from GitHub repo, release URL, or local checkout +- [ ] Detect `nupm.nuon`, layouts, Cargo metadata, assets, license, homepage, tags, Nu deps, platform matrix +- [ ] Separate discovered facts from guessed fields and maintainer decisions + +### Stage 4: Candidate generation + +- [ ] Draft specs only (not committed registry entries) +- [ ] Provenance per inferred field; unresolved decisions marked explicitly +- [ ] Stable, reviewable generated JSON + +### Stage 5: Validation reports + +- [ ] Machine + human validation evidence per candidate +- [ ] Cover download, hash, archive layout, install, activation readiness, doctor, list, deactivate/remove/gc, final state +- [ ] Production secrets unavailable to validation jobs + +### Stage 6: Registry PR generation + +- [ ] PR branch from validated specs + evidence +- [ ] Summary: type, provenance, targets, lifecycle results, limitations, publish plan +- [ ] Human review and protected signing remain mandatory + +--- + +## Ongoing Safety And Pipeline Hygiene + +### `numan-plugins` + +- [ ] Third-party Actions pinned to reviewed commit SHAs +- [ ] Workflow permissions read-only except release publication +- [ ] macOS runner labels current and tested +- [ ] Deterministic archive tests (`.zip`, `.tar.gz`) +- [ ] Release-absence tests (existing tags/assets fail before upload) +- [ ] Strict manifest validation (duplicates, missing targets, malformed commits, tag-to-commit drift) +- [ ] Strict generated-spec validation (packaged SHA records, complete target coverage) + +### `numan-registry` + +- [ ] Never commit or print private key material +- [ ] Never treat source-tree placeholder signature as production evidence +- [ ] Never publish before artifacts are hash-pinned and reviewable +- [ ] Never add lifecycle-activatable packages without lifecycle evidence +- [ ] Never mix catalog expansion with workflow/signing refactors unless the catalog change depends on the safety change + +--- + +## Unified 1.0 Gate + +Ship 1.0 when **all** of the following are true: + +| Area | Criterion | +|------|-----------| +| Catalog depth | Official registry has enough packages for first-use demos to feel real on Windows, macOS, and Linux | +| Lifecycle | install / activate / update / deactivate / remove / gc / snapshots / doctor have green local + CI evidence across the OS matrix | +| Intake | Routine package additions are spec-driven and reproducible; no ad hoc hand editing for routine packages | +| Evidence | Every activatable package has lifecycle evidence or a documented exception | +| Compat UX | Package compatibility failures are discoverable before install | +| Trust | Production signing is boring, protected, auditable; mirrors and outreach status are clear | +| Client sync | `numan registry sync` + search/info/install reflect the catalog accurately | +| Distribution | Release packaging and winget updates are routine | +| Risk | No open P0/P1 lifecycle, trust, or data-loss issues | + +### Repo-local health checks (supporting) + +**Plugins healthy when:** one or two plugins can be added without touching publication safety code; every active manifest entry traces to upstream tag + immutable commit; every release asset is immutable and hash-pinned downstream; backlog explains promote/defer/block; registry receives specs needing no manual repair. + +**Registry healthy when:** same as 1.0 registry rows above, plus meaningful multi-OS coverage in the live catalog. + +--- + +## Explicitly Deferred (All Repos) + +- Silent side-by-side Nu profile switching +- Source builds hidden inside registry intake +- Publishing registry entries without review +- Calling packages "approved" or "audited" solely because they are in the official registry +- Broad maintained forks of upstream plugins before the catalog pipeline has exhausted ordinary CI-built upstream tags +- Pre-0.112 plugin support (unless product chooses older Nu minors again) +- macOS/Linux system package managers without a verified maintained formula + +--- + +## Suggested Execution Order + +1. **Now:** Merge plugins PR #4 → dispatch Wave 1 build → registry intake PR → lifecycle-prove → production → client smoke. +2. **Next:** Wave 2 research (one or two backlog candidates) through the same pipeline; keep client compat UX and doctor honest against new packages. +3. **Parallel (non-blocking):** Registry Stage 2 lint hardening; intake-candidates / outreach maintenance; winget release monitoring. +4. **Later:** Intake Stages 3–6; install-only activation contracts; active-update default-on decision; Phase 5.2 source builds only after intake is steady. +5. **1.0:** When the unified gate above is green. From 9a60847f69c8567e0add87cd21caf6a9ff4f52d2 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 05:10:07 -0700 Subject: [PATCH 11/11] Harden release tag shell handoff and SemVer check Pass dispatch/release tags through env vars instead of expanding them into run scripts, and tighten WinGet tag validation to real SemVer. Co-authored-by: Cursor --- .github/workflows/release.yml | 25 +++++++++++++++++-------- .github/workflows/winget.yml | 4 +++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03e2b4e2..240acf4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,14 +108,16 @@ jobs: - name: Resolve release version id: meta shell: bash + env: + REF_TYPE: ${{ github.ref_type }} + INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "${{ github.ref_type }}" == "tag" ]]; then + if [[ "$REF_TYPE" == "tag" ]]; then echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" else - tag="${{ inputs.tag }}" - echo "tag=${tag}" >> "$GITHUB_OUTPUT" - echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT" + echo "version=${INPUT_TAG#v}" >> "$GITHUB_OUTPUT" fi - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable @@ -171,11 +173,14 @@ jobs: - name: Resolve release tag id: meta shell: bash + env: + REF_TYPE: ${{ github.ref_type }} + INPUT_TAG: ${{ inputs.tag }} run: | - if [[ "${{ github.ref_type }}" == "tag" ]]; then + if [[ "$REF_TYPE" == "tag" ]]; then echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" else - echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT" fi - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -190,7 +195,9 @@ jobs: cat SHA256SUMS - name: Prepare release notes from CHANGELOG - run: bash scripts/release-notes-from-changelog.sh "${{ steps.meta.outputs.tag }}" > release-notes.md + env: + RELEASE_TAG: ${{ steps.meta.outputs.tag }} + run: bash scripts/release-notes-from-changelog.sh "$RELEASE_TAG" > release-notes.md - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -203,9 +210,11 @@ jobs: # Stable producer/consumer contract for winget.yml (do not rename this artifact). - name: Emit WinGet release marker + env: + RELEASE_TAG: ${{ steps.meta.outputs.tag }} run: | mkdir -p winget-marker - printf '%s\n' "${{ steps.meta.outputs.tag }}" > winget-marker/release-tag.txt + printf '%s\n' "$RELEASE_TAG" > winget-marker/release-tag.txt - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 03214f50..52e22674 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -45,7 +45,9 @@ jobs: shell: bash run: | set -euo pipefail - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$ ]]; then + # SemVer core (no leading zeros) with optional prerelease / build metadata. + # Rejects extra numeric segments such as v1.2.3.4. + if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then echo "::error::RELEASE_TAG must be a v-prefixed SemVer tag (got: ${RELEASE_TAG:-})" exit 1 fi