개요
quanto는 pull request가 바꾼 GitHub Actions 워크플로 파일을 읽어, 변경 전과 후 버전을 각각 실행 모델로 해석하고, 워크플로가 실제로 어떻게 달라지는지 보고하는 Go 도구다. matrix를 전개한 뒤 한 번의 run이 만드는 잡 수, 가장 긴 needs 체인, 최대 동시 잡 수, 권한 변화, 새로 참조된 시크릿, 새 서드파티 액션, 커밋 SHA 고정이 풀린 액션, 추가된 트리거, 스케줄 빈도, 그리고 과거 run을 근거로 한 러너 분(runner minutes) 추정을 센다.
판단하지 않는다. 수치와 사실만 보고하고, 머지를 막지 않는다. 자신의 워크플로 안에서 GitHub Action으로 돌고, 직접 호스팅하는 GitHub App이 고급 옵션으로 있다.
포지셔닝은 linter가 아니라 계기판이다. actionlint나 zizmor 같은 정적 검사·보안 린트와 겹치지 않는다. 그 도구들은 워크플로가 올바른지 본다. quanto는 머지 전에 실행 모델이 어떻게 달라지는지를 숫자로 보여준다. 조사한 시점에 이 자리를 채우는 도구는 없었다.
무엇을 보여주나
각 finding은 유의도(significance)를 가진다 — high, normal, low. 아래는 test 잡 matrix에 shard 축을 더하고 os·node를 넓히면서, push와 15분 주기 스케줄을 켜고, 워크플로 권한을 read에서 write로 올리고, SHA로 고정하지 않은 서드파티 액션과 새 시크릿을 들인 변경을 quanto diff로 비교한 실제 출력이다.
`.github/workflows/ci.yml` (modified)
Jobs per run: 5 → 38
Longest `needs` chain: 2 → 3
Max concurrent jobs: 4 → 36
- [high] Job `test` matrix: 4 → 36 jobs
- [high] `contents` permission (workflow): `read` → `write`
- [high] New third-party action: `some-user/setup-thing@main` (mutable ref)
- [normal] Trigger added: `push`
- [normal] Schedule: `(none)` → `'*/15 * * * *' (96 runs/day)`
- [normal] Job added: `setup`
- [normal] Longest `needs` chain: 2 → 3 jobs
- [normal] Max concurrent jobs: 4 → 36
- [normal] New secret referenced: `NPM_TOKEN`잡 수가 5에서 38로, 동시 잡 수가 4에서 36으로 늘었고, 스케줄은 하루 96번이다. 이 숫자들 어디에도 "위험하다"는 말은 없다.
왜 계기판인가
모든 분석은 Job Summary(App에서는 Check Run)에 지표 표와 모든 finding을 쓰고, 위치가 있는 finding마다 워크플로 파일에 notice 주석을 단다. PR 코멘트는 임계값을 넘을 때만 만들고 이후 푸시에서 갱신한다.
코멘트 임계값은 좁다. high 유의도 finding이 하나라도 있거나, matrix(matrix.*)·needs 그래프(graph.*)·러너 분 추정(estimate.*)에 관한 finding이 하나라도 있을 때만 코멘트를 단다. 나머지 normal finding(트리거 추가, 필터 변경, 액션 ref 상승 등)은 Job Summary나 Check Run에만 남는다. low 유의도(권한 축소, 1st-party 액션 추가)는 코멘트에 절대 나오지 않는다. 이후 푸시가 다시 임계값 아래로 내려가면 기존 코멘트를 "임계값을 넘는 변경이 없다"로 갱신하고 새 코멘트를 만들지 않는다.
계기판은 "느리다"고 말하지 않고 속도를 표시한다. quanto도 "이 변경이 위험하다"고 말하지 않고 잡 수가 5에서 38로 늘었다고만 쓴다. 읽고 판단하는 건 사람이다.
실제 PR 검증
공개 오픈소스 PR 11건에 직접 적용해 봤다 — psf/requests, pydantic, astral-sh/ruff, fastapi, vitejs/vite 2건, numpy, prometheus, neovim, cli/cli, tokio-rs/tokio. 첫 실행에서 유용 4·무의미 5·오류 2였다. 무의미와 오류를 만든 원인을 고친 뒤 오류는 0이 됐고, 코멘트가 달린 4건은 모두 유용으로 판정했다.
배포와 공급망
GitHub Action은 composite이고 체크아웃 단계가 필요 없다. 참조된 태그와 러너에 맞는 release 바이너리를 내려받아 같은 release의 checksums.txt에 대해 SHA-256을 검증하고, 일치하지 않으면 실행을 거부한다. 바이너리는 빌드 출처 증명(build provenance attestation)을 갖고, gh attestation verify <file> --repo BETAER-08/quanto로 확인할 수 있다. 타겟은 다섯 개 — Linux·macOS·Windows x64, Linux·macOS ARM64.
@v1은 최신 v1.x.y를 따른다. release 워크플로가 안정 릴리스마다 v1 태그를 옮기고 v1 release 자산을 갱신한다. v1.1.0-rc.1 같은 프리릴리스는 태그를 옮기지 않는다.
데이터는 잡 안에만 머문다. 저장소의 GitHub API와, 설치 단계의 release 다운로드하고만 통신한다. 워크플로 내용과 분석 결과를 다른 곳으로 보내거나 저장하지 않으며, PR 코드를 체크아웃하거나 실행하지 않는다(pull_request_target에서도 같다).
고급 옵션인 자체 호스팅 GitHub App은 PostgreSQL을 쓰고 같은 분석을 quanto Check Run으로 제공한다. 작업 큐는 SKIP LOCKED로 잠가 워커가 서로 다른 작업을 집어간다.
구현은 Go 1.24, yaml.v3 노드 트리 직접 순회, 표현식 파서, GitHub의 include/exclude 의미론을 따르는 matrix 전개, needs DAG 분석이다. 퍼즈 테스트 다섯 종(템플릿 파서, matrix 전개, 의미 diff, 소스 로더, action 커맨드)이 경계를 지킨다.
한계
if:조건을 평가하지 않는다. 건너뛸 잡도 jobs per run과 최대 동시 잡 수에 센다.- matrix는 조합 수로만 비교한다. 축 값을 교체해 커버하는 플랫폼·버전이 줄어도 조합 수가 같으면 보고하지 않는다.
with:로 넘기는 도구 버전 고정 해제(go-version: '1.22.3'→stable)는 탐지하지 않는다. 액션 참조 자체만 비교한다.- reusable workflow 내부는 열지 않는다. 그것을 부르는 잡은 matrix 인스턴스마다 한 잡으로 세고, 안에서 바뀐 것은 보고하지 않는다.
fromJSON(...)처럼 런타임에 계산되는 matrix는 알 수 없음(?)으로 보고한다.
사용법
name: quanto
on:
pull_request:
paths: ['.github/workflows/**']
permissions:
contents: read
pull-requests: write
actions: read
jobs:
quanto:
runs-on: ubuntu-latest
steps:
- uses: BETAER-08/quanto@v1같은 분석을 GitHub 접근 없이 로컬에서도 돌릴 수 있다. quanto inspect <file>은 한 워크플로의 트리거·권한·matrix·그래프를 보여주고, quanto diff <before> <after>는 두 버전을 비교한다. /dev/null을 한쪽에 주면 파일이 그쪽에 없다는 뜻이다.