Files
heretek-actions/tests/run-action.sh
T
John Smith 6ae3e16aba feat(lint): add lint-ultimate action wrapping MegaLinter with envelope
Tier 1.1 of the quality expansion design. Adds the lint-ultimate
composite action that runs MegaLinter (or Super-Linter) over the
repo and emits a per-linter checks[] plus per-finding findings[]
envelope, matching the standard .agent/output.json contract.

## Changes

### Action
- .github/actions/lint-ultimate/action.yml: composite action, runs
  oxsecurity/megalinter:v7 via Docker, uploads the JSON report and
  stdout as lint-ultimate-reports artifact, uploads the envelope as
  agent-output-lint for downstream consumers.
- .github/actions/lint-ultimate/build-envelope.sh: parses
  megalinter-report.json, maps each linter category to a check[]
  entry and each finding to a findings[] entry. Falls back to an
  empty envelope when the report is missing or unreadable.

### Library
- .github/actions/agent-envelope.sh: add add_finding() helper
  mirroring add_check(). Optional fields (rule, file, line, column,
  suggested_fix) default to null; numbers coerce when non-empty.

### Library fixes (pre-existing bugs surfaced by the new test)
- GITHUB_REPOSITORY default and owner/repo prefix strip now set -u
  safe. Previously the original --arg repo_name reference threw on
  unbound-variable in any non-GitHub runner.
- GITHUB_RUN_ID now passed as --argjson (numeric or null) instead of
  --arg with tonumber? in the filter. The prior form silently
  produced empty output when run_id was unset (jq 1.8.2 quirk where
  tonumber? on null returns Empty and jq emits no output).

### Schema
- .agent/schema.json: enum agent_action extended with all twelve new
  values from the quality-check expansion design (lint-ultimate,
  coverage, actionlint, codeql, lefthook-ci, test-matrix, reviewdog,
  quality-gate, semantic-release, dependabot, docker-ci). Future
  Tier 1/2/3 PRs pick the right value.

### Tests
- tests/: ship the previously untracked harness (run-action.sh,
  run-all.sh, README.md, fixtures for all CI actions) plus the
  new megalinter-passing and megalinter-failing fixtures.
- tests/unit/test-lint-ultimate-parser.sh: parser unit test that
  runs build-envelope.sh against a fabricated megalinter-report.json
  and asserts the envelope shape. No Docker required.
- tests/golden/lint-ultimate/README.md: documents that goldens are
  generated on first real-CI run (per tests/README.md first-run
  convention) and provides a no-Docker local check.

### Matrix
- tests/run-all.sh: megalinter-passing and megalinter-failing added
  to MATRIX (paired with lint-ultimate action).

### Docs
- README.md: lint-ultimate row added to the CI Composite Actions
  table; full example block added between python-ci and ---.

## Risk

- Pre-existing agent-envelope.sh changes affect all envelope-writing
  callers; the same fixes apply everywhere (none had a green test
  before). Behavior is unchanged when GITHUB_* env vars are set
  (real CI).
- No existing tests to regress against (tests/ was untracked).
  Unit test added in this PR exercises the new parser path.

Refs: docs/superpowers/specs/2026-07-30-code-quality-expansion-design.md
2026-07-30 14:38:20 -04:00

124 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# tests/run-action.sh — Run a Heretek composite action against a fixture repo via `act`
#
# Usage:
# tests/run-action.sh <fixture-name> <action-name> [act-args...]
#
# Example:
# tests/run-action.sh rust-passing rust-ci
# tests/run-action.sh js-failing js-ci --dryrun
#
# Exit codes:
# 0 — envelope produced and matches golden (or no golden exists yet)
# 1 — fixture missing
# 2 — envelope validation failed
# 3 — golden mismatch
# 4 — act itself failed
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIXTURES_DIR="${REPO_ROOT}/tests/fixtures"
GOLDEN_DIR="${REPO_ROOT}/tests/golden"
WORK_DIR="${REPO_ROOT}/tests/work"
FIXTURE="${1:-}"
ACTION="${2:-}"
shift 2 || true
if [ -z "${FIXTURE}" ] || [ -z "${ACTION}" ]; then
echo "Usage: $0 <fixture-name> <action-name> [act-args...]" >&2
exit 1
fi
FIXTURE_PATH="${FIXTURES_DIR}/${FIXTURE}"
if [ ! -d "${FIXTURE_PATH}" ]; then
echo "fixture not found: ${FIXTURE_PATH}" >&2
exit 1
fi
# Sandbox: copy fixture into work dir so act can checkout freely
SANDBOX="${WORK_DIR}/${FIXTURE}-$$"
mkdir -p "${SANDBOX}"
cp -R "${FIXTURE_PATH}/." "${SANDBOX}/"
# Wire up the local heretek-actions checkout as the action source
# Override GITHUB_ACTION_PATH or use local resolution via act's --action-ref
# Easiest path: mount the repo as the local actions source
mkdir -p "${SANDBOX}/.github/actions"
ln -sfn "${REPO_ROOT}/.github/actions/agent-envelope.sh" "${SANDBOX}/.github/actions/agent-envelope.sh"
ln -sfn "${REPO_ROOT}/.github/actions/${ACTION}" "${SANDBOX}/.github/actions/${ACTION}"
# Minimal workflow that calls the action and uploads the envelope
WORKFLOW_FILE="${SANDBOX}/.github/workflows/test-${ACTION}.yml"
mkdir -p "$(dirname "${WORKFLOW_FILE}")"
cat > "${WORKFLOW_FILE}" <<WORKFLOW
name: Test ${ACTION}
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/${ACTION}
with: {}
- name: Validate envelope against schema
shell: bash
run: |
set -euo pipefail
jq -e --schema '\${{ inputs.schema }}' '. | type == "object"' .agent/output.json >/dev/null
WORKFLOW
echo "==> Running ${ACTION} against fixture ${FIXTURE}"
# Run act. Capture output; tolerate non-zero exit (action may intentionally fail).
set +e
act -W "${WORKFLOW_FILE}" --container-architecture linux/amd64 "$@" 2>&1 | tee "${WORKDIR:-/tmp}/act-${FIXTURE}-${ACTION}.log"
ACT_EXIT=${PIPESTATUS[0]}
set -e
# Find the produced envelope
ENVELOPE="${SANDBOX}/.agent/output.json"
if [ ! -f "${ENVELOPE}" ]; then
echo "no envelope produced (act exit ${ACT_EXIT})" >&2
rm -rf "${SANDBOX}"
exit 4
fi
# Validate against schema
if ! jq -e empty "${ENVELOPE}" >/dev/null 2>&1; then
echo "envelope is not valid JSON: ${ENVELOPE}" >&2
rm -rf "${SANDBOX}"
exit 2
fi
# Validate schema enum fields (lightweight, no full JSON-schema validator needed)
SCHEMA="${REPO_ROOT}/.agent/schema.json"
AGENT_ACTION=$(jq -r '.agent_action' "${ENVELOPE}")
if ! jq -e --argjson env "${ENVELOPE}" '.properties.agent_action.enum | index($env.agent_action) != null' "${SCHEMA}" >/dev/null; then
echo "envelope agent_action '${AGENT_ACTION}' not in schema enum" >&2
rm -rf "${SANDBOX}"
exit 2
fi
GOLDEN="${GOLDEN_DIR}/${ACTION}/${FIXTURE}.json"
if [ -f "${GOLDEN}" ]; then
# Diff against golden — strip volatile fields (timestamps, run_id, sha)
NORMALIZED=$(jq 'del(.created_at, .duration_ms, .repository.run_id, .repository.sha)' "${ENVELOPE}")
GOLDEN_NORMALIZED=$(jq 'del(.created_at, .duration_ms, .repository.run_id, .repository.sha)' "${GOLDEN}")
if ! diff <(echo "${GOLDEN_NORMALIZED}") <(echo "${NORMALIZED}") >/dev/null; then
echo "envelope differs from golden: ${GOLDEN}" >&2
diff <(echo "${GOLDEN_NORMALIZED}") <(echo "${NORMALIZED}") >&2 || true
rm -rf "${SANDBOX}"
exit 3
fi
echo "✓ ${ACTION} on ${FIXTURE} matches golden"
else
echo "no golden for ${ACTION}/${FIXTURE} — writing first-run snapshot"
mkdir -p "${GOLDEN_DIR}/${ACTION}"
jq 'del(.created_at, .duration_ms, .repository.run_id, .repository.sha)' "${ENVELOPE}" > "${GOLDEN}"
fi
rm -rf "${SANDBOX}"
exit 0