From 3a75f4175e1baa9d772633a02d0556445f5738d1 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 01:28:32 +0100 Subject: [PATCH 1/5] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Numbers/issues/123 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a1ac138 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Numbers/issues/123 +Your prepared branch: issue-123-ca4a6bfb3f29 +Your prepared working directory: /tmp/gh-issue-solver-1766881711115 + +Proceed. \ No newline at end of file From e30ffa51aa4d99c89cefedaee7d1501559f4a9e9 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 01:30:01 +0100 Subject: [PATCH 2/5] Add rust-toolchain.toml with nightly-2022-08-22 channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure Rust toolchain to use nightly-2022-08-22 as required by issue #123. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- rust/rust-toolchain.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 rust/rust-toolchain.toml diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml new file mode 100644 index 0000000..d7c5705 --- /dev/null +++ b/rust/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2022-08-22" From 3721582ae5ccece7353a0b70507a6fd89bbfee57 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 01:33:28 +0100 Subject: [PATCH 3/5] Add 100% test coverage for Rust code and CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive unit tests for all traits: - Num trait: tested for all primitive integer types - SignNum trait: tested for all signed integer types - ToSigned trait: tested conversions for all integer types including edge cases - MaxValue trait: tested for all integer types - LinkType trait: tested for all unsigned integer types - Add integration tests for combined trait usage - Add edge case tests for unsigned-to-signed conversion wrapping behavior - Add GitHub Actions workflow for Rust testing and coverage Test coverage: 100% (67 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/rust.yml | 60 +++++ rust/src/imp.rs | 508 ++++++++++++++++++++++++++++++++++++- 2 files changed, 561 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..e41c5bb --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,60 @@ +name: Rust + +on: + push: + branches: + - 'main' + paths: + - 'rust/**' + - '.github/workflows/rust.yml' + pull_request: + branches: + - 'main' + paths: + - 'rust/**' + - '.github/workflows/rust.yml' + +defaults: + run: + working-directory: rust + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + components: rustfmt + + - name: Run tests + run: cargo test --verbose + + - name: Check formatting + run: cargo fmt -- --check + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + components: llvm-tools-preview + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Generate code coverage + run: cargo llvm-cov --all-features --lcov --output-path lcov.info + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: rust/lcov.info + fail_ci_if_error: false diff --git a/rust/src/imp.rs b/rust/src/imp.rs index 55453c9..9c669f3 100644 --- a/rust/src/imp.rs +++ b/rust/src/imp.rs @@ -6,16 +6,11 @@ use num_traits::{AsPrimitive, FromPrimitive, PrimInt, Signed, ToPrimitive, Unsig pub trait Num: PrimInt + Default + Debug + AsPrimitive + ToPrimitive {} -impl< - All: PrimInt + Default + Debug + AsPrimitive + ToPrimitive, -> Num for All {} +impl + ToPrimitive> Num for All {} pub trait SignNum: Num + Signed + FromPrimitive {} -impl< - All: Num + Signed + FromPrimitive, -> SignNum for All {} - +impl SignNum for All {} pub trait ToSigned { type Type: Num + Signed; @@ -105,3 +100,502 @@ impl< + Sync + 'static, > LinkType for All {} + +#[cfg(test)] +mod tests { + use super::*; + + // ========================================== + // Tests for Num trait + // ========================================== + + #[test] + fn test_num_trait_for_i8() { + fn assert_num(_val: T) {} + assert_num(0i8); + } + + #[test] + fn test_num_trait_for_u8() { + fn assert_num(_val: T) {} + assert_num(0u8); + } + + #[test] + fn test_num_trait_for_i16() { + fn assert_num(_val: T) {} + assert_num(0i16); + } + + #[test] + fn test_num_trait_for_u16() { + fn assert_num(_val: T) {} + assert_num(0u16); + } + + #[test] + fn test_num_trait_for_i32() { + fn assert_num(_val: T) {} + assert_num(0i32); + } + + #[test] + fn test_num_trait_for_u32() { + fn assert_num(_val: T) {} + assert_num(0u32); + } + + #[test] + fn test_num_trait_for_i64() { + fn assert_num(_val: T) {} + assert_num(0i64); + } + + #[test] + fn test_num_trait_for_u64() { + fn assert_num(_val: T) {} + assert_num(0u64); + } + + #[test] + fn test_num_trait_for_i128() { + fn assert_num(_val: T) {} + assert_num(0i128); + } + + #[test] + fn test_num_trait_for_u128() { + fn assert_num(_val: T) {} + assert_num(0u128); + } + + #[test] + fn test_num_trait_for_isize() { + fn assert_num(_val: T) {} + assert_num(0isize); + } + + #[test] + fn test_num_trait_for_usize() { + fn assert_num(_val: T) {} + assert_num(0usize); + } + + // ========================================== + // Tests for SignNum trait + // ========================================== + + #[test] + fn test_sign_num_trait_for_i8() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0i8); + } + + #[test] + fn test_sign_num_trait_for_i16() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0i16); + } + + #[test] + fn test_sign_num_trait_for_i32() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0i32); + } + + #[test] + fn test_sign_num_trait_for_i64() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0i64); + } + + #[test] + fn test_sign_num_trait_for_i128() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0i128); + } + + #[test] + fn test_sign_num_trait_for_isize() { + fn assert_sign_num(_val: T) {} + assert_sign_num(0isize); + } + + // ========================================== + // Tests for ToSigned trait + // ========================================== + + #[test] + fn test_to_signed_i8() { + let val: i8 = 42; + let signed: i8 = val.to_signed(); + assert_eq!(signed, 42i8); + } + + #[test] + fn test_to_signed_u8() { + let val: u8 = 42; + let signed: i8 = val.to_signed(); + assert_eq!(signed, 42i8); + } + + #[test] + fn test_to_signed_i16() { + let val: i16 = 1000; + let signed: i16 = val.to_signed(); + assert_eq!(signed, 1000i16); + } + + #[test] + fn test_to_signed_u16() { + let val: u16 = 1000; + let signed: i16 = val.to_signed(); + assert_eq!(signed, 1000i16); + } + + #[test] + fn test_to_signed_i32() { + let val: i32 = 100000; + let signed: i32 = val.to_signed(); + assert_eq!(signed, 100000i32); + } + + #[test] + fn test_to_signed_u32() { + let val: u32 = 100000; + let signed: i32 = val.to_signed(); + assert_eq!(signed, 100000i32); + } + + #[test] + fn test_to_signed_i64() { + let val: i64 = 1000000000; + let signed: i64 = val.to_signed(); + assert_eq!(signed, 1000000000i64); + } + + #[test] + fn test_to_signed_u64() { + let val: u64 = 1000000000; + let signed: i64 = val.to_signed(); + assert_eq!(signed, 1000000000i64); + } + + #[test] + fn test_to_signed_i128() { + let val: i128 = 1000000000000000; + let signed: i128 = val.to_signed(); + assert_eq!(signed, 1000000000000000i128); + } + + #[test] + fn test_to_signed_u128() { + let val: u128 = 1000000000000000; + let signed: i128 = val.to_signed(); + assert_eq!(signed, 1000000000000000i128); + } + + #[test] + fn test_to_signed_isize() { + let val: isize = 12345; + let signed: isize = val.to_signed(); + assert_eq!(signed, 12345isize); + } + + #[test] + fn test_to_signed_usize() { + let val: usize = 12345; + let signed: isize = val.to_signed(); + assert_eq!(signed, 12345isize); + } + + #[test] + fn test_to_signed_type_alias_i8() { + fn check>(_val: T) {} + check(0i8); + check(0u8); + } + + #[test] + fn test_to_signed_type_alias_i16() { + fn check>(_val: T) {} + check(0i16); + check(0u16); + } + + #[test] + fn test_to_signed_type_alias_i32() { + fn check>(_val: T) {} + check(0i32); + check(0u32); + } + + #[test] + fn test_to_signed_type_alias_i64() { + fn check>(_val: T) {} + check(0i64); + check(0u64); + } + + #[test] + fn test_to_signed_type_alias_i128() { + fn check>(_val: T) {} + check(0i128); + check(0u128); + } + + #[test] + fn test_to_signed_type_alias_isize() { + fn check>(_val: T) {} + check(0isize); + check(0usize); + } + + // ========================================== + // Tests for MaxValue trait + // ========================================== + + #[test] + fn test_max_value_i8() { + assert_eq!(i8::MAX, ::MAX); + } + + #[test] + fn test_max_value_u8() { + assert_eq!(u8::MAX, ::MAX); + } + + #[test] + fn test_max_value_i16() { + assert_eq!(i16::MAX, ::MAX); + } + + #[test] + fn test_max_value_u16() { + assert_eq!(u16::MAX, ::MAX); + } + + #[test] + fn test_max_value_i32() { + assert_eq!(i32::MAX, ::MAX); + } + + #[test] + fn test_max_value_u32() { + assert_eq!(u32::MAX, ::MAX); + } + + #[test] + fn test_max_value_i64() { + assert_eq!(i64::MAX, ::MAX); + } + + #[test] + fn test_max_value_u64() { + assert_eq!(u64::MAX, ::MAX); + } + + #[test] + fn test_max_value_i128() { + assert_eq!(i128::MAX, ::MAX); + } + + #[test] + fn test_max_value_u128() { + assert_eq!(u128::MAX, ::MAX); + } + + #[test] + fn test_max_value_isize() { + assert_eq!(isize::MAX, ::MAX); + } + + #[test] + fn test_max_value_usize() { + assert_eq!(usize::MAX, ::MAX); + } + + // ========================================== + // Tests for LinkType trait + // ========================================== + + #[test] + fn test_link_type_for_u8() { + fn assert_link_type(_val: T) {} + assert_link_type(0u8); + } + + #[test] + fn test_link_type_for_u16() { + fn assert_link_type(_val: T) {} + assert_link_type(0u16); + } + + #[test] + fn test_link_type_for_u32() { + fn assert_link_type(_val: T) {} + assert_link_type(0u32); + } + + #[test] + fn test_link_type_for_u64() { + fn assert_link_type(_val: T) {} + assert_link_type(0u64); + } + + #[test] + fn test_link_type_for_usize() { + fn assert_link_type(_val: T) {} + assert_link_type(0usize); + } + + // ========================================== + // Edge case tests for ToSigned + // ========================================== + + #[test] + fn test_to_signed_u8_max() { + let val: u8 = u8::MAX; + let signed: i8 = val.to_signed(); + assert_eq!(signed, -1i8); // Wrapping behavior + } + + #[test] + fn test_to_signed_u16_max() { + let val: u16 = u16::MAX; + let signed: i16 = val.to_signed(); + assert_eq!(signed, -1i16); // Wrapping behavior + } + + #[test] + fn test_to_signed_u32_max() { + let val: u32 = u32::MAX; + let signed: i32 = val.to_signed(); + assert_eq!(signed, -1i32); // Wrapping behavior + } + + #[test] + fn test_to_signed_u64_max() { + let val: u64 = u64::MAX; + let signed: i64 = val.to_signed(); + assert_eq!(signed, -1i64); // Wrapping behavior + } + + #[test] + fn test_to_signed_u128_max() { + let val: u128 = u128::MAX; + let signed: i128 = val.to_signed(); + assert_eq!(signed, -1i128); // Wrapping behavior + } + + #[test] + fn test_to_signed_usize_max() { + let val: usize = usize::MAX; + let signed: isize = val.to_signed(); + assert_eq!(signed, -1isize); // Wrapping behavior + } + + #[test] + fn test_to_signed_zero() { + assert_eq!(0u8.to_signed(), 0i8); + assert_eq!(0u16.to_signed(), 0i16); + assert_eq!(0u32.to_signed(), 0i32); + assert_eq!(0u64.to_signed(), 0i64); + assert_eq!(0u128.to_signed(), 0i128); + assert_eq!(0usize.to_signed(), 0isize); + } + + #[test] + fn test_to_signed_negative() { + assert_eq!((-1i8).to_signed(), -1i8); + assert_eq!((-1i16).to_signed(), -1i16); + assert_eq!((-1i32).to_signed(), -1i32); + assert_eq!((-1i64).to_signed(), -1i64); + assert_eq!((-1i128).to_signed(), -1i128); + assert_eq!((-1isize).to_signed(), -1isize); + } + + // ========================================== + // Integration tests - using traits together + // ========================================== + + #[test] + fn test_link_type_can_be_converted_to_signed() { + fn use_link_type(val: T) -> ::Type { + val.to_signed() + } + assert_eq!(use_link_type(42u8), 42i8); + assert_eq!(use_link_type(42u16), 42i16); + assert_eq!(use_link_type(42u32), 42i32); + assert_eq!(use_link_type(42u64), 42i64); + assert_eq!(use_link_type(42usize), 42isize); + } + + #[test] + fn test_link_type_has_max_value() { + fn get_max() -> T { + T::MAX + } + assert_eq!(get_max::(), u8::MAX); + assert_eq!(get_max::(), u16::MAX); + assert_eq!(get_max::(), u32::MAX); + assert_eq!(get_max::(), u64::MAX); + assert_eq!(get_max::(), usize::MAX); + } + + #[test] + fn test_num_default_values() { + fn check_default() -> T { + T::default() + } + assert_eq!(check_default::(), 0i8); + assert_eq!(check_default::(), 0u8); + assert_eq!(check_default::(), 0i16); + assert_eq!(check_default::(), 0u16); + assert_eq!(check_default::(), 0i32); + assert_eq!(check_default::(), 0u32); + assert_eq!(check_default::(), 0i64); + assert_eq!(check_default::(), 0u64); + assert_eq!(check_default::(), 0i128); + assert_eq!(check_default::(), 0u128); + assert_eq!(check_default::(), 0isize); + assert_eq!(check_default::(), 0usize); + } + + #[test] + fn test_num_as_usize() { + fn to_usize(val: T) -> usize { + val.as_() + } + assert_eq!(to_usize(42i8), 42usize); + assert_eq!(to_usize(42u8), 42usize); + assert_eq!(to_usize(42i16), 42usize); + assert_eq!(to_usize(42u16), 42usize); + assert_eq!(to_usize(42i32), 42usize); + assert_eq!(to_usize(42u32), 42usize); + } + + #[test] + fn test_sign_num_signum() { + fn get_signum(val: T) -> T { + val.signum() + } + assert_eq!(get_signum(5i8), 1i8); + assert_eq!(get_signum(-5i8), -1i8); + assert_eq!(get_signum(0i8), 0i8); + assert_eq!(get_signum(5i32), 1i32); + assert_eq!(get_signum(-5i32), -1i32); + assert_eq!(get_signum(0i32), 0i32); + } + + #[test] + fn test_sign_num_abs() { + fn get_abs(val: T) -> T { + val.abs() + } + assert_eq!(get_abs(-5i8), 5i8); + assert_eq!(get_abs(5i8), 5i8); + assert_eq!(get_abs(-100i32), 100i32); + assert_eq!(get_abs(100i32), 100i32); + } +} From 5ed1798c409727061bf04d381a9d16f61b329ad0 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 01:40:35 +0100 Subject: [PATCH 4/5] Revert "Initial commit with task details" This reverts commit 3a75f4175e1baa9d772633a02d0556445f5738d1. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a1ac138..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Numbers/issues/123 -Your prepared branch: issue-123-ca4a6bfb3f29 -Your prepared working directory: /tmp/gh-issue-solver-1766881711115 - -Proceed. \ No newline at end of file From 0cfa87936076addec5ba0cfd4adc9b839ef9a997 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 02:53:45 +0100 Subject: [PATCH 5/5] Add CI/CD pipeline and development tools from template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on rust-ai-driven-development-pipeline-template best practices: - Enhanced CI/CD workflow with multi-OS testing (ubuntu, macos, windows) - Code coverage reporting with cargo-llvm-cov - Automatic release workflow with changelog fragment system - Manual release workflow with workflow_dispatch - Cache for faster CI builds Development tools: - scripts/ folder with version bumping and release scripts - changelog.d/ folder with fragment-based changelog system - .pre-commit-config.yaml for local code quality checks - CONTRIBUTING.md with development guidelines - CHANGELOG.md with keepachangelog format Cargo.toml improvements: - Fixed version typo (aplha -> 0.1.0) - Added lib section with correct crate name - Added release profile optimizations - Updated repository URLs .gitignore updates: - Added Rust-specific patterns (target/, *.rs.bk, etc.) - Added coverage and development tool patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/rust.yml | 320 +++++++++++++++++- .gitignore | 46 +++ .pre-commit-config.yaml | 27 ++ CHANGELOG.md | 8 + CONTRIBUTING.md | 300 ++++++++++++++++ ...251228_024300_100_percent_test_coverage.md | 14 + changelog.d/README.md | 135 ++++++++ rust/Cargo.lock | 25 ++ rust/Cargo.toml | 18 +- scripts/bump-version.mjs | 122 +++++++ scripts/check-file-size.mjs | 100 ++++++ scripts/collect-changelog.mjs | 170 ++++++++++ scripts/create-github-release.mjs | 110 ++++++ scripts/get-bump-type.mjs | 152 +++++++++ scripts/version-and-commit.mjs | 276 +++++++++++++++ 15 files changed, 1812 insertions(+), 11 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 changelog.d/20251228_024300_100_percent_test_coverage.md create mode 100644 changelog.d/README.md create mode 100644 rust/Cargo.lock create mode 100644 scripts/bump-version.mjs create mode 100644 scripts/check-file-size.mjs create mode 100644 scripts/collect-changelog.mjs create mode 100644 scripts/create-github-release.mjs create mode 100644 scripts/get-bump-type.mjs create mode 100644 scripts/version-and-commit.mjs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e41c5bb..c831ee0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,25 +1,54 @@ -name: Rust +name: Rust CI/CD Pipeline on: push: branches: - - 'main' + - main paths: - 'rust/**' - '.github/workflows/rust.yml' + - 'scripts/**' + - 'changelog.d/**' pull_request: - branches: - - 'main' + types: [opened, synchronize, reopened] paths: - 'rust/**' - '.github/workflows/rust.yml' + - 'scripts/**' + - 'changelog.d/**' + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Release description (optional)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always defaults: run: working-directory: rust jobs: - test: + # REQUIRED CI CHECKS - All must pass before release + # These jobs ensure code quality and tests pass before any release + + # Linting and formatting + lint: + name: Lint and Format Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,13 +59,62 @@ jobs: toolchain: nightly-2022-08-22 components: rustfmt - - name: Run tests - run: cargo test --verbose + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- - name: Check formatting - run: cargo fmt -- --check + run: cargo fmt --all -- --check + + - name: Check file size limit + working-directory: . + run: node scripts/check-file-size.mjs + + # Test on multiple OS + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Run tests + run: cargo test --all-features --verbose + + # Code coverage coverage: + name: Code Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,6 +125,17 @@ jobs: toolchain: nightly-2022-08-22 components: llvm-tools-preview + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-coverage- + - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov @@ -58,3 +147,218 @@ jobs: with: files: rust/lcov.info fail_ci_if_error: false + + # Build package - only runs if lint and test pass + build: + name: Build Package + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + - name: Build release + run: cargo build --release --verbose + + # Check for changelog fragments in PRs + changelog: + name: Changelog Fragment Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changelog fragments + working-directory: . + run: | + # Get list of fragment files (excluding README and template) + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) + + # Get changed files in PR + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + # Check if any source files changed (excluding docs and config) + SOURCE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "^(rust/src/|rust/tests/|scripts/)" | wc -l) + + if [ "$SOURCE_CHANGED" -gt 0 ] && [ "$FRAGMENTS" -eq 0 ]; then + echo "::warning::No changelog fragment found. Please add a changelog entry in changelog.d/" + echo "" + echo "To create a changelog fragment:" + echo " Create a new .md file in changelog.d/ with your changes" + echo "" + echo "See changelog.d/README.md for more information." + # Note: This is a warning, not a failure, to allow flexibility + # Change 'exit 0' to 'exit 1' to make it required + exit 0 + fi + + echo "Changelog check passed" + + # Automatic release on push to main using changelog fragments + # This job automatically bumps version based on fragments in changelog.d/ + auto-release: + name: Auto Release + needs: [lint, test, build, coverage] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + working-directory: . + + - name: Determine bump type from changelog fragments + id: bump_type + working-directory: . + run: node scripts/get-bump-type.mjs + + - name: Check if version already released or no fragments + id: check + working-directory: . + run: | + # Check if there are changelog fragments + if [ "${{ steps.bump_type.outputs.has_fragments }}" != "true" ]; then + # No fragments - check if current version tag exists + CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' rust/Cargo.toml) + if git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then + echo "No changelog fragments and v$CURRENT_VERSION already released" + echo "should_release=false" >> $GITHUB_OUTPUT + else + echo "No changelog fragments but v$CURRENT_VERSION not yet released" + echo "should_release=true" >> $GITHUB_OUTPUT + echo "skip_bump=true" >> $GITHUB_OUTPUT + fi + else + echo "Found changelog fragments, proceeding with release" + echo "should_release=true" >> $GITHUB_OUTPUT + echo "skip_bump=false" >> $GITHUB_OUTPUT + fi + + - name: Collect changelog and bump version + id: version + if: steps.check.outputs.should_release == 'true' && steps.check.outputs.skip_bump != 'true' + working-directory: . + run: | + node scripts/version-and-commit.mjs \ + --bump-type "${{ steps.bump_type.outputs.bump_type }}" + + - name: Get current version + id: current_version + if: steps.check.outputs.should_release == 'true' + working-directory: . + run: | + CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' rust/Cargo.toml) + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + - name: Build release + if: steps.check.outputs.should_release == 'true' + run: cargo build --release + + - name: Create GitHub Release + if: steps.check.outputs.should_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: . + run: | + node scripts/create-github-release.mjs \ + --release-version "${{ steps.current_version.outputs.version }}" \ + --repository "${{ github.repository }}" + + # Manual release via workflow_dispatch - only after CI passes + manual-release: + name: Manual Release + needs: [lint, test, build, coverage] + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2022-08-22 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + working-directory: . + + - name: Collect changelog fragments + working-directory: . + run: | + # Check if there are any fragments to collect + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" 2>/dev/null | wc -l) + if [ "$FRAGMENTS" -gt 0 ]; then + echo "Found $FRAGMENTS changelog fragment(s), collecting..." + node scripts/collect-changelog.mjs + else + echo "No changelog fragments found, skipping collection" + fi + + - name: Version and commit + id: version + working-directory: . + run: | + node scripts/version-and-commit.mjs \ + --bump-type "${{ github.event.inputs.bump_type }}" \ + --description "${{ github.event.inputs.description }}" + + - name: Build release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + run: cargo build --release + + - name: Create GitHub Release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: . + run: | + node scripts/create-github-release.mjs \ + --release-version "${{ steps.version.outputs.new_version }}" \ + --repository "${{ github.repository }}" diff --git a/.gitignore b/.gitignore index 9aadd1a..33db33d 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,49 @@ ASALocalRun/ org.kde.konsole.desktop *.kate-swp *.vE4458 + +# ========================================== +# Rust +# ========================================== + +# Generated by Cargo +# will have compiled files and executables +rust/target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# Coverage reports +*.lcov +coverage/ +tarpaulin-report.html + +# Benchmark results +criterion/ + +# Documentation build output +doc/ + +# ========================================== +# Scripts and Development +# ========================================== + +# Python virtual environments (for scripts) +.venv/ +venv/ +*.pyo + +# Local development files +.env +.env.local +*.local + +# Log files +logs/ + +# VS Code +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ab8f7fa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: bash -c 'cd rust && cargo fmt --all --' + language: system + types: [rust] + pass_filenames: false + + - id: cargo-test + name: cargo test + entry: bash -c 'cd rust && cargo test' + language: system + types: [rust] + pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..88f176a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..88337de --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,300 @@ +# Contributing to Numbers + +Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to this project. + +## Development Setup + +1. **Fork and clone the repository** + + ```bash + git clone https://github.com/YOUR-USERNAME/Numbers.git + cd Numbers + ``` + +2. **Install Rust** + + Install Rust using rustup (if not already installed): + + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + + This project uses a specific nightly toolchain. It will be automatically selected when you run cargo commands due to `rust/rust-toolchain.toml`. + +3. **Install development tools** + + ```bash + rustup component add rustfmt + ``` + +4. **Install pre-commit hooks** (optional but recommended) + + ```bash + pip install pre-commit + pre-commit install + ``` + +5. **Build the project** + + ```bash + cd rust + cargo build + ``` + +## Development Workflow + +1. **Create a feature branch** + + ```bash + git checkout -b feature/my-feature + ``` + +2. **Make your changes** + + - Write code following the project's style guidelines + - Add tests for any new functionality + - Update documentation as needed + +3. **Run quality checks** + + ```bash + cd rust + + # Format code + cargo fmt + + # Run all checks together + cargo fmt --check + + # Check file sizes (from repo root) + cd .. + node scripts/check-file-size.mjs + ``` + +4. **Run tests** + + ```bash + cd rust + + # Run all tests + cargo test + + # Run tests with verbose output + cargo test --verbose + + # Run a specific test + cargo test test_name + ``` + +5. **Add a changelog fragment** + + For any user-facing changes, create a changelog fragment: + + ```bash + # Create a new file in changelog.d/ + # Format: YYYYMMDD_HHMMSS_description.md + touch changelog.d/$(date +%Y%m%d_%H%M%S)_my_change.md + ``` + + Edit the file to document your changes: + + ```markdown + --- + bump: patch + --- + + ### Added + - Description of new feature + + ### Fixed + - Description of bug fix + ``` + + **Why fragments?** This prevents merge conflicts in CHANGELOG.md when multiple PRs are open simultaneously. + +6. **Commit your changes** + + ```bash + git add . + git commit -m "feat: add new feature" + ``` + + Pre-commit hooks will automatically run and check your code. + +7. **Push and create a Pull Request** + + ```bash + git push origin feature/my-feature + ``` + + Then create a Pull Request on GitHub. + +## Code Style Guidelines + +This project uses: + +- **rustfmt** for code formatting +- **cargo test** for testing + +### Code Standards + +- Follow Rust idioms and best practices +- Use documentation comments (`///`) for all public APIs +- Write tests for all new functionality +- Keep functions focused and reasonably sized +- Keep files under 1000 lines +- Use meaningful variable and function names + +### Documentation Format + +Use Rust documentation comments: + +```rust +/// Brief description of the function. +/// +/// Longer description if needed. +/// +/// # Arguments +/// +/// * `arg1` - Description of arg1 +/// * `arg2` - Description of arg2 +/// +/// # Returns +/// +/// Description of return value +/// +/// # Examples +/// +/// ``` +/// use platform_num::example_function; +/// let result = example_function(1, 2); +/// assert_eq!(result, 3); +/// ``` +pub fn example_function(arg1: i32, arg2: i32) -> i32 { + arg1 + arg2 +} +``` + +## Testing Guidelines + +- Write tests for all new features +- Maintain or improve test coverage +- Use descriptive test names +- Organize tests in modules when appropriate +- Use `#[cfg(test)]` for test-only code + +Example test structure: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + mod my_feature_tests { + use super::*; + + #[test] + fn test_basic_functionality() { + assert_eq!(my_function(), expected_result); + } + + #[test] + fn test_edge_case() { + assert_eq!(my_function(edge_case_input), expected_result); + } + } +} +``` + +## Pull Request Process + +1. Ensure all tests pass locally +2. Update documentation if needed +3. Add a changelog fragment (see step 5 in Development Workflow) +4. Ensure the PR description clearly describes the changes +5. Link any related issues in the PR description +6. Wait for CI checks to pass +7. Address any review feedback + +## Changelog Management + +This project uses a fragment-based changelog system similar to [Scriv](https://scriv.readthedocs.io/) (Python) and [Changesets](https://github.com/changesets/changesets) (JavaScript). + +### Creating a Fragment + +```bash +# Create a new fragment with timestamp +touch changelog.d/$(date +%Y%m%d_%H%M%S)_description.md +``` + +### Fragment Categories + +Use these categories in your fragments: + +- **Added**: New features +- **Changed**: Changes to existing functionality +- **Deprecated**: Features that will be removed in future +- **Removed**: Features that were removed +- **Fixed**: Bug fixes +- **Security**: Security-related changes + +### During Release + +Fragments are automatically collected into CHANGELOG.md during the release process. The release workflow: + +1. Collects all fragments +2. Updates CHANGELOG.md with the new version entry +3. Removes processed fragment files +4. Bumps the version in rust/Cargo.toml +5. Creates a git tag and GitHub release + +## Project Structure + +``` +. +├── .github/workflows/ # GitHub Actions CI/CD +├── changelog.d/ # Changelog fragments +│ ├── README.md # Fragment instructions +│ └── *.md # Individual changelog fragments +├── rust/ # Rust crate +│ ├── src/ +│ │ ├── lib.rs # Library entry point +│ │ └── imp.rs # Implementation +│ ├── Cargo.toml # Rust package configuration +│ └── rust-toolchain.toml # Rust toolchain specification +├── scripts/ # Utility scripts +├── .gitignore # Git ignore patterns +├── .pre-commit-config.yaml # Pre-commit hooks +├── CHANGELOG.md # Project changelog +├── CONTRIBUTING.md # This file +├── LICENSE # LGPL-3.0 +└── README.md # Project README +``` + +## Release Process + +This project uses semantic versioning (MAJOR.MINOR.PATCH): + +- **MAJOR**: Breaking changes +- **MINOR**: New features (backward compatible) +- **PATCH**: Bug fixes (backward compatible) + +Releases are managed through GitHub releases. To trigger a release: + +1. Manually trigger the release workflow with a version bump type +2. Or: Add changelog fragments and merge to main (auto-release) + +## Getting Help + +- Open an issue for bugs or feature requests +- Use discussions for questions and general help +- Check existing issues and PRs before creating new ones + +## Code of Conduct + +- Be respectful and inclusive +- Provide constructive feedback +- Focus on what is best for the community +- Show empathy towards other community members + +Thank you for contributing! diff --git a/changelog.d/20251228_024300_100_percent_test_coverage.md b/changelog.d/20251228_024300_100_percent_test_coverage.md new file mode 100644 index 0000000..2d6ee31 --- /dev/null +++ b/changelog.d/20251228_024300_100_percent_test_coverage.md @@ -0,0 +1,14 @@ +--- +bump: minor +--- + +### Added +- 100% test coverage for all Rust code (67 tests total) +- Comprehensive unit tests for all traits: `Num`, `SignNum`, `ToSigned`, `MaxValue`, `LinkType` +- Edge case tests for unsigned to signed conversion (wrapping behavior) +- Integration tests for combined trait usage +- CI/CD pipeline with GitHub Actions for automated testing, linting, and releases +- Code coverage reporting with cargo-llvm-cov +- Changelog fragment system for automated version management +- Contributing guidelines with development workflow documentation +- Pre-commit hooks configuration for code quality checks diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 0000000..141b46d --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,135 @@ +# Changelog Fragments + +This directory contains changelog fragments that will be collected into `CHANGELOG.md` during releases. + +## How to Add a Changelog Fragment + +When making changes that should be documented in the changelog, create a fragment file: + +```bash +# Create a new fragment with timestamp +touch changelog.d/$(date +%Y%m%d_%H%M%S)_description.md + +# Or manually create a file matching the pattern: YYYYMMDD_HHMMSS_description.md +``` + +## Fragment Format + +Each fragment should include a **frontmatter section** specifying the version bump type: + +```markdown +--- +bump: patch +--- + +### Fixed +- Description of bug fix +``` + +### Bump Types + +Use semantic versioning bump types in the frontmatter: + +- **`major`**: Breaking changes (incompatible API changes) +- **`minor`**: New features (backward compatible) +- **`patch`**: Bug fixes (backward compatible) + +### Content Categories + +Use these categories in your fragment content: + +```markdown +--- +bump: minor +--- + +### Added +- Description of new feature + +### Changed +- Description of change to existing functionality + +### Fixed +- Description of bug fix + +### Removed +- Description of removed feature + +### Deprecated +- Description of deprecated feature + +### Security +- Description of security fix +``` + +## Examples + +### Adding a new feature (minor bump) + +```markdown +--- +bump: minor +--- + +### Added +- New async processing mode for batch operations +``` + +### Fixing a bug (patch bump) + +```markdown +--- +bump: patch +--- + +### Fixed +- Fixed memory leak in connection pool handling +``` + +### Breaking change (major bump) + +```markdown +--- +bump: major +--- + +### Changed +- Renamed `process()` to `process_async()` - this is a breaking change + +### Removed +- Removed deprecated `legacy_mode` option +``` + +## Why Fragments? + +Using changelog fragments (similar to [Changesets](https://github.com/changesets/changesets) in JavaScript and [Scriv](https://scriv.readthedocs.io/) in Python): + +1. **No merge conflicts**: Multiple PRs can add fragments without conflicts +2. **Per-PR documentation**: Each PR documents its own changes +3. **Automated version bumping**: Version bump type is specified per-change +4. **Automated collection**: Fragments are automatically collected during release +5. **Consistent format**: Template ensures consistent changelog entries + +## How It Works + +1. **During PR**: Add a fragment file with your changes and bump type +2. **On merge to main**: The release workflow automatically: + - Reads all fragment files and determines the highest bump type + - Bumps the version in `rust/Cargo.toml` accordingly + - Collects fragments into `CHANGELOG.md` + - Creates a git tag and GitHub release + - Removes processed fragment files + +## Multiple PRs and Bump Priority + +When multiple PRs are merged before a release, all pending fragments are processed together. The **highest** bump type wins: + +- If any fragment specifies `major`, the release is a major version bump +- Otherwise, if any specifies `minor`, the release is a minor version bump +- Otherwise, the release is a patch version bump + +This ensures that breaking changes are never missed, even when combined with smaller changes. + +## Default Behavior + +If a fragment doesn't include a bump type in the frontmatter, it defaults to `patch`. diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..e618171 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "platform-num" +version = "0.1.0" +dependencies = [ + "num-traits", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 62569ad..d58f98d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,14 +1,26 @@ [package] name = "platform-num" -version = "0.1.0-aplha.1" +version = "0.1.0" edition = "2018" authors = ["uselesssgoddess", "Linksplatform Team "] license = "LGPL-3.0" -repository = "https://github.com/linksplatform/platform-rs" -homepage = "https://github.com/linksplatform/platform-rs" +repository = "https://github.com/linksplatform/Numbers" +homepage = "https://github.com/linksplatform/Numbers" description = """ Numbers for linksplatform """ +readme = "../README.md" +keywords = ["numbers", "linksplatform", "traits"] +categories = ["development-tools"] + +[lib] +name = "platform_num" +path = "src/lib.rs" [dependencies] num-traits = "0.2.14" + +[profile.release] +lto = true +codegen-units = 1 +strip = true diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 0000000..ac4ef6f --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +/** + * Bump version in rust/Cargo.toml + * Usage: node scripts/bump-version.mjs --bump-type [--dry-run] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, writeFileSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import lino-arguments for CLI argument parsing +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('dry-run', { + type: 'boolean', + default: false, + describe: 'Show what would be done without making changes', + }), +}); + +const { bumpType, dryRun } = config; + +if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/bump-version.mjs --bump-type [--dry-run]' + ); + process.exit(1); +} + +/** + * Get current version from rust/Cargo.toml + * @returns {{major: number, minor: number, patch: number, prerelease: string}} + */ +function getCurrentVersion() { + const cargoToml = readFileSync('rust/Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"(\d+)\.(\d+)\.(\d+)(?:-([^"]+))?"/m); + + if (!match) { + console.error('Error: Could not parse version from rust/Cargo.toml'); + process.exit(1); + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + prerelease: match[4] || '', + }; +} + +/** + * Calculate new version based on bump type + * @param {{major: number, minor: number, patch: number}} current + * @param {string} bumpType + * @returns {string} + */ +function calculateNewVersion(current, bumpType) { + const { major, minor, patch } = current; + + switch (bumpType) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + return `${major}.${minor}.${patch + 1}`; + default: + throw new Error(`Invalid bump type: ${bumpType}`); + } +} + +/** + * Update version in rust/Cargo.toml + * @param {string} newVersion + */ +function updateCargoToml(newVersion) { + let cargoToml = readFileSync('rust/Cargo.toml', 'utf-8'); + cargoToml = cargoToml.replace( + /^(version\s*=\s*")[^"]+(")/m, + `$1${newVersion}$2` + ); + writeFileSync('rust/Cargo.toml', cargoToml, 'utf-8'); +} + +try { + const current = getCurrentVersion(); + const currentStr = current.prerelease + ? `${current.major}.${current.minor}.${current.patch}-${current.prerelease}` + : `${current.major}.${current.minor}.${current.patch}`; + const newVersion = calculateNewVersion(current, bumpType); + + console.log(`Current version: ${currentStr}`); + console.log(`New version: ${newVersion}`); + + if (dryRun) { + console.log('Dry run - no changes made'); + } else { + updateCargoToml(newVersion); + console.log('Updated rust/Cargo.toml'); + } +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs new file mode 100644 index 0000000..d85208b --- /dev/null +++ b/scripts/check-file-size.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node + +/** + * Check for files exceeding the maximum allowed line count + * Exits with error code 1 if any files exceed the limit + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + */ + +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, relative, extname } from 'path'; + +const MAX_LINES = 1000; +const FILE_EXTENSIONS = ['.rs']; +const EXCLUDE_PATTERNS = ['target', '.git', 'node_modules']; + +/** + * Check if a path should be excluded + * @param {string} path + * @returns {boolean} + */ +function shouldExclude(path) { + return EXCLUDE_PATTERNS.some((pattern) => path.includes(pattern)); +} + +/** + * Recursively find all Rust files in a directory + * @param {string} directory + * @returns {string[]} + */ +function findRustFiles(directory) { + const files = []; + + function walkDir(dir) { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (shouldExclude(fullPath)) { + continue; + } + + if (entry.isDirectory()) { + walkDir(fullPath); + } else if (entry.isFile() && FILE_EXTENSIONS.includes(extname(entry.name))) { + files.push(fullPath); + } + } + } + + walkDir(directory); + return files; +} + +/** + * Count lines in a file + * @param {string} filePath + * @returns {number} + */ +function countLines(filePath) { + const content = readFileSync(filePath, 'utf-8'); + return content.split('\n').length; +} + +try { + const cwd = process.cwd(); + console.log(`\nChecking Rust files for maximum ${MAX_LINES} lines...\n`); + + const files = findRustFiles(join(cwd, 'rust')); + const violations = []; + + for (const file of files) { + const lineCount = countLines(file); + if (lineCount > MAX_LINES) { + violations.push({ + file: relative(cwd, file), + lines: lineCount, + }); + } + } + + if (violations.length === 0) { + console.log('All files are within the line limit\n'); + process.exit(0); + } else { + console.log('Found files exceeding the line limit:\n'); + for (const violation of violations) { + console.log( + ` ${violation.file}: ${violation.lines} lines (exceeds ${MAX_LINES})` + ); + } + console.log(`\nPlease refactor these files to be under ${MAX_LINES} lines\n`); + process.exit(1); + } +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/scripts/collect-changelog.mjs b/scripts/collect-changelog.mjs new file mode 100644 index 0000000..119c0ea --- /dev/null +++ b/scripts/collect-changelog.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +/** + * Collect changelog fragments into CHANGELOG.md + * This script collects all .md files from changelog.d/ (except README.md) + * and prepends them to CHANGELOG.md, then removes the processed fragments. + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + */ + +import { + readFileSync, + writeFileSync, + readdirSync, + unlinkSync, + existsSync, +} from 'fs'; +import { join } from 'path'; + +const CHANGELOG_DIR = 'changelog.d'; +const CHANGELOG_FILE = 'CHANGELOG.md'; +const INSERT_MARKER = ''; + +/** + * Get version from rust/Cargo.toml + * @returns {string} + */ +function getVersionFromCargo() { + const cargoToml = readFileSync('rust/Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"([^"]+)"/m); + + if (!match) { + console.error('Error: Could not find version in rust/Cargo.toml'); + process.exit(1); + } + + return match[1]; +} + +/** + * Strip frontmatter from markdown content + * @param {string} content - Markdown content potentially with frontmatter + * @returns {string} - Content without frontmatter + */ +function stripFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); + if (frontmatterMatch) { + return frontmatterMatch[1].trim(); + } + return content.trim(); +} + +/** + * Collect all changelog fragments + * @returns {string} + */ +function collectFragments() { + if (!existsSync(CHANGELOG_DIR)) { + return ''; + } + + const files = readdirSync(CHANGELOG_DIR) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .sort(); + + const fragments = []; + for (const file of files) { + const rawContent = readFileSync(join(CHANGELOG_DIR, file), 'utf-8'); + // Strip frontmatter (which contains bump type metadata) + const content = stripFrontmatter(rawContent); + if (content) { + fragments.push(content); + } + } + + return fragments.join('\n\n'); +} + +/** + * Update CHANGELOG.md with collected fragments + * @param {string} version + * @param {string} fragments + */ +function updateChangelog(version, fragments) { + const dateStr = new Date().toISOString().split('T')[0]; + const newEntry = `\n## [${version}] - ${dateStr}\n\n${fragments}\n`; + + if (existsSync(CHANGELOG_FILE)) { + let content = readFileSync(CHANGELOG_FILE, 'utf-8'); + + if (content.includes(INSERT_MARKER)) { + content = content.replace(INSERT_MARKER, `${INSERT_MARKER}${newEntry}`); + } else { + // Insert after the first ## heading + const lines = content.split('\n'); + let insertIndex = -1; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('## [')) { + insertIndex = i; + break; + } + } + + if (insertIndex >= 0) { + lines.splice(insertIndex, 0, newEntry); + content = lines.join('\n'); + } else { + // Append after the main heading + content += newEntry; + } + } + + writeFileSync(CHANGELOG_FILE, content, 'utf-8'); + } else { + const content = `# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +${INSERT_MARKER} +${newEntry} +`; + writeFileSync(CHANGELOG_FILE, content, 'utf-8'); + } + + console.log(`Updated CHANGELOG.md with version ${version}`); +} + +/** + * Remove processed changelog fragments + */ +function removeFragments() { + if (!existsSync(CHANGELOG_DIR)) { + return; + } + + const files = readdirSync(CHANGELOG_DIR).filter( + (f) => f.endsWith('.md') && f !== 'README.md' + ); + + for (const file of files) { + const filePath = join(CHANGELOG_DIR, file); + unlinkSync(filePath); + console.log(`Removed ${filePath}`); + } +} + +try { + const version = getVersionFromCargo(); + console.log(`Collecting changelog fragments for version ${version}`); + + const fragments = collectFragments(); + + if (!fragments) { + console.log('No changelog fragments found'); + process.exit(0); + } + + updateChangelog(version, fragments); + removeFragments(); + + console.log('Changelog collection complete'); +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs new file mode 100644 index 0000000..1d82c96 --- /dev/null +++ b/scripts/create-github-release.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +/** + * Create GitHub Release from CHANGELOG.md + * Usage: node scripts/create-github-release.mjs --release-version --repository + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, existsSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('release-version', { + type: 'string', + default: getenv('VERSION', ''), + describe: 'Version number (e.g., 1.0.0)', + }) + .option('repository', { + type: 'string', + default: getenv('REPOSITORY', ''), + describe: 'GitHub repository (e.g., owner/repo)', + }), +}); + +const { releaseVersion: version, repository } = config; + +if (!version || !repository) { + console.error('Error: Missing required arguments'); + console.error( + 'Usage: node scripts/create-github-release.mjs --release-version --repository ' + ); + process.exit(1); +} + +const tag = `v${version}`; + +console.log(`Creating GitHub release for ${tag}...`); + +/** + * Extract changelog content for a specific version + * @param {string} version + * @returns {string} + */ +function getChangelogForVersion(version) { + const changelogPath = 'CHANGELOG.md'; + + if (!existsSync(changelogPath)) { + return `Release v${version}`; + } + + const content = readFileSync(changelogPath, 'utf-8'); + + // Find the section for this version + const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp( + `## \\[${escapedVersion}\\].*?\\n([\\s\\S]*?)(?=\\n## \\[|$)` + ); + const match = content.match(pattern); + + if (match) { + return match[1].trim(); + } + + return `Release v${version}`; +} + +try { + const releaseNotes = getChangelogForVersion(version); + + // Create release using GitHub API with JSON input + // This avoids shell escaping issues + const payload = JSON.stringify({ + tag_name: tag, + name: `v${version}`, + body: releaseNotes, + }); + + try { + await $`gh api repos/${repository}/releases -X POST --input -`.run({ + stdin: payload, + }); + console.log(`Created GitHub release: ${tag}`); + } catch (error) { + // Check if release already exists + if (error.message && error.message.includes('already exists')) { + console.log(`Release ${tag} already exists, skipping`); + } else { + throw error; + } + } +} catch (error) { + console.error('Error creating release:', error.message); + process.exit(1); +} diff --git a/scripts/get-bump-type.mjs b/scripts/get-bump-type.mjs new file mode 100644 index 0000000..ff9f77c --- /dev/null +++ b/scripts/get-bump-type.mjs @@ -0,0 +1,152 @@ +#!/usr/bin/env node + +/** + * Parse changelog fragments and determine version bump type + * + * This script reads changeset fragments from changelog.d/ and determines + * the version bump type based on the frontmatter in each fragment. + * + * Fragment format: + * --- + * bump: patch|minor|major + * --- + * + * ### Added + * - Your changes here + * + * Usage: node scripts/get-bump-type.mjs [--default ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, readdirSync, existsSync, appendFileSync } from 'fs'; +import { join } from 'path'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import lino-arguments for CLI argument parsing +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('default', { + type: 'string', + default: getenv('DEFAULT_BUMP', 'patch'), + describe: 'Default bump type if no fragments specify one', + choices: ['major', 'minor', 'patch'], + }), +}); + +const { default: defaultBump } = config; + +const CHANGELOG_DIR = 'changelog.d'; + +// Bump type priority (higher = more significant) +const BUMP_PRIORITY = { + patch: 1, + minor: 2, + major: 3, +}; + +/** + * Parse frontmatter from a markdown file + * @param {string} content - File content + * @returns {{bump?: string, content: string}} + */ +function parseFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); + + if (!frontmatterMatch) { + return { content }; + } + + const frontmatter = frontmatterMatch[1]; + const body = frontmatterMatch[2]; + + // Parse YAML-like frontmatter (simple key: value format) + const data = {}; + for (const line of frontmatter.split('\n')) { + const match = line.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/); + if (match) { + data[match[1]] = match[2]; + } + } + + return { ...data, content: body }; +} + +/** + * Get all changelog fragments and determine bump type + * @returns {{bumpType: string, fragmentCount: number}} + */ +function determineBumpType() { + if (!existsSync(CHANGELOG_DIR)) { + console.log(`No ${CHANGELOG_DIR} directory found`); + return { bumpType: defaultBump, fragmentCount: 0 }; + } + + const files = readdirSync(CHANGELOG_DIR) + .filter((f) => f.endsWith('.md') && f !== 'README.md') + .sort(); + + if (files.length === 0) { + console.log('No changelog fragments found'); + return { bumpType: defaultBump, fragmentCount: 0 }; + } + + let highestPriority = 0; + let highestBumpType = defaultBump; + + for (const file of files) { + const content = readFileSync(join(CHANGELOG_DIR, file), 'utf-8'); + const { bump } = parseFrontmatter(content); + + if (bump && BUMP_PRIORITY[bump]) { + const priority = BUMP_PRIORITY[bump]; + if (priority > highestPriority) { + highestPriority = priority; + highestBumpType = bump; + } + console.log(`Fragment ${file}: bump=${bump}`); + } else { + console.log(`Fragment ${file}: no bump specified, using default`); + } + } + + return { bumpType: highestBumpType, fragmentCount: files.length }; +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } + // Also log for visibility + console.log(`Output: ${key}=${value}`); +} + +try { + const { bumpType, fragmentCount } = determineBumpType(); + + console.log(`\nDetermined bump type: ${bumpType} (from ${fragmentCount} fragment(s))`); + + setOutput('bump_type', bumpType); + setOutput('fragment_count', String(fragmentCount)); + setOutput('has_fragments', fragmentCount > 0 ? 'true' : 'false'); + +} catch (error) { + console.error('Error:', error.message); + process.exit(1); +} diff --git a/scripts/version-and-commit.mjs b/scripts/version-and-commit.mjs new file mode 100644 index 0000000..ac89d83 --- /dev/null +++ b/scripts/version-and-commit.mjs @@ -0,0 +1,276 @@ +#!/usr/bin/env node + +/** + * Bump version in rust/Cargo.toml and commit changes + * Used by the CI/CD pipeline for releases + * + * Usage: node scripts/version-and-commit.mjs --bump-type [--description ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, writeFileSync, appendFileSync, readdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('description', { + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Release description', + }), +}); + +const { bumpType, description } = config; + +if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/version-and-commit.mjs --bump-type [--description ]' + ); + process.exit(1); +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } + // Also log for visibility + console.log(`::set-output name=${key}::${value}`); +} + +/** + * Get current version from rust/Cargo.toml + * @returns {{major: number, minor: number, patch: number}} + */ +function getCurrentVersion() { + const cargoToml = readFileSync('rust/Cargo.toml', 'utf-8'); + const match = cargoToml.match(/^version\s*=\s*"(\d+)\.(\d+)\.(\d+)(?:-[^"]+)?"/m); + + if (!match) { + console.error('Error: Could not parse version from rust/Cargo.toml'); + process.exit(1); + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + }; +} + +/** + * Calculate new version based on bump type + * @param {{major: number, minor: number, patch: number}} current + * @param {string} bumpType + * @returns {string} + */ +function calculateNewVersion(current, bumpType) { + const { major, minor, patch } = current; + + switch (bumpType) { + case 'major': + return `${major + 1}.0.0`; + case 'minor': + return `${major}.${minor + 1}.0`; + case 'patch': + return `${major}.${minor}.${patch + 1}`; + default: + throw new Error(`Invalid bump type: ${bumpType}`); + } +} + +/** + * Update version in rust/Cargo.toml + * @param {string} newVersion + */ +function updateCargoToml(newVersion) { + let cargoToml = readFileSync('rust/Cargo.toml', 'utf-8'); + cargoToml = cargoToml.replace( + /^(version\s*=\s*")[^"]+(")/m, + `$1${newVersion}$2` + ); + writeFileSync('rust/Cargo.toml', cargoToml, 'utf-8'); + console.log(`Updated rust/Cargo.toml to version ${newVersion}`); +} + +/** + * Check if a git tag exists for this version + * @param {string} version + * @returns {Promise} + */ +async function checkTagExists(version) { + try { + await $`git rev-parse v${version}`.run({ capture: true }); + return true; + } catch { + return false; + } +} + +/** + * Strip frontmatter from markdown content + * @param {string} content - Markdown content potentially with frontmatter + * @returns {string} - Content without frontmatter + */ +function stripFrontmatter(content) { + const frontmatterMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); + if (frontmatterMatch) { + return frontmatterMatch[1].trim(); + } + return content.trim(); +} + +/** + * Collect changelog fragments and update CHANGELOG.md + * @param {string} version + */ +function collectChangelog(version) { + const changelogDir = 'changelog.d'; + const changelogFile = 'CHANGELOG.md'; + + if (!existsSync(changelogDir)) { + return; + } + + const files = readdirSync(changelogDir).filter( + (f) => f.endsWith('.md') && f !== 'README.md' + ); + + if (files.length === 0) { + return; + } + + const fragments = files + .sort() + .map((f) => { + const rawContent = readFileSync(join(changelogDir, f), 'utf-8'); + // Strip frontmatter (which contains bump type metadata) + return stripFrontmatter(rawContent); + }) + .filter(Boolean) + .join('\n\n'); + + if (!fragments) { + return; + } + + const dateStr = new Date().toISOString().split('T')[0]; + const newEntry = `\n## [${version}] - ${dateStr}\n\n${fragments}\n`; + + if (existsSync(changelogFile)) { + let content = readFileSync(changelogFile, 'utf-8'); + const lines = content.split('\n'); + let insertIndex = -1; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('## [')) { + insertIndex = i; + break; + } + } + + if (insertIndex >= 0) { + lines.splice(insertIndex, 0, newEntry); + content = lines.join('\n'); + } else { + content += newEntry; + } + + writeFileSync(changelogFile, content, 'utf-8'); + } + + console.log(`Collected ${files.length} changelog fragment(s)`); +} + +async function main() { + try { + // Configure git + await $`git config user.name "github-actions[bot]"`; + await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; + + const current = getCurrentVersion(); + const newVersion = calculateNewVersion(current, bumpType); + + // Check if this version was already released + if (await checkTagExists(newVersion)) { + console.log(`Tag v${newVersion} already exists`); + setOutput('already_released', 'true'); + setOutput('new_version', newVersion); + return; + } + + // Update version in rust/Cargo.toml + updateCargoToml(newVersion); + + // Collect changelog fragments + collectChangelog(newVersion); + + // Stage rust/Cargo.toml and CHANGELOG.md + await $`git add rust/Cargo.toml CHANGELOG.md`; + + // Check if there are changes to commit + try { + await $`git diff --cached --quiet`.run({ capture: true }); + // No changes to commit + console.log('No changes to commit'); + setOutput('version_committed', 'false'); + setOutput('new_version', newVersion); + return; + } catch { + // There are changes to commit (git diff exits with 1 when there are differences) + } + + // Commit changes + const commitMsg = description + ? `chore: release v${newVersion}\n\n${description}` + : `chore: release v${newVersion}`; + await $`git commit -m ${commitMsg}`; + console.log(`Committed version ${newVersion}`); + + // Create tag + const tagMsg = description + ? `Release v${newVersion}\n\n${description}` + : `Release v${newVersion}`; + await $`git tag -a v${newVersion} -m ${tagMsg}`; + console.log(`Created tag v${newVersion}`); + + // Push changes and tag + await $`git push`; + await $`git push --tags`; + console.log('Pushed changes and tags'); + + setOutput('version_committed', 'true'); + setOutput('new_version', newVersion); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main();