GitHub Actions Workflow Lockfiles: uses@SHA만 고정해도 충분하지 않은 이유
GitHub Actions Workflow Lockfiles: uses@SHA만 고정해도 충분하지 않은 이유
GitHub Actions의 uses: some-org/action@v2를 전체 커밋 SHA로 바꾸면 해당 진입점의 코드는 고정됩니다. 하지만 고정된 Composite Action이 내부에서 다른 example-org/upload@v1을 호출한다면 어떨까요? 바깥 SHA는 그대로여도 안쪽 태그는 바뀔 수 있습니다. 워크플로 YAML만 읽어서는 실제로 실행되는 외부 Action의 전체 의존성 그래프를 파악하기 어렵습니다.
GitHub가 공개 프리뷰로 발표한 Workflow Lockfiles는 .github/workflows/actions.lock에 직접 및 전이 Action 의존성을 기록하고 실행 시 잠긴 커밋과 저장소 ID를 검증하는 기능입니다.
이미 모든 직접 Action을 uses@SHA로 고정했다면, 우선 의존성 그래프를 확인하세요. Lockfile의 실질적인 이점은 같은 SHA를 두 번 잠그는 것이 아니라, 전이 의존성과 저장소 ID를 검토할 수 있는 목록을 얻는 데 있습니다.
| 현재 CI 구성 | 먼저 할 일 | 이유 |
|---|---|---|
| 직접 Action은 모두 전체 SHA로 고정했고 구조가 단순함 | 목록을 정리하고 전체 전환은 보류 | 유지 관리 비용 대비 추가 효과가 작을 수 있음 |
Composite Action 안에서 @v1 또는 @main 사용 | 중요도가 낮은 워크플로부터 시범 적용 | 전이 Action 의존성을 추적할 수 있음 |
| 여러 저장소가 Reusable Workflow 공유 | 호출 측과 정의 측의 책임부터 확인 | 내부 Action은 Reusable Workflow 정의 저장소가 관리 |
| 조직에 Allowed Actions / SHA 강제 정책 적용 | 정책부터 점검 | 잠긴 커밋 SHA와 허용 목록이 불일치할 수 있음 |
이 상황들은 서로 배타적이지 않습니다. 아래 내용은 공식 문서와 고정된 gh-actions-lock v0.2.0 구현을 기준으로 합니다.
진입점 SHA 고정과 전체 Action 그래프 고정은 다르다
ci.yml이 example-org/build를 전체 SHA로 호출하고, 그 action.yml은 example-org/upload@v1을 호출한다고 가정해 봅시다. 바깥 소스는 변하지 않아도 아래쪽 태그의 대상은 이동할 수 있습니다.
| 의존성 유형 | 직접 uses@SHA | Workflow Lockfile 적용 후 |
|---|---|---|
| 워크플로가 직접 호출하는 외부 Action | 해당 진입점만 고정 | 커밋 및 저장소 ID 기록 |
| Composite Action 내부 Action | 안쪽 태그까지 자동 고정하지 않음 | 전이 Action 의존성 포함 |
| 다른 저장소의 Reusable Workflow | 호출 지점을 고정 | 호출은 고정하되 내부는 정의 저장소의 Lockfile이 관리 |
같은 저장소의 uses: $/… | 현재 저장소 커밋 사용 | 자체 고정되므로 별도 Pin 불필요 |
run: 단계의 다운로드나 가변 컨테이너 이미지 | 범위 밖 | Action Lockfile만으로 보호되지 않음 |
GitHub 개념 문서에 따르면 한 저장소의 Lockfile은 해당 저장소가 정의한 워크플로를 대상으로 합니다. 다른 저장소에 정의된 Reusable Workflow 안에서 사용하는 Action은 그 워크플로가 정의된 저장소의 Lockfile이 책임집니다. 호출하는 쪽에서 Reusable Workflow의 SHA를 고정했다고 해서 상대방의 모든 내부 Action까지 검토한 것은 아닙니다.
actions.lock은 무엇을 기록하나
workflows는 워크플로 경로와 필요한 Pin을 연결합니다. dependencies는 각 ref, commit, owner_id, repo_id 및 하위 Action 호출의 uses를 기록합니다. 아래 YAML은 공식 포맷 저장소의 고정 커밋에 포함된 benchV003 파서 테스트 데이터입니다. v0.0.3 형식의 실제 테스트 fixture이지만 반복되는 a/b SHA 값은 테스트용 가상 값이며 배포에서 생성된 결과가 아닙니다.
version: v0.0.3
workflows:
.github/workflows/ci.yml:
- actions/checkout@v4
- actions/setup-go@v5
- actions/cache@v4
dependencies:
actions/checkout@v4:
hostname: github.com
ref: v4
commit: sha1-11bd71901bbe5b1630ceea73d27597364c9af683
owner_id: 44036562
repo_id: 197814629
actions/setup-go@v5:
hostname: github.com
ref: v5
commit: sha1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
owner_id: 44036562
repo_id: 249058325
actions/cache@v4:
hostname: github.com
ref: v4
commit: sha1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
owner_id: 44036562
repo_id: 251882839
uses:
- actions/checkout@v4
이 테스트 데이터는 hostname: github.com을 명시하지만, 기본 github.com에서 실제로 생성된 파일은 기본 호스트를 생략할 수도 있습니다. workflows의 Pin 목록과 dependencies의 uses 관계는 서로 다른 관점의 정보입니다.
고정 버전 state_marshal.go 소스에도 필드 직렬화가 나옵니다.
a := file.Dependencies[k]
entry := &yaml.Node{Kind: yaml.MappingNode}
// hostname 처리 분기는 생략
if a.Ref != "" {
addQuotedField(entry, "ref", a.Ref)
}
if a.Commit != "" {
addQuotedField(entry, "commit", a.Commit)
}
addIntField(entry, "owner_id", a.OwnerID)
addIntField(entry, "repo_id", a.RepoID)
Commit은 Git 객체를 지정하고 숫자로 된 저장소/소유자 ID는 이름 뒤에 있는 저장소의 정체성을 확인합니다. 공식 개념 페이지에는 version: v0.0.2 예시가 남아 있지만 이번에 검토한 v0.2.0 Release와 포맷 README는 **v0.0.3**을 사용합니다. CLI 버전과 Lockfile Schema 버전은 별개입니다.
강제 정책을 켜기 전에 실제 변경 사항부터 확인하기
되돌릴 수 있는 테스트 브랜치에서 중요도가 낮은 워크플로 하나를 선택합니다. GitHub CLI가 설치·로그인되어 있고 참조 저장소에 대한 읽기 권한이 있다는 전제의 명령입니다.
# 이 글에서 검토한 버전으로 확장 설치
gh extension install github/gh-actions-lock --pin v0.2.0
gh actions-lock .github/workflows/ci.yml
# 워크플로, 로컬 Action 및 새 파일 확인
git status --short
git diff
cat .github/workflows/actions.lock
# 시범 워크플로만 검증
gh actions-lock --verify .github/workflows/ci.yml
파일 경로로 범위를 제한하는 검증은 단순한 추측이 아닙니다. 고정 소스의 root.go는 워크플로 경로 인자를 받고, verify.go와 관련 테스트도 그 범위를 사용합니다. 공유 actions.lock 자체는 정상 파싱되어야 하지만, 다른 워크플로까지 자동으로 등록하지는 않습니다.
도구는 Lockfile만 만드는 것이 아닙니다. 일부 버전 태그를 더 구체적인 버전으로 바꾸거나, 실제 action.yml이 존재하는 로컬 Action을 ./actions/build에서 $/actions/build로 변경할 수 있습니다.
steps:
- - uses: ./actions/build
+ - uses: $/actions/build
$/ 구문은 실행 중인 저장소 커밋에 연결됩니다. 잘못된 ./ 경로를 자동 복구하는 것은 아닙니다. 또 git diff에는 아직 추적하지 않는 새 actions.lock이 표시되지 않으므로 git status --short와 파일 내용도 함께 점검해야 합니다.
조직의 Allowed Actions 정책은 또 다른 위험 지점입니다. Lockfile이 적용된 워크플로에서는 허용 목록이 고정된 커밋 SHA를 기준으로 평가될 수 있습니다. 이전에 actions/checkout@v6 태그로 허용했다면 불일치가 발생할 수 있으므로 실제 검토한 커밋을 기준으로 규칙을 수정하는 편이 낫습니다.
로컬 검증과 온라인 검증은 별개의 단계입니다.
gh actions-lock --verify-local .github/workflows/ci.yml
gh actions-lock --verify .github/workflows/ci.yml
--verify-local은 네트워크 없이 잠금 기록의 범위를 검사합니다. 상위 GitHub 저장소의 실제 ID와 신뢰 조건은 확인하지 못합니다. 온라인 --verify가 더 강한 확인을 수행하므로 PR 검증에 적합합니다. 모든 워크플로를 등록한 다음 경로가 없는 gh actions-lock --verify로 저장소 전체를 검사하세요. 아직 등록되지 않은 워크플로와 사용되지 않는 Pin 때문에 시범 단계에서 전역 검증이 실패할 수 있습니다.
문제 분석에는 --json=valid,findings를 활용할 수 있습니다. 종료 코드 1은 차단 Finding, 2는 명령·I/O·네트워크 오류입니다. 고정 소스의 Finding 분류에 not-pinned, stale, local-action, repo-replaced, unreachable-pin이 정의되어 있습니다.
되돌릴 수 있는 다섯 단계의 시범 적용:
- 의존성 조사: 직접
uses, 중첩 Composite Action, 외부 Reusable Workflow를 나열하고 정의 저장소 담당자를 정합니다. - 테스트 브랜치에서 생성: 고정 버전 도구로 워크플로를 등록한 후 YAML,
action.yml,actions.lock의 변경을 확인합니다. - 정책 검토: 잠긴 SHA와 Allowed Actions를 비교하고 모든 차이를 정상적인 PR 리뷰에 올립니다.
- 두 단계 검증:
--verify-local로 커버리지를,--verify로 온라인 신뢰 조건을 확인합니다.not-pinned,stale,local-action은 경로와 Diff를,repo-replaced,unreachable-pin은 상위 이력을 먼저 조사합니다. - 롤백 유지: 파싱이나 정책 문제로 CI가 막히면 테스트 브랜치 변경을 되돌립니다. 소규모 적용이 안정화된 뒤 전체 조직으로 넓힙니다.
도입한 후 의존성은 어떻게 업데이트되나
최초로 잠기는 커밋은 원래의 uses 참조와 해석 결과에 따라 달라집니다. v0.2.0에 무조건 7일을 기다리는 기본값은 없습니다. .github/dependabot.yml의 github-actions에 양수 cooldown.default-days가 있으면 우선 적용하고, 그다음 사용자 설정의 cooldown_days를 봅니다. 둘 다 없다면 발행일 기준 필터가 없고, 최근 3일 이내 태그에 비차단 fresh-tag 알림만 냅니다. 설정 우선순위와 알림 로직을 참고하세요. 실제 생성된 ref, commit, Diff를 확인해야 합니다.
Sticky pins 정책 때문에 일반적인 gh actions-lock 재실행은 상위 main이나 v3가 움직였다는 이유만으로 기존 커밋을 덮어쓰지 않습니다. 의도적으로 업데이트하려면 gh actions-lock --relock을 실행하고 변경된 커밋을 리뷰해야 합니다. 강제 푸시 등으로 이전 커밋이 도달 불가능해졌을 때 쓰는 --relock --accept-moved는 별도 신뢰 판단입니다. CI 오류를 손쉽게 해결하려고 무심코 넣을 플래그가 아닙니다.
Dependabot은 이미 등록된 워크플로의 Action과 Lockfile을 업데이트할 수 있지만, 새로운 워크플로를 처음 등록해 주지는 않습니다. GitHub의 강제 적용 안내는 작은 범위에서 시험하고 CODEOWNERS로 Lockfile 변경을 검토한 다음 조직 수준의 Require lockfile을 고민하도록 권고합니다. 계정 플랜과 정책 권한을 먼저 확인하세요. 고정 버전 README에 따르면 GitHub Enterprise Server(GHES)는 이 프리뷰에서 지원되지 않으며 Enterprise Cloud와는 다릅니다.
Lockfile은 어떤 Action 코드가 실행될지를 확인하지만 코드의 취약점이나 악성 여부를 판단하지는 않습니다. run:의 다운로드, Digest로 고정하지 않은 이미지, 패키지 의존성, CI Token 권한은 각각 따로 관리해야 합니다. 시범 적용을 마친 팀이라면 어떤 중첩 Action을 새로 고정했고, 누가 저장소 ID를 검토했으며, 공유 Reusable Workflow 내부는 누가 책임지는지 답할 수 있어야 합니다. 탐지와 병합 차단의 차이는 GitHub AI Scan 분석에서 별도로 다뤘습니다.
근거와 시점: GitHub는 2026년 10월 14일 Workflow Lockfiles Public Preview를 발표했습니다. 이 글은 gh-actions-lock v0.2.0 커밋 d5dca22a06ff8590d50b41612038996800cfb336와 포맷 저장소 커밋 ac158834765237ee94c36606cfc7d87316aa49ab를 10월 18일 기준으로 소스 검토했습니다. YAML은 합성 SHA를 포함한 상위 테스트 Fixture이며 실제 Runner 구동, CLI 마이그레이션, 태그 이동 실험은 실행하지 않았습니다. 프리뷰 사용 가능 여부는 계정 및 정책 설정에 따릅니다.