
GHWM은 GitHub Actions 워크플로를 중앙 레지스트리에서 패키지처럼 설치하고 버전별로 관리하는 명령줄 도구다. 팀은 ghwm.yml에 사용할 워크플로를 적고, 설치된 버전은 ghwm.lock에 기록해 저장소마다 같은 구성을 유지할 수 있다.
워크플로는 공식 ghwfxlab/ghwm-registry나 직접 만든 레지스트리에서 가져올 수 있다. ghwm install은 워크플로 파일을 .github/workflows/에 기록한다. 업데이트할 때는 기본적으로 저장소에 있는 on: 트리거를 보존하며, GHWM이 관리하지 않는 파일은 --force를 지정하지 않는 한 건너뛴다.
GHWM은 원격에서 호출하는 재사용 워크플로와 달리 저장소에 실제 파일을 설치하므로 파일을 읽고 검토하거나 수정할 수 있다. 한 번 복사하고 나면 업데이트 경로가 없는 스타터 워크플로와 달리, GHWM은 버전과 해시를 추적해 ghwm update로 새 릴리스를 가져온다.
설치와 인증
권장 설치 방법은 Python 패키지 관리자 uv를 사용하는 것이다.
uv tool install ghwm
특정 버전을 고정하려면 다음과 같이 설치한다.
uv tool install ghwm==X.Y.Z
다른 설치 방법으로는 pipx나 활성화된 가상 환경에서 사용하는 pip가 있다. GitHub의 main 브랜치에 있는 미출시 최신 코드를 설치하는 명령도 제공한다.
pipx install ghwm
pip install ghwm
uv tool install git+https://github.com/ghwfxlab/ghwm.git
GHWM은 공개 레지스트리에서도 워크플로 패키지를 GitHub Packages에서 내려받으므로 GitHub 토큰이 필요하다. gh auth login으로 로그인한 뒤 gh auth refresh -s read:packages를 실행하거나, read:packages 권한이 있는 GH_TOKEN을 설정한다. GitHub Actions에서는 packages: read 권한을 가진 GITHUB_TOKEN을 사용한다. 인증 정보는 활성화된 gh CLI 자격 증명(gh auth token 출력), GH_TOKEN, GITHUB_TOKEN 순으로 찾는다.
export GH_TOKEN=ghp_...
# or
export GITHUB_TOKEN=ghp_...
gh CLI 토큰에 read:packages 권한을 추가하는 명령은 다음과 같다.
gh auth refresh -s read:packages
공개 레지스트리를 설정할 때는 GitHub Packages에 게시한 패키지의 공개 범위도 Private에서 Public으로 직접 변경해야 한다. 패키지가 비공개로 남아 있으면 조직 외부에서 ghwm install을 실행할 때 404 Not Found 또는 401 Unauthorized 오류가 발생할 수 있다.
워크플로 설치와 업데이트
저장소 루트에 ghwm.yml을 만든다. 최소 매니페스트에는 레지스트리 source와 각 워크플로의 name, version이 필요하다. 원격 설치에서는 version을 지정해야 한다.
source: ghwfxlab/ghwm-registry
workflows:
- name: super-linter # Required.
version: "2.0.1" # Required for remote installs.
다음 예시는 지원하는 모든 키를 사용한다. source는 선택 사항이며 기본값은 owner/ghwm-registry 형식의 전역 레지스트리다. 워크플로별 source는 전역 설정을 덮어쓴다. target을 지정하면 생성할 워크플로 파일 이름을 바꿀 수 있다. update-* 설정은 업데이트에만 적용되며, false는 저장소의 값을 유지하고 true는 패키지에 있는 값으로 바꾼다.
# Default registry in owner/repository form.
# Optional. Default: owner/ghwm-registry
source: ghwfxlab/ghwm-registry
workflows:
- name: auto-assign-pr # Required.
version: "1.0.2" # Required for remote installs. Default: none (only allowed with --local)
source: other-owner/other-registry # Optional. Default: the global `source`
target: assign-pr.yml # Optional. Default: the filename packaged in the workflow
# The update-* options only apply on update. false keeps the local value, true replaces it with the packaged one.
update-triggers: true # Default: false. Replaces the local `on:` block
update-envs: true # Default: false. Replaces the local `env:` block
update-runs-on: true # Default: false. Replaces each job's local `runs-on`
update-config-files: true # Default: false. Overwrites packaged config files
각 워크플로는 기본적으로 전역 source에서 찾는다. 워크플로 항목에 source: other-owner/other-registry를 추가하면 해당 항목만 다른 레지스트리를 사용한다. CLI는 워크플로를 @<source owner>/<name> 형식의 GitHub Packages npm 패키지로 해석한다. 전체 필드와 자료형, 기본값은 매니페스트와 잠금 파일 참조 문서에 설명돼 있다.
주요 명령은 다음과 같다.
ghwm install # installs workflows listed in ghwm.yml
ghwm install --force # overwrites even if files were modified locally
ghwm install --no-prune # skip removal of stale workflows
ghwm install --update-triggers # replace workflow triggers with the packaged version
ghwm install --update-envs # replace workflow env variables with the packaged version
ghwm install --update-runs-on # replace each job's runs-on with the packaged version
ghwm upgrade # automatically resolve, pin, and install latest versions
ghwm update # re-downloads all workflows (respects versions)
ghwm update --prune # also removes managed workflows no longer in ghwm.yml
ghwm update --update-triggers # replace workflow triggers with the packaged version
ghwm update --update-envs # replace workflow env variables with the packaged version
ghwm update --update-runs-on # replace each job's runs-on with the packaged version
ghwm list # shows workflows declared in ghwm.yml
ghwm audit # audits managed workflows for security vulnerabilities
워크플로 파일은 관리 파일임을 나타내는 헤더와 함께 .github/workflows/에 기록된다. 패키지에 설정 파일이 더 들어 있으면 해당 파일도 설치된다. 최초 설치에서는 대상 파일이 이미 있으면 설정 파일을 만들지 않는다. 업데이트에서는 해당 워크플로에 update-config-files: true가 설정된 경우에만 설정 파일을 덮어쓴다.
| 파일 종류 | 최초 설치 | 업데이트 | 정리(prune) |
|---|---|---|---|
워크플로 파일(.github/workflows/*) | 관리 헤더를 붙여 설치 | 기존 on:을 기본적으로 보존하며 제자리에서 업데이트 | 제거 |
| 패키지 설정 파일 | 파일이 없을 때만 생성 | update-config-files: true일 때만 덮어쓰기 | 유지 |
관리되는 워크플로 파일의 헤더 예시는 다음과 같다.
# Managed by ghwm (linter@0.1.4)
# Source: @owner/linter:linter.yml
# Hash: sha256:...
# Re-run `ghwm install` to refresh this file.
CLI는 이 헤더를 이용해 관리 파일과 직접 작성한 파일을 구분한다. 관리되지 않는 파일은 --force 없이 덮어쓰지 않는다. 업데이트 때는 update-triggers: true 또는 --update-triggers를 쓰지 않으면 기존 on: 규칙을 보존한다. update-envs: true 또는 --update-envs를 쓰지 않으면 기존 env: 변수도 유지한다.
각 작업의 기존 runs-on 값은 작업 ID를 기준으로 대응시켜 보존한다. 이를 패키지 값으로 바꾸려면 update-runs-on: true 또는 --update-runs-on을 지정한다. 새 패키지 버전에 추가된 작업에는 패키지의 runs-on 값이 적용된다. 보존하던 작업이 새 패키지에서 사라지면 알림이 표시된다. 매니페스트에서 삭제된 오래된 워크플로 파일은 정리할 수 있지만, 설정 파일에는 관리 헤더가 붙지 않으며 정리 과정에서도 삭제되지 않는다.
ghwm update --prune은 ghwm.yml에 남아 있는 워크플로를 새로고침하고 매니페스트에서 삭제된 관리 워크플로를 제거한다. 이 명령은 ghwm.lock도 갱신한다.
잠금 파일과 보안 점검
ghwm.lock은 설치된 각 패키지와 패키지가 관리하는 파일을 기록하는 JSON 파일이다. 예시는 다음과 같다.
{
"lockfileVersion": 1,
"packages": [
{
"name": "auto-assign-pr",
"version": "2.0.0",
"source": "@owner/auto-assign-pr",
"files": [
{
"target": ".github/workflows/auto-assign-pr.yaml",
"source_hash": "sha256:..."
},
{
"target": ".github/auto_assign.yaml",
"source_hash": "sha256:...",
"overwrite": false
}
]
}
]
}
ghwm.lock을 ghwm.yml과 함께 커밋하면 CI와 팀원이 정확히 같은 워크플로 패키지 구성을 설치할 수 있다. 이전 tarball 방식으로 만든 잠금 파일은 거부되므로 다시 생성해야 한다.
ghwm audit은 설치된 모든 관리 워크플로를 zizmor로 정적 보안 분석하고 0부터 100까지의 보안 점수를 계산한다. zizmor가 로컬에 설치돼 있지 않으면 ghwm이 uvx zizmor로 실행을 시도한다. 점수는 로그 스케일의 지수 감소 방식으로 계산해 음수가 되지 않도록 한다.
$$\text{Score} = \text{round}\left(100 \times e^{-\text{Deductions} / 100}\right)$$
감점은 심각도별로 높음 20점, 중간 10점, 낮음 5점, 정보성 1점이다. 높음 또는 중간 심각도의 취약점이 하나라도 발견되면 ghwm audit은 종료 코드 1을 반환하므로 CI 파이프라인의 통과 조건으로 사용할 수 있다. 자세한 안내는 워크플로 감사와 보안 점수를 참고한다.
Renovate로 업데이트 자동화
GHWM은 워크플로를 GitHub Packages에 게시된 npm 패키지(예: @owner/<name>)로 해석한다. 따라서 Renovate를 사용하면 새 버전을 감지해 ghwm.yml의 버전 업데이트를 위한 풀 리퀘스트를 열 수 있다. 다음 regexManagers 설정을 renovate.json 또는 renovate.json5에 추가한다. @owner는 레지스트리 패키지를 게시하는 GitHub 조직 또는 사용자 이름으로 바꿔야 한다.
{
"regexManagers": [
{
"fileMatch": ["^ghwm\\.yml$"],
"matchStrings": [
"name:\\s+(?<depName>\\S+)\\s+version:\\s+[\"']?(?<currentValue>[^\"'\\s]+)[\"']?"
],
"datasourceTemplate": "npm",
"depNameTemplate": "@owner/{{depName}}",
"registryUrlTemplates": ["https://npm.pkg.github.com"]
}
]
}
원격 레지스트리 대신 로컬에서 시험하기
테스트할 때는 체크아웃한 로컬 레지스트리를 지정할 수 있다.
ghwm install --local ../ghwm-registry
--local을 쓰면 CLI는 npm 패키지 tarball을 내려받는 대신 체크아웃된 저장소의 workflows/<name>/workflow.yml에서 워크플로를 읽는다. 로컬 설치에서는 매니페스트의 version을 생략할 수 있다.
텔레메트리와 개인정보
텔레메트리는 기본적으로 켜져 있다. ghwm install과 ghwm update는 공개 레지스트리에서 가져온 워크플로에 한해 익명 이벤트를 보낸다. 수집 항목은 레지스트리 출처(예: owner/ghwm-registry), 워크플로 이름(예: linter), 버전(예: 1.2.3), 이벤트 유형(처음 추가한 install 또는 다시 설치할 때 워크플로가 바뀐 updated)이다. 워크플로 메타데이터가 있으면 제목, 설명, 태그, 아이콘, 소유자, 버전, 출처, 생성 시각도 수집 대상에 포함될 수 있다. 메타데이터 형식은 워크플로 frontmatter 명세를 참고한다.
IP 주소, 호스트 이름, 사용자 이름, 파일 경로, 저장소 이름, 환경 변수, 그 밖의 시스템·사용자 정보는 수집하지 않는다고 GHWM은 설명한다. 텔레메트리는 워크플로 레지스트리가 공개 GitHub 저장소일 때만 전송된다. 데이터를 보내기 전에 인증 없이 GitHub REST API의 GET /repos/{owner}/{repo}를 호출해 워크플로 출처 레지스트리의 공개 여부를 확인한다. 레지스트리가 비공개 또는 내부용이거나 네트워크 오류, 요청 제한, 권한 부족 등으로 확인에 실패하면 해당 워크플로의 텔레메트리는 조용히 건너뛰며 설치는 계속된다. 따라서 비공개 또는 내부용 기업 레지스트리의 워크플로는 추적하지 않는다고 안내한다.
한 번 실행할 때 텔레메트리를 끄려면 --no-telemetry를 사용한다.
ghwm install --no-telemetry
ghwm update --no-telemetry
ghwm upgrade --no-telemetry
영구적으로 끄려면 CI/CD 파이프라인이나 전역 셸 설정에서 DO_NOT_TRACK=1 또는 GHWM_NO_TELEMETRY=1을 설정한다. DO_NOT_TRACK 환경 변수에 관한 정보는 consoledonottrack.com에서 확인할 수 있다.
export DO_NOT_TRACK=1
# or
export GHWM_NO_TELEMETRY=1
자체 레지스트리 만들기
레지스트리는 workflows/ 폴더에 워크플로별 npm 패키지를 두고 이를 GitHub Packages에 게시하는 GitHub 저장소다. 공식 ghwfxlab/ghwm-registry를 참고할 수 있다. 새 저장소를 예로 들어 my-org/ghwm-registry라고 하면 소유자 이름이 npm 범위 @my-org/<name>이 된다.
각 워크플로마다 workflows/linter/ 같은 폴더를 만들고 워크플로 파일, 설정 파일, package.json, workflow.yml을 넣는다. 다음은 workflow.yml의 예다.
# workflows/linter/workflow.yml
name: linter
files:
- source: linter.yaml target: .github/workflows/linter.yaml
선택적으로 주석형 frontmatter를 넣어 제목, 설명, 태그를 지정할 수 있다. package.json에는 "name": "@my-org/linter"와 "publishConfig": {"registry": "https://npm.pkg.github.com"}를 설정하고, 게시할 파일을 "files"에도 열거한다.
게시 전에 소비자 저장소에서 다음 명령으로 로컬 레지스트리를 시험할 수 있다.
ghwm install --local ../ghwm-registry
그 뒤 각 패키지를 GitHub Packages에 게시한다. 공식 레지스트리는 main에 푸시할 때마다 실행되는 게시 워크플로를 사용한다. 조직 외부 사용자도 이용해야 한다면 게시한 패키지의 공개 범위를 Public으로 설정한다. 소비자 저장소의 ghwm.yml에서 레지스트리를 지정하는 예시는 다음과 같다.
source: my-org/ghwm-registry
workflows:
- linter@1.0.0
개발과 설계
저장소는 다음 Make 명령을 개발 작업에 제공한다.
make install # uv sync (dev deps)
make test # pytest
make test-e2e # run end-to-end tests using testcontainers
make lint # ruff check
make format # ruff format
make type-check # mypy
make lang # textlint for prose/documentation
make precommit # run pre-commit hooks on all files
make super-linter # run super-linter via Docker
make clean # remove build artifacts
설계 세부 사항은 ARCHITECTURE.md에 있다. 라이선스는 MIT다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요