From f8b942db923d38e242de9895b6be14b05bfa21b5 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Thu, 16 Jan 2025 17:23:58 -0700 Subject: [PATCH] Set up release-plz --- .github/workflows/create-release-pr.yml | 35 ++++++++ .github/workflows/publish-release.yml | 32 +++++++ CONTRIBUTING.md | 19 ++-- Cargo.toml | 5 +- README.md | 113 +----------------------- crates/twirp-build/Cargo.toml | 11 ++- crates/twirp-build/LICENSE | 21 +++++ crates/twirp-build/README.md | 113 ++++++++++++++++++++++++ crates/twirp/Cargo.toml | 11 ++- crates/twirp/LICENSE | 21 +++++ crates/twirp/README.md | 3 + release-plz.toml | 6 ++ script/publish.sh | 88 ------------------ 13 files changed, 257 insertions(+), 221 deletions(-) create mode 100644 .github/workflows/create-release-pr.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 crates/twirp-build/LICENSE create mode 100644 crates/twirp-build/README.md create mode 100644 crates/twirp/LICENSE create mode 100644 crates/twirp/README.md create mode 100644 release-plz.toml delete mode 100755 script/publish.sh diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml new file mode 100644 index 0000000..b4fafdb --- /dev/null +++ b/.github/workflows/create-release-pr.yml @@ -0,0 +1,35 @@ +name: Create release PR + +permissions: + pull-requests: write + contents: write + +on: workflow_dispatch + +jobs: + +# Create a PR with the new versions and changelog, preparing the next release. When merged to main, +# the publish-release.yml workflow will automatically publish any Rust package versions. +create-release-pr: + name: Create release PR + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + concurrency: # Don't run overlapping instances of this workflow + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..0c290cc --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,32 @@ +name: Release and unpublished twirp/twirp-build packages + +permissions: + contents: write + +on: + push: + branches: + - main + +jobs: + + # Release any unpublished packages + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51d7e26..90c7a4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,22 +41,15 @@ cargo build && cargo test Run clippy and fix any lints: ```sh -cargo fmt --all -- --check -cargo clippy -- --deny warnings -D clippy::unwrap_used -cargo clippy --tests -- --deny warnings -A clippy::unwrap_used +make lint ``` -## Releasing (write access required) +## Releasing -If you are one of the maintainers of this package then follow this process: - -1. Create a PR for this release with following changes: - - Updated `CHANGELOG.md` with desired change comments and ensure that it has the version to be released with date at the top. - - Go through all recent PRs and make sure they are properly accounted for. - - Make sure all changelog entries have links back to their PR(s) if appropriate. - - Update package version in Cargo.toml. -1. Get an approval and merge your PR. -1. Run ./script/publish from the `main` branch supplying your token and version information. +1. Go to the `Create Release PR` action and press the button to run the action. This will use `release-plz` to create a new release PR. +1. Adjust the generated changelog and version number(s) as necessary. +1. Get PR approval +1. Merge the PR. The `publish-release.yml` workflow will automatically publish a new release of any crate whose version has changed. ## Resources diff --git a/Cargo.toml b/Cargo.toml index c55945d..efb2fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,4 @@ [workspace] -members = [ - "crates/*", - "example", -] +members = ["crates/*", "example"] resolver = "2" diff --git a/README.md b/README.md index a274241..b03febf 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,6 @@ # twirp-rs -[Twirp is an RPC protocol](https://twitchtv.github.io/twirp/docs/spec_v7.html) based on HTTP and Protocol Buffers (proto). The protocol uses HTTP URLs to specify the RPC endpoints, and sends/receives proto messages as HTTP request/response bodies. Services are defined in a [.proto file](https://developers.google.com/protocol-buffers/docs/proto3), allowing easy implementation of RPC services with auto-generated clients and servers in different languages. +This repository contains the following crates published to crates.io. Please see their respective README files for more information. -The [canonical implementation](https://github.com/twitchtv/twirp) is in Go, this is a Rust implementation of the protocol. Rust protocol buffer support is provided by the [`prost`](https://github.com/tokio-rs/prost) ecosystem. - -Unlike [`prost-twirp`](https://github.com/sourcefrog/prost-twirp), the generated traits for serving and accessing RPCs are implemented atop `async` functions. Because traits containing `async` functions [are not directly supported](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/) in Rust versions prior to 1.75, this crate uses the [`async_trait`](https://github.com/dtolnay/async-trait) macro to encapsulate the scaffolding required to make them work. - -## Usage - -See the [example](./example) for a complete example project. - -Define services and messages in a `.proto` file: - -```proto -// service.proto -package service.haberdash.v1; - -service HaberdasherAPI { - rpc MakeHat(MakeHatRequest) returns (MakeHatResponse); -} -message MakeHatRequest { } -message MakeHatResponse { } -``` - -Add the `twirp-build` crate as a build dependency in your `Cargo.toml` (you'll need `prost-build` too): - -```toml -# Cargo.toml -[build-dependencies] -twirp-build = "0.3" -prost-build = "0.13" -``` - -Add a `build.rs` file to your project to compile the protos and generate Rust code: - -```rust -fn main() { - let proto_source_files = ["./service.proto"]; - - // Tell Cargo to rerun this build script if any of the proto files change - for entry in &proto_source_files { - println!("cargo:rerun-if-changed={}", entry); - } - - prost_build::Config::new() - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // enable support for JSON encoding - .service_generator(twirp_build::service_generator()) - .compile_protos(&proto_source_files, &["./"]) - .expect("error compiling protos"); -} -``` - -This generates code that you can find in `target/build/your-project-*/out/example.service.rs`. In order to use this code, you'll need to implement the trait for the proto defined service and wire up the service handlers to a hyper web server. See [the example `main.rs`]( example/src/main.rs) for details. - -Include the generated code, create a router, register your service, and then serve those routes in the hyper server: - -```rust -mod haberdash { - include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); -} - -use axum::Router; -use haberdash::{MakeHatRequest, MakeHatResponse}; - -#[tokio::main] -pub async fn main() { - let api_impl = Arc::new(HaberdasherApiServer {}); - let twirp_routes = Router::new() - .nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); - let app = Router::new() - .nest("/twirp", twirp_routes) - .fallback(twirp::server::not_found_handler); - - let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); - if let Err(e) = axum::serve(tcp_listener, app).await { - eprintln!("server error: {}", e); - } -} - -// Define the server and implement the trait. -struct HaberdasherApiServer; - -#[async_trait] -impl haberdash::HaberdasherApi for HaberdasherApiServer { - async fn make_hat(&self, ctx: twirp::Context, req: MakeHatRequest) -> Result { - todo!() - } -} -``` - -This code creates an `axum::Router`, then hands it off to `axum::serve()` to handle networking. -This use of `axum::serve` is optional. After building `app`, you can instead invoke it from any -`hyper`-based server by importing `twirp::tower::Service` and doing `app.call(request).await`. - -## Usage (client side) - -On the client side, you also get a generated twirp client (based on the rpc endpoints in your proto). Include the generated code, create a client, and start making rpc calls: - -``` rust -mod haberdash { - include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); -} - -use haberdash::{HaberdasherApiClient, MakeHatRequest, MakeHatResponse}; - -#[tokio::main] -pub async fn main() { - let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?; - let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; - eprintln!("{:?}", resp); -} -``` +- [`twirp-build`](https://github.com/github/twirp-rs/tree/main/crates/twirp/twirp-build) - A crate for generating twirp client and server interfaces. This is probably what you are looking for. +- [`twirp`](https://github.com/github/twirp-rs/tree/main/crates/twirp/) - A crate used by code that is generated by `twirp-build` diff --git a/crates/twirp-build/Cargo.toml b/crates/twirp-build/Cargo.toml index e690bc2..9a84091 100644 --- a/crates/twirp-build/Cargo.toml +++ b/crates/twirp-build/Cargo.toml @@ -1,13 +1,18 @@ [package] name = "twirp-build" version = "0.7.0" -authors = ["The blackbird team "] edition = "2021" description = "Code generation for async-compatible Twirp RPC interfaces." readme = "README.md" -keywords = ["twirp"] -categories = ["network-programming"] +keywords = ["twirp", "prost", "protocol-buffers"] +categories = [ + "development-tools::build-utils", + "network-programming", + "asynchronous", +] repository = "https://github.com/github/twirp-rs" +license = "MIT" +license-file = "./LICENSE" [dependencies] prost-build = "0.13" diff --git a/crates/twirp-build/LICENSE b/crates/twirp-build/LICENSE new file mode 100644 index 0000000..2586e0e --- /dev/null +++ b/crates/twirp-build/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/twirp-build/README.md b/crates/twirp-build/README.md new file mode 100644 index 0000000..7b320cf --- /dev/null +++ b/crates/twirp-build/README.md @@ -0,0 +1,113 @@ +# `twirp-build` + +[Twirp is an RPC protocol](https://twitchtv.github.io/twirp/docs/spec_v7.html) based on HTTP and Protocol Buffers (proto). The protocol uses HTTP URLs to specify the RPC endpoints, and sends/receives proto messages as HTTP request/response bodies. Services are defined in a [.proto file](https://developers.google.com/protocol-buffers/docs/proto3), allowing easy implementation of RPC services with auto-generated clients and servers in different languages. + +The [canonical implementation](https://github.com/twitchtv/twirp) is in Go, this is a Rust implementation of the protocol. Rust protocol buffer support is provided by the [`prost`](https://github.com/tokio-rs/prost) ecosystem. + +Unlike [`prost-twirp`](https://github.com/sourcefrog/prost-twirp), the generated traits for serving and accessing RPCs are implemented atop `async` functions. Because traits containing `async` functions [are not directly supported](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/) in Rust versions prior to 1.75, this crate uses the [`async_trait`](https://github.com/dtolnay/async-trait) macro to encapsulate the scaffolding required to make them work. + +## Usage + +See the [example](./example) for a complete example project. + +Define services and messages in a `.proto` file: + +```proto +// service.proto +package service.haberdash.v1; + +service HaberdasherAPI { + rpc MakeHat(MakeHatRequest) returns (MakeHatResponse); +} +message MakeHatRequest { } +message MakeHatResponse { } +``` + +Add the `twirp-build` crate as a build dependency in your `Cargo.toml` (you'll need `prost-build` too): + +```toml +# Cargo.toml +[build-dependencies] +twirp-build = "0.3" +prost-build = "0.13" +``` + +Add a `build.rs` file to your project to compile the protos and generate Rust code: + +```rust +fn main() { + let proto_source_files = ["./service.proto"]; + + // Tell Cargo to rerun this build script if any of the proto files change + for entry in &proto_source_files { + println!("cargo:rerun-if-changed={}", entry); + } + + prost_build::Config::new() + .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // enable support for JSON encoding + .service_generator(twirp_build::service_generator()) + .compile_protos(&proto_source_files, &["./"]) + .expect("error compiling protos"); +} +``` + +This generates code that you can find in `target/build/your-project-*/out/example.service.rs`. In order to use this code, you'll need to implement the trait for the proto defined service and wire up the service handlers to a hyper web server. See [the example `main.rs`]( example/src/main.rs) for details. + +Include the generated code, create a router, register your service, and then serve those routes in the hyper server: + +```rust +mod haberdash { + include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); +} + +use axum::Router; +use haberdash::{MakeHatRequest, MakeHatResponse}; + +#[tokio::main] +pub async fn main() { + let api_impl = Arc::new(HaberdasherApiServer {}); + let twirp_routes = Router::new() + .nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); + let app = Router::new() + .nest("/twirp", twirp_routes) + .fallback(twirp::server::not_found_handler); + + let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); + if let Err(e) = axum::serve(tcp_listener, app).await { + eprintln!("server error: {}", e); + } +} + +// Define the server and implement the trait. +struct HaberdasherApiServer; + +#[async_trait] +impl haberdash::HaberdasherApi for HaberdasherApiServer { + async fn make_hat(&self, ctx: twirp::Context, req: MakeHatRequest) -> Result { + todo!() + } +} +``` + +This code creates an `axum::Router`, then hands it off to `axum::serve()` to handle networking. +This use of `axum::serve` is optional. After building `app`, you can instead invoke it from any +`hyper`-based server by importing `twirp::tower::Service` and doing `app.call(request).await`. + +## Usage (client side) + +On the client side, you also get a generated twirp client (based on the rpc endpoints in your proto). Include the generated code, create a client, and start making rpc calls: + +``` rust +mod haberdash { + include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); +} + +use haberdash::{HaberdasherApiClient, MakeHatRequest, MakeHatResponse}; + +#[tokio::main] +pub async fn main() { + let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?; + let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; + eprintln!("{:?}", resp); +} +``` diff --git a/crates/twirp/Cargo.toml b/crates/twirp/Cargo.toml index 597ee6f..caa218f 100644 --- a/crates/twirp/Cargo.toml +++ b/crates/twirp/Cargo.toml @@ -1,13 +1,18 @@ [package] name = "twirp" version = "0.7.0" -authors = ["The blackbird team "] edition = "2021" description = "An async-compatible library for Twirp RPC in Rust." readme = "README.md" -keywords = ["twirp"] -categories = ["network-programming"] +keywords = ["twirp", "prost", "protocol-buffers"] +categories = [ + "development-tools::build-utils", + "network-programming", + "asynchronous", +] repository = "https://github.com/github/twirp-rs" +license = "MIT" +license-file = "./LICENSE" [features] test-support = [] diff --git a/crates/twirp/LICENSE b/crates/twirp/LICENSE new file mode 100644 index 0000000..2586e0e --- /dev/null +++ b/crates/twirp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/twirp/README.md b/crates/twirp/README.md new file mode 100644 index 0000000..f8288e9 --- /dev/null +++ b/crates/twirp/README.md @@ -0,0 +1,3 @@ +# `twirp` + +This crate is mainly used by the code generated by [`twirp-builder`](https://github.com/github/twirp-rs/tree/main/crates/twirp-build/). Please see its readme for more details and usage information. diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..d21a211 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,6 @@ +[workspace] +release_always = false + +[[package]] +name = "example" # Ignore the example crate +release = false diff --git a/script/publish.sh b/script/publish.sh deleted file mode 100755 index d20cdf6..0000000 --- a/script/publish.sh +++ /dev/null @@ -1,88 +0,0 @@ -#! /usr/bin/env bash -#/ Publishes twirp-rs to crates.io -#/ Usage: script/publish --version --token [--upload] -#/ -#/ version: Version to publish to crates.io. -#/ token: crates.io token to use for publishing. -#/ upload: Actually publishes the the crate to crates.io. By default this script -#/ runs cargo publish in dry-run mode - -set -euo pipefail -IFS=$'\n\t' - -function usage { - grep "^#/" "${BASH_SOURCE[0]}" | cut -c 4- -} - - -PKG_VERSION=$(cargo metadata --format-version 1 | jq -r '.workspace_members[0]' | awk '{print $2}') -MODE="--dry-run" -TOKEN= -VERSION= - -if [[ -z "$(command -v jq)" ]]; then - echo "jq is required to run this script" - exit -fi - - -if [[ "$#" == 0 ]] -then - usage - exit -fi -# Ensure that the tests are still passing before publishing -cargo test --verbose - -while [ $# -gt 0 ]; do - case "$1" in - --help|-h) - usage; shift ;; - --upload) - MODE=; shift ;; - --token) - TOKEN=$2; shift 2;; - --version) - VERSION=$2; shift 2 ;; - --) - shift; break ;; - *) - usage - exit - ;; - esac -done - -if [[ -z "${VERSION}" ]]; then - echo "Version is required" - exit -fi - -if [[ -z "${TOKEN}" ]]; then - echo "Token is required" - exit -fi - -if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]] -then - echo "You can only publish from main branch" - exit -fi - -if [[ "${PKG_VERSION}" != "${VERSION}" ]]; then - echo "Version mismatched, cargo file: ${PKG_VERSION}, You supplied: ${VERSION}" - exit -fi - -cargo package - -if [[ "${MODE}" == "--dry-run" ]]; then - CMD="cargo publish --dry-run --token ${TOKEN}" - eval $CMD -else - git tag $VERSION - CMD="cargo publish --token ${TOKEN}" - echo "$CMD" - eval $CMD - git push origin $VERSION -fi