From af80012661e989f8e35802a4dee5d4b1b485dfc0 Mon Sep 17 00:00:00 2001 From: Mike Garde Date: Mon, 29 Jun 2026 14:39:56 -0400 Subject: [PATCH] Align process to commitbot - Implements comprehensive CI/CD workflows for controlled releases on GitHub Actions. - Refactors internal deployment scripts, centralizing version validation and asset handling logic. - Standardizes cross-platform building capabilities in `Taskfile.yaml`. - Deprecates old or redundant release preparation scripts across the `devops` directory. - Updates metadata and dependency files (`package.*`, `Cargo.*`) to reflect new versioning strategy. --- .github/workflows/release-assets.yml | 31 +-- .github/workflows/release.yml | 221 ++++++++++++++++ .gitignore | 1 + Cargo.lock | 281 +++----------------- Cargo.toml | 2 +- Taskfile.yaml | 108 ++++---- devops/ga-release-plan.sh | 68 +++++ devops/publish-if-complete.sh | 55 ++++ devops/release.sh | 287 +++++++++++++++++++++ devops/release/1-bump-version.sh | 55 ---- devops/release/2-tag-version.sh | 39 --- devops/release/3-generate-release-notes.sh | 94 ------- devops/release/finalize-release.sh | 84 ------ devops/release/release-config.sh | 118 --------- devops/release/release.sh | 128 --------- devops/render-release-notes.sh | 46 ++++ devops/templates/release-notes.md | 43 --- package-lock.json | 12 +- package.json | 2 +- 19 files changed, 786 insertions(+), 889 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 devops/ga-release-plan.sh create mode 100755 devops/publish-if-complete.sh create mode 100755 devops/release.sh delete mode 100755 devops/release/1-bump-version.sh delete mode 100755 devops/release/2-tag-version.sh delete mode 100755 devops/release/3-generate-release-notes.sh delete mode 100755 devops/release/finalize-release.sh delete mode 100755 devops/release/release-config.sh delete mode 100755 devops/release/release.sh create mode 100755 devops/render-release-notes.sh delete mode 100644 devops/templates/release-notes.md diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index 24398be..d9041ca 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -22,10 +22,6 @@ permissions: contents: write packages: write -concurrency: - group: release-assets-${{ inputs.tag }} - cancel-in-progress: false - env: CARGO_TERM_COLOR: always @@ -51,14 +47,14 @@ jobs: binary_name: dotenv install_cmd: "" linker: "" - - name: Linux static (musl) arm64 + - name: Amazon Linux / CentOS / RHEL arm64 runner: ubuntu-24.04-arm target: aarch64-unknown-linux-musl asset_target: unknown-linux-musl-aarch64 binary_name: dotenv install_cmd: sudo apt-get update && sudo apt-get install -y musl-tools linker: musl-gcc - - name: Linux static (musl) x86_64 + - name: Amazon Linux / CentOS / RHEL x86_64 runner: ubuntu-24.04 target: x86_64-unknown-linux-musl asset_target: unknown-linux-musl-x86_64 @@ -88,7 +84,7 @@ jobs: fi - name: Check out tagged source - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ steps.meta.outputs.tag }} @@ -144,46 +140,45 @@ jobs: needs: build-release-assets runs-on: ubuntu-24.04 steps: - - name: Check out release tag - uses: actions/checkout@v4 + - name: Check out source + uses: actions/checkout@v7 with: - fetch-depth: 0 ref: ${{ inputs.tag }} - name: Publish release and trigger Homebrew tap update - id: finalize + id: publish env: GH_TOKEN: ${{ github.token }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - RELEASE_VERSION: ${{ inputs.tag }} - run: devops/release/finalize-release.sh + shell: bash + run: bash devops/publish-if-complete.sh "${{ inputs.tag }}" - name: Set up Node for npm - if: steps.finalize.outputs.published == 'true' + if: steps.publish.outputs.published == 'true' uses: actions/setup-node@v4 with: node-version: 24 registry-url: https://registry.npmjs.org - name: Install package dependencies - if: steps.finalize.outputs.published == 'true' + if: steps.publish.outputs.published == 'true' run: npm ci --ignore-scripts - name: Publish to npm - if: steps.finalize.outputs.published == 'true' + if: steps.publish.outputs.published == 'true' run: npm run publish:npm env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Set up Node for GitHub Packages - if: steps.finalize.outputs.published == 'true' + if: steps.publish.outputs.published == 'true' uses: actions/setup-node@v4 with: node-version: 24 registry-url: https://npm.pkg.github.com - name: Publish to GitHub Packages - if: steps.finalize.outputs.published == 'true' + if: steps.publish.outputs.published == 'true' run: npm run publish:github env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9718ba3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,221 @@ +name: Release + +on: + workflow_dispatch: + inputs: + target_branch: + description: Release branch + required: true + type: choice + options: + - develop + - main + version: + description: Release version (X.Y.Z) + required: true + type: string + +permissions: + actions: write + contents: write + +concurrency: + group: release-${{ inputs.target_branch }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + +jobs: + plan: + runs-on: ubuntu-latest + outputs: + previous_version: ${{ steps.plan.outputs.previous_version }} + version: ${{ steps.plan.outputs.version }} + target_branch: ${{ steps.plan.outputs.target_branch }} + steps: + - name: Checkout selected branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.target_branch }} + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --force --tags + + - name: Plan release + id: plan + env: + TARGET_BRANCH: ${{ inputs.target_branch }} + RELEASE_VERSION: ${{ inputs.version }} + run: devops/ga-release-plan.sh + + prepare_release: + needs: plan + runs-on: ubuntu-latest + outputs: + release_sha: ${{ steps.release_commit.outputs.release_sha }} + steps: + - name: Checkout target branch + uses: actions/checkout@v4 + with: + ref: ${{ needs.plan.outputs.target_branch }} + fetch-depth: 0 + + - name: Fetch tags + run: git fetch --force --tags + + - name: Verify tag does not already exist + shell: bash + run: | + if git rev-parse "refs/tags/${{ needs.plan.outputs.version }}" >/dev/null 2>&1; then + echo "Tag ${{ needs.plan.outputs.version }} already exists." + exit 1 + fi + + - name: Configure git author + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Create release commit + id: release_commit + shell: bash + run: | + set -euo pipefail + + version="${{ needs.plan.outputs.version }}" + + perl -pi -e 's/^version = "[0-9]+\.[0-9]+\.[0-9]+"/version = "'"$version"'"/' Cargo.toml + perl -0pi -e 's/name = "dotenv-cli"\nversion = "[0-9]+\.[0-9]+\.[0-9]+"/name = "dotenv-cli"\nversion = "'"$version"'"/m' Cargo.lock + npm version "$version" --no-git-tag-version --allow-same-version >/dev/null + + if git diff --quiet -- Cargo.toml Cargo.lock package.json package-lock.json; then + echo "Release version ${version} is already present in the package metadata." >&2 + exit 1 + fi + + git add Cargo.toml Cargo.lock package.json package-lock.json + git commit -m "Release ${version}" + git tag "${version}" + + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Push release commit and tag + shell: bash + run: | + set -euo pipefail + git push origin "HEAD:${{ needs.plan.outputs.target_branch }}" + git push origin "refs/tags/${{ needs.plan.outputs.version }}" + + create_release: + needs: [plan, prepare_release] + runs-on: ubuntu-latest + steps: + - name: Check out release commit + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare_release.outputs.release_sha }} + + - name: Generate release notes + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + NOTES_ARGS=() + if [[ -n "${{ needs.plan.outputs.previous_version }}" ]]; then + NOTES_ARGS+=(-f previous_tag_name="${{ needs.plan.outputs.previous_version }}") + fi + + gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="${{ needs.plan.outputs.version }}" \ + -f target_commitish="${{ needs.prepare_release.outputs.release_sha }}" \ + "${NOTES_ARGS[@]}" \ + --jq .body > generated-release-notes.md + + bash devops/render-release-notes.sh \ + "${{ needs.plan.outputs.version }}" \ + "${{ github.repository }}" \ + generated-release-notes.md > release-notes.md + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + PRERELEASE_ARGS=() + if [[ "${{ needs.plan.outputs.target_branch }}" == "develop" ]]; then + PRERELEASE_ARGS+=(--prerelease) + fi + + gh release create "${{ needs.plan.outputs.version }}" \ + --draft \ + --verify-tag \ + "${PRERELEASE_ARGS[@]}" \ + --target "${{ needs.prepare_release.outputs.release_sha }}" \ + --title "${{ needs.plan.outputs.version }}" \ + --notes-file release-notes.md + + - name: Dispatch release-assets workflow + id: dispatch_assets + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + dispatch_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + source_run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + gh workflow run release-assets.yml \ + --ref "${{ needs.plan.outputs.target_branch }}" \ + -f tag="${{ needs.plan.outputs.version }}" \ + -f source_run_id="${{ github.run_id }}" \ + -f source_run_url="${source_run_url}" + + assets_run_url="" + for _ in {1..12}; do + sleep 5 + + assets_run_url="$( + gh api "/repos/${{ github.repository }}/actions/workflows/release-assets.yml/runs?event=workflow_dispatch&branch=${{ needs.plan.outputs.target_branch }}&per_page=20" \ + --jq '.workflow_runs + | map(select(.head_sha == "${{ needs.prepare_release.outputs.release_sha }}" and .status != null)) + | sort_by(.created_at) + | reverse + | .[0].html_url // ""' + )" + + if [[ -n "${assets_run_url}" ]]; then + break + fi + done + + echo "dispatch_started_at=${dispatch_started_at}" >> "$GITHUB_OUTPUT" + echo "assets_run_url=${assets_run_url}" >> "$GITHUB_OUTPUT" + + - name: Add release summary + shell: bash + run: | + release_url="https://github.com/${{ github.repository }}/releases/tag/${{ needs.plan.outputs.version }}" + assets_url="${{ steps.dispatch_assets.outputs.assets_run_url }}" + + { + echo "## Release" + echo + echo "- Version: \`${{ needs.plan.outputs.version }}\`" + echo "- Release: [${{ needs.plan.outputs.version }}](${release_url})" + echo "- macOS assets are built and uploaded locally via \`task release:${{ needs.plan.outputs.version }}\` (join mode)." + if [[ -n "${assets_url}" ]]; then + echo "- Release assets workflow: [Open run](${assets_url})" + else + echo "- Release assets workflow: dispatched, but the run URL was not discovered yet." + echo "- Release assets workflow page: [Open workflow](https://github.com/${{ github.repository }}/actions/workflows/release-assets.yml)" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 9eb3da8..2dadf67 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules dotenv-*.tgz target +dist diff --git a/Cargo.lock b/Cargo.lock index a71dcbb..6bee07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,17 +61,11 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "assert_cmd" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -84,34 +78,34 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] @@ -183,9 +177,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -209,7 +203,7 @@ dependencies = [ [[package]] name = "dotenv-cli" -version = "1.0.0" +version = "0.6.1" dependencies = [ "assert_cmd", "clap", @@ -252,39 +246,22 @@ dependencies = [ "num-traits", ] -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", - "wasip3", ] [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -300,19 +277,13 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" version = "2.14.0" @@ -320,9 +291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -348,12 +317,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -366,17 +329,11 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "normalize-line-endings" @@ -435,16 +392,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -456,9 +403,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -471,9 +418,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -494,9 +441,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustix" @@ -511,12 +458,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -548,9 +489,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", @@ -579,9 +520,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -609,9 +550,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -619,12 +560,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -640,58 +575,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -707,100 +590,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 0b9b7b0..076a6ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dotenv-cli" -version = "1.0.0" +version = "0.6.1" edition = "2021" description = "Read and update dotenv files from the cli" license = "GPL-3.0-or-later" diff --git a/Taskfile.yaml b/Taskfile.yaml index 1846c62..07dd8b0 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -7,7 +7,7 @@ tasks: - task --list --sort alphanumeric setup: - desc: "Setup development environment" + desc: "Add the macOS Rust targets used for local release builds" silent: true status: - rustup target list --installed | grep -q x86_64-apple-darwin @@ -23,39 +23,70 @@ tasks: - cargo test --all-features build: - desc: Build the project in release mode + desc: "Build debug binary for the host" + silent: true cmds: - - task: build:release + - cargo build + - du -h target/debug/dotenv - build:*: - desc: Build the Rust binary with the requested Cargo profile + build:release: + desc: "Build optimized release binary for the host" + silent: true + cmds: + - cargo build --release + - du -h target/release/dotenv + + build:release:*: + desc: "Build macOS release binaries for all targets and package into dist/" vars: - TARGET: '{{index .MATCH 0}}' + VERSION: '{{index .MATCH 0}}' + silent: true cmds: - - cargo build --profile {{.TARGET}} + - mkdir -p dist + - rm -f dist/dotenv-cli-*.tar.gz + - | + set -euo pipefail + + echo "Building macOS release binaries..." + cargo build --release --target aarch64-apple-darwin + cargo build --release --target x86_64-apple-darwin + + du -h target/aarch64-apple-darwin/release/dotenv + du -h target/x86_64-apple-darwin/release/dotenv + + echo "Packaging into dist with version {{.VERSION}}..." + + tar -C target/aarch64-apple-darwin/release \ + -czf dist/dotenv-cli-{{.VERSION}}-apple-darwin-aarch64.tar.gz \ + dotenv + + tar -C target/x86_64-apple-darwin/release \ + -czf dist/dotenv-cli-{{.VERSION}}-apple-darwin-x86_64.tar.gz \ + dotenv + + echo "Artifacts created:" + ls -lh dist install: desc: "Install binary locally via cargo install --path ." cmds: - task: uninstall - cargo install --path . + install:brew: - desc: Install via Homebrew tap + desc: "Install via Homebrew tap" cmds: - task: uninstall - brew install mikegarde/tap/dotenv-cli + install:npm: - desc: Local development setup + desc: "Local development install via npm" cmds: - task: uninstall - npm install - task: build:release - npm install -g - install:npm:repo: - desc: Local development setup - cmds: - - task: uninstall - - npm install -g @mikegarde/dotenv-cli@0.6.1 + uninstall: desc: "Uninstall all local installations of dotenv-cli (cargo, npm, brew)" internal: true @@ -64,57 +95,22 @@ tasks: - cargo uninstall --path . || true - brew uninstall mikegarde/tap/dotenv-cli || true - npm uninstall -g @mikegarde/dotenv-cli || true - - npm uninstall -g || true - - is-branch-safe: - desc: Check if the current branch is main - vars: - BRANCH: - sh: git rev-parse --abbrev-ref HEAD - cmds: - - | - if [ "{{.BRANCH}}" != "main" ] && [ "{{.BRANCH}}" != "develop" ]; then - echo "Not on main or develop branch" - exit 1 - fi release:*: - desc: "Bump version, build & upload macOS assets, then dispatch GitHub Actions for Linux/Windows & Homebrew tap update. Usage: task release:[patch|minor|major|X.Y.Z]" - silent: true - preconditions: - - sh: task is-branch-safe - msg: "Not on main or develop branch" + desc: "Create a release: bump version, build & upload macOS assets, then GitHub Actions builds Linux/Windows, publishes, and updates the Homebrew tap. Usage: task release:[patch|minor|major|X.Y.Z]" + interactive: true vars: - STEP: '{{index .MATCH 0}}' - cmds: - - devops/release/1-bump-version.sh {{.STEP}} - - task test - - devops/release/2-tag-version.sh - - devops/release/release.sh --macos - - release:finalize: - desc: Publish the draft release if all expected assets have been uploaded - cmds: - - devops/release/finalize-release.sh - - publish: - desc: Publish a release to npmjs and GitHub Packages - preconditions: - - task is-branch-safe + VERSION: '{{index .MATCH 0}}' cmds: - - task: publish:npm - - task: publish:github + - task: test + - bash devops/release.sh {{.VERSION}} publish:npm: - desc: Publish a release to npmjs - preconditions: - - task is-branch-safe + desc: "Manually publish the current version to npmjs (normally handled by GitHub Actions)" cmds: - npm run publish:npm publish:github: - desc: Publish a release to GitHub Packages - preconditions: - - task is-branch-safe + desc: "Manually publish the current version to GitHub Packages (normally handled by GitHub Actions)" cmds: - npm run publish:github diff --git a/devops/ga-release-plan.sh b/devops/ga-release-plan.sh new file mode 100755 index 0000000..f849235 --- /dev/null +++ b/devops/ga-release-plan.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET_BRANCH="${TARGET_BRANCH:-${1:-}}" +RELEASE_VERSION="${RELEASE_VERSION:-${2:-}}" + +if [[ -z "${TARGET_BRANCH}" ]]; then + echo "usage: TARGET_BRANCH= RELEASE_VERSION=X.Y.Z $0" >&2 + exit 1 +fi + +case "${TARGET_BRANCH}" in + main|develop) ;; + *) + echo "Unsupported target branch: ${TARGET_BRANCH}" >&2 + exit 1 + ;; +esac + +semver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$' + +if [[ ! "${RELEASE_VERSION}" =~ ${semver_pattern} ]]; then + echo "RELEASE_VERSION must be a semantic version like 1.0.0" >&2 + exit 1 +fi + +# Newest released version (empty when no semver tags exist yet). +PREVIOUS_VERSION="$( + git tag --list \ + | grep -E "${semver_pattern}" || true +)" +PREVIOUS_VERSION="$( + printf '%s\n' "${PREVIOUS_VERSION}" \ + | sort -V \ + | tail -n 1 +)" + +# Baseline for the next version: the newest of the latest tag and the version +# currently in Cargo.toml, so the requested version can never move backwards. +CARGO_VERSION="$( + sed -nE 's/^version = "([0-9]+\.[0-9]+\.[0-9]+)"/\1/p' Cargo.toml | head -n1 +)" + +CURRENT_VERSION="$( + printf '%s\n%s\n' "${PREVIOUS_VERSION}" "${CARGO_VERSION}" \ + | sort -V \ + | tail -n 1 +)" + +newest="$(printf '%s\n%s\n' "${CURRENT_VERSION}" "${RELEASE_VERSION}" | sort -V | tail -n 1)" +if [[ "${newest}" != "${RELEASE_VERSION}" ]] || [[ "${RELEASE_VERSION}" == "${CURRENT_VERSION}" ]]; then + echo "RELEASE_VERSION ${RELEASE_VERSION} must be newer than ${CURRENT_VERSION}" >&2 + exit 1 +fi + +VERSION="${RELEASE_VERSION}" + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "previous_version=${PREVIOUS_VERSION}" + echo "version=${VERSION}" + echo "target_branch=${TARGET_BRANCH}" + } >> "${GITHUB_OUTPUT}" +fi + +echo "previous_version=${PREVIOUS_VERSION}" +echo "version=${VERSION}" diff --git a/devops/publish-if-complete.sh b/devops/publish-if-complete.sh new file mode 100755 index 0000000..d7b423d --- /dev/null +++ b/devops/publish-if-complete.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TAG="${1:?usage: publish-if-complete.sh }" + +EXPECTED=( + "dotenv-cli-${TAG}-apple-darwin-aarch64.tar.gz" + "dotenv-cli-${TAG}-apple-darwin-x86_64.tar.gz" + "dotenv-cli-${TAG}-unknown-linux-gnu-aarch64.tar.gz" + "dotenv-cli-${TAG}-unknown-linux-gnu-x86_64.tar.gz" + "dotenv-cli-${TAG}-unknown-linux-musl-aarch64.tar.gz" + "dotenv-cli-${TAG}-unknown-linux-musl-x86_64.tar.gz" + "dotenv-cli-${TAG}-pc-windows-gnu-x86_64.tar.gz" +) + +ACTUAL="$(gh release view "${TAG}" --json assets --jq '[.assets[].name]')" + +for asset in "${EXPECTED[@]}"; do + if ! printf '%s' "${ACTUAL}" | jq -e --arg n "${asset}" 'any(.[]; . == $n)' > /dev/null 2>&1; then + echo "Release not yet complete (missing: ${asset}). Skipping publish." + exit 0 + fi +done + +IS_PRERELEASE="$(gh release view "${TAG}" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo false)" + +echo "All expected assets present. Publishing release ${TAG}..." +gh release edit "${TAG}" --draft=false +echo "Published." + +# Prereleases (develop line) publish the GitHub release but skip the npm and +# Homebrew steps that target stable consumers. +if [[ "${IS_PRERELEASE}" == "true" ]]; then + echo "Release ${TAG} is a prerelease; skipping npm publish and Homebrew tap update." + exit 0 +fi + +# Signal callers (the release-assets workflow) that this run published the +# release so the npm / GitHub Packages publish steps run exactly once. +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "published=true" >> "${GITHUB_OUTPUT}" +fi + +echo "Triggering Homebrew tap formula update..." +if GH_TOKEN="${HOMEBREW_TAP_TOKEN:-${GH_TOKEN:-}}" gh workflow run update-formula.yml \ + --repo MikeGarde/homebrew-tap \ + -f formula=dotenv-cli \ + -f repo=MikeGarde/dotenv-cli \ + -f tag="${TAG}"; then + echo "Homebrew tap update dispatched." +else + echo "Warning: could not dispatch Homebrew tap update. Trigger it manually with:" + echo " gh workflow run update-formula.yml --repo MikeGarde/homebrew-tap -f formula=dotenv-cli -f repo=MikeGarde/dotenv-cli -f tag=${TAG}" +fi diff --git a/devops/release.sh b/devops/release.sh new file mode 100755 index 0000000..c6f8ad4 --- /dev/null +++ b/devops/release.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +set -euo pipefail + +# +# Config +# +DIST_DIR="dist" + +STEP=${1:?usage: release.sh [major|minor|patch|X.Y.Z]} + +# +# Pre-flight checks +# +if ! command -v task >/dev/null 2>&1; then + echo "Error: 'task' command not found. Install Taskfile runner first." + exit 1 +fi + +if ! command -v gh >/dev/null 2>&1; then + echo "Error: 'gh' CLI not found. Install GitHub CLI (gh) first." + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "Error: 'jq' not found. Install jq first." + exit 1 +fi + +REPO_SLUG=$(gh repo view --json nameWithOwner -q .nameWithOwner) + +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "develop" ]; then + echo "Error: releases may only be created from 'main' or 'develop' (current: $BRANCH)." + exit 1 +fi + +PRERELEASE_ARGS=() +if [ "$BRANCH" = "develop" ]; then + PRERELEASE_ARGS+=(--prerelease) +fi + +# Ensure working tree is clean +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Error: working tree is not clean. Commit or stash changes before releasing." + exit 1 +fi + +# +# Read current version from Cargo.toml +# +CURRENT_VERSION=$( + grep -E '^version = "[0-9]+\.[0-9]+\.[0-9]+"' Cargo.toml \ + | head -n1 \ + | sed -E 's/version = "([^"]+)"/\1/' +) + +if [ -z "$CURRENT_VERSION" ]; then + echo "Error: could not determine current version from Cargo.toml" + exit 1 +fi + +MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) +MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) +PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) +LATEST=true + +# +# Compute new version +# +case "$STEP" in + major) + MAJOR=$((MAJOR+1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR+1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH+1)) + ;; + *) + if [[ "$STEP" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + MAJOR=$(echo "$STEP" | cut -d. -f1) + MINOR=$(echo "$STEP" | cut -d. -f2) + PATCH=$(echo "$STEP" | cut -d. -f3) + LATEST=false + else + echo "Invalid step: $STEP" + echo "Usage: release.sh [major|minor|patch|X.Y.Z]" + exit 1 + fi + ;; +esac + +NEW_VERSION="$MAJOR.$MINOR.$PATCH" + +# +# Join mode: tag already exists (e.g. GitHub Actions created the release first) +# +if git rev-parse -q --verify "refs/tags/$NEW_VERSION" >/dev/null 2>&1; then + echo "Tag $NEW_VERSION already exists. Joining existing draft release to upload macOS assets..." + echo + + RELEASE_IS_DRAFT=$(gh release view "$NEW_VERSION" --json isDraft --jq .isDraft 2>/dev/null || echo "not_found") + if [ "$RELEASE_IS_DRAFT" = "false" ]; then + echo "Release $NEW_VERSION is already published. Nothing to do." + exit 0 + fi + if [ "$RELEASE_IS_DRAFT" = "not_found" ]; then + echo "Error: tag $NEW_VERSION exists in git but no GitHub release was found." + exit 1 + fi + + CARGO_VERSION=$( + grep -E '^version = "[0-9]+\.[0-9]+\.[0-9]+"' Cargo.toml \ + | head -n1 \ + | sed -E 's/version = "([^"]+)"/\1/' + ) + if [ "$CARGO_VERSION" != "$NEW_VERSION" ]; then + echo "Error: Cargo.toml version ($CARGO_VERSION) does not match release version ($NEW_VERSION)." + echo "Run 'git pull' to sync the release commit, then retry." + exit 1 + fi + + echo "Building macOS binaries for $NEW_VERSION..." + task setup + task "build:release:${NEW_VERSION}" + + MAC_ASSETS=("$DIST_DIR"/dotenv-cli-"$NEW_VERSION"-*apple-darwin*.tar.gz) + if [ ${#MAC_ASSETS[@]} -eq 0 ]; then + echo "Error: no macOS assets found in $DIST_DIR after build." + exit 1 + fi + + echo "Uploading macOS assets to existing draft release $NEW_VERSION:" + for a in "${MAC_ASSETS[@]}"; do + echo " - $a" + done + gh release upload "$NEW_VERSION" "${MAC_ASSETS[@]}" --clobber + + bash devops/publish-if-complete.sh "$NEW_VERSION" + exit 0 +fi + +# +# Full release flow +# +echo "Current version: $CURRENT_VERSION" +echo "New version: $NEW_VERSION" +echo "Branch: $BRANCH" +echo + +read -r -p "Proceed with release? (y/N): " CONFIRM +if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 +fi + +# +# Bump versions (Cargo + npm) with rollback on failure +# +BAK_DIR="$(mktemp -d)" +cp Cargo.toml Cargo.lock package.json package-lock.json "$BAK_DIR/" + +cleanup_on_error() { + rc=$? + echo "Error during release (exit code $rc). Restoring version files..." + cp "$BAK_DIR"/Cargo.toml Cargo.toml + cp "$BAK_DIR"/Cargo.lock Cargo.lock + cp "$BAK_DIR"/package.json package.json + cp "$BAK_DIR"/package-lock.json package-lock.json + rm -rf "$BAK_DIR" + exit $rc +} +trap cleanup_on_error INT TERM ERR + +echo "Updating Cargo.toml and package.json to version $NEW_VERSION..." +perl -pi -e 's/^version = "[0-9]+\.[0-9]+\.[0-9]+"/version = "'"$NEW_VERSION"'"/' Cargo.toml +npm version "$NEW_VERSION" --no-git-tag-version --allow-same-version >/dev/null + +# +# Run tasks: setup, build (which refreshes Cargo.lock as well) +# +echo "Running task setup..." +task setup + +echo "Running task build:release:${NEW_VERSION}..." +task "build:release:${NEW_VERSION}" + +# If we got here, tasks succeeded - stop rollback trap +trap - INT TERM ERR +rm -rf "$BAK_DIR" + +# +# Commit, tag, push +# +if git diff --quiet -- Cargo.toml Cargo.lock package.json package-lock.json; then + echo "Warning: version files did not change; nothing to commit." +else + git commit Cargo.toml Cargo.lock package.json package-lock.json -m "Release $NEW_VERSION" +fi + +HASH=$(git rev-parse HEAD) + +echo "Tagging $NEW_VERSION..." +git tag "$NEW_VERSION" "$HASH" + +echo "Pushing $BRANCH and tag..." +git push origin "$BRANCH" +git push origin "refs/tags/$NEW_VERSION" + +# +# Create draft GitHub release with macOS assets +# +if [ ! -d "$DIST_DIR" ]; then + echo "Error: build artifacts directory '$DIST_DIR' not found." + exit 1 +fi + +MAC_ASSETS=("$DIST_DIR"/dotenv-cli-"$NEW_VERSION"-*apple-darwin*.tar.gz) +if [ ${#MAC_ASSETS[@]} -eq 0 ]; then + echo "Error: no macOS assets found in '$DIST_DIR'." + exit 1 +fi + +echo "Creating draft GitHub release $NEW_VERSION with macOS assets:" +for a in "${MAC_ASSETS[@]}"; do + echo " - $a" +done + +NOTES_FILE="$(mktemp)" +COMBINED_NOTES_FILE="$(mktemp)" + +cleanup_notes() { + rm -f "$NOTES_FILE" "$COMBINED_NOTES_FILE" +} +trap cleanup_notes EXIT + +gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/$REPO_SLUG/releases/generate-notes" \ + -f tag_name="$NEW_VERSION" \ + -f target_commitish="$HASH" \ + --jq .body > "$NOTES_FILE" + +bash devops/render-release-notes.sh "$NEW_VERSION" "$REPO_SLUG" "$NOTES_FILE" > "$COMBINED_NOTES_FILE" + +LATEST_ARGS=() +if [ "$LATEST" = true ] && [ "$BRANCH" = "main" ]; then + LATEST_ARGS+=(--latest) +fi + +gh release create "$NEW_VERSION" \ + --draft \ + --fail-on-no-commits \ + "${LATEST_ARGS[@]}" \ + "${PRERELEASE_ARGS[@]}" \ + --notes-file "$COMBINED_NOTES_FILE" \ + --target "$HASH" \ + "${MAC_ASSETS[@]}" + +# +# Dispatch release-assets workflow for Linux + Windows +# +echo +echo "Dispatching release-assets workflow for Linux and Windows builds..." + +SOURCE_RUN_URL="local:$(hostname)" + +gh workflow run release-assets.yml \ + --ref "$BRANCH" \ + -f tag="$NEW_VERSION" \ + -f source_run_url="${SOURCE_RUN_URL}" + +echo "Dispatched. Linux and Windows assets will be uploaded by GitHub Actions." +echo "The release will be published automatically once all assets are present." +echo + +# +# Check if all assets happen to already be present (rare but possible) +# +bash devops/publish-if-complete.sh "$NEW_VERSION" + +echo "Release $NEW_VERSION draft created successfully." +exit 0 diff --git a/devops/release/1-bump-version.sh b/devops/release/1-bump-version.sh deleted file mode 100755 index a5b1787..0000000 --- a/devops/release/1-bump-version.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -cd "$REPO_ROOT" -source "$SCRIPT_DIR/release-config.sh" - -STEP="${1:-}" - -case "$STEP" in - patch|minor|major|[0-9]*.[0-9]*.[0-9]*) ;; - *) - echo 'Usage: devops/release/1-bump-version.sh ' >&2 - exit 1 - ;; -esac - -CURRENT_VERSION="$(package_version)" - -if printf '%s\n' "$STEP" | grep -Eq '^[v]?[0-9]+\.[0-9]+\.[0-9]+'; then - NEXT_VERSION="${STEP#v}" -else - major="$(printf '%s\n' "$CURRENT_VERSION" | cut -d. -f1)" - minor="$(printf '%s\n' "$CURRENT_VERSION" | cut -d. -f2)" - patch="$(printf '%s\n' "$CURRENT_VERSION" | cut -d. -f3)" - - case "$STEP" in - major) NEXT_VERSION="$((major + 1)).0.0" ;; - minor) NEXT_VERSION="$major.$((minor + 1)).0" ;; - patch) NEXT_VERSION="$major.$minor.$((patch + 1))" ;; - esac -fi - -awk -v version="$NEXT_VERSION" ' - !done && /"version": "/ { - sub(/"version": "[^"]*"/, "\"version\": \"" version "\"") - done = 1 - } - { print } -' package.json > package.json.tmp -mv package.json.tmp package.json - -awk -v version="$NEXT_VERSION" ' - !done && /^version = "/ { - print "version = \"" version "\"" - done = 1 - next - } - { print } -' Cargo.toml > Cargo.toml.tmp -mv Cargo.toml.tmp Cargo.toml - -echo "$NEXT_VERSION" diff --git a/devops/release/2-tag-version.sh b/devops/release/2-tag-version.sh deleted file mode 100755 index f71ee66..0000000 --- a/devops/release/2-tag-version.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -cd "$REPO_ROOT" -source "$SCRIPT_DIR/release-config.sh" - -COMMIT=true - -case "${1:-}" in - '') - ;; - --no-commit) - COMMIT=false - ;; - -h|--help) - echo 'Usage: devops/release/2-tag-version.sh [--no-commit]' - exit 0 - ;; - *) - echo 'Usage: devops/release/2-tag-version.sh [--no-commit]' >&2 - exit 1 - ;; -esac - -TAG_NAME="$(package_version)" - -if [ "$COMMIT" = true ]; then - git add package.json package-lock.json Cargo.toml Cargo.lock - - if ! git diff --cached --quiet; then - git commit -m "chore: release $TAG_NAME" - fi -fi - -git tag "$TAG_NAME" -git push --follow-tags diff --git a/devops/release/3-generate-release-notes.sh b/devops/release/3-generate-release-notes.sh deleted file mode 100755 index bcbace0..0000000 --- a/devops/release/3-generate-release-notes.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -cd "$REPO_ROOT" -source "$SCRIPT_DIR/release-config.sh" - -command -v jq >/dev/null 2>&1 || { - echo 'jq is required to parse GitHub release-note JSON.' >&2 - exit 1 -} - -DIST_DIR="$REPO_ROOT/dist/release" -TEMPLATE_FILE="$REPO_ROOT/devops/templates/release-notes.md" -VERSION="${RELEASE_VERSION:-$(package_version)}" -TARGET_COMMITISH="${RELEASE_TARGET:-$(current_branch)}" -REPO="${RELEASE_REPO:-$(github_repo)}" -RESPONSE_FILE="$DIST_DIR/release-notes.response.json" -BODY_FILE="$DIST_DIR/release-notes-body.md" -LINK_MATRIX_FILE="$DIST_DIR/release-notes-link-matrix.md" - -if [ ! -f "$TEMPLATE_FILE" ]; then - echo "Release notes template not found: $TEMPLATE_FILE" >&2 - exit 1 -fi - -mkdir -p "$DIST_DIR" - -args=( - "repos/$REPO/releases/generate-notes" - --method POST - -f "tag_name=$VERSION" - -f "target_commitish=$TARGET_COMMITISH" -) - -if [ -n "${PREVIOUS_TAG:-}" ]; then - args+=(-f "previous_tag_name=$PREVIOUS_TAG") -fi - -gh api "${args[@]}" > "$RESPONSE_FILE" - -jq -r '.name' "$RESPONSE_FILE" > "$DIST_DIR/release-notes-title.txt" -jq -r '.body' "$RESPONSE_FILE" > "$BODY_FILE" - -{ - declare -A intel_links=() - declare -A arm_links=() - - for target in "${TARGET_ROWS[@]}"; do - IFS='|' read -r os cpu rust_target platform arch executable <<< "$target" - name="$(asset_name "$VERSION" "$platform" "$arch")" - url="https://github.com/$REPO/releases/download/$VERSION/$name" - link="[$arch]($url)" - - if [ "$cpu" = 'Intel' ]; then - intel_links["$os"]="$link" - else - arm_links["$os"]="$link" - fi - done - - for os_name in "${OS_ORDER[@]}"; do - printf '| %s | %s | %s |\n' \ - "$os_name" \ - "${intel_links[$os_name]:--}" \ - "${arm_links[$os_name]:--}" - done -} > "$LINK_MATRIX_FILE" - -awk \ - -v notes_file="$BODY_FILE" \ - -v link_matrix_file="$LINK_MATRIX_FILE" \ - ' - $0 == "{NOTES}" { - while ((getline line < notes_file) > 0) print line - close(notes_file) - next - } - - $0 == "{LINK_MATRIX}" { - while ((getline line < link_matrix_file) > 0) print line - close(link_matrix_file) - next - } - - { print } - ' "$TEMPLATE_FILE" > "$DIST_DIR/release-notes.md" - -perl -pi -e "s/\{VERSION\}/${VERSION}/g" "$DIST_DIR/release-notes.md" - -echo "Wrote $DIST_DIR/release-notes.md" -echo "Wrote $DIST_DIR/release-notes-title.txt" diff --git a/devops/release/finalize-release.sh b/devops/release/finalize-release.sh deleted file mode 100755 index d716983..0000000 --- a/devops/release/finalize-release.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# Publishes the GitHub draft release once all expected assets have been uploaded. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$REPO_ROOT" -source "$SCRIPT_DIR/release-config.sh" - -command -v gh >/dev/null 2>&1 || { echo 'gh CLI is required.' >&2; exit 1; } - -VERSION="${RELEASE_VERSION:-$(package_version)}" -TAG="$VERSION" -REPO="${RELEASE_REPO:-$(github_repo)}" -HOMEBREW_FORMULA="${HOMEBREW_FORMULA:-dotenv-cli}" -HOMEBREW_TAP_REPO="${HOMEBREW_TAP_REPO:-MikeGarde/homebrew-tap}" - -# Record whether this run actually flipped the draft to published so callers -# (e.g. the release-assets workflow) can run follow-on publish steps once. -signal_published() { - if [ -n "${GITHUB_OUTPUT:-}" ]; then - echo 'published=true' >> "$GITHUB_OUTPUT" - fi -} - -trigger_homebrew_update() { - # Stable releases only; prereleases must not bump the published formula. - local is_prerelease - is_prerelease="$(gh release view "$TAG" --repo "$REPO" --json isPrerelease --jq '.isPrerelease')" - if [ "$is_prerelease" = 'true' ]; then - echo "Release $TAG is a prerelease; skipping Homebrew tap update." - return - fi - - echo "Triggering Homebrew tap formula update..." - if GH_TOKEN="${HOMEBREW_TAP_TOKEN:-${GH_TOKEN:-}}" gh workflow run update-formula.yml \ - --repo "$HOMEBREW_TAP_REPO" \ - -f formula="$HOMEBREW_FORMULA" \ - -f repo="$REPO" \ - -f tag="$TAG"; then - echo "Homebrew tap update dispatched." - else - echo "Warning: could not dispatch Homebrew tap update. Trigger it manually with:" >&2 - echo " gh workflow run update-formula.yml --repo $HOMEBREW_TAP_REPO -f formula=$HOMEBREW_FORMULA -f repo=$REPO -f tag=$TAG" >&2 - fi -} - -if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then - echo "Release $TAG does not exist yet; nothing to finalize." - exit 0 -fi - -is_draft="$(gh release view "$TAG" --repo "$REPO" --json isDraft --jq '.isDraft')" -if [ "$is_draft" != 'true' ]; then - echo "Release $TAG is already published." - exit 0 -fi - -mapfile -t uploaded < <( - gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' 2>/dev/null || true -) - -missing=() -for target_row in "${TARGET_ROWS[@]}"; do - IFS='|' read -r _os _cpu _rust_target platform arch _exe <<< "$target_row" - expected="$(asset_name "$VERSION" "$platform" "$arch")" - if ! printf '%s\n' "${uploaded[@]}" | grep -qx "$expected"; then - missing+=("$expected") - fi -done - -if [ "${#missing[@]}" -gt 0 ]; then - echo "Release $TAG is still a draft; waiting on ${#missing[@]} asset(s):" - printf ' - %s\n' "${missing[@]}" - exit 0 -fi - -echo "All assets uploaded. Publishing release $TAG..." -gh release edit "$TAG" --repo "$REPO" --draft=false -echo "Release $TAG is now published." - -signal_published -trigger_homebrew_update diff --git a/devops/release/release-config.sh b/devops/release/release-config.sh deleted file mode 100755 index fe1099e..0000000 --- a/devops/release/release-config.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -PACKAGE_NAME='@mikegarde/dotenv-cli' -BINARY_NAME='dotenv' -OS_ORDER=('macOS' 'Linux glibc' 'Linux static' 'Windows') -TARGET_ROWS=( - 'macOS|Intel|x86_64-apple-darwin|apple-darwin|x86_64|dotenv' - 'macOS|Arm|aarch64-apple-darwin|apple-darwin|aarch64|dotenv' - 'Linux glibc|Intel|x86_64-unknown-linux-gnu|unknown-linux-gnu|x86_64|dotenv' - 'Linux glibc|Arm|aarch64-unknown-linux-gnu|unknown-linux-gnu|aarch64|dotenv' - 'Linux static|Intel|x86_64-unknown-linux-musl|unknown-linux-musl|x86_64|dotenv' - 'Linux static|Arm|aarch64-unknown-linux-musl|unknown-linux-musl|aarch64|dotenv' - 'Windows|Intel|x86_64-pc-windows-gnu|pc-windows-gnu|x86_64|dotenv.exe' -) - -require_bash_5() { - if [ -z "${BASH_VERSION:-}" ] || [ "${BASH_VERSINFO[0]}" -lt 5 ]; then - echo 'Release scripts require Bash 5.x or newer.' >&2 - echo 'On macOS, install modern Bash with Homebrew and run these scripts with that bash.' >&2 - exit 1 - fi -} - -release_targets() { - printf '%s\n' "${TARGET_ROWS[@]}" -} - -targets_matching() { - local requested rust_target target - - for target in "${TARGET_ROWS[@]}"; do - IFS='|' read -r _os _cpu rust_target _platform _arch _executable <<< "$target" - - for requested in "$@"; do - if [[ "$requested" == "$rust_target" ]]; then - printf '%s\n' "$target" - fi - done - done -} - -selected_targets() { - case "${1:-}" in - --all) - release_targets - return - ;; - --macos) - release_targets | awk -F'|' '$4 == "apple-darwin" { print }' - return - ;; - --linux) - release_targets | awk -F'|' '$4 ~ /linux/ { print }' - return - ;; - --windows) - release_targets | awk -F'|' '$4 ~ /windows/ { print }' - return - ;; - --no-macos) - release_targets | awk -F'|' '$4 != "apple-darwin" { print }' - return - ;; - esac - - if [ -n "${RELEASE_TARGETS:-}" ]; then - IFS=',' read -r -a requested_targets <<< "$RELEASE_TARGETS" - targets_matching "${requested_targets[@]}" - return - fi - - if [ "$#" -gt 0 ]; then - targets_matching "$@" - return - fi - - local platform arch - case "$(uname -s)" in - Darwin) platform='apple-darwin' ;; - Linux) platform='unknown-linux-gnu' ;; - MINGW*|MSYS*|CYGWIN*) platform='pc-windows-gnu' ;; - *) echo "Unsupported platform: $(uname -s)" >&2; exit 1 ;; - esac - - case "$(uname -m)" in - x86_64|amd64) arch='x86_64' ;; - arm64|aarch64) arch='aarch64' ;; - *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; - esac - - release_targets | awk -F'|' -v platform="$platform" -v arch="$arch" \ - '$4 == platform && $5 == arch { print }' -} - -asset_name() { - local version="$1" - local platform="$2" - local arch="$3" - - printf 'dotenv-cli-%s-%s-%s.tar.gz\n' "$version" "$platform" "$arch" -} - -package_version() { - sed -n 's/.*"version": "\([^"]*\)".*/\1/p' package.json | head -n 1 -} - -current_branch() { - git rev-parse --abbrev-ref HEAD -} - -github_repo() { - gh repo view --json nameWithOwner --jq '.nameWithOwner' -} - -require_bash_5 diff --git a/devops/release/release.sh b/devops/release/release.sh deleted file mode 100755 index 09a452e..0000000 --- a/devops/release/release.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# Idempotent release asset build, package, and upload. -# Usage: release.sh [--macos|--linux|--windows|--no-macos|--all|...] -# -# Environment variables: -# RELEASE_VERSION Override the version (defaults to package.json) -# RELEASE_TARGET Git commit/branch for the release target (defaults to current branch) -# RELEASE_REPO GitHub repo (defaults to current repo) -# PRERELEASE Set to 'true' to mark the release as a prerelease -# CARGO_BUILD_CMD Override the cargo build command (e.g. 'cargo zigbuild') -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$REPO_ROOT" -source "$SCRIPT_DIR/release-config.sh" - -command -v gh >/dev/null 2>&1 || { echo 'gh CLI is required.' >&2; exit 1; } - -DIST_DIR="$REPO_ROOT/dist/release" -VERSION="${RELEASE_VERSION:-$(package_version)}" -TAG="$VERSION" -TARGET_COMMITISH="${RELEASE_TARGET:-$(current_branch)}" -REPO="${RELEASE_REPO:-$(github_repo)}" -read -r -a BUILD_CMD <<< "${CARGO_BUILD_CMD:-cargo build}" - -mkdir -p "$DIST_DIR" - -# --- Step 1: Ensure draft release exists --- -if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then - echo "Creating draft release $TAG..." - - RELEASE_VERSION="$VERSION" \ - RELEASE_TARGET="$TARGET_COMMITISH" \ - RELEASE_REPO="$REPO" \ - "$SCRIPT_DIR/3-generate-release-notes.sh" - - create_args=( - release create "$TAG" - --draft - --title "$(cat "$DIST_DIR/release-notes-title.txt")" - --notes-file "$DIST_DIR/release-notes.md" - --target "$TARGET_COMMITISH" - --repo "$REPO" - ) - - if [ "$TARGET_COMMITISH" = 'develop' ] || [ "${PRERELEASE:-false}" = 'true' ]; then - create_args+=(--prerelease) - fi - - if ! gh "${create_args[@]}"; then - # Handle race: another runner may have created the release first - if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then - echo "Draft release created by a concurrent process, continuing..." - else - echo "Failed to create draft release $TAG." >&2 - exit 1 - fi - else - echo "Draft release $TAG created." - fi -else - echo "Release $TAG already exists, skipping creation." -fi - -# --- Step 2: Build, package, and upload each selected target --- - -get_uploaded() { - gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' 2>/dev/null || true -} - -any_failure=false - -while IFS='|' read -r _os _cpu rust_target platform arch executable; do - asset_base="$(asset_name "$VERSION" "$platform" "$arch")" - asset_path="$DIST_DIR/$asset_base" - - if get_uploaded | grep -qx "$asset_base"; then - echo "Skipping $asset_base (already uploaded)" - continue - fi - - echo "Building $rust_target..." - if ! "${BUILD_CMD[@]}" --release --target "$rust_target"; then - echo "WARNING: Build failed for $rust_target, skipping." >&2 - any_failure=true - continue - fi - - binary_dir="$REPO_ROOT/target/$rust_target/release" - tar -czf "$asset_path" -C "$binary_dir" "$executable" - echo "Packaged $asset_base" - - if ! gh release upload "$TAG" "$asset_path" --repo "$REPO"; then - echo "WARNING: Upload failed for $asset_base." >&2 - any_failure=true - continue - fi - - echo "Uploaded $asset_base" -done < <(selected_targets "$@") - -# --- Step 2b: Dispatch GitHub Actions for the non-macOS targets --- -# macOS binaries are built locally; Linux and Windows assets are produced by the -# release-assets workflow. Only dispatch from the macOS-only local release path. -if [ "${1:-}" = '--macos' ] || [ "${DISPATCH_RELEASE_ASSETS:-false}" = 'true' ]; then - echo "Dispatching release-assets workflow for Linux and Windows builds..." - if gh workflow run release-assets.yml \ - --repo "$REPO" \ - --ref "$TARGET_COMMITISH" \ - -f tag="$TAG" \ - -f source_run_url="local:$(hostname)"; then - echo "Dispatched. Linux and Windows assets will be uploaded by GitHub Actions," - echo "which will publish the release and update the Homebrew tap once complete." - else - echo "WARNING: could not dispatch release-assets workflow. Trigger it manually with:" >&2 - echo " gh workflow run release-assets.yml --ref $TARGET_COMMITISH -f tag=$TAG" >&2 - any_failure=true - fi -fi - -# --- Step 3: Publish the release if all expected assets are present --- -RELEASE_VERSION="$VERSION" RELEASE_REPO="$REPO" "$SCRIPT_DIR/finalize-release.sh" - -if [ "$any_failure" = true ]; then - echo "WARNING: Some builds or uploads failed. Re-run this script to retry missing assets." >&2 - exit 1 -fi diff --git a/devops/render-release-notes.sh b/devops/render-release-notes.sh new file mode 100755 index 0000000..726a0e8 --- /dev/null +++ b/devops/render-release-notes.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -euo pipefail + +VERSION="${1:?usage: render-release-notes.sh [notes-file]}" +REPO="${2:?usage: render-release-notes.sh [notes-file]}" +NOTES_FILE="${3:-}" + +asset_url() { + local filename="$1" + printf 'https://github.com/%s/releases/download/%s/%s' "${REPO}" "${VERSION}" "${filename}" +} + +cat <=18" } }, - "node_modules/tar/node_modules/yallist": { + "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", diff --git a/package.json b/package.json index 29edb53..7b66a6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mikegarde/dotenv-cli", - "version": "1.0.0", + "version": "0.6.1", "description": "Read and update dotenv files from the cli", "bin": { "dotenv": "./bin/dotenv"