From da960774f1a728fdff6f22af68dd52d1d143aca0 Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 15 May 2026 11:22:45 -0400 Subject: [PATCH 1/3] ci: fix subscriptions client publishing Configure Rust client publishing to package generated sources and use the subscriptions crate name while keeping the program crate as subscriptions-program. --- .github/workflows/publish-rust.yml | 58 ++++++++++++++++++++++-------- CLAUDE.md | 4 +-- Cargo.lock | 36 +++++++++---------- README.md | 4 +-- clients/rust/Cargo.toml | 4 ++- justfile | 10 +++--- program/Cargo.toml | 2 +- program/build.rs | 5 ++- tests/integration-tests/Cargo.toml | 2 +- 9 files changed, 80 insertions(+), 45 deletions(-) diff --git a/.github/workflows/publish-rust.yml b/.github/workflows/publish-rust.yml index 18826af..51298b8 100644 --- a/.github/workflows/publish-rust.yml +++ b/.github/workflows/publish-rust.yml @@ -37,10 +37,16 @@ jobs: ;; esac + test: + name: Run release tests + needs: guard-allowed-branch + uses: ./.github/workflows/test.yml + secrets: inherit + publish: - name: Publish subscriptions-client crate + name: Publish subscriptions crate runs-on: ubuntu-latest - needs: guard-allowed-branch + needs: [guard-allowed-branch, test] defaults: run: working-directory: clients/rust @@ -61,15 +67,36 @@ jobs: with: components: rustfmt, clippy + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + working-directory: . + run: pnpm install --frozen-lockfile + + - name: Generate IDL and clients + working-directory: . + run: | + cd program && cargo build + cd .. && pnpm run generate-clients + - name: Build check run: cargo build + - name: Verify crate package + run: cargo publish --dry-run --locked --allow-dirty + - name: Get current version id: version run: | VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Publishing subscriptions-client v$VERSION" + echo "Publishing subscriptions v$VERSION" - name: Check if prerelease id: prerelease @@ -80,6 +107,17 @@ jobs: echo "is_prerelease=false" >> $GITHUB_OUTPUT fi + - name: Authenticate crates.io trusted publisher + if: ${{ inputs.publish-to-crates }} + id: crates_io_auth + uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 + + - name: Publish to crates.io + if: ${{ inputs.publish-to-crates }} + env: + CARGO_REGISTRY_TOKEN: ${{ steps.crates_io_auth.outputs.token }} + run: cargo publish --locked --allow-dirty + - name: Create and push tag working-directory: . run: | @@ -92,14 +130,6 @@ jobs: fi git push origin "$TAG" 2>/dev/null || echo "Tag already pushed" - - name: Authenticate crates.io trusted publisher - if: ${{ inputs.publish-to-crates }} - uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 - - - name: Publish to crates.io - if: ${{ inputs.publish-to-crates }} - run: cargo publish --locked - - name: Create GitHub Release if: ${{ inputs.create-github-release }} uses: actions/github-script@v8 @@ -127,20 +157,20 @@ jobs: repo: context.repo.repo, tag_name: tagName, name: releaseName, - body: `Release of subscriptions-client v${{ steps.version.outputs.version }}\n\n**crates.io:** https://crates.io/crates/subscriptions-client/${{ steps.version.outputs.version }}`, + body: `Release of subscriptions v${{ steps.version.outputs.version }}\n\n**crates.io:** https://crates.io/crates/subscriptions/${{ steps.version.outputs.version }}`, draft: false, prerelease: isPrerelease, }); - name: Publish summary run: | - echo "## subscriptions-client Published" >> $GITHUB_STEP_SUMMARY + echo "## subscriptions Published" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Version**: \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY echo "**Tag**: \`rust-client-v${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ inputs.publish-to-crates }}" == "true" ]; then - echo "Published to crates.io: https://crates.io/crates/subscriptions-client/${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "Published to crates.io: https://crates.io/crates/subscriptions/${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY else echo "Skipped crates.io publish" >> $GITHUB_STEP_SUMMARY fi diff --git a/CLAUDE.md b/CLAUDE.md index 4bc0e80..acdad39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,8 +102,8 @@ Audited by Cantina. See [audits/AUDIT_STATUS.md](audits/AUDIT_STATUS.md) for the ## Workspace Structure -- `program/` — Pinocchio program (workspace member `subscriptions`) -- `clients/rust/` — Codama-generated Rust client (`subscriptions-client`) +- `program/` — Pinocchio program (workspace member `subscriptions-program`) +- `clients/rust/` — Codama-generated Rust client (`subscriptions`) - `clients/typescript/` — Hand-written SDK wrapping Codama-generated TS (`@solana/subscriptions`) - `webapp/` — Vite + React 19 + Node API demo (faucet, deploy wizard, marketplace) - `docs/` — Numbered ADRs diff --git a/Cargo.lock b/Cargo.lock index b00d606..e82286b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5288,23 +5288,6 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subscriptions" version = "0.1.0" -dependencies = [ - "codama", - "const-crypto", - "pinocchio", - "pinocchio-associated-token-account", - "pinocchio-system", - "pinocchio-token", - "pinocchio-token-2022", - "serde_json", - "solana-address", - "solana-security-txt", - "thiserror 2.0.18", -] - -[[package]] -name = "subscriptions-client" -version = "0.1.0" dependencies = [ "borsh 1.6.0", "num-derive", @@ -5319,6 +5302,23 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "subscriptions-program" +version = "0.1.0" +dependencies = [ + "codama", + "const-crypto", + "pinocchio", + "pinocchio-associated-token-account", + "pinocchio-system", + "pinocchio-token", + "pinocchio-token-2022", + "serde_json", + "solana-address", + "solana-security-txt", + "thiserror 2.0.18", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5424,7 +5424,7 @@ dependencies = [ "spl-associated-token-account", "spl-token", "spl-token-2022", - "subscriptions", + "subscriptions-program", "tabled", ] diff --git a/README.md b/README.md index 9d43c7d..fda4cef 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ Delegation accounts include a version field and the program implements a three-t This repository contains: -- A Rust Solana program built with [Pinocchio](https://github.com/febo/pinocchio) +- A Rust Solana program built with [Pinocchio](https://github.com/anza-xyz/pinocchio) - IDL generation via [Codama](https://github.com/codama-idl/codama) - Generated clients via Codama: - TypeScript client (`@solana/subscriptions`) in `clients/typescript` - - Rust client (`subscriptions-client`) in `clients/rust` + - Rust client (`subscriptions`) in `clients/rust` - A local demo webapp in `webapp/` - CI pipeline with build, test, lint, and CU benchmarking diff --git a/clients/rust/Cargo.toml b/clients/rust/Cargo.toml index ac79ae9..801ecf8 100644 --- a/clients/rust/Cargo.toml +++ b/clients/rust/Cargo.toml @@ -1,9 +1,11 @@ [package] -name = "subscriptions-client" +name = "subscriptions" version = "0.1.0" edition = "2021" +description = "Rust client for the Subscriptions Solana program" license = { workspace = true } repository = { workspace = true } +include = ["Cargo.toml", "src/**/*.rs"] [lints] workspace = true diff --git a/justfile b/justfile index 6cf7577..7d96007 100644 --- a/justfile +++ b/justfile @@ -99,7 +99,7 @@ test *args: unit-test (integration-test args) test-client # Run Rust unit tests unit-test: - cargo test -p subscriptions + cargo test -p subscriptions-program # Backwards-compatible alias for the old recipe name test-program: unit-test @@ -270,7 +270,7 @@ clean: # Check formatting without fixing fmt-check: @echo "Checking Rust formatting..." - @cargo fmt -p subscriptions -p tests-subscriptions --check + @cargo fmt -p subscriptions-program -p tests-subscriptions --check @echo "Checking TypeScript formatting..." @pnpm run format:check @echo "✓ Format check passed" @@ -278,7 +278,7 @@ fmt-check: # Auto-format all code fmt: @echo "Formatting Rust..." - @cargo fmt -p subscriptions -p tests-subscriptions + @cargo fmt -p subscriptions-program -p tests-subscriptions @echo "Formatting TypeScript..." @pnpm run format @echo "✓ Code formatted" @@ -286,7 +286,7 @@ fmt: # Lint with auto-fix lint: @echo "Linting Rust..." - @cargo clippy --workspace --exclude subscriptions-client --all-targets --no-deps --fix -- -D warnings + @cargo clippy --workspace --exclude subscriptions --all-targets --no-deps --fix -- -D warnings @echo "Linting TypeScript..." @pnpm run lint:fix @echo "✓ Code linted" @@ -294,7 +294,7 @@ lint: # Check linting without fixing lint-check: @echo "Checking Rust lint..." - @cargo clippy --workspace --exclude subscriptions-client --all-targets --no-deps -- -D warnings + @cargo clippy --workspace --exclude subscriptions --all-targets --no-deps -- -D warnings @echo "Checking TypeScript lint..." @pnpm run lint @echo "✓ Lint check passed" diff --git a/program/Cargo.toml b/program/Cargo.toml index 003f064..8198c42 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "subscriptions" +name = "subscriptions-program" version = { workspace = true } edition = { workspace = true } license = { workspace = true } diff --git a/program/build.rs b/program/build.rs index 1c469b4..8bf2cb1 100644 --- a/program/build.rs +++ b/program/build.rs @@ -22,7 +22,10 @@ fn generate_idl() -> Result<(), Box> { let idl_json = codama.get_json_idl()?; // Parse and format the JSON with pretty printing. - let parsed: serde_json::Value = serde_json::from_str(&idl_json)?; + let mut parsed: serde_json::Value = serde_json::from_str(&idl_json)?; + if let Some(program) = parsed.get_mut("program").and_then(serde_json::Value::as_object_mut) { + program.insert("name".to_string(), serde_json::Value::String("subscriptions".to_string())); + } let mut formatted_json = serde_json::to_string_pretty(&parsed)?; formatted_json.push('\n'); diff --git a/tests/integration-tests/Cargo.toml b/tests/integration-tests/Cargo.toml index a676bb3..4b97888 100644 --- a/tests/integration-tests/Cargo.toml +++ b/tests/integration-tests/Cargo.toml @@ -9,7 +9,7 @@ repository = { workspace = true } workspace = true [dependencies] -subscriptions = { path = "../../program", features = ["no-entrypoint"] } +subscriptions = { package = "subscriptions-program", path = "../../program", features = ["no-entrypoint"] } pinocchio = { workspace = true } pinocchio-associated-token-account = { workspace = true } pinocchio-system = { workspace = true } From d988f07bcc2db8a81a61a959c8596f976e5ddbf7 Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 15 May 2026 11:28:53 -0400 Subject: [PATCH 2/3] ci: prepare mainnet release buffers Stop mainnet releases at Squads-owned buffers so the Doppler keypair only writes buffers and does not need Squads membership. --- .github/workflows/release.yml | 40 ++++++--------------------- docs/004-program-upgrade-mechanism.md | 12 ++++++-- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b87638b..3eedb29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,7 +123,7 @@ jobs: skip-build: 'false' # ============================================ - # mainnet: bundle into Squads multisig proposal + # mainnet: prepare Squads-owned buffers # ============================================ - if: inputs.network == 'mainnet' id: metadata-buffer @@ -137,34 +137,11 @@ jobs: priority-fees: ${{ inputs.priority-fee }} - if: inputs.network == 'mainnet' - id: verify-pda - name: Build verify PDA transaction (mainnet) - uses: solana-developers/github-actions/verify-build@eb606791e11d06eb92593dfd3404bf0d4c809121 - with: - program: ${{ env.PROGRAM }} - program-id: ${{ env.PROGRAM_ID }} - rpc-url: ${{ env.RPC_URL }} - keypair: ${{ env.DEPLOYER_KEYPAIR }} - repo-url: ${{ env.REPO_URL }} - network: mainnet - mount-path: program - commit-hash: ${{ github.sha }} - use-squads: 'true' - vault-address: ${{ env.SQUADS_VAULT }} - skip-build: 'true' - - - if: inputs.network == 'mainnet' - name: Propose Squads upgrade transaction (mainnet) - uses: solana-developers/squads-program-action@1b4575c62c01ffc5fae9316f61486f7d37cb7b91 # v0.4.3 - with: - rpc: ${{ env.RPC_URL }} - program: ${{ env.PROGRAM_ID }} - buffer: ${{ steps.write-buffer.outputs.buffer }} - metadata-buffer: ${{ steps.metadata-buffer.outputs.buffer }} - multisig: ${{ env.SQUADS_MULTISIG }} - keypair: /tmp/deployer.json - priority-fee: ${{ inputs.priority-fee }} - pda-tx: ${{ steps.verify-pda.outputs.pda_tx }} + name: Confirm mainnet buffer handoff + run: | + : "${SQUADS_VAULT:?Set SQUADS_VAULT in Doppler prd_github}" + echo "Program buffer authority assigned to Squads vault: $SQUADS_VAULT" + echo "IDL metadata buffer authority assigned to Squads vault: $SQUADS_VAULT" - name: Cleanup keypair if: always() @@ -179,6 +156,7 @@ jobs: echo "- Buffer: \`${{ steps.write-buffer.outputs.buffer }}\`" >> $GITHUB_STEP_SUMMARY if [ "${{ inputs.network }}" = "mainnet" ]; then echo "- IDL buffer: \`${{ steps.metadata-buffer.outputs.buffer }}\`" >> $GITHUB_STEP_SUMMARY - echo "- Squads multisig: \`${{ env.SQUADS_MULTISIG }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Action required**: review + approve transaction in Squads UI" >> $GITHUB_STEP_SUMMARY + echo "- Buffer authority: \`${{ env.SQUADS_VAULT }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Action required**: create the Squads upgrade proposal using the listed buffers" >> $GITHUB_STEP_SUMMARY + echo "- CI keypair role: fee payer and buffer writer only; it does not need Squads membership" >> $GITHUB_STEP_SUMMARY fi diff --git a/docs/004-program-upgrade-mechanism.md b/docs/004-program-upgrade-mechanism.md index 3c311f4..8d954a3 100644 --- a/docs/004-program-upgrade-mechanism.md +++ b/docs/004-program-upgrade-mechanism.md @@ -1,6 +1,6 @@ # Program Upgrade Mechanism -Program upgrades are governed by a [Squads](https://squads.xyz/) multisig and deployed via [txtx/Surfpool](https://docs.surfpool.run/). +Program upgrades are governed by a [Squads](https://squads.xyz/) multisig. Mainnet CI only writes upgrade buffers and transfers buffer authority to the Squads vault; it does not use a Squads member keypair. ## Signer Configuration @@ -23,6 +23,14 @@ signer "authority" "svm::squads" { ## Deploying / Upgrading +### Mainnet CI + +Run the `Release` GitHub Actions workflow with `network = mainnet`. The workflow loads the deployer keypair from Doppler, builds the verified program, writes the program buffer, writes the program-metadata IDL buffer, and transfers both buffer authorities to the Squads vault. + +The CI keypair is only a fee payer and buffer writer. It does not need to be a Squads member. After CI finishes, create the Squads upgrade proposal manually using the program buffer and IDL metadata buffer from the workflow summary. + +### Surfpool Runbooks + Run the deployment runbook with the appropriate environment: ```bash @@ -50,7 +58,7 @@ solana-verify get-executable-hash target/deploy/subscriptions.so ### 3. Hash the on-chain buffer -The buffer address is shown in the Squads proposal. +The buffer address is shown in the GitHub Actions release summary and in the Squads proposal. ```bash solana-verify get-buffer-hash -u From c1471608dd9cb49f8446a7e91d06dff2815c20fb Mon Sep 17 00:00:00 2001 From: Jo D Date: Fri, 15 May 2026 11:37:57 -0400 Subject: [PATCH 3/3] ci: keep program artifact name stable Pin the program lib target to subscriptions so build-sbf continues emitting target/deploy/subscriptions.so after the package rename. --- program/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/program/Cargo.toml b/program/Cargo.toml index 8198c42..1b9ca55 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -6,6 +6,7 @@ license = { workspace = true } repository = { workspace = true } [lib] +name = "subscriptions" crate-type = ["lib", "cdylib"] [lints]