rusty-ts 0.2.0

Prefix each line of stdin with a timestamp — a Rust port of moreutils `ts` with strict-compat mode, IANA timezone control, and a reusable byte-typed library API.
Documentation
# rusty-ts CI workflow.
#
# v0.2.0 (E011 Phase 3) expands the v0.1.0 pipeline with the portfolio-wide
# Cargo Features Convention 5-tier matrix per spec 00011 §Per-Port v0.2.0
# CI Matrix + FR-010..FR-014:
#
#   Tier 1 — test-default                (full DDR-003 cross-compile matrix)
#   Tier 2 — test-no-default             (Linux x86_64 only + dep-tree audit)
#   Tier 3 — test-<bundle>               (per preset, Linux only)
#   Tier 4 — check-leaf-<leaf>           (SKIPPED — zero leaves, see below)
#   Tier 5 — lint-convention             (vendored feature-lint, Linux only)
#
# rusty-ts is a single-capability port per spec 00011 §Scope Edge Cases —
# minimum-convention surface with zero carved leaves. Tier 4 is intentionally
# empty (no `check-leaf-<leaf>` jobs) because there are no leaves to check
# per docs/feature-layout.md §v0.2.0 Carved Leaves.
#
# Pre-existing v0.1.0 gates (rustfmt, clippy, audit, MSRV) are preserved
# unchanged.

name: CI

on:
  push:
    branches: [main]
    tags: ["v[0-9]+.[0-9]+.[0-9]+"]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

permissions:
  contents: read

jobs:
  fmt:
    name: rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

  clippy:
    name: clippy (deny warnings)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo clippy --all-targets --all-features -- -D warnings

  audit:
    name: cargo audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Install a stable toolchain so any rustup-aware tool that runs outside
      # the workspace gets a workable Rust. We do NOT cd into the repo before
      # invoking cargo-audit so the repo's `rust-toolchain.toml` (1.85 pin)
      # doesn't engage during the install step.
      - uses: dtolnay/rust-toolchain@stable
      # Prebuilt cargo-audit binary — avoids compiling from source, which
      # would fail under 1.85 due to icu_*/home/time crates bumping MSRV
      # to 1.86+.
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-audit
      - run: cargo audit
        env:
          # Force stable for this step in case `cargo` is wrapped by rustup
          # and detects the workspace's rust-toolchain.toml.
          RUSTUP_TOOLCHAIN: stable

  msrv:
    name: MSRV (1.85)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.85
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --all-features
      - run: cargo test --all-features

  # -----------------------------------------------------------------------------
  # Tier 1 — test-default: full DDR-003 cross-compile matrix on `default`
  # features. `default = ["full"]` post-v0.2.0; for rusty-ts (single-cap port)
  # `full` resolves to `cli` alone, so the kitchen-sink test covers the full
  # documented surface.
  # -----------------------------------------------------------------------------
  test-default:
    name: test-default ${{ matrix.target }}
    needs: [fmt, clippy]
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
          - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, cross: true }
          - { os: macos-latest, target: x86_64-apple-darwin }
          - { os: macos-latest, target: aarch64-apple-darwin }
          - { os: windows-latest, target: x86_64-pc-windows-msvc }
    env:
      TZ: UTC
      LC_ALL: C.UTF-8
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      # The workspace pins to 1.85 via rust-toolchain.toml. dtolnay added the
      # target to the stable channel, but cargo will switch to 1.85 when run
      # from the workspace — and 1.85 doesn't have the cross-compile target
      # installed. Add it explicitly here.
      - name: Add cross-compile target to 1.85 toolchain
        if: '!matrix.cross'
        run: rustup target add ${{ matrix.target }} --toolchain 1.85
        shell: bash
      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}
      - name: Install cross
        if: matrix.cross
        run: cargo install cross --locked
      - name: Build
        run: |
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --target ${{ matrix.target }} --all-features
          else
            cargo build --release --target ${{ matrix.target }} --all-features
          fi
        shell: bash
      - name: Test
        if: matrix.cross != true
        run: cargo test --release --target ${{ matrix.target }} --all-features
      # SC-002 default-install smoke (Linux x86_64 only — the binary is the
      # same on every target). Runs the binary against representative input
      # and asserts the documented capability emits the expected prefix.
      - name: Default install + capability smoke (SC-002)
        if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.cross != true
        run: |
          cargo install --path . --no-track --force --locked
          printf 'hello\nworld\n' | rusty-ts '[%Y]' > smoke.out
          if ! grep -q '^\[20[0-9][0-9]\] hello$' smoke.out; then
            echo "::error::SC-002 smoke failed — expected timestamped 'hello' line"
            cat smoke.out
            exit 1
          fi
          if ! grep -q '^\[20[0-9][0-9]\] world$' smoke.out; then
            echo "::error::SC-002 smoke failed — expected timestamped 'world' line"
            cat smoke.out
            exit 1
          fi
          echo "SC-002 default-install smoke: PASS"
        shell: bash

  # -----------------------------------------------------------------------------
  # Tier 2 — test-no-default: bare library on Linux x86_64. Verifies CLI-only
  # deps are absent from the library build (SC-001 evidence).
  # -----------------------------------------------------------------------------
  test-no-default:
    name: test-no-default (linux-x86_64)
    needs: [fmt, clippy]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --no-default-features
      - run: cargo test --no-default-features --lib
      - name: Dep-tree audit (SC-001)
        run: |
          # Assert no CLI-only deps leak into the bare-library build.
          if cargo tree --no-default-features --prefix none --edges normal --no-dedupe \
              | grep -E '^(clap|clap_complete|anyhow)\b'; then
            echo "::error::CLI-only dep leaked into --no-default-features build"
            exit 1
          fi
          echo "SC-001 dep-tree audit: PASS"

  # -----------------------------------------------------------------------------
  # Tier 3 — test-<bundle>: one job per preset bundle. Linux x86_64 only.
  # rusty-ts declares 2 preset bundles per FR-007 (single-cap minimum).
  # -----------------------------------------------------------------------------
  test-ts-classic:
    name: test-ts-classic (linux-x86_64)
    needs: [fmt, clippy]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --no-default-features --features ts-classic
      - run: cargo test --no-default-features --features ts-classic --lib
      # SC-003 per-preset install + size check.
      - name: Per-preset install + size check (SC-003)
        run: |
          cargo install --path . --no-track --force --locked \
            --no-default-features --features ts-classic
          bundle_size=$(stat -c%s "$(which rusty-ts)")
          echo "ts-classic binary size: $bundle_size bytes"
          echo "$bundle_size" > /tmp/ts-classic-size.txt
          # Baseline sanity: assert the binary is not zero-sized.
          if [ "$bundle_size" -le "0" ]; then
            echo "::error::SC-003 preset install produced empty binary"
            exit 1
          fi
          echo "SC-003 ts-classic install: PASS"
        shell: bash

  test-ts-minimal:
    name: test-ts-minimal (linux-x86_64)
    needs: [fmt, clippy]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --no-default-features --features ts-minimal
      - run: cargo test --no-default-features --features ts-minimal --lib
      # SC-003 per-preset install + size check.
      - name: Per-preset install + size check (SC-003)
        run: |
          cargo install --path . --no-track --force --locked \
            --no-default-features --features ts-minimal
          bundle_size=$(stat -c%s "$(which rusty-ts)")
          echo "ts-minimal binary size: $bundle_size bytes"
          if [ "$bundle_size" -le "0" ]; then
            echo "::error::SC-003 preset install produced empty binary"
            exit 1
          fi
          echo "SC-003 ts-minimal install: PASS"
        shell: bash

  # -----------------------------------------------------------------------------
  # Tier 4 — check-leaf-<leaf>: SKIPPED for rusty-ts.
  #
  # rusty-ts is a single-capability port per spec 00011 §Scope Edge Cases.
  # No leaves are carved beyond the required umbrellas (see
  # docs/feature-layout.md §v0.2.0 Carved Leaves — explicit rejection
  # rationale for each candidate leaf). The feature-lint script's
  # leaf-CI-matrix sub-check passes vacuously (no leaves to enumerate).
  # -----------------------------------------------------------------------------

  # -----------------------------------------------------------------------------
  # SC-004 — keep-list workaround smoke. Single-capability port has a
  # trivial keep-list (just `cli` plus optional `ts-alias`), but the job
  # exists to validate the FR-023 documented invocation path.
  # -----------------------------------------------------------------------------
  test-keeplist:
    name: test-keeplist (linux-x86_64)
    needs: [fmt, clippy]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install with explicit keep-list
        run: |
          cargo install --path . --no-track --force --locked \
            --no-default-features --features cli
          printf 'hello\n' | rusty-ts '[%H:%M]' > keeplist.out
          if ! grep -q ' hello$' keeplist.out; then
            echo "::error::SC-004 keep-list install: binary did not emit expected output"
            cat keeplist.out
            exit 1
          fi
          echo "SC-004 keep-list (--features cli) install: PASS"
        shell: bash

  # -----------------------------------------------------------------------------
  # Tier 5 — lint-convention: feature-lint script is vendored into the port
  # (tools/feature-lint/) so the CI does not depend on cross-repo checkout
  # of the (private) umbrella. Each port keeps its own copy in sync with
  # the umbrella source of truth.
  # -----------------------------------------------------------------------------
  lint-convention:
    name: lint-convention (linux-x86_64)
    needs: [fmt, clippy]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout port
        uses: actions/checkout@v4

      - name: Run feature-lint
        env:
          UMBRELLA_PATH: ${{ github.workspace }}
          PORT_PATH: ${{ github.workspace }}
        run: bash tools/feature-lint/run.sh

  # -----------------------------------------------------------------------------
  # Convention-violation self-test (SC-007 evidence). Manually triggered;
  # creates a temp Cargo.toml without `ts-classic` and asserts the lint
  # script catches the violation. Spec 00011 T046.
  # -----------------------------------------------------------------------------
  convention-lint-self-test:
    name: convention-lint-self-test
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout port
        uses: actions/checkout@v4

      - name: Remove ts-classic umbrella from Cargo.toml
        run: |
          sed -i '/^ts-classic\s*=/d' Cargo.toml
          # Verify removal
          if grep -E '^ts-classic\s*=' Cargo.toml; then
            echo "::error::ts-classic removal failed"
            exit 1
          fi

      - name: Invoke feature-lint and expect non-zero exit
        env:
          UMBRELLA_PATH: ${{ github.workspace }}
          PORT_PATH: ${{ github.workspace }}
        run: |
          set +e
          bash tools/feature-lint/run.sh 2>stderr.log
          rc=$?
          set -e
          cat stderr.log
          if [ "$rc" = "0" ]; then
            echo "::error::feature-lint should have failed with exit 2 but returned 0"
            exit 1
          fi
          if ! grep -q "required umbrella 'ts-classic' missing" stderr.log; then
            echo "::error::expected diagnostic 'required umbrella ts-classic missing' not found"
            exit 1
          fi
          echo "convention-lint-self-test: PASS"

  # -----------------------------------------------------------------------------
  # Library-without-binary build (v0.1.x parity gate). Retained verbatim;
  # the Tier 2 test-no-default job covers a superset of this check, but
  # this job runs in parallel + isolation for the publish-dry-run gate.
  # -----------------------------------------------------------------------------
  library-no-default-features:
    name: library-without-binary
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --no-default-features
      - run: cargo test --no-default-features --lib

  # -----------------------------------------------------------------------------
  # Publish-dry-run + final gate. Runs after every tier above.
  # -----------------------------------------------------------------------------
  publish-dry-run:
    name: cargo publish --dry-run
    needs:
      - fmt
      - clippy
      - audit
      - msrv
      - test-default
      - test-no-default
      - test-ts-classic
      - test-ts-minimal
      - test-keeplist
      - lint-convention
      - library-no-default-features
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo publish --dry-run --all-features