diff --git a/cargo-publish/action.yaml b/cargo-publish/action.yaml new file mode 100644 index 0000000..5d0e309 --- /dev/null +++ b/cargo-publish/action.yaml @@ -0,0 +1,195 @@ +name: "Cargo Publish" +description: "Publishes one Rust crate to crates.io using Trusted Publishing" +inputs: + package: + description: "Package name to publish. Leave empty to infer from the current package." + required: false + default: "" + working-directory: + description: "Directory containing the Cargo manifest or workspace." + required: false + default: "." + toolchain: + description: "Rust toolchain to install and use." + required: false + default: "stable" + locked: + description: "Pass --locked to cargo publish." + required: false + default: "true" + allow-dirty: + description: "Pass --allow-dirty to cargo publish." + required: false + default: "false" + dry-run: + description: "Run publish validation without publishing to crates.io." + required: false + default: "false" + check-version-available: + description: "Fail early when this crate version already exists on crates.io." + required: false + default: "true" + skip-existing: + description: "Skip publishing successfully when this crate version already exists on crates.io." + required: false + default: "false" +outputs: + package: + description: "Published package name." + value: ${{ steps.package.outputs.package }} + version: + description: "Published package version." + value: ${{ steps.package.outputs.version }} + published: + description: "Whether the action published to crates.io." + value: ${{ steps.status.outputs.published }} + already-published: + description: "Whether this crate version already existed on crates.io." + value: ${{ steps.version_exists.outputs.exists }} + +runs: + using: "composite" + steps: + - name: Install Rust toolchain + shell: bash + env: + RUST_TOOLCHAIN: ${{ inputs.toolchain }} + run: | + set -euo pipefail + rustup toolchain install --profile minimal --no-self-update "$RUST_TOOLCHAIN" + rustup default "$RUST_TOOLCHAIN" + + - name: Show Cargo version + shell: bash + run: cargo --version + + - name: Resolve package version + id: package + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_INPUT: ${{ inputs.package }} + run: | + set -euo pipefail + + if [ -n "$PACKAGE_INPUT" ]; then + if ! PKG_ID="$(cargo pkgid -p "$PACKAGE_INPUT" 2>&1)"; then + echo "$PKG_ID" + echo "::error::Could not resolve package '$PACKAGE_INPUT'." + exit 1 + fi + else + if ! PKG_ID="$(cargo pkgid 2>&1)"; then + echo "$PKG_ID" + echo "::error::Could not infer package. Set the package input when publishing from a workspace root." + exit 1 + fi + fi + + NAME_VERSION="${PKG_ID##*#}" + PACKAGE="${NAME_VERSION%@*}" + VERSION="${NAME_VERSION##*@}" + + if [ -z "$PACKAGE" ] || [ -z "$VERSION" ] || [ "$PACKAGE" = "$NAME_VERSION" ] || [ "$VERSION" = "$NAME_VERSION" ]; then + echo "::error::Could not parse package and version from cargo pkgid output: $PKG_ID" + exit 1 + fi + + echo "package=$PACKAGE" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Publishing target: $PACKAGE@$VERSION" + + - name: Verify crate package + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + ALLOW_DIRTY: ${{ inputs.allow-dirty }} + LOCKED: ${{ inputs.locked }} + PACKAGE: ${{ steps.package.outputs.package }} + run: | + set -euo pipefail + + PUBLISH_ARGS=(-p "$PACKAGE") + if [ "$LOCKED" = "true" ]; then + PUBLISH_ARGS+=(--locked) + fi + if [ "$ALLOW_DIRTY" = "true" ]; then + PUBLISH_ARGS+=(--allow-dirty) + fi + + cargo publish "${PUBLISH_ARGS[@]}" --dry-run + + - name: Check crates.io version availability + id: version_exists + shell: bash + env: + CHECK_VERSION_AVAILABLE: ${{ inputs.check-version-available }} + PACKAGE: ${{ steps.package.outputs.package }} + SKIP_EXISTING: ${{ inputs.skip-existing }} + VERSION: ${{ steps.package.outputs.version }} + run: | + set -euo pipefail + + if [ "$CHECK_VERSION_AVAILABLE" != "true" ] && [ "$SKIP_EXISTING" != "true" ]; then + echo "exists=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + REGISTRY_CHECK_DIR="${RUNNER_TEMP:-/tmp}" + + if (cd "$REGISTRY_CHECK_DIR" && cargo info "$PACKAGE@$VERSION" >/dev/null 2>&1); then + echo "exists=true" >> "$GITHUB_OUTPUT" + if [ "$SKIP_EXISTING" = "true" ]; then + echo "$PACKAGE@$VERSION already exists on crates.io; skipping publish." + exit 0 + fi + + echo "::error::$PACKAGE@$VERSION already exists on crates.io. Bump the crate version before publishing." + exit 1 + fi + + echo "exists=false" >> "$GITHUB_OUTPUT" + + - name: Authenticate crates.io trusted publisher + if: inputs.dry-run != 'true' && steps.version_exists.outputs.exists != 'true' + id: crates_io_auth + uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 + + - name: Publish to crates.io + if: inputs.dry-run != 'true' && steps.version_exists.outputs.exists != 'true' + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + ALLOW_DIRTY: ${{ inputs.allow-dirty }} + CARGO_REGISTRY_TOKEN: ${{ steps.crates_io_auth.outputs.token }} + LOCKED: ${{ inputs.locked }} + PACKAGE: ${{ steps.package.outputs.package }} + run: | + set -euo pipefail + + PUBLISH_ARGS=(-p "$PACKAGE") + if [ "$LOCKED" = "true" ]; then + PUBLISH_ARGS+=(--locked) + fi + if [ "$ALLOW_DIRTY" = "true" ]; then + PUBLISH_ARGS+=(--allow-dirty) + fi + + cargo publish "${PUBLISH_ARGS[@]}" + + - name: Set publish status + id: status + shell: bash + env: + DRY_RUN: ${{ inputs.dry-run }} + VERSION_EXISTS: ${{ steps.version_exists.outputs.exists }} + run: | + set -euo pipefail + + if [ "$DRY_RUN" = "true" ]; then + echo "published=false" >> "$GITHUB_OUTPUT" + elif [ "$VERSION_EXISTS" = "true" ]; then + echo "published=false" >> "$GITHUB_OUTPUT" + else + echo "published=true" >> "$GITHUB_OUTPUT" + fi diff --git a/prepare-squads-release/action.yaml b/prepare-squads-release/action.yaml index 8d35e0b..590eea0 100644 --- a/prepare-squads-release/action.yaml +++ b/prepare-squads-release/action.yaml @@ -81,7 +81,7 @@ runs: - name: Write program buffer id: write-program-buffer - uses: solana-developers/github-actions/write-program-buffer@main + uses: solana-developers/github-actions/write-program-buffer@e15ede4246abd98badb4701d34394865e382a2de with: program-id: ${{ inputs.program-id }} program: ${{ inputs.program }} @@ -97,7 +97,7 @@ runs: - name: Write metadata buffer id: write-metadata-buffer if: inputs.metadata-path != '' - uses: solana-developers/github-actions/write-metadata-buffer@main + uses: solana-developers/github-actions/write-metadata-buffer@e15ede4246abd98badb4701d34394865e382a2de with: idl-path: ${{ inputs.metadata-path }} rpc-url: ${{ inputs.rpc-url }} diff --git a/readme.md b/readme.md index c9cc386..5571d5b 100644 --- a/readme.md +++ b/readme.md @@ -82,6 +82,89 @@ Customize the workflow to your needs! - `program`: Program name to build - `features`: Optional Cargo features to enable +### Cargo Publishing + +- `cargo-publish`: Publishes one Rust crate to crates.io using Trusted Publishing + - Uses GitHub OIDC and `rust-lang/crates-io-auth-action` + - Runs `cargo publish --dry-run` before publishing + - Optionally checks whether the crate version already exists on crates.io + - Leaves checkout, tests, generated clients, tags, and GitHub releases to the caller workflow + - Inputs: + - `package`: Package name to publish, or empty to infer from the current package + - `working-directory`: Directory containing the Cargo manifest or workspace + - `toolchain`: Rust toolchain to install and use + - `locked`: Pass `--locked` to `cargo publish` + - `allow-dirty`: Pass `--allow-dirty` to `cargo publish` for generated files that are created during the workflow and are intentionally not checked into source control + - `dry-run`: Validate without publishing to crates.io + - `check-version-available`: Fail early when this crate version already exists on crates.io + - `skip-existing`: Skip publishing successfully when this crate version already exists on crates.io + - Outputs: + - `package`: Published package name + - `version`: Published package version + - `published`: Whether the action published to crates.io + - `already-published`: Whether this crate version already existed on crates.io + +Caller workflows must grant OIDC token access and configure Trusted Publishing for the crate on crates.io. The Trusted Publisher configuration should match the caller repository and workflow file, not this shared action repository: + +```yaml +permissions: + contents: read + id-token: write +``` + +Pin this action to a released tag or commit SHA instead of `main`. + +Only set `allow-dirty: "true"` when a prior workflow step intentionally generated files that must be included in the crate but are not checked into source control. + +Single crate: + +```yaml +- uses: actions/checkout@v6 + +- name: Build package + working-directory: path/to/package + run: cargo build + +- uses: solana-developers/github-actions/cargo-publish@ + with: + package: my-crate + working-directory: path/to/package +``` + +Workspace package: + +```yaml +- uses: actions/checkout@v6 + +- name: Build workspace + working-directory: path/to/workspace + run: cargo build --workspace + +- uses: solana-developers/github-actions/cargo-publish@ + with: + package: my-workspace-crate + working-directory: path/to/workspace +``` + +Generated client: + +```yaml +- uses: actions/checkout@v6 + +- run: pnpm run generate-clients + +- name: Build generated client + working-directory: path/to/generated-crate + run: cargo build + +- uses: solana-developers/github-actions/cargo-publish@ + with: + package: my-generated-crate + working-directory: path/to/generated-crate + allow-dirty: "true" + skip-existing: "true" +``` + ### Deployment - `write-program-buffer`: Writes a buffer that will then later be set either from the provided keypair or from the squads multisig @@ -172,9 +255,11 @@ These actions use the [program-metadata](https://github.com/solana-program/progr 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. +Pin this action to a released tag or commit SHA instead of `main`. + ```yaml - name: Prepare Squads release buffers - uses: solana-developers/github-actions/prepare-squads-release@main + uses: solana-developers/github-actions/prepare-squads-release@ with: program: ${{ env.PROGRAM }} program-id: ${{ env.PROGRAM_ID }}