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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions cargo-publish/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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"
build-command:
description: "Build command to run before publishing. Leave empty to skip."
required: false
default: "cargo build"
locked:
description: "Pass --locked to cargo publish."
required: false
default: "true"
allow-dirty:
Comment thread
dev-jodee marked this conversation as resolved.
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
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you trust this action? Or is just rustup enough?

with:
toolchain: ${{ inputs.toolchain }}

- name: Show Cargo version
shell: bash
run: cargo --version

- name: Run build command
if: inputs.build-command != ''
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -euo pipefail
${{ inputs.build-command }}
Comment thread
dev-jodee marked this conversation as resolved.

- name: Resolve package version
id: package
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -euo pipefail

PACKAGE_INPUT="${{ inputs.package }}"
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 }}
run: |
set -euo pipefail

PUBLISH_ARGS=(-p "${{ steps.package.outputs.package }}")
if [ "${{ inputs.locked }}" = "true" ]; then
PUBLISH_ARGS+=(--locked)
fi
if [ "${{ inputs.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
run: |
set -euo pipefail

if [ "${{ inputs.check-version-available }}" != "true" ] && [ "${{ inputs.skip-existing }}" != "true" ]; then
echo "exists=false" >> "$GITHUB_OUTPUT"
exit 0
fi

REGISTRY_CHECK_DIR="${RUNNER_TEMP:-/tmp}"
PACKAGE="${{ steps.package.outputs.package }}"
VERSION="${{ steps.package.outputs.version }}"

if (cd "$REGISTRY_CHECK_DIR" && cargo info "$PACKAGE@$VERSION" >/dev/null 2>&1); then
echo "exists=true" >> "$GITHUB_OUTPUT"
if [ "${{ inputs.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:
CARGO_REGISTRY_TOKEN: ${{ steps.crates_io_auth.outputs.token }}
run: |
set -euo pipefail

PUBLISH_ARGS=(-p "${{ steps.package.outputs.package }}")
if [ "${{ inputs.locked }}" = "true" ]; then
PUBLISH_ARGS+=(--locked)
fi
if [ "${{ inputs.allow-dirty }}" = "true" ]; then
PUBLISH_ARGS+=(--allow-dirty)
fi

cargo publish "${PUBLISH_ARGS[@]}"

- name: Set publish status
id: status
shell: bash
run: |
set -euo pipefail

if [ "${{ inputs.dry-run }}" = "true" ]; then
echo "published=false" >> "$GITHUB_OUTPUT"
elif [ "${{ steps.version_exists.outputs.exists }}" = "true" ]; then
echo "published=false" >> "$GITHUB_OUTPUT"
else
echo "published=true" >> "$GITHUB_OUTPUT"
fi
68 changes: 68 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,74 @@ 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 a build command and `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
- `build-command`: Build command to run before publishing, or empty to skip
- `locked`: Pass `--locked` to `cargo publish`
- `allow-dirty`: Pass `--allow-dirty` to `cargo publish`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably good to add an explanation here what this does and that ppl should not use it

- `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
```

Single crate:

```yaml
- uses: actions/checkout@v6

- uses: solana-developers/github-actions/cargo-publish@main
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not recommend @main

with:
package: solana-keychain
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather write: Path to package root or path to where the cargo toml lives or smth like that

working-directory: rust
```

Workspace package:

```yaml
- uses: actions/checkout@v6

- uses: solana-developers/github-actions/cargo-publish@main
with:
package: kora-lib
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of kora-lib could write "path to package root or smth" not everyone knows kora

build-command: cargo build --workspace
```

Generated client:

```yaml
- uses: actions/checkout@v6

- run: pnpm run generate-clients

- uses: solana-developers/github-actions/cargo-publish@main
with:
package: subscriptions
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. Readers of the readme dont know what subscriptions means

working-directory: clients/rust
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
Expand Down