diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index fb80fcd..b97d54a 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -1,4 +1,4 @@ -name: Publish Docker Images +name: Run checks and build archives on: push: @@ -6,7 +6,7 @@ on: - main - develop tags: - - 'v*.*.*' + - "v*.*.*" pull_request: types: [opened, synchronize, reopened] @@ -56,32 +56,36 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - - name: Build Linux release binary - run: nix build --no-pure-eval --accept-flake-config + build: + needs: coverage-and-sonar + strategy: + matrix: + target: ["linux-x86_64", "linux-aarch64", "windows-x86_64"] - - name: Prepare Linux binary - run: | - mkdir dist-linux - cp result/bin/jj-cz dist-linux/ - cp LICENSE.*.md dist-linux/ + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 - - name: Upload Linux artifact + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Set up cachix + uses: cachix/cachix-action@v17 + with: + name: phundrak + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build jj-cz archive + run: nix build .#${{matrix.target}}-archive + + - name: Upload artifact uses: actions/upload-artifact@v3 with: - name: jj-cz-x86_64-unknown-linux-gnu - path: dist-linux/* - - - name: Build Windows release binary - run: nix build .#windows --no-pure-eval --accept-flake-config - - - name: Prepare Windows binary - run: | - mkdir -p dist-windows - cp result/bin/jj-cz.exe dist-windows/ - cp LICENSE.*.md dist-windows/ - - - name: Upload Windows artifact - uses: actions/upload-artifact@v3 - with: - name: jj-cz-x86_64-pc-windows-gnu - path: dist-windows/* + name: jj-cz-${{matrix.target}} + path: result/dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f33b6b..0eca35c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,33 +2,16 @@ name: Release on: push: - branches: - - main + branches: + - main jobs: - checks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: Set up cachix - uses: cachix/cachix-action@v17 - with: - name: phundrak - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Run Checks - shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" - run: just check-all - release: - needs: checks runs-on: ubuntu-latest + outputs: + release: ${{ steps.releasable.outputs.release }} + release_id: ${{ steps.create_release.outputs.release_id }} + version: ${{ steps.next_version.outputs.version }} steps: - uses: actions/checkout@v4 with: @@ -49,58 +32,103 @@ jobs: - name: Check for releasable commits id: releasable run: | - COUNT=$(nix develop --no-pure-eval --command just cliff-count) - echo "count=$COUNT" >> $GITHUB_OUTPUT + COUNT=$(nix develop --no-pure-eval --accept-flake-config --command just cliff-count) + if [ "$COUNT" -gt 0 ]; then + echo "release=true" >> $GITHUB_OUTPUT + else + echo "release=false" >> $GITHUB_OUTPUT + fi - name: Determine next version - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' id: next_version run: | - CLIFF_NEXT_VERSION=$(nix develop --no-pure-eval --command just cliff-next-version) + CLIFF_NEXT_VERSION=$(nix develop --no-pure-eval --accept-flake-config --command just cliff-next-version) echo "version=$CLIFF_NEXT_VERSION" >> $GITHUB_OUTPUT - name: Update changelog - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: just cliff-bump - name: Create release commit - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' env: VERSION: ${{ steps.next_version.outputs.version }} shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: just commit-release $VERSION - name: Create version tag - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' env: VERSION: ${{ steps.next_version.outputs.version }} shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: just create-release-tag $VERSION - - name: Build Linux release binaries - if: steps.releasable.outputs.count > 0 - run: nix build - - - name: Build Windows release binaries - if: steps.releasable.outputs.count > 0 - run: nix build .#windows + - name: Create Gitea release + if: steps.releasable.outputs.release == 'true' + id: create_release + env: + VERSION: ${{ steps.next_version.outputs.version }} + CI_TOKEN: ${{ secrets.CI_TOKEN }} + shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" + run: | + RESPONSE=$(curl -s -X POST \ + -H "Authorization: token $CI_TOKEN" \ + -H "Content-Type: application/json" \ + "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \ + -d "{\"tag_name\": \"v${VERSION}\", \"name\": \"v${VERSION}\"}") + echo "release_id=$(echo "$RESPONSE" | jq -r '.id')" >> $GITHUB_OUTPUT - name: Publish on crates.io - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: cargo publish - name: Rebase develop onto main - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: just rebase-develop - name: Bump to next dev version - if: steps.releasable.outputs.count > 0 + if: steps.releasable.outputs.release == 'true' env: VERSION: ${{ steps.next_version.outputs.version }} shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}" run: just update-develop-version $VERSION + + build: + needs: release + if: needs.release.outputs.release == 'true' + strategy: + matrix: + target: ["linux-x86_64", "linux-aarch64", "windows-x86_64"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Set up cachix + uses: cachix/cachix-action@v17 + with: + name: phundrak + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Build jj-cz archive + run: nix build .#${{ matrix.target }}-archive + + - name: Upload release asset + env: + CI_TOKEN: ${{ secrets.CI_TOKEN }} + RELEASE_ID: ${{ needs.release.outputs.release_id }} + run: | + curl -s -X POST \ + -H "Authorization: token $CI_TOKEN" \ + -F "attachment=@$(ls result/dist/*.zip)" \ + "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" diff --git a/flake.nix b/flake.nix index fffea3f..601a0b3 100644 --- a/flake.nix +++ b/flake.nix @@ -39,58 +39,10 @@ overlays = [(import rust-overlay)]; pkgs = import nixpkgs {inherit system overlays;}; rustVersion = pkgs.rust-bin.stable.latest.default; - targets = { - linux-x86_64 = { - crossPkgs = pkgs; - triple = "x86_64-unknown-linux-gnu"; - exeSuffix = ""; - }; - linux-aarch64 = { - crossPkgs = pkgs.pkgsCross.aarch64-multiplatform; - triple = "aarch64-unknown-linux-gnu"; - exeSuffix = ""; - }; - windows-x86_64 = { - crossPkgs = pkgs.pkgsCross.mingwW64; - triple = "x86_64-pc-windows-gnu"; - exeSuffix = ".exe"; - }; - windows-aarch64 = { - crossPkgs = pkgs.pkgsCross.aarch64-windows; - triple = "aarch64-pc-windows-gnu"; - exeSuffix = ".exe"; - }; - macos-x86_64 = { - crossPkgs = pkgs.pkgsCross.x86_64-darwin; - triple = "x86_64-apple-darwin"; - exeSuffix = ""; - }; - macos-aarch64 = { - crossPkgs = pkgs.pkgsCross.aarch64-darwin; - triple = "aarch64-apple-darwin"; - exeSuffix = ""; - }; - }; - mkRustBuild = import ./nix/package.nix; - packages = { - linux-x86_64 = mkRustBuild {inherit pkgs; target = targets.linux-x86_64; }; - linux-aarch64 = mkRustBuild { inherit pkgs; target = targets.linux-aarch64; }; - windows-x86_64 = mkRustBuild { inherit pkgs; target = targets.windows-x86_64; }; - macos-aarch64 = mkRustBuild { inherit pkgs; target = targets.macos-aarch64; }; - }; - defaultBySystem = { - "x86_64-linux" = packages.linux-x86_64; - "aarch64-linux" = packages.linux-aarch64; - "x86_64-windows" = packages.windows-x86_64; - "aarch64-macos" = packages.macos-aarch64; - }; + packages = import ./nix/packages.nix {inherit pkgs system;}; in { + inherit packages; formatter = alejandra.defaultPackage.${system}; - packages = - packages - // { - default = defaultBySystem.${system} or packages.linux-x86_64; - }; devShell = import ./nix/shell.nix {inherit pkgs rustVersion;}; } ); diff --git a/nix/make-archive.nix b/nix/make-archive.nix new file mode 100644 index 0000000..340027c --- /dev/null +++ b/nix/make-archive.nix @@ -0,0 +1,16 @@ +{ + bin, + pkgs, + archiveName +}: +pkgs.stdenv.mkDerivation rec { + name = "jj-cz-${archiveName}"; + src = pkgs.lib.cleanSource ../.; + nativeBuildInputs = [pkgs.zip]; + buildPhase = '' + mkdir -p $out/dist + zip -j $out/dist/${name}.zip ${bin}/bin/jj-cz* ${src}/README.md ${src}/LICENSE.* + ''; + installPhase = ""; + dontConfigure = true; +} diff --git a/nix/package.nix b/nix/make-binary.nix similarity index 100% rename from nix/package.nix rename to nix/make-binary.nix diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..1e4a5bc --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,82 @@ +{ + pkgs, + system, + ... +}: let + mkRustBuild = import ./make-binary.nix; + mkArchive = import ./make-archive.nix; + targets = { + linux-x86_64 = { + crossPkgs = pkgs; + triple = "x86_64-unknown-linux-gnu"; + exeSuffix = ""; + }; + linux-aarch64 = { + crossPkgs = pkgs.pkgsCross.aarch64-multiplatform; + triple = "aarch64-unknown-linux-gnu"; + exeSuffix = ""; + }; + windows-x86_64 = { + crossPkgs = pkgs.pkgsCross.mingwW64; + triple = "x86_64-pc-windows-gnu"; + exeSuffix = ".exe"; + }; + windows-aarch64 = { + crossPkgs = pkgs.pkgsCross.aarch64-windows; + triple = "aarch64-pc-windows-gnu"; + exeSuffix = ".exe"; + }; + macos-x86_64 = { + crossPkgs = pkgs.pkgsCross.x86_64-darwin; + triple = "x86_64-apple-darwin"; + exeSuffix = ""; + }; + macos-aarch64 = { + crossPkgs = pkgs.pkgsCross.aarch64-darwin; + triple = "aarch64-apple-darwin"; + exeSuffix = ""; + }; + }; + bins = { + linux-x86_64 = mkRustBuild { + inherit pkgs; + target = targets.linux-x86_64; + }; + linux-aarch64 = mkRustBuild { + inherit pkgs; + target = targets.linux-aarch64; + }; + windows-x86_64 = mkRustBuild { + inherit pkgs; + target = targets.windows-x86_64; + }; + }; + packages = + { + linux-x86_64-archive = mkArchive { + inherit pkgs; + bin = bins.linux-x86_64; + archiveName = "x86_64-linux"; + }; + linux-aarch64-archive = mkArchive { + inherit pkgs; + bin = bins.linux-aarch64; + archiveName = "aarch64-linux"; + }; + windows-x86_64-archive = mkArchive { + inherit pkgs; + bin = bins.windows-x86_64; + archiveName = "x86_64-windows"; + }; + } + // bins; + defaultBySystem = { + "x86_64-linux" = packages.linux-x86_64; + "aarch64-linux" = packages.linux-aarch64; + "x86_64-windows" = packages.windows-x86_64; + }; +in + packages + // { + default = defaultBySystem.${system} or packages.linux-x86_64; + } diff --git a/nix/shell.nix b/nix/shell.nix index 0c5c3ab..6ce3cf2 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -20,5 +20,8 @@ pkgs.mkShell { git-cliff just typos + + # for CI + jq ]; } diff --git a/tests/cli.rs b/tests/cli.rs deleted file mode 100644 index d27468d..0000000 --- a/tests/cli.rs +++ /dev/null @@ -1,78 +0,0 @@ -use assert_fs::TempDir; -#[cfg(feature = "test-utils")] -use jj_cz::{Body, BreakingChange, CommitType, Description, MockJjExecutor, MockPrompts, Scope}; -use jj_cz::{CommitWorkflow, Error, JjLib}; - -#[cfg(feature = "test-utils")] -#[tokio::test] -async fn test_happy_path_integration() { - // T037: Happy path integration test - let mock_executor = MockJjExecutor::new(); - let mock_prompts = MockPrompts::new() - .with_commit_type(CommitType::Feat) - .with_scope(Scope::empty()) - .with_description(Description::parse("add new feature").unwrap()) - .with_breaking_change(BreakingChange::No) - .with_body(Body::default()) - .with_confirm(true); - - let workflow = CommitWorkflow::with_prompts(mock_executor, mock_prompts); - let result = workflow.run_for_revset("@").await; - - assert!( - result.is_ok(), - "Workflow should complete successfully: {:?}", - result - ); -} - -#[tokio::test] -async fn test_not_in_repo() { - // T038: Not-in-repo integration test - with_working_dir itself returns the error - let temp_dir = TempDir::new().unwrap(); - - let result = JjLib::with_working_dir(temp_dir.path()).await; - - assert!(matches!(result, Err(Error::NotARepository))); -} - -#[cfg(feature = "test-utils")] -#[tokio::test] -async fn test_cancellation() { - // T039: Cancellation integration test - // This is tricky to test directly without a TTY - // We'll test the error handling path instead - - // Create a mock executor that simulates cancellation - struct CancelMock; - - #[async_trait::async_trait(?Send)] - impl jj_cz::JjExecutor for CancelMock { - async fn is_repository(&self) -> Result { - Ok(true) - } - - async fn describe(&self, _revset: &str, _message: &str) -> Result<(), Error> { - Err(Error::Cancelled) - } - - async fn get_description(&self, _revset: &str) -> Result { - Ok(String::new()) - } - } - - let executor = CancelMock; - let mock_prompts = MockPrompts::new() - .with_commit_type(CommitType::Feat) - .with_scope(Scope::empty()) - .with_description(Description::parse("test").unwrap()) - .with_breaking_change(BreakingChange::No) - .with_body(Body::default()) - .with_confirm(true); - let workflow = CommitWorkflow::with_prompts(executor, mock_prompts); - - let result = workflow.run_for_revset("@").await; - - // Should fail with Cancelled error - assert!(matches!(result, Err(Error::Cancelled))); -}