fix: add -L flag for screen log capture on macOS 4.00.03 (issue #96) #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Rust CI/CD | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'rust/**' | |
| - 'lib/**' | |
| - 'scripts/**' | |
| - '.github/workflows/rust.yml' | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - 'rust/**' | |
| - 'lib/**' | |
| - 'scripts/**' | |
| - '.github/workflows/rust.yml' | |
| 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: rust-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: -Dwarnings | |
| jobs: | |
| # === DETECT CHANGES === | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'workflow_dispatch' | |
| outputs: | |
| rs-changed: ${{ steps.changes.outputs.rs-changed }} | |
| toml-changed: ${{ steps.changes.outputs.toml-changed }} | |
| mjs-changed: ${{ steps.changes.outputs.mjs-changed }} | |
| docs-changed: ${{ steps.changes.outputs.docs-changed }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow-changed }} | |
| any-rust-code-changed: ${{ steps.changes.outputs.any-rust-code-changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Detect changes | |
| id: changes | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: node scripts/detect-code-changes.mjs | |
| # === VERSION CHANGE CHECK === | |
| # Prohibit manual version changes in Cargo.toml - versions should only be changed by CI/CD | |
| version-check: | |
| name: Check for Manual Version Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for version changes in Cargo.toml | |
| run: | | |
| # Get the diff for Cargo.toml | |
| VERSION_DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- rust/Cargo.toml | grep -E '^\+version\s*=' || true) | |
| if [ -n "$VERSION_DIFF" ]; then | |
| echo "::error::Manual version change detected in rust/Cargo.toml" | |
| echo "" | |
| echo "Version changes in Cargo.toml are prohibited in pull requests." | |
| echo "Versions are managed automatically by the CI/CD pipeline using changelog fragments." | |
| echo "" | |
| echo "To request a release:" | |
| echo " 1. Add a changelog fragment in rust/changelog.d/ describing your changes" | |
| echo " 2. The release workflow will automatically bump the version when merged" | |
| echo "" | |
| echo "Detected change:" | |
| echo "$VERSION_DIFF" | |
| exit 1 | |
| fi | |
| echo "No manual version changes detected - check passed" | |
| # === CHANGELOG CHECK === | |
| changelog: | |
| name: Changelog Fragment Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-rust-code-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for changelog fragments | |
| run: | | |
| # Get changed files in PR | |
| CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) | |
| # Check if any Rust source files changed | |
| SOURCE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "^rust/(src/|tests/|Cargo\.toml)" | wc -l) | |
| # Check if a changelog fragment was ADDED in this PR (not just existing) | |
| # We need to check the diff, not just the directory contents | |
| FRAGMENT_ADDED=$(echo "$CHANGED_FILES" | grep -E "^rust/changelog\.d/.*\.md$" | grep -v "README.md" | wc -l) | |
| echo "Source files changed: $SOURCE_CHANGED" | |
| echo "Changelog fragments added in PR: $FRAGMENT_ADDED" | |
| if [ "$SOURCE_CHANGED" -gt 0 ] && [ "$FRAGMENT_ADDED" -eq 0 ]; then | |
| echo "::error::No changelog fragment found in this PR. Please add a changelog entry in rust/changelog.d/" | |
| echo "" | |
| echo "To create a changelog fragment:" | |
| echo " Create a new .md file in rust/changelog.d/ with your changes" | |
| echo " Name it after your PR number, e.g., '123.md'" | |
| echo "" | |
| echo "See rust/changelog.d/README.md for more information." | |
| exit 1 | |
| fi | |
| echo "Changelog check passed" | |
| # === LINT AND FORMAT CHECK === | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: | | |
| github.event_name == 'push' || | |
| github.event_name == 'workflow_dispatch' || | |
| needs.detect-changes.outputs.rs-changed == 'true' || | |
| needs.detect-changes.outputs.toml-changed == 'true' || | |
| needs.detect-changes.outputs.mjs-changed == 'true' || | |
| needs.detect-changes.outputs.docs-changed == 'true' || | |
| needs.detect-changes.outputs.workflow-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - 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 | |
| working-directory: rust | |
| run: cargo fmt --all -- --check | |
| - name: Run Clippy | |
| working-directory: rust | |
| run: cargo clippy --all-targets --all-features | |
| - name: Check file size limit | |
| run: node scripts/check-file-size.mjs | |
| # === TEST === | |
| test: | |
| name: Test (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: [detect-changes, changelog] | |
| if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - 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 | |
| working-directory: rust | |
| run: cargo test --all-features --verbose | |
| - name: Run doc tests | |
| working-directory: rust | |
| run: cargo test --doc --verbose | |
| # Build the binary for execution tracking tests | |
| - name: Build release | |
| working-directory: rust | |
| run: cargo build --release | |
| # Test execution tracking (Unix) | |
| - name: Test execution tracking (Unix) | |
| if: runner.os != 'Windows' | |
| working-directory: rust | |
| env: | |
| START_VERBOSE: '1' | |
| run: | | |
| ./target/release/start echo "Testing execution tracking" | |
| ls -la ~/.start-command/ | |
| cat ~/.start-command/executions.lino | |
| echo "Execution tracking test passed" | |
| # Test execution tracking (Windows) | |
| - name: Test execution tracking (Windows) | |
| if: runner.os == 'Windows' | |
| working-directory: rust | |
| env: | |
| START_VERBOSE: '1' | |
| shell: bash | |
| run: | | |
| ./target/release/start.exe echo "Testing execution tracking" | |
| ls -la "$USERPROFILE/.start-command/" | |
| cat "$USERPROFILE/.start-command/executions.lino" | |
| echo "Execution tracking test passed" | |
| # === TEST PARITY CHECK === | |
| # Fail if Rust has at least 10% fewer test cases than JavaScript | |
| test-parity: | |
| name: Test Count Parity (Rust vs JS) | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.any-rust-code-changed == 'true') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20.x' | |
| - name: Check Rust/JS test count parity | |
| run: node scripts/check-test-parity.mjs | |
| # === CODE COVERAGE === | |
| # Fail if Rust test coverage drops below 80% | |
| coverage: | |
| name: Code Coverage (Rust, Linux only) | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, changelog] | |
| if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - 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-tarpaulin | |
| run: cargo install cargo-tarpaulin | |
| - name: Run coverage | |
| working-directory: rust | |
| run: | | |
| # tarpaulin.toml excludes src/bin/main.rs (CLI entry point, not library logic) | |
| mkdir -p coverage | |
| cargo tarpaulin --all-features --timeout 120 --out Xml --output-dir coverage/ 2>&1 | tee coverage/tarpaulin.log | |
| # tarpaulin outputs: "50.73% coverage, 908/1790 lines covered" | |
| COVERAGE=$(grep -oP '^\d+\.\d+(?=% coverage)' coverage/tarpaulin.log | tail -1) | |
| echo "Coverage: ${COVERAGE}%" | |
| echo "coverage=${COVERAGE}" >> $GITHUB_OUTPUT | |
| # Fail if coverage is below 50% (lib code only; main.rs excluded via tarpaulin.toml) | |
| if python3 -c "import sys; c=float('${COVERAGE}') if '${COVERAGE}' else 0; sys.exit(0 if c >= 50.0 else 1)"; then | |
| echo "✅ Coverage ${COVERAGE}% meets the 50% threshold" | |
| else | |
| echo "❌ Coverage ${COVERAGE}% is below the 50% threshold" | |
| exit 1 | |
| fi | |
| # === BUILD === | |
| # Only build on push to main, not on PRs (tests already verify the code builds) | |
| build: | |
| name: Build Package | |
| runs-on: ubuntu-latest | |
| needs: [lint, test] | |
| if: always() && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - 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 | |
| working-directory: rust | |
| run: cargo build --release --verbose | |
| - name: Check package | |
| working-directory: rust | |
| run: cargo package --list | |
| # === AUTO RELEASE === | |
| auto-release: | |
| name: Auto Release | |
| needs: [lint, test, build] | |
| 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: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - 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" | |
| - name: Determine bump type from changelog fragments | |
| id: bump_type | |
| run: node scripts/get-bump-type.mjs | |
| - name: Check if version already released or no fragments | |
| id: check | |
| working-directory: rust | |
| run: | | |
| if [ "${{ steps.bump_type.outputs.has_fragments }}" != "true" ]; then | |
| CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| if git rev-parse "rust-v$CURRENT_VERSION" >/dev/null 2>&1; then | |
| echo "No changelog fragments and rust-v$CURRENT_VERSION already released" | |
| echo "should_release=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "No changelog fragments but rust-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' | |
| run: | | |
| node scripts/version-and-commit.mjs \ | |
| --bump-type "${{ steps.bump_type.outputs.bump_type }}" \ | |
| --working-dir rust \ | |
| --mode changelog | |
| - name: Get current version | |
| id: current_version | |
| if: steps.check.outputs.should_release == 'true' | |
| working-directory: rust | |
| run: | | |
| CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| - name: Build release | |
| if: steps.check.outputs.should_release == 'true' | |
| working-directory: rust | |
| run: cargo build --release | |
| - name: Create GitHub Release | |
| if: steps.check.outputs.should_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| node scripts/create-github-release.mjs \ | |
| --release-version "${{ steps.current_version.outputs.version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --prefix "rust-" | |
| # === MANUAL RELEASE === | |
| manual-release: | |
| name: Manual Release | |
| needs: [lint, test, build] | |
| 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: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - 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" | |
| - name: Collect changelog fragments | |
| run: | | |
| FRAGMENTS=$(find rust/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 | |
| run: | | |
| node scripts/version-and-commit.mjs \ | |
| --bump-type "${{ github.event.inputs.bump_type }}" \ | |
| --description "${{ github.event.inputs.description }}" \ | |
| --working-dir rust \ | |
| --mode manual | |
| - name: Build release | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| working-directory: rust | |
| 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 }} | |
| run: | | |
| node scripts/create-github-release.mjs \ | |
| --release-version "${{ steps.version.outputs.new_version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --prefix "rust-" |