diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml new file mode 100644 index 0000000..4454cd5 --- /dev/null +++ b/.github/workflows/cli.yml @@ -0,0 +1,54 @@ +name: CLI CI + +on: + push: + branches: [main, develop] + paths: + - 'cli/**' + - '.github/workflows/cli.yml' + pull_request: + branches: [main, develop] + paths: + - 'cli/**' + - '.github/workflows/cli.yml' + +env: + CARGO_TERM_COLOR: always + +concurrency: + group: cli-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cli: + name: CLI Validation + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "cli -> target" + + - name: Check formatting + working-directory: ./cli + run: cargo fmt --check + + - name: Run clippy + working-directory: ./cli + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Run tests + working-directory: ./cli + run: cargo test + + - name: Build CLI + working-directory: ./cli + run: cargo build diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 0000000..b3539d4 --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -0,0 +1,164 @@ +name: Publish CLI Crate + +on: + push: + branches: [main] + paths: + - 'cli/**' + - '.github/workflows/publish-cli.yml' + workflow_dispatch: + inputs: + publish: + description: 'Publish the current cougr-cli version to crates.io' + required: true + type: boolean + default: false + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: write + +concurrency: + group: publish-cli-${{ github.ref }} + cancel-in-progress: false + +jobs: + package: + name: Package CLI crate + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "cli -> target" + + - name: Check package + working-directory: ./cli + run: cargo package + + publish: + name: Publish CLI to crates.io + needs: package + if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && inputs.publish) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "cli -> target" + + - name: Read crate metadata + id: crate + working-directory: ./cli + run: | + metadata="$(cargo metadata --no-deps --format-version 1)" + echo "name=$(echo "$metadata" | jq -r '.packages[0].name')" >> "$GITHUB_OUTPUT" + echo "version=$(echo "$metadata" | jq -r '.packages[0].version')" >> "$GITHUB_OUTPUT" + + - name: Check crates.io version + id: crates + run: | + # crates.io rejects requests with no descriptive User-Agent (403) + response="$(curl -fsSL \ + -H "User-Agent: cougr-cli-publish-workflow (+https://github.com/salazarsebas/Cougr)" \ + "https://crates.io/api/v1/crates/${{ steps.crate.outputs.name }}")" || true + published=false + + # If curl succeeded and the version is in the response, it's published. + # If curl failed (e.g. 404 because the crate is totally new), jq will fail, leaving published=false. + if echo "$response" | jq -e --arg version "${{ steps.crate.outputs.version }}" '.versions[]? | select(.num == $version)' >/dev/null 2>&1; then + published=true + fi + + echo "published=$published" >> "$GITHUB_OUTPUT" + + - name: Publish crate + if: steps.crates.outputs.published != 'true' + working-directory: ./cli + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish + + - name: Skip already published version + if: steps.crates.outputs.published == 'true' + run: echo "${{ steps.crate.outputs.name }} ${{ steps.crate.outputs.version }} is already published on crates.io" + + release: + name: Create GitHub Release + needs: publish + if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && inputs.publish) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Read crate version + id: crate + working-directory: ./cli + run: | + version="$(grep -m1 '^version' Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=cli-v$version" >> "$GITHUB_OUTPUT" + + - name: Check if release already exists + id: check + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "${{ steps.crate.outputs.tag }}" &>/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Extract changelog section + if: steps.check.outputs.exists != 'true' + id: changelog + run: | + version="${{ steps.crate.outputs.version }}" + notes="$(awk "/^## $version/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md)" + if [ -z "$notes" ]; then + notes="See [CHANGELOG.md](CHANGELOG.md) for details." + fi + { + echo "notes<> "$GITHUB_OUTPUT" + + - name: Ensure tag exists + if: steps.check.outputs.exists != 'true' + run: | + tag="${{ steps.crate.outputs.tag }}" + if ! git rev-parse "$tag" &>/dev/null; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "$tag" + git push origin "$tag" + fi + + - name: Create release + if: steps.check.outputs.exists != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ steps.crate.outputs.tag }}" \ + --title "cougr-cli v${{ steps.crate.outputs.version }}" \ + --notes "${{ steps.changelog.outputs.notes }}" \ + --generate-notes