diff --git a/prepare-squads-release/action.yaml b/prepare-squads-release/action.yaml new file mode 100644 index 0000000..8d35e0b --- /dev/null +++ b/prepare-squads-release/action.yaml @@ -0,0 +1,138 @@ +name: "Prepare Squads Release" +description: "Creates Solana release buffers and assigns their authority to a Squads vault without creating a Squads proposal" +inputs: + program-id: + description: "Program ID to upgrade" + required: true + program: + description: "Program name" + required: true + rpc-url: + description: "Solana RPC URL" + required: true + keypair: + description: "Payer keypair used for buffer preparation. It does not need to be a Squads member." + required: true + squads-vault: + description: "Squads vault address to set as buffer authority" + required: true + metadata-path: + description: "Optional IDL or metadata JSON path for a program-metadata buffer" + required: false + default: "" + priority-fee: + description: "Priority fee in microlamports" + required: false + default: "100000" + program-buffer-max-sign-attempts: + description: "Maximum Solana CLI signing attempts for the program buffer" + required: false + default: "100" + program-buffer-write-timeout-minutes: + description: "Maximum minutes to let a single program buffer write run before printing diagnostics" + required: false + default: "55" + program-buffer-retry-attempts: + description: "Maximum program buffer write attempts" + required: false + default: "1" + program-buffer-use-rpc: + description: "Send program buffer write transactions through the configured RPC instead of validator TPUs" + required: false + default: "false" + export-verify-pda: + description: "Export a solana-verify PDA transaction for the Squads vault" + required: false + default: "false" + repo-url: + description: "GitHub repository URL for solana-verify PDA export" + required: false + default: "" + commit-hash: + description: "Git commit hash to verify against" + required: false + default: "" + +outputs: + buffer: + description: "Created program buffer address" + value: ${{ steps.write-program-buffer.outputs.buffer }} + metadata-buffer: + description: "Created program-metadata buffer address" + value: ${{ steps.write-metadata-buffer.outputs.buffer }} + pda-tx: + description: "Base64-encoded verify PDA transaction" + value: ${{ steps.export-verify-pda.outputs.pda_tx }} + +runs: + using: "composite" + steps: + - name: Validate inputs + shell: bash + run: | + if [ "${{ inputs.export-verify-pda }}" = "true" ] && [ -z "${{ inputs.repo-url }}" ]; then + echo "Error: repo-url is required when export-verify-pda is true" + exit 1 + fi + if [ "${{ inputs.export-verify-pda }}" = "true" ] && [ -z "${{ inputs.commit-hash }}" ]; then + echo "Error: commit-hash is required when export-verify-pda is true" + exit 1 + fi + + - name: Write program buffer + id: write-program-buffer + uses: solana-developers/github-actions/write-program-buffer@main + with: + program-id: ${{ inputs.program-id }} + program: ${{ inputs.program }} + rpc-url: ${{ inputs.rpc-url }} + keypair: ${{ inputs.keypair }} + buffer-authority-address: ${{ inputs.squads-vault }} + priority-fee: ${{ inputs.priority-fee }} + max-sign-attempts: ${{ inputs.program-buffer-max-sign-attempts }} + write-timeout-minutes: ${{ inputs.program-buffer-write-timeout-minutes }} + retry-attempts: ${{ inputs.program-buffer-retry-attempts }} + use-rpc: ${{ inputs.program-buffer-use-rpc }} + + - name: Write metadata buffer + id: write-metadata-buffer + if: inputs.metadata-path != '' + uses: solana-developers/github-actions/write-metadata-buffer@main + with: + idl-path: ${{ inputs.metadata-path }} + rpc-url: ${{ inputs.rpc-url }} + keypair: ${{ inputs.keypair }} + buffer-authority: ${{ inputs.squads-vault }} + priority-fees: ${{ inputs.priority-fee }} + + - name: Export verify PDA transaction + id: export-verify-pda + if: inputs.export-verify-pda == 'true' + uses: solana-developers/github-actions/verify-build@15e43c56b2ad2047dde732d807082570de2b8486 + with: + program-id: ${{ inputs.program-id }} + program: ${{ inputs.program }} + rpc-url: ${{ inputs.rpc-url }} + keypair: ${{ inputs.keypair }} + repo-url: ${{ inputs.repo-url }} + commit-hash: ${{ inputs.commit-hash }} + network: "mainnet" + use-squads: "true" + vault-address: ${{ inputs.squads-vault }} + + - name: Write summary + shell: bash + run: | + echo "## Squads release buffers" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Program: \`${{ inputs.program-id }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Program buffer: \`${{ steps.write-program-buffer.outputs.buffer }}\`" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.write-metadata-buffer.outputs.buffer }}" ]; then + echo "- Metadata buffer: \`${{ steps.write-metadata-buffer.outputs.buffer }}\`" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ inputs.export-verify-pda }}" = "true" ]; then + echo "- Verify PDA transaction exported: \`true\`" >> $GITHUB_STEP_SUMMARY + fi + echo "- Buffer authority: \`${{ inputs.squads-vault }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Create the program upgrade from Squads using the buffer above. This action does not create a Squads proposal." >> $GITHUB_STEP_SUMMARY diff --git a/readme.md b/readme.md index 8bc1332..c9cc386 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,7 @@ There are three examples: - [Anchor Program](https://github.com/Woody4618/anchor-github-action-example) - [Native Program](https://github.com/Woody4618/native-solana-github-action-example) -- [Anchor Program using Squads](https://github.com/Woody4618/workflow-tutorial) +- [Anchor Program using Squads](https://github.com/Woody4618/workflow-tutorial) ### Required Secrets for specific actions @@ -97,6 +97,25 @@ Customize the workflow to your needs! - `buffer-authority-address`: Authority for the buffer - `priority-fee`: Transaction priority fee +- `prepare-squads-release`: Creates release buffers for a Squads-controlled upgrade without creating a Squads proposal from CI + + - Creates the program buffer + - Transfers program buffer authority to the Squads vault + - Optionally creates a program-metadata buffer and transfers its authority to the Squads vault + - Optionally exports a `solana-verify` PDA transaction using the Squads vault as uploader + - Does not require the deployer keypair to be a Squads member + - Inputs: + - `program-id`: Target program ID + - `program`: Program name + - `rpc-url`: Solana RPC endpoint + - `keypair`: Payer keypair used for buffer preparation + - `squads-vault`: Squads vault to set as buffer authority + - `metadata-path`: Optional IDL or metadata JSON path + - `priority-fee`: Transaction priority fee + - `export-verify-pda`: Export a verify PDA transaction + - `repo-url`: GitHub repository URL for the verify PDA transaction + - `commit-hash`: Git commit hash for the verify PDA transaction + - `write-idl-buffer`: Writes an Anchor IDL buffer that will then later be set either from the provided keypair or from the squads multisig - Creates IDL buffer - Sets up IDL authority @@ -113,6 +132,7 @@ Customize the workflow to your needs! These actions use the [program-metadata](https://github.com/solana-program/program-metadata) program to attach metadata (IDL, security.txt, etc.) to any Solana program. This is the newer alternative to Anchor's built-in IDL commands and supports any program, not just Anchor programs. - `metadata-upload`: Writes metadata directly to a program or from a pre-created buffer + - Supports any seed type (idl, security, or custom) - Handles both direct upload and Squads multisig workflows - Can export transactions for Squads signing @@ -148,6 +168,28 @@ These actions use the [program-metadata](https://github.com/solana-program/progr - `idl-upload`: Either sets the Anchor IDL buffer or skips that in case of squads deploy - `verify-build`: Verifies on-chain programs match source using solana-verify andthe osec api +### Squads buffer-only release + +For teams that do not want to add a CI-owned keypair as a Squads proposer, use `prepare-squads-release`. The keypair only pays for buffer preparation transactions. The resulting program buffer is owned by the Squads vault, and the upgrade proposal can be created manually in Squads. + +```yaml +- name: Prepare Squads release buffers + uses: solana-developers/github-actions/prepare-squads-release@main + with: + program: ${{ env.PROGRAM }} + program-id: ${{ env.PROGRAM_ID }} + rpc-url: ${{ env.RPC_URL }} + keypair: ${{ secrets.MAINNET_DEPLOYER_KEYPAIR }} + squads-vault: ${{ secrets.MAINNET_MULTISIG_VAULT }} + metadata-path: ./target/idl/${{ env.PROGRAM }}.json + priority-fee: ${{ inputs.priority-fee }} + export-verify-pda: "true" + repo-url: ${{ github.server_url }}/${{ github.repository }} + commit-hash: ${{ github.sha }} +``` + +After the action completes, use the program buffer from the job summary when creating the program upgrade in Squads. + ## 📝 Todo List ### Program Verification diff --git a/verify-build/action.yaml b/verify-build/action.yaml index 03ea19f..53e2841 100644 --- a/verify-build/action.yaml +++ b/verify-build/action.yaml @@ -58,7 +58,6 @@ runs: echo "Network: ${{ github.event.inputs.network }}" echo "Verify: ${{ github.event.inputs.verify }}" echo "Has keypair: ${{ inputs.keypair != '' }}" - echo "RPC URL: ${{ inputs.rpc-url }}" - name: Verify Build shell: bash diff --git a/write-metadata-buffer/action.yaml b/write-metadata-buffer/action.yaml index fbf3c86..9cd2fee 100644 --- a/write-metadata-buffer/action.yaml +++ b/write-metadata-buffer/action.yaml @@ -50,12 +50,25 @@ runs: echo "$OUTPUT" + if echo "$OUTPUT" | grep -q "\[Error\]"; then + echo "Error: program-metadata create-buffer reported a failed transaction" + exit 1 + fi + BUFFER=$(echo "$OUTPUT" | grep -i "buffer:" | head -1 | grep -oE '[1-9A-HJ-NP-Za-km-z]{32,44}') if [ -z "$BUFFER" ]; then echo "Error: Could not extract buffer address from output" exit 1 fi + if ! NO_COLOR=1 npx @solana-program/program-metadata@0.5.1 fetch-buffer "$BUFFER" \ + --keypair ./deploy-keypair.json \ + --rpc "${{ inputs.rpc-url }}" \ + --raw >/dev/null 2>&1; then + echo "Error: Metadata buffer $BUFFER was not found after create-buffer" + exit 1 + fi + echo "Found buffer: $BUFFER" echo "buffer=$BUFFER" >> $GITHUB_OUTPUT diff --git a/write-program-buffer/action.yaml b/write-program-buffer/action.yaml index ba257b0..940844b 100644 --- a/write-program-buffer/action.yaml +++ b/write-program-buffer/action.yaml @@ -20,6 +20,22 @@ inputs: description: "Priority fee in microlamports" required: false default: "100000" + max-sign-attempts: + description: "Maximum Solana CLI signing attempts for write-buffer" + required: false + default: "100" + write-timeout-minutes: + description: "Maximum minutes to let a single write-buffer command run before printing diagnostics" + required: false + default: "55" + retry-attempts: + description: "Maximum write-buffer attempts" + required: false + default: "3" + use-rpc: + description: "Send write transactions through the configured RPC instead of validator TPUs" + required: false + default: "true" outputs: buffer: @@ -45,9 +61,6 @@ runs: echo "Checking program info..." PROGRAM_INFO=$(solana program show ${{ inputs.program-id }} -u ${{ inputs.rpc-url }} 2>&1) EXIT_CODE=$? - echo "Raw program info output:" - echo "$PROGRAM_INFO" - if [ $EXIT_CODE -eq 0 ]; then echo "exists=true" >> $GITHUB_OUTPUT echo "Program exists, checking size..." @@ -88,40 +101,102 @@ runs: - name: Write program buffer id: write-buffer - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 - with: - timeout_minutes: 60 - max_attempts: 3 - shell: bash - command: | - echo "Creating program buffer... (Attempt $RETRY_ATTEMPT of 3)" - OUTPUT=$(solana program write-buffer \ - ./target/deploy/${{ inputs.program }}.so \ + shell: bash + run: | + set -euo pipefail + + PROGRAM_SO="./target/deploy/${{ inputs.program }}.so" + BUFFER_KEYPAIR=$(mktemp ./buffer-keypair.XXXXXX.json) + rm -f "$BUFFER_KEYPAIR" + solana-keygen new --no-bip39-passphrase --silent --outfile "$BUFFER_KEYPAIR" + BUFFER=$(solana-keygen pubkey "$BUFFER_KEYPAIR") + DEPLOYER=$(solana-keygen pubkey ./deploy-keypair.json) + DUMPED_BUFFER=$(mktemp ./buffer-dump.XXXXXX.so) + + echo "Creating program buffer $BUFFER" + echo "Deployer authority: $DEPLOYER" + echo "Program artifact size: $(wc -c < "$PROGRAM_SO") bytes" + echo "Program artifact sha256: $(shasum -a 256 "$PROGRAM_SO" | awk '{print $1}')" + echo "Max sign attempts: ${{ inputs.max-sign-attempts }}" + echo "Write timeout: ${{ inputs.write-timeout-minutes }} minutes" + echo "Write attempts: ${{ inputs.retry-attempts }}" + echo "Use RPC transport: ${{ inputs.use-rpc }}" + + USE_RPC_ARGS=() + if [ "${{ inputs.use-rpc }}" = "true" ]; then + USE_RPC_ARGS=(--use-rpc) + fi + + ATTEMPT=1 + STATUS=1 + while [ "$ATTEMPT" -le "${{ inputs.retry-attempts }}" ]; do + WRITE_LOG="write-buffer-${ATTEMPT}.log" + echo "Starting write attempt $ATTEMPT of ${{ inputs.retry-attempts }} for buffer $BUFFER" + + set +e + timeout "${{ inputs.write-timeout-minutes }}m" solana program write-buffer \ + "$PROGRAM_SO" \ --url ${{ inputs.rpc-url }} \ --keypair ./deploy-keypair.json \ - --max-sign-attempts 100 \ + --buffer "$BUFFER_KEYPAIR" \ + --max-sign-attempts ${{ inputs.max-sign-attempts }} \ --with-compute-unit-price ${{ inputs.priority-fee }} \ - --use-rpc \ - 2>&1) + "${USE_RPC_ARGS[@]}" \ + 2>&1 | tee "$WRITE_LOG" + STATUS=${PIPESTATUS[0]} + set -e - # Print output in real-time - echo "$OUTPUT" + if [ "$STATUS" -eq 0 ]; then + if ! grep -q "Buffer:" "$WRITE_LOG"; then + echo "Warning: solana CLI output did not include a Buffer line; using pre-generated buffer address" + fi - # Check for success - if ! echo "$OUTPUT" | grep -q "Buffer:"; then - echo "Error: Buffer creation failed" - exit 1 + echo "Found buffer: $BUFFER" + echo "buffer=$BUFFER" >> $GITHUB_OUTPUT + rm -f "$DUMPED_BUFFER" + exit 0 + fi + + if [ "$STATUS" -eq 124 ]; then + echo "Error: Buffer write attempt $ATTEMPT timed out after ${{ inputs.write-timeout-minutes }} minutes" + else + echo "Error: Buffer write attempt $ATTEMPT failed with exit code $STATUS" + fi + + echo "Buffer account: $BUFFER" + echo "Checking whether the buffer was written despite the CLI exit status..." + + rm -f "$DUMPED_BUFFER" + if solana program dump "$BUFFER" "$DUMPED_BUFFER" -u ${{ inputs.rpc-url }}; then + echo "Dumped buffer size: $(wc -c < "$DUMPED_BUFFER") bytes" + echo "Dumped buffer sha256: $(shasum -a 256 "$DUMPED_BUFFER" | awk '{print $1}')" + + if cmp -s "$PROGRAM_SO" "$DUMPED_BUFFER"; then + echo "Buffer contents match the program artifact; continuing with buffer $BUFFER" + echo "buffer=$BUFFER" >> $GITHUB_OUTPUT + rm -f "$DUMPED_BUFFER" + exit 0 + fi + + echo "Dumped buffer does not match the program artifact" + else + echo "Could not dump buffer $BUFFER" fi - # Extract buffer if successful - BUFFER=$(echo "$OUTPUT" | grep "Buffer:" | cut -d " " -f2) - if [ -z "$BUFFER" ]; then - echo "Error: Could not extract buffer address" - exit 1 + if [ "$ATTEMPT" -lt "${{ inputs.retry-attempts }}" ]; then + echo "Retrying the same buffer so solana CLI can write only the missing chunks" + ATTEMPT=$((ATTEMPT + 1)) + continue fi - echo "Found buffer: $BUFFER" - echo "buffer=$BUFFER" >> $GITHUB_OUTPUT + echo "Recent transactions for buffer $BUFFER:" + solana transaction-history "$BUFFER" -u ${{ inputs.rpc-url }} --limit 10 || true + echo "Open buffers still owned by deployer $DEPLOYER:" + solana program show --buffers --buffer-authority "$DEPLOYER" -u ${{ inputs.rpc-url }} || true + rm -f "$DUMPED_BUFFER" + exit "$STATUS" + done + rm -f "$DUMPED_BUFFER" # We can not set authority and upgrade program in the same instruction so it needs to be here # The rent for the buffer will be returned to the local keypair though once the program gets deployed in squads. So its fine. @@ -147,3 +222,4 @@ runs: run: | rm -f ./deploy-keypair.json rm -f ./authority-keypair.json + rm -f ./buffer-keypair.*.json