KeiSeiKit-1.0/scripts/install-actionlint.sh
Parfii-bot c778b7d9a3 feat(v0.20.1): workflow-file validation infrastructure
Three layers of defense against the dtolnay-SHA-class bug reaching main
(today's incident: agent SHA-pinned dtolnay/rust-toolchain with a pin
that was real but semantically wrong — lost 'install current stable'
meaning, locked to rust 1.94.1 branch tip, broke CI).

Layer 1 — actionlint static lint
  scripts/install-actionlint.sh (65 LOC) — installs rhysd/actionlint
    v1.7.12 [VERIFIED] to ~/.local/bin or suggests brew install.
  scripts/lint-workflows.sh (40 LOC) — runs actionlint on
    .github/workflows/*.yml, exit 0 on clean, advisory when binary
    missing.

Layer 2 — SHA existence check (today's bug class)
  scripts/validate-workflow-shas.sh (98 LOC) — extracts every
    'uses: <repo>@<40-hex>' from workflow files + dependabot.yml,
    checks each via GitHub REST commits API (exit 200/404/422).
    Supports 'validate-workflow-shas: skip=<reason>' trailing
    comment for intentional exceptions. Falls back to anonymous
    API (60/hr quota) if GITHUB_TOKEN probe fails.
  DESIGN PIVOT from spec: spec said 'git ls-remote <repo> <sha>'
    but that only resolves REFS (branch/tag tips), not arbitrary
    commit SHAs — would have given false-positive 100% MISSING
    report. Switched to REST API /commits/{sha} for unambiguous
    200/404/422.

Layer 3 — CI gate
  .github/workflows/ci.yml — new 'workflow-lint' job after
    shell-lint. Installs actionlint + runs both scripts on every
    push to main and PR. Blocks CI on any fabricated SHA.

Layer 4 — optional pre-commit hook
  scripts/pre-commit-workflow-lint.sh (54 LOC) — detects staged
    .github/workflows/*.{yml,yaml} + .github/dependabot.yml
    changes, runs layers 1+2, blocks commit on failure.
  Install via: ln -sf ../../scripts/pre-commit-workflow-lint.sh
    .git/hooks/pre-commit

REAL EXECUTION VERIFIED (not claim-only):
  - actionlint ran: zero findings on current workflows
  - validate-workflow-shas.sh ran: 21 SHA pins checked, 21 OK,
    0 MISSING (confirms all current v0.19.1+ pins resolve)
  - bash -n on every new script: clean
  - bash-3.2 parser bug workaround: case-in-subshell → grep -E

RULE 0.2 exception #6 (shell is external convention for git hooks
+ GH Actions runs — Rust rewrite would add zero value).
RULE 0.13 respected — no git invocations except read-only API calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:50:23 +08:00

65 lines
2.1 KiB
Bash
Executable file

#!/bin/sh
# install-actionlint.sh — idempotent installer for rhysd/actionlint.
# Detects OS+arch, downloads the pinned release tarball to ~/.local/bin/actionlint.
# No-op if the binary is already on PATH. On macOS with Homebrew available and
# no local binary, suggests `brew install actionlint` as a faster alternative.
#
# Version pinned after WebFetch verification 2026-04-22.
# [VERIFIED: https://github.com/rhysd/actionlint/releases/tag/v1.7.12]
# Checksums from upstream checksums.txt (same release page).
set -eu
ACTIONLINT_VERSION="1.7.12"
INSTALL_DIR="${HOME}/.local/bin"
BIN="${INSTALL_DIR}/actionlint"
if command -v actionlint >/dev/null 2>&1; then
printf 'actionlint already on PATH: %s\n' "$(command -v actionlint)"
exit 0
fi
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH_RAW=$(uname -m)
case "${ARCH_RAW}" in
x86_64|amd64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) printf 'unsupported arch: %s\n' "${ARCH_RAW}" >&2; exit 2 ;;
esac
case "${OS}" in
darwin|linux) : ;;
*) printf 'unsupported os: %s\n' "${OS}" >&2; exit 2 ;;
esac
# Homebrew fast-path on macOS.
if [ "${OS}" = "darwin" ] && command -v brew >/dev/null 2>&1; then
printf 'Homebrew detected. Fast path:\n brew install actionlint\n'
printf 'Falling through to tarball install (~/.local/bin) anyway.\n'
fi
ASSET="actionlint_${ACTIONLINT_VERSION}_${OS}_${ARCH}.tar.gz"
URL="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${ASSET}"
mkdir -p "${INSTALL_DIR}"
TMP=$(mktemp -d)
trap 'rm -rf "${TMP}"' EXIT INT TERM
printf 'downloading %s\n' "${URL}"
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "${TMP}/${ASSET}" "${URL}"
elif command -v wget >/dev/null 2>&1; then
wget -qO "${TMP}/${ASSET}" "${URL}"
else
printf 'neither curl nor wget is installed\n' >&2
exit 2
fi
tar -xzf "${TMP}/${ASSET}" -C "${TMP}" actionlint
install -m 0755 "${TMP}/actionlint" "${BIN}"
printf 'installed: %s\n' "${BIN}"
case ":${PATH}:" in
*:"${INSTALL_DIR}":*) : ;;
*) printf 'note: %s is not on PATH — add it to your shell profile.\n' "${INSTALL_DIR}" ;;
esac