# GitHub Actions Workflow Lockfiles: uses@SHA만 고정해도 충분하지 않은 이유

> Canonical: https://vxi.me/ko/posts/github-actions-workflow-lockfiles-transitive-dependencies/
> Locale: ko-kr
> Published: 2026-10-18
> Updated: 2026-10-18
> Markdown: https://vxi.me/ko/posts/github-actions-workflow-lockfiles-transitive-dependencies.md

# GitHub Actions Workflow Lockfiles: uses@SHA만 고정해도 충분하지 않은 이유

GitHub Actions의 `uses: some-org/action@v2`를 전체 커밋 SHA로 바꾸면 해당 진입점의 코드는 고정됩니다. 하지만 고정된 Composite Action이 내부에서 다른 `example-org/upload@v1`을 호출한다면 어떨까요? 바깥 SHA는 그대로여도 안쪽 태그는 바뀔 수 있습니다. 워크플로 YAML만 읽어서는 실제로 실행되는 외부 Action의 전체 의존성 그래프를 파악하기 어렵습니다.

GitHub가 [공개 프리뷰](https://github.blog/changelog/2026-10-14-github-actions-workflow-lockfiles-in-public-preview)로 발표한 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](https://github.com/github/gh-actions-lock/releases/tag/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 개념 문서](https://docs.github.com/en/actions/concepts/security/workflow-dependency-lockfiles)에 따르면 한 저장소의 Lockfile은 해당 저장소가 정의한 워크플로를 대상으로 합니다. 다른 저장소에 정의된 Reusable Workflow 안에서 사용하는 Action은 **그 워크플로가 정의된 저장소**의 Lockfile이 책임집니다. 호출하는 쪽에서 Reusable Workflow의 SHA를 고정했다고 해서 상대방의 모든 내부 Action까지 검토한 것은 아닙니다.

## actions.lock은 무엇을 기록하나

`workflows`는 워크플로 경로와 필요한 Pin을 연결합니다. `dependencies`는 각 `ref`, `commit`, `owner_id`, `repo_id` 및 하위 Action 호출의 `uses`를 기록합니다. 아래 YAML은 [공식 포맷 저장소의 고정 커밋에 포함된 `benchV003` 파서 테스트 데이터](https://github.com/github/actions-lockfile/blob/ac158834765237ee94c36606cfc7d87316aa49ab/go/pkg/lockfile/bench_test.go#L31-L58)입니다. **v0.0.3 형식의 실제 테스트 fixture**이지만 반복되는 a/b SHA 값은 테스트용 가상 값이며 배포에서 생성된 결과가 아닙니다.

```yaml
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` 소스](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/internal/lockfile/state_marshal.go)에도 필드 직렬화가 나옵니다.

```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](https://github.com/github/gh-actions-lock/releases/tag/v0.2.0)와 [포맷 README](https://github.com/github/actions-lockfile/blob/ac158834765237ee94c36606cfc7d87316aa49ab/README.md)는 **`v0.0.3`**을 사용합니다. CLI 버전과 Lockfile Schema 버전은 별개입니다.

## 강제 정책을 켜기 전에 실제 변경 사항부터 확인하기

되돌릴 수 있는 테스트 브랜치에서 중요도가 낮은 워크플로 하나를 선택합니다. GitHub CLI가 설치·로그인되어 있고 참조 저장소에 대한 읽기 권한이 있다는 전제의 명령입니다.

```bash
# 이 글에서 검토한 버전으로 확장 설치
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](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/root.go#L64-L70)는 워크플로 경로 인자를 받고, [verify.go](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/verify.go#L24-L40)와 관련 테스트도 그 범위를 사용합니다. 공유 `actions.lock` 자체는 정상 파싱되어야 하지만, 다른 워크플로까지 자동으로 등록하지는 않습니다.

도구는 Lockfile만 만드는 것이 아닙니다. 일부 버전 태그를 더 구체적인 버전으로 바꾸거나, 실제 `action.yml`이 존재하는 로컬 Action을 `./actions/build`에서 `$/actions/build`로 변경할 수 있습니다.

```diff
 steps:
-  - uses: ./actions/build
+  - uses: $/actions/build
```

`$/` 구문은 실행 중인 저장소 커밋에 연결됩니다. 잘못된 `./` 경로를 자동 복구하는 것은 아닙니다. 또 `git diff`에는 아직 추적하지 않는 새 `actions.lock`이 표시되지 않으므로 `git status --short`와 파일 내용도 함께 점검해야 합니다.

조직의 Allowed Actions 정책은 또 다른 위험 지점입니다. Lockfile이 적용된 워크플로에서는 허용 목록이 **고정된 커밋 SHA**를 기준으로 평가될 수 있습니다. 이전에 `actions/checkout@v6` 태그로 허용했다면 불일치가 발생할 수 있으므로 실제 검토한 커밋을 기준으로 규칙을 수정하는 편이 낫습니다.

로컬 검증과 온라인 검증은 별개의 단계입니다.

```bash
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 분류](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/internal/pipeline/checks/category.go)에 `not-pinned`, `stale`, `local-action`, `repo-replaced`, `unreachable-pin`이 정의되어 있습니다.

**되돌릴 수 있는 다섯 단계의 시범 적용:**
1. **의존성 조사**: 직접 `uses`, 중첩 Composite Action, 외부 Reusable Workflow를 나열하고 정의 저장소 담당자를 정합니다.
2. **테스트 브랜치에서 생성**: 고정 버전 도구로 워크플로를 등록한 후 YAML, `action.yml`, `actions.lock`의 변경을 확인합니다.
3. **정책 검토**: 잠긴 SHA와 Allowed Actions를 비교하고 모든 차이를 정상적인 PR 리뷰에 올립니다.
4. **두 단계 검증**: `--verify-local`로 커버리지를, `--verify`로 온라인 신뢰 조건을 확인합니다. `not-pinned`, `stale`, `local-action`은 경로와 Diff를, `repo-replaced`, `unreachable-pin`은 상위 이력을 먼저 조사합니다.
5. **롤백 유지**: 파싱이나 정책 문제로 CI가 막히면 테스트 브랜치 변경을 되돌립니다. 소규모 적용이 안정화된 뒤 전체 조직으로 넓힙니다.

## 도입한 후 의존성은 어떻게 업데이트되나

최초로 잠기는 커밋은 원래의 `uses` 참조와 해석 결과에 따라 달라집니다. v0.2.0에 **무조건 7일을 기다리는 기본값은 없습니다**. `.github/dependabot.yml`의 `github-actions`에 양수 `cooldown.default-days`가 있으면 우선 적용하고, 그다음 사용자 설정의 `cooldown_days`를 봅니다. 둘 다 없다면 발행일 기준 필터가 없고, 최근 3일 이내 태그에 비차단 `fresh-tag` 알림만 냅니다. [설정 우선순위](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/fresh_tag.go#L16-L32)와 [알림 로직](https://github.com/github/gh-actions-lock/blob/d5dca22a06ff8590d50b41612038996800cfb336/cmd/gh-actions-lock/fresh_tag.go#L48-L75)을 참고하세요. 실제 생성된 `ref`, `commit`, Diff를 확인해야 합니다.

**Sticky pins** 정책 때문에 일반적인 `gh actions-lock` 재실행은 상위 `main`이나 `v3`가 움직였다는 이유만으로 기존 커밋을 덮어쓰지 않습니다. 의도적으로 업데이트하려면 `gh actions-lock --relock`을 실행하고 변경된 커밋을 리뷰해야 합니다. 강제 푸시 등으로 이전 커밋이 도달 불가능해졌을 때 쓰는 `--relock --accept-moved`는 별도 신뢰 판단입니다. CI 오류를 손쉽게 해결하려고 무심코 넣을 플래그가 아닙니다.

Dependabot은 **이미 등록된** 워크플로의 Action과 Lockfile을 업데이트할 수 있지만, 새로운 워크플로를 처음 등록해 주지는 않습니다. [GitHub의 강제 적용 안내](https://docs.github.com/en/actions/how-tos/administer/enforce-workflow-lockfiles)는 작은 범위에서 시험하고 CODEOWNERS로 Lockfile 변경을 검토한 다음 조직 수준의 `Require lockfile`을 고민하도록 권고합니다. 계정 플랜과 정책 권한을 먼저 확인하세요. 고정 버전 README에 따르면 GitHub Enterprise Server(GHES)는 이 프리뷰에서 지원되지 않으며 Enterprise Cloud와는 다릅니다.

Lockfile은 어떤 Action 코드가 실행될지를 확인하지만 코드의 취약점이나 악성 여부를 판단하지는 않습니다. `run:`의 다운로드, Digest로 고정하지 않은 이미지, 패키지 의존성, CI Token 권한은 각각 따로 관리해야 합니다. 시범 적용을 마친 팀이라면 **어떤 중첩 Action을 새로 고정했고, 누가 저장소 ID를 검토했으며, 공유 Reusable Workflow 내부는 누가 책임지는지** 답할 수 있어야 합니다. 탐지와 병합 차단의 차이는 [GitHub AI Scan 분석](/posts/github-ai-scan-pr-security-checks/)에서 별도로 다뤘습니다.

---

*근거와 시점: GitHub는 2026년 10월 14일 Workflow Lockfiles Public Preview를 발표했습니다. 이 글은 `gh-actions-lock v0.2.0` 커밋 `d5dca22a06ff8590d50b41612038996800cfb336`와 포맷 저장소 커밋 `ac158834765237ee94c36606cfc7d87316aa49ab`를 10월 18일 기준으로 소스 검토했습니다. YAML은 합성 SHA를 포함한 상위 테스트 Fixture이며 실제 Runner 구동, CLI 마이그레이션, 태그 이동 실험은 실행하지 않았습니다. 프리뷰 사용 가능 여부는 계정 및 정책 설정에 따릅니다.*
