murk-cli 0.10.1

Encrypted secrets manager for developers — one file, age encryption, git-friendly
Documentation
name: CI

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: rustfmt, clippy
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - run: cargo fmt --check
      - run: cargo clippy --all-features --all-targets -- -D warnings
      - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
      - uses: taiki-e/install-action@f092c064826410a38929a5791d2c0225b94432fe # cargo-audit
        with:
          tool: cargo-audit
      - run: cargo audit --ignore RUSTSEC-2023-0071
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: "22"
      - name: Check version consistency across all release-bumped files
        run: node scripts/check-versions.cjs
      - name: Check the generated CLI and env references are in sync with the model
        run: cargo run --features doc-gen --bin gen-docs -- --check
      - name: Check guide examples map to real commands and tested demo flows
        run: node scripts/check-guide-commands.cjs
      - name: Check for stray local-only reference IDs in committed files
        run: |
          node scripts/check-no-local-refs.cjs --self-test
          node scripts/check-no-local-refs.cjs

  msrv:
    name: MSRV
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          toolchain: "1.89"
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - run: cargo check --all-features --all-targets --locked

  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - uses: taiki-e/install-action@f092c064826410a38929a5791d2c0225b94432fe # nextest
      - run: cargo nextest run --all-features --profile ci
      - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
        if: always()
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: target/nextest/ci/junit.xml

  test-gate:
    name: Test
    runs-on: ubuntu-latest
    needs: test
    if: always()
    permissions: {}
    steps:
      - run: |
          if [[ "${{ needs.test.result }}" != "success" ]]; then
            echo "Test matrix failed"
            exit 1
          fi

  coverage:
    name: Coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - uses: taiki-e/install-action@f092c064826410a38929a5791d2c0225b94432fe # cargo-llvm-cov
        with:
          tool: cargo-llvm-cov
      - run: cargo llvm-cov --codecov --output-path codecov.json -- --test-threads=1
      - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: codecov.json
          fail_ci_if_error: false

  demo-test:
    name: VHS Dress Rehearsal
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - run: sudo apt-get install -y direnv
      - run: make test-demos
      # Hand the release binary the rehearsal just built to vhs-record — the
      # vhs image runs glibc newer than this runner's, so the host build works
      # there and no separate musl build is needed.
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
        with:
          name: murk-linux
          path: target/release/murk
          retention-days: 1

  # Exercises the end-user install path: install.sh against a locally-built
  # release fixture, then a real encrypt/decrypt roundtrip with the installed
  # binary. The other channels are intentionally not installed here — they are
  # built and tested elsewhere: crates.io (cargo build/test in this file),
  # PyPI bindings (python.yaml), npm bindings (node.yaml), and Homebrew (the
  # tap formula is bumped by release.yaml). This job guards install.sh, the
  # path with no other coverage.
  install-smoke:
    name: Install smoke (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
      - run: cargo build --release
      - name: Smoke-test install.sh against a local fixture
        shell: bash
        run: |
          set -euo pipefail
          target=$(rustc -vV | sed -n 's/^host: //p')
          tag=v0.0.0-smoke
          archive="murk-${tag}-${target}.tar.gz"

          # Build a fixture shaped like a real GitHub release: a tarball holding
          # the murk binary, plus a matching SHA256SUMS.
          rel="$RUNNER_TEMP/release"
          mkdir -p "$rel" "$RUNNER_TEMP/stage"
          cp target/release/murk "$RUNNER_TEMP/stage/murk"
          tar czf "$rel/$archive" -C "$RUNNER_TEMP/stage" murk
          if command -v sha256sum >/dev/null; then
            ( cd "$rel" && sha256sum "$archive" > SHA256SUMS )
          else
            ( cd "$rel" && shasum -a 256 "$archive" > SHA256SUMS )
          fi

          # Serve the fixture and install through install.sh as a fresh user
          # would, pointed at the fixture instead of github.com.
          python3 -m http.server 8099 --directory "$rel" >/dev/null 2>&1 &
          server=$!
          trap 'kill "$server" 2>/dev/null || true' EXIT
          for _ in $(seq 1 40); do
            curl -fsS http://localhost:8099/SHA256SUMS -o /dev/null 2>/dev/null && break
            sleep 0.25
          done

          bindir="$RUNNER_TEMP/bin"
          mkdir -p "$bindir"
          MURK_TAG="$tag" \
          MURK_BASE_URL=http://localhost:8099 \
          MURK_SKIP_ATTESTATION=1 \
          MURK_INSTALL_DIR="$bindir" \
            sh install.sh

          # The installed binary must start and complete an encrypt/decrypt
          # roundtrip in a clean temp directory with an isolated HOME.
          export PATH="$bindir:$PATH"
          murk --version
          export HOME="$RUNNER_TEMP/home"
          vault="$RUNNER_TEMP/vault"
          mkdir -p "$HOME" "$vault"
          cd "$vault"
          printf 'smoke-user\n' | murk init
          . ./.env
          printf 'hunter2\n' | murk add SMOKE_KEY
          got=$(murk get SMOKE_KEY)
          test "$got" = "hunter2"
          echo "install smoke passed: install.sh + init/add/get roundtrip"

  vhs-record:
    name: VHS (${{ matrix.tape }})
    needs: demo-test
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
    strategy:
      fail-fast: false
      matrix:
        tape: [hero, team, offboard, eve, recovery, github, direnv, mallory, ssh, agent-plan, agent-exec, agent-scan]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: murk-linux
          path: target/release/
      - run: chmod +x target/release/murk
      - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
      - name: Build VHS image with git and direnv
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: .github/vhs
          tags: vhs-git
          load: true
          cache-from: type=gha,scope=vhs-git
          cache-to: type=gha,mode=max,scope=vhs-git
      # The host-built glibc binary must run inside the image (its glibc must
      # be >= the runner's). Verify before recording so a runner-image bump
      # that outpaces upstream vhs fails here with a clear name, not mid-tape.
      - name: Check binary runs inside the vhs image
        run: docker run --rm --entrypoint /vhs/target/release/murk -v "$PWD":/vhs vhs-git --version
      - name: Record tape
        run: docker run --rm -v "$PWD":/vhs -e PATH="/vhs/target/release:$PATH" vhs-git demo/${{ matrix.tape }}.tape
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: gif-${{ matrix.tape }}
          path: demo/${{ matrix.tape }}.gif
          retention-days: 1

  vhs:
    name: VHS
    needs: vhs-record
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
    concurrency:
      group: vhs-publish-demo
      cancel-in-progress: false
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: gif-*
          merge-multiple: true
          path: demo/
      - name: Extract hero still frame for preview cards (kept in sync with hero.gif)
        run: |
          sudo apt-get update && sudo apt-get install -y ffmpeg
          dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 demo/hero.gif)
          ts=$(awk "BEGIN{print $dur*0.84}")
          ffmpeg -y -loglevel error -ss "$ts" -i demo/hero.gif -frames:v 1 demo/hero-still.png
      - uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./demo
          publish_branch: demo
          keep_files: true