communique 1.4.0

Editorialized release notes powered by AI
name: Release

permissions:
  contents: write

on:
  workflow_call:
    inputs:
      tag:
        required: true
        type: string
    secrets:
      CERTIFICATES_P12:
        required: true
      CERTIFICATES_P12_PASS:
        required: true
      APPLE_API_KEY_P8:
        required: true
      APPLE_API_KEY_ID:
        required: true
      APPLE_API_ISSUER_ID:
        required: true

env:
  CARGO_TERM_COLOR: always

jobs:
  upload-assets:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
          - target: aarch64-pc-windows-msvc
            os: windows-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          ref: refs/tags/${{ inputs.tag }}
          persist-credentials: false
      - name: Import codesign certs
        if: startsWith(matrix.os, 'macos')
        uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7
        with:
          p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
          p12-password: ${{ secrets.CERTIFICATES_P12_PASS }}
      - name: Install cross-compilation tools
        uses: taiki-e/setup-cross-toolchain-action@12b7ad4acfa95a1476779d6c06699b96ec1691f8 # v1
        with:
          target: ${{ matrix.target }}
      - name: Upload binary (macOS signed)
        if: startsWith(matrix.os, 'macos')
        uses: taiki-e/upload-rust-binary-action@f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6 # v1
        with:
          bin: communique
          target: ${{ matrix.target }}
          ref: refs/tags/${{ inputs.tag }}
          token: ${{ secrets.GITHUB_TOKEN }}
          codesign: "Developer ID Application: Jeffrey Dickey (4993Y37DX6)"
          codesign_prefix: dev.jdx.
          # Apple will not notarize a binary without the hardened runtime.
          codesign_options: runtime
      # Signing settles who built the binary; notarization is what Gatekeeper
      # demands once an archive carries the quarantine bit, which is to say
      # whenever someone downloads it in a browser rather than with curl.
      # Without a ticket it is blocked behind a "cannot be verified" dialog that
      # only a deliberate override gets past.
      #
      # The ticket lives on Apple's side and is keyed to the binary's cdhash, so
      # nothing here rewrites what was already built or uploaded -- stapling
      # would, but `stapler` only writes into bundles, disk images, and
      # installer packages, and this is a bare Mach-O in an archive. Gatekeeper
      # resolves the ticket online instead.
      - name: Notarize macOS binary
        if: startsWith(matrix.os, 'macos')
        shell: bash
        env:
          APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
          APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
          BINARIES: target/${{ matrix.target }}/release/communique
        run: |
          set -euo pipefail

          if [ -z "$APPLE_API_KEY_P8" ] || [ -z "$APPLE_API_KEY_ID" ] || [ -z "$APPLE_API_ISSUER_ID" ]; then
            echo "::error title=Notarization credentials missing::App Store Connect credentials are required for macOS releases."
            exit 1
          fi

          # Outside the workspace, so no later step can sweep the key into an
          # artifact, and removed even when notarization fails.
          work="$(mktemp -d)"
          trap 'rm -rf "$work"' EXIT
          printf '%s' "$APPLE_API_KEY_P8" | base64 --decode > "$work/key.p8"

          # notarytool takes a container, never a bare executable. Copies are
          # fine: a Mach-O carries its signature inline, so the cdhash the
          # ticket is keyed to is the one already shipped.
          mkdir "$work/stage"
          for binary in $BINARIES; do
            cp "$binary" "$work/stage/"
          done
          # Apple rejects a binary without the hardened runtime or a secure
          # timestamp. Naming a missing one here turns an opaque Invalid verdict
          # into a diagnosis; Apple stays the authority on whether to accept.
          for binary in $BINARIES; do
            details="$(codesign --display --verbose=4 "$binary" 2>&1 || true)"
            case "$details" in
              *runtime*) ;;
              *) echo "::warning title=No hardened runtime::$binary is signed without --options runtime, which Apple requires; expect an Invalid verdict below." ;;
            esac
            case "$details" in
              *Timestamp=*) ;;
              *) echo "::warning title=No secure timestamp::$binary carries no secure timestamp, which Apple requires; expect an Invalid verdict below." ;;
            esac
          done

          ditto -c -k --norsrc --noextattr "$work/stage" "$work/submission.zip"

          # `--wait` is not a gate on its own: it can return zero on an Invalid
          # submission, so the reported status is what decides. plutil reads
          # JSON and ships with macOS, which keeps this off jq.
          result="$(xcrun notarytool submit "$work/submission.zip" \
            --key "$work/key.p8" \
            --key-id "$APPLE_API_KEY_ID" \
            --issuer "$APPLE_API_ISSUER_ID" \
            --output-format json \
            --timeout 30m \
            --wait)"
          echo "$result"

          status="$(printf '%s' "$result" | plutil -extract status raw -o - -)"
          id="$(printf '%s' "$result" | plutil -extract id raw -o - -)"
          if [ "$status" != "Accepted" ]; then
            echo "::error::notarization returned $status for submission $id"
            xcrun notarytool log "$id" \
              --key "$work/key.p8" \
              --key-id "$APPLE_API_KEY_ID" \
              --issuer "$APPLE_API_ISSUER_ID" || true
            exit 1
          fi
          echo "notarized: submission $id"
      - name: Upload binary (non-macOS)
        if: ${{ !startsWith(matrix.os, 'macos') }}
        uses: taiki-e/upload-rust-binary-action@f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6 # v1
        with:
          bin: communique
          target: ${{ matrix.target }}
          ref: refs/tags/${{ inputs.tag }}
          token: ${{ secrets.GITHUB_TOKEN }}

  # The packslip is this release's signed inventory: every archive's digest,
  # the executable inside it, the shared objects it needs from the host, and
  # the workflow identity that signed for all of it. An installer checks a
  # download against jdx/communique's identity rather than against a key this
  # project would have to hold. See https://packslip.dev.
  #
  # Its own job because the matrix above uploads straight to the release from
  # seven runners; one document covering the whole release can only be written
  # once every one of them has landed.
  packslip:
    name: Publish the packslip
    needs: upload-assets
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      attestations: write
    steps:
      - name: Download the release assets
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ inputs.tag }}
          REPO: ${{ github.repository }}
        run: |
          mkdir -p dist
          gh release download "$TAG" --repo "$REPO" --dir dist --clobber \
            --pattern 'communique-*.tar.gz' --pattern 'communique-*.zip'
          ls -la dist
      # The spec supports documentation generation without running the binary.
      # Native completion scripts call the installed binary on each completion.
      - name: Install completion syntax checkers
        timeout-minutes: 5
        run: |
          if [[ -f /etc/apt/apt-mirrors.txt ]]; then
            sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt
          fi
          sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update
          sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish
          # Hosted runners include PowerShell; Namespace images have its signed apt repository.
          if ! command -v pwsh >/dev/null 2>&1; then
            sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y powershell
          fi
      - name: Publish the CLI specification and completions
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ inputs.tag }}
          REPO: ${{ github.repository }}
        run: |
          mkdir -p bin
          tar -xzf dist/communique-x86_64-unknown-linux-gnu.tar.gz -C bin
          bin/communique usage > communique.usage.kdl
          for shell in bash zsh fish powershell; do
            bin/communique completion "$shell" > communique.$shell
            test -s communique.$shell
            grep -Fq '__complete_word__' communique.$shell
            bin/communique __complete_word__ --shell "$shell" --line 'communique ge' | grep -Fq -- 'generate'
          done
          bash -n communique.bash
          zsh -n communique.zsh
          fish --no-execute communique.fish
          cat > completion-syntax-check.ps1 <<'POWERSHELL'
          $ErrorActionPreference = 'Stop'
          foreach ($script in Get-ChildItem -LiteralPath "." -Filter '*.powershell') {
            $parseErrors = $null
            [void][System.Management.Automation.Language.Parser]::ParseFile($script.FullName, [ref]$null, [ref]$parseErrors)
            if ($parseErrors.Count) { $parseErrors | Write-Error; exit 1 }
          }
          POWERSHELL
          pwsh -NoProfile -NonInteractive -File completion-syntax-check.ps1
          gh release upload "$TAG" communique.usage.kdl communique.bash communique.zsh communique.fish communique.powershell --repo "$REPO" --clobber
      - uses: jdx/packslip@a0d434ad57571c62dbd0ab5095b4eff607a71fd3 # v1.1.1
        # The action uploads with `gh release upload` and no `--repo`, and
        # this job has no checkout for gh to infer the repository from.
        env:
          GH_REPO: ${{ github.repository }}
        with:
          tag: ${{ inputs.tag }}
          artifacts: dist/communique-*.tar.gz dist/communique-*.zip
          bin: communique
          resources: |
            cli-spec/usage=asset:communique.usage.kdl
            completion/bash=asset:communique.bash
            completion/zsh=asset:communique.zsh
            completion/fish=asset:communique.fish
            completion/powershell=asset:communique.powershell