murk-cli 0.9.0

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

on:
  push:
    tags: ["v*"]

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  preflight:
    name: Preflight
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      # Reading main's full branch protection needs the `administration` scope,
      # which the workflow GITHUB_TOKEN cannot carry. Mint a short-lived (~1h)
      # installation token instead, down-scoped to the three read permissions
      # preflight needs — smaller blast radius than a long-lived admin PAT.
      #
      # Setup (one-time): create a GitHub App on this repo with repository
      # permissions Administration: read, Contents: read, Pull requests: read;
      # install it on iicky/murk; store its App ID as the PREFLIGHT_APP_ID
      # secret and its private key as PREFLIGHT_APP_PRIVATE_KEY. Until then this
      # job fails closed — by design, an unverifiable protection baseline must
      # not let a release through.
      - name: Mint protection-read token
        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
        id: app-token
        with:
          app-id: ${{ secrets.PREFLIGHT_APP_ID }}
          private-key: ${{ secrets.PREFLIGHT_APP_PRIVATE_KEY }}
          permission-administration: read
          permission-contents: read
          permission-pull-requests: read

      - name: Preflight checks
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          # Required status checks main must enforce. Extra checks are fine; a
          # *dropped* one is drift. Keep in sync with the protection config.
          REQUIRED_CHECKS: "Test VHS Lint"
        run: |
          set -euo pipefail
          git fetch origin main:refs/remotes/origin/main
          TAG_SHA=$(git rev-parse "${GITHUB_REF}^{commit}")

          git merge-base --is-ancestor "$TAG_SHA" refs/remotes/origin/main \
            || { echo "::error::Tag ${GITHUB_REF_NAME} ($TAG_SHA) is not on main"; exit 1; }

          # Read the *full* protection config and assert the baseline. A 404 here
          # means main is unprotected (or the App lacks administration:read) —
          # either way fail loudly: the old `.protected` flag couldn't see admin
          # enforcement or which checks are required, so a silently-weakened
          # config (admins exempted, a required check dropped) would slip through.
          PROT=$(gh api "repos/${GITHUB_REPOSITORY}/branches/main/protection") \
            || { echo "::error::cannot read branch protection for main (unprotected, or the protection App lacks administration:read)"; exit 1; }

          drift=0
          assert() {  # assert <label> <actual> <expected>
            if [ "$2" != "$3" ]; then
              echo "::error::branch-protection drift: $1 is '$2', expected '$3'"
              drift=1
            fi
          }

          assert "enforce_admins"     "$(jq -r '.enforce_admins.enabled'     <<<"$PROT")" "true"
          assert "allow_force_pushes" "$(jq -r '.allow_force_pushes.enabled' <<<"$PROT")" "false"
          assert "allow_deletions"    "$(jq -r '.allow_deletions.enabled'    <<<"$PROT")" "false"
          for ctx in $REQUIRED_CHECKS; do
            assert "required check '$ctx'" \
              "$(jq -r --arg c "$ctx" 'any(.required_status_checks.contexts[]?; . == $c)' <<<"$PROT")" "true"
          done

          [ "$drift" = "0" ] \
            || { echo "::error::main's branch protection does not match the release baseline"; exit 1; }

          [ "$(gh api "repos/${GITHUB_REPOSITORY}/commits/${TAG_SHA}/pulls" --jq '[.[] | select(.merged_at != null and .base.ref == "main")] | length')" != "0" ] \
            || { echo "::error::Tag ${GITHUB_REF_NAME} ($TAG_SHA) has no PR merged into main"; exit 1; }

          echo "Preflight ok: $TAG_SHA on main, protection baseline matched, merged via PR"

  build:
    name: Build (${{ matrix.target }})
    needs: preflight
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            cross: true
          - target: arm-unknown-linux-gnueabihf
            os: ubuntu-latest
            cross: true
          - target: x86_64-apple-darwin
            os: macos-14
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2

      - name: Install cross
        if: matrix.cross
        run: cargo install cross --locked --version 0.2.5

      - name: Build
        run: |
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --locked --target ${{ matrix.target }}
          else
            cargo build --release --locked --target ${{ matrix.target }}
          fi
        shell: bash

      - name: Package (Unix)
        if: runner.os != 'Windows'
        run: |
          cd target/${{ matrix.target }}/release
          tar czf ../../../murk-${{ github.ref_name }}-${{ matrix.target }}.tar.gz murk

      - name: Package (Windows)
        if: runner.os == 'Windows'
        shell: bash
        run: |
          cd target/${{ matrix.target }}/release
          7z a ../../../murk-${{ github.ref_name }}-${{ matrix.target }}.zip murk.exe

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: murk-${{ matrix.target }}
          path: murk-${{ github.ref_name }}-${{ matrix.target }}.*

  release:
    name: GitHub Release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0

      - name: Generate release notes
        uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4
        id: cliff
        with:
          config: cliff.toml
          args: --latest --strip header
        env:
          OUTPUT: CHANGES.md

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          merge-multiple: true

      - name: Checksums
        run: sha256sum murk-* > SHA256SUMS

      - name: Attest build provenance
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-path: |
            murk-*
            SHA256SUMS

      - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v2
        with:
          body_path: CHANGES.md
          files: |
            murk-*
            SHA256SUMS

  homebrew:
    name: Update Homebrew tap
    needs: release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - name: Download checksums
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh release download ${{ github.ref_name }} --pattern SHA256SUMS

      - name: Render formula
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          cp .github/formula/murk.rb.template murk.rb

          for target in x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
            HASH=$(grep "murk-${{ github.ref_name }}-${target}.tar.gz" SHA256SUMS | awk '{print $1}')
            PLACEHOLDER="__SHA256_$(echo "$target" | tr '[:lower:]-' '[:upper:]_')__"
            sed -i "s/${PLACEHOLDER}/${HASH}/" murk.rb
          done

          sed -i "s/__VERSION__/${VERSION}/" murk.rb
          cat murk.rb

      - name: Push to tap
        env:
          TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          git clone https://x-access-token:${TAP_TOKEN}@github.com/iicky/homebrew-murk.git tap
          mkdir -p tap/Formula
          cp murk.rb tap/Formula/murk.rb
          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/murk.rb
          git commit -m "murk ${GITHUB_REF_NAME}"
          git push

  publish:
    name: Publish to crates.io
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - name: Authenticate to crates.io via OIDC
        uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
        id: crates-auth
      - name: Publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
        shell: bash
        run: |
          set -o pipefail
          cargo publish 2>&1 | tee /tmp/publish.log || {
            if grep -q "already uploaded" /tmp/publish.log; then
              echo "Version already published"
              exit 0
            fi
            exit 1
          }