safe-migrate 0.4.4

Analyze PostgreSQL migrations for schema and locking risks
Documentation
name: safe-migrate
description: Produce JSON and Markdown PostgreSQL migration-review artifacts without synchronizing a database.
author: safe-migrate contributors

inputs:
  mode:
    description: Analyze one migration (`lint`) or an ordered migration directory (`lint-chain`).
    required: false
    default: lint-chain
  path:
    description: Migration file for `lint` or directory for `lint-chain`.
    required: true
  cache:
    description: Reviewed local cache path to read when no-cache is false. The action never refreshes it.
    required: false
    default: .safe-migrate.cache
  config:
    description: Explicitly trusted safe-migrate TOML path. Empty uses built-in defaults and never reads workspace configuration.
    required: false
    default: ""
  no-cache:
    description: Run with an unavailable baseline instead of reading a cache. Defaults to true because repository caches are PR-controlled unless explicitly supplied from a trusted workflow artifact.
    required: false
    default: "true"
  output-dir:
    description: Directory for `safe-migrate-report.json` and `safe-migrate-report.md`.
    required: false
    default: safe-migrate-artifacts

outputs:
  json-report:
    description: Path to the generated JSON report.
    value: ${{ steps.report.outputs.json-report }}
  markdown-report:
    description: Path to the generated Markdown report.
    value: ${{ steps.report.outputs.markdown-report }}
  exit-code:
    description: "Analyzer exit status: 0 completed, 1 operational failure, 2 blocking findings."
    value: ${{ steps.report.outputs.exit-code }}
  diagnostic-log:
    description: Path to analyzer diagnostics, including the reason for operational failures.
    value: ${{ steps.report.outputs.diagnostic-log }}

runs:
  using: composite
  steps:
    - name: Install Rust
      uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable 2026-07-16

    - name: Analyze migrations
      id: report
      shell: bash
      env:
        INPUT_MODE: ${{ inputs.mode }}
        INPUT_PATH: ${{ inputs.path }}
        INPUT_CACHE: ${{ inputs.cache }}
        INPUT_CONFIG: ${{ inputs.config }}
        INPUT_NO_CACHE: ${{ inputs.no-cache }}
        INPUT_OUTPUT_DIR: ${{ inputs.output-dir }}
      run: |
        set -euo pipefail

        case "$INPUT_MODE" in
          lint)
            command=(lint --file "$INPUT_PATH")
            ;;
          lint-chain)
            command=(lint-chain --dir "$INPUT_PATH")
            ;;
          *)
            echo "mode must be lint or lint-chain, got: $INPUT_MODE" >&2
            exit 1
            ;;
        esac

        command+=(--cache "$INPUT_CACHE")
        if [ -n "$INPUT_CONFIG" ]; then
          command+=(--config "$INPUT_CONFIG")
        else
          default_config="$(mktemp "${RUNNER_TEMP}/safe-migrate-default.XXXXXX")"
          command+=(--config "$default_config")
        fi
        if [ "$INPUT_NO_CACHE" = "true" ]; then
          command+=(--no-cache)
        fi

        mkdir -p "$INPUT_OUTPUT_DIR"
        install_root="${RUNNER_TEMP}/safe-migrate-action"
        cargo install --path "$GITHUB_ACTION_PATH" --locked --root "$install_root"
        binary="${install_root}/bin/safe-migrate"
        if [ "$RUNNER_OS" = "Windows" ]; then
          binary="${binary}.exe"
        fi
        json_report="${INPUT_OUTPUT_DIR}/safe-migrate-report.json"
        markdown_report="${INPUT_OUTPUT_DIR}/safe-migrate-report.md"
        diagnostic_log="${INPUT_OUTPUT_DIR}/safe-migrate-diagnostics.log"
        : > "$diagnostic_log"

        # Database synchronization is intentionally outside the Action's
        # default path. A separate workflow step can prepare a reviewed cache.
        set +e
        env -u DATABASE_URL "$binary" "${command[@]}" --json \
          > "$json_report" 2> >(tee -a "$diagnostic_log" >&2)
        json_status=$?
        env -u DATABASE_URL "$binary" "${command[@]}" --markdown \
          > "$markdown_report" 2> >(tee -a "$diagnostic_log" >&2)
        markdown_status=$?
        set -e

        final_status="$json_status"
        if [ "$json_status" -ne "$markdown_status" ]; then
          final_status=1
          echo "JSON and Markdown runs returned different statuses: ${json_status} and ${markdown_status}" \
            | tee -a "$diagnostic_log" >&2
        fi

        if [ "$final_status" -eq 1 ]; then
          if [ ! -s "$diagnostic_log" ]; then
            echo "safe-migrate failed without emitting a diagnostic" \
              | tee -a "$diagnostic_log" >&2
          fi
          printf '%s\n' \
            '{' \
            '  "artifact_schema_version": 1,' \
            '  "status": "operational_error",' \
            '  "exit_code": 1,' \
            '  "message": "safe-migrate did not produce an analysis report; see safe-migrate-diagnostics.log"' \
            '}' > "$json_report"
          printf '%s\n' \
            '# safe-migrate operational error' \
            '' \
            'Analysis did not complete. See `safe-migrate-diagnostics.log` for the underlying error.' \
            > "$markdown_report"
        fi

        echo "json-report=${json_report}" >> "$GITHUB_OUTPUT"
        echo "markdown-report=${markdown_report}" >> "$GITHUB_OUTPUT"
        echo "diagnostic-log=${diagnostic_log}" >> "$GITHUB_OUTPUT"
        echo "exit-code=${final_status}" >> "$GITHUB_OUTPUT"
        exit "$final_status"