diff --git a/.github/workflows/publish-xtax-blob-storage.yml b/.github/workflows/publish-xtax-blob-storage.yml deleted file mode 100644 index 9710efb..0000000 --- a/.github/workflows/publish-xtax-blob-storage.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish xtax-blob-storage - -on: - push: - tags: ["xtax-blob-storage-v*"] - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" - -jobs: - dry-run: - name: Dry-run publish check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-blob-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-blob- - - - name: Publish dry-run (xtax-blob-storage) - run: cargo publish -p xtax-blob-storage --dry-run - - publish: - name: Publish to crates.io - runs-on: ubuntu-latest - needs: dry-run - environment: release - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-blob-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-blob- - - - name: Publish xtax-blob-storage - run: cargo publish -p xtax-blob-storage --token ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-xtax-encryption.yml b/.github/workflows/publish-xtax-encryption.yml deleted file mode 100644 index c6ad21e..0000000 --- a/.github/workflows/publish-xtax-encryption.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish xtax-encryption - -on: - push: - tags: ["xtax-encryption-v*"] - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" - -jobs: - dry-run: - name: Dry-run publish check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-enc-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-enc- - - - name: Publish dry-run (xtax-encryption) - run: cargo publish -p xtax-encryption --dry-run - - publish: - name: Publish to crates.io - runs-on: ubuntu-latest - needs: dry-run - environment: release - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-enc-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-enc- - - - name: Publish xtax-encryption - run: cargo publish -p xtax-encryption --token ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-xtax.yml b/.github/workflows/publish-xtax.yml deleted file mode 100644 index ba838fe..0000000 --- a/.github/workflows/publish-xtax.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish xtax - -on: - push: - tags: ["xtax-v*"] - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-D warnings" - -jobs: - dry-run: - name: Dry-run publish check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-xtax-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-xtax- - - - name: Publish dry-run (xtax) - run: cargo publish -p xtax --dry-run - - publish: - name: Publish to crates.io - runs-on: ubuntu-latest - needs: dry-run - environment: release - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - - name: Cache cargo registry - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ runner.os }}-cargo-publish-xtax-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-publish-xtax- - - - name: Publish xtax - run: cargo publish -p xtax --token ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7538be6..95ae95a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,105 +31,124 @@ jobs: key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo-release- - # ── xtax-encryption ── - - name: Publish xtax-encryption (if changed) - id: publish-enc + - name: Resolve release version + id: release run: | - VERSION="$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "xtax-encryption") | .version')" - RESPONSE="$(curl -sf "https://crates.io/api/v1/crates/xtax-encryption" || echo "")" - if [ -z "$RESPONSE" ]; then - PUBLISHED="0" - else - PUBLISHED="$(echo "$RESPONSE" | jq -r '.versions[].num' | grep -c "^${VERSION}$" || true)" - fi + set -euo pipefail - if [ "$PUBLISHED" = "0" ]; then - echo "Publishing xtax-encryption $VERSION..." - cargo publish -p xtax-encryption --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - echo "published=true" >> "$GITHUB_OUTPUT" - else - echo "xtax-encryption $VERSION already published — skipping" - echo "published=false" >> "$GITHUB_OUTPUT" - fi - - - name: Wait for crates.io index (xtax-encryption) - if: steps.publish-enc.outputs.published == 'true' - run: sleep 30 - - # ── xtax-blob-storage ── - - name: Publish xtax-blob-storage (if changed) - id: publish-blob - run: | - VERSION="$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "xtax-blob-storage") | .version')" - RESPONSE="$(curl -sf "https://crates.io/api/v1/crates/xtax-blob-storage" || echo "")" - if [ -z "$RESPONSE" ]; then - PUBLISHED="0" - else - PUBLISHED="$(echo "$RESPONSE" | jq -r '.versions[].num' | grep -c "^${VERSION}$" || true)" - fi + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" - if [ "$PUBLISHED" = "0" ]; then - echo "Publishing xtax-blob-storage $VERSION..." - cargo publish -p xtax-blob-storage --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - echo "published=true" >> "$GITHUB_OUTPUT" - else - echo "xtax-blob-storage $VERSION already published — skipping" - echo "published=false" >> "$GITHUB_OUTPUT" + if [ -z "$VERSION" ] || [ "$VERSION" = "$TAG" ]; then + echo "Invalid release tag: $TAG" + exit 1 fi + + echo "Release version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - name: Wait for xtax-blob-storage on crates.io - if: steps.publish-blob.outputs.published == 'true' + - name: Validate lockstep crate versions run: | - VERSION="$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "xtax-blob-storage") | .version')" - echo "Waiting for xtax-blob-storage $VERSION on crates.io index..." - for i in $(seq 1 60); do - echo "Attempt $i/60..." - RESPONSE="$(curl -sf "https://crates.io/api/v1/crates/xtax-blob-storage" || echo "")" - if [ -n "$RESPONSE" ]; then - MATCH="$(echo "$RESPONSE" | jq -r '.versions[].num' | grep -c "^${VERSION}$" || true)" - if [ "$MATCH" -gt 0 ]; then - echo "xtax-blob-storage $VERSION is now visible in API index" - exit 0 - fi + set -euo pipefail + + RELEASE_VERSION="${{ steps.release.outputs.version }}" + CRATES="xtax-encryption xtax-blob-storage xtax" + + for CRATE in $CRATES; do + CRATE_VERSION="$( + cargo metadata --format-version 1 --no-deps | + jq -r --arg name "$CRATE" '.packages[] | select(.name == $name) | .version' + )" + + if [ -z "$CRATE_VERSION" ]; then + echo "Crate $CRATE not found in workspace" + exit 1 fi - sleep 10 + + if [ "$CRATE_VERSION" != "$RELEASE_VERSION" ]; then + echo "::error::Version mismatch for $CRATE: Cargo.toml has $CRATE_VERSION, tag requires $RELEASE_VERSION" + exit 1 + fi + + echo "$CRATE version OK: $CRATE_VERSION" done - echo "Timed out waiting for xtax-blob-storage $VERSION" - exit 1 - # ── xtax (facade) ── - - name: Publish xtax (if changed) - id: publish-xtax + - name: Publish crates + id: publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | - VERSION="$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "xtax") | .version')" - RESPONSE="$(curl -sf "https://crates.io/api/v1/crates/xtax" || echo "")" - if [ -z "$RESPONSE" ]; then - PUBLISHED="0" - else - PUBLISHED="$(echo "$RESPONSE" | jq -r '.versions[].num' | grep -c "^${VERSION}$" || true)" - fi + set -euo pipefail - if [ "$PUBLISHED" = "0" ]; then - echo "Publishing xtax $VERSION..." - cargo publish -p xtax --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - echo "published=true" >> "$GITHUB_OUTPUT" - else - echo "xtax $VERSION already published — skipping" - echo "published=false" >> "$GITHUB_OUTPUT" - fi + VERSION="${{ steps.release.outputs.version }}" + PUBLISHED_ANY=false + + crate_version_exists() { + local crate="$1" + local version="$2" + + local response + response="$(curl -sf "https://crates.io/api/v1/crates/${crate}" || true)" + + if [ -z "$response" ]; then + return 1 + fi + + echo "$response" | + jq -e --arg version "$version" '.versions[].num == $version' >/dev/null + } + + wait_for_crate() { + local crate="$1" + local version="$2" + + echo "Waiting for $crate $version to become visible on crates.io..." + + for i in $(seq 1 60); do + echo "Attempt $i/60..." + + if crate_version_exists "$crate" "$version"; then + echo "$crate $version is visible on crates.io" + return 0 + fi + + sleep 10 + done + + echo "Timed out waiting for $crate $version" + exit 1 + } + + publish_crate() { + local crate="$1" + + if crate_version_exists "$crate" "$VERSION"; then + echo "$crate $VERSION already published — skipping" + return 0 + fi + + echo "Publishing $crate $VERSION..." + cargo publish -p "$crate" --token "$CARGO_REGISTRY_TOKEN" + + PUBLISHED_ANY=true + wait_for_crate "$crate" "$VERSION" + } + + publish_crate xtax-encryption + publish_crate xtax-blob-storage + publish_crate xtax + + echo "published_any=$PUBLISHED_ANY" >> "$GITHUB_OUTPUT" - # ── GitHub Release ── - - name: Create GitHub Release (only if something was published) - if: | - steps.publish-enc.outputs.published == 'true' || - steps.publish-blob.outputs.published == 'true' || - steps.publish-xtax.outputs.published == 'true' + - name: Create GitHub Release + if: steps.publish.outputs.published_any == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ github.ref_name }}" - echo "Some crates were published — creating GitHub Release $TAG..." + + echo "Creating GitHub Release $TAG..." gh release create "$TAG" \ --repo "${{ github.repository }}" \ --title "Release $TAG" \ - --generate-notes + --generate-notes \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c97fb87..c71e4ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,37 +54,33 @@ Run them locally before pushing to avoid round-trips. ## Releasing / Publishing -Releases are driven by Git tags. Pushing a tag with the right prefix triggers -a GitHub Actions workflow that publishes the crate to [crates.io](https://crates.io). +Releases use **lockstep versioning**: all crates share the same version number. +Pushing a single `v*` tag triggers `release.yml`, which validates that all three +crates have matching versions, then publishes them to [crates.io](https://crates.io) +in order: `xtax-encryption` → `xtax-blob-storage` → `xtax`. -| Tag prefix | Workflow | What it publishes | -|-------------------------------|-------------------------------------------|---------------------| -| `xtax-encryption-v*` | `publish-xtax-encryption.yml` | `xtax-encryption` | -| `xtax-blob-storage-v*` | `publish-xtax-blob-storage.yml` | `xtax-blob-storage` | -| `xtax-v*` | `publish-xtax.yml` | `xtax` | +| Tag | Workflow | What it publishes | +|------------|------------------|---------------------------------------------| +| `v*` | `release.yml` | `xtax-encryption`, `xtax-blob-storage`, `xtax` | ### Step-by-step -1. Bump the version in the crate's `Cargo.toml` (and downstream dependency versions if needed). -2. Commit and push to `main`. Wait for CI to pass. -3. Create and push the tags **in this order** (each subsequent crate depends on the previous): - -```bash -# 1. Publish xtax-encryption (do this FIRST — xtax-blob-storage depends on it) -git tag xtax-encryption-v0.1.1 -git push origin xtax-encryption-v0.1.1 - -# 2. Publish xtax-blob-storage (xtax depends on it) -git tag xtax-blob-storage-v0.1.1 -git push origin xtax-blob-storage-v0.1.1 - -# 3. Publish xtax facade -git tag xtax-v0.1.1 -git push origin xtax-v0.1.1 -``` - -4. GitHub Actions will run `cargo publish --dry-run` first, then publish. +1. Bump the version in **all three** `Cargo.toml` files to the same version: + - `Cargo.toml` (root — facade + dependency versions) + - `crates/xtax-encryption/Cargo.toml` + - `crates/xtax-blob-storage/Cargo.toml` +2. Update any intra-workspace dependency versions (e.g. `xtax-blob-storage` → `xtax-encryption`). +3. Commit and push to `main`. Wait for CI to pass. +4. Create and push a single tag: + ```bash + git tag v0.1.1 + git push origin v0.1.1 + ``` +5. `release.yml` will: + - Extract the version from the tag + - Validate that all three crates have the same version + - Publish each crate (skipping any that already exist on crates.io) + - Create a GitHub Release with auto-generated notes > **Note:** Publishing requires a `release` environment with a `CARGO_REGISTRY_TOKEN` -> secret configured in the GitHub repository settings. Both publish workflows use -> `environment: release`, so the token is only exposed to those jobs. \ No newline at end of file +> secret configured in the GitHub repository settings. diff --git a/Cargo.lock b/Cargo.lock index 79675d0..dae1e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2928,7 +2928,7 @@ dependencies = [ [[package]] name = "xtax-encryption" -version = "0.1.0" +version = "0.1.1" dependencies = [ "async-trait", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 3e5d1f0..eda5bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ full = ["blob-storage-full", "encryption"] [dependencies] xtax-blob-storage = { version = "0.1.1", path = "crates/xtax-blob-storage", optional = true, default-features = false } -xtax-encryption = { version = "0.1.0", path = "crates/xtax-encryption", optional = true } +xtax-encryption = { version = "0.1.1", path = "crates/xtax-encryption", optional = true } [workspace] members = [ diff --git a/crates/xtax-blob-storage/Cargo.toml b/crates/xtax-blob-storage/Cargo.toml index 13a5dea..9ebc140 100644 --- a/crates/xtax-blob-storage/Cargo.toml +++ b/crates/xtax-blob-storage/Cargo.toml @@ -23,7 +23,7 @@ fs = ["tokio/fs"] s3 = ["dep:aws-sdk-s3", "dep:aws-smithy-types", "dep:aws-config"] [dependencies] -xtax-encryption = { version = "0.1.0", path = "../xtax-encryption" } +xtax-encryption = { version = "0.1.1", path = "../xtax-encryption" } async-trait = "0.1" tracing = { version = "0.1", features = ["attributes"] } tokio = { version = "1.52", features = ["io-util", "rt", "time", "sync", "macros"] } diff --git a/crates/xtax-encryption/Cargo.toml b/crates/xtax-encryption/Cargo.toml index 02c93d3..50ef4a3 100644 --- a/crates/xtax-encryption/Cargo.toml +++ b/crates/xtax-encryption/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xtax-encryption" -version = "0.1.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true