Repository navigation
152 lines (140 loc) · 9.14 KB
/
Copy pathpython-release.yml
File metadata and controls
152 lines (140 loc) · 9.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
name: Python Release
# 태그 드리븐 PyPI 릴리스 (WBS 7.1). py-v로 시작하는 태그 push에서만 트리거된다.
# ⚠️ 거버넌스: 실제 배포는 사람이 `py-v*` 태그를 push해야만 실행된다(human-gated).
# 인증은 저장된 토큰/시크릿이 아니라 PyPI Trusted Publisher(OIDC)를 사용한다 —
# https://docs.pypi.org/trusted-publishers/ 에서 이 저장소(repo owner/name),
# 워크플로 파일명(`python-release.yml`), environment(미사용 시 비움)를 PyPI 프로젝트
# `keycloak-sdk`에 사전 등록해야 동작한다. 미설정 시 publish 스텝에서 OIDC 클레임이
# PyPI에 등록된 publisher와 일치하지 않아 인증 실패로 종료된다. 저장소에 별도로
# 추가해야 하는 GitHub Secret은 없다.
on:
push:
tags: ['py-v*']
# 최소권한: 워크플로 레벨 기본값을 두지 않고 **모든 잡이 자기 권한을 직접 선언**한다. 워크플로 레벨에
# `contents: read`를 두면 그 스코프를 실제로 쓰지 않는 잡까지 토큰에 읽기 권한을 갖기 때문이다
# (SonarQube `githubactions:S7637`. 이 저장소에서 그 규칙이 걸린 워크플로는 정확히 "체크아웃 없는 잡을
# 가진 넷" — java·dotnet·go·php — 이었다. 나머지 다섯은 모든 잡이 체크아웃해서 걸리지 않았다).
# ⚠️ 새 잡에는 반드시 `permissions:`를 함께 적을 것 — 빠뜨리면 워크플로 레벨 기본값이 아니라
# **저장소 기본 권한**(Settings → Actions)을 물려받는다. 그 값은 이 파일 바깥에서 바뀔 수 있다.
# ⚠️ 같은 릴리스가 **겹쳐 돌지 않게** 한다. `cancel-in-progress: false` 는 의도다 — 게시 도중
# 취소하는 것이 큐에서 기다리는 것보다 나쁘다(반쯤 올라간 아티팩트는 되돌릴 수 없다).
# 그룹 키에 ref 를 넣는다 — 서로 다른 버전은 각자 다른 아티팩트를 올리므로 직렬화할 이유가 없고, 막아야 하는 것은 **같은 태그가 두 번 도는 것**(실패 후 태그를 옮겨 다시 미는 흐름)이다.
# ⚠️ concurrency 가 지켜주는 것은 **실행 중**인 잡뿐이다. 같은 그룹에서 **대기 중**이던 실행은
# 세 번째가 큐에 들어올 때 GitHub 이 취소한다 — 태그를 연달아 셋 밀지 말 것(DEPLOY.md §4).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# 태그 접미사 = 릴리스 버전. 이 워크플로에서 버전을 유도하는 **유일한** 지점이다 — install-smoke와
# publish가 각자 태그를 파싱하면 "게이트가 검증한 버전"과 "실제로 게시하는 버전"이 갈릴 수 있다.
# 태그↔매니페스트 정합성 가드도 여기로 옮겼다: 45분짜리 install-smoke를 태우기 전에 실패한다.
version:
runs-on: ubuntu-latest
# 태그↔매니페스트 가드가 매니페스트를 읽어야 하므로 체크아웃이 필요하다
permissions:
contents: read
outputs:
version: ${{ steps.derive.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# 태그↔소스 정합성(fail-fast): 태그가 함의하는 버전과 pyproject.toml이 선언한 버전이
# 다르면 어떤 게이트도 돌기 전에 중단한다. 추출 실패(빈 문자열)도 실패로 취급한다 —
# 조용히 빈 값이 되면 어떤 값과도 "다르다"고 판정되지 않는 오검증이 생기기 때문이다.
- id: derive
name: Derive version from tag and verify it matches pyproject
run: |
VERSION="${GITHUB_REF_NAME#py-v}"
if [ -z "$VERSION" ] || [ "$VERSION" = "$GITHUB_REF_NAME" ]; then
echo "::error::태그 ${GITHUB_REF_NAME}에서 'py-v' 접두를 벗겨내지 못했다 — 버전 유도 불가(fail-closed)."
exit 1
fi
ACTUAL=$(grep -m1 -E '^version[[:space:]]*=' python/pyproject.toml | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/' || true)
if [ -z "$ACTUAL" ]; then
echo "::error::python/pyproject.toml에서 [project] version을 추출하지 못했다 — 정합성 검증 불가(fail-closed)."
exit 1
fi
if [ "$VERSION" != "$ACTUAL" ]; then
echo "::error::tag ${GITHUB_REF_NAME} implies version ${VERSION} but python/pyproject.toml declares ${ACTUAL}"
exit 1
fi
echo "tag ${GITHUB_REF_NAME} ↔ python/pyproject.toml ${ACTUAL} 일치"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# 게시 직전 마지막 게이트 — 패키징된 산출물을 로컬 레지스트리에 올린 뒤 모노레포 바깥의 깨끗한
# 컨테이너에서 설치해 quickstart를 돌린다. 소스 테스트가 통과해도 패키징 층(매니페스트 메타데이터·
# 파일 포함규칙·게시 아티팩트 호환성)의 결함은 여기서만 드러난다 — 레지스트리에 태우기 전 마지막 기회다.
# ⚠️ `version`은 **반드시** 태그 접미사와 같은 값으로 넘긴다 — 이 게이트가 검증하는 것은 그 값의
# 산출물이라, 다른 값을 넘기면 **배포하지 않을 것을 보증**한다(install-smoke.yml 주석 참고).
# 빠뜨리면 `required: true`·기본값 없음이라 워크플로 호출 자체가 실패한다(조용히 통과하지 않는다).
install-smoke:
needs: version
# 호출되는 install-smoke.yml이 체크아웃한다. ⚠️ 재사용 워크플로를 부르는 잡에서 `permissions:`를
# 생략하면 호출된 워크플로는 저장소 기본 권한을 상한으로 받는다(GitHub 문서: "If jobs.<job_id>.permissions
# is not specified in the calling job, the called workflow will have the default permissions for the
# GITHUB_TOKEN"). 즉 여기 한 줄이 곧 호출된 워크플로의 권한 상한이다.
permissions:
contents: read
uses: ./.github/workflows/install-smoke.yml
with:
lang: python
version: ${{ needs.version.outputs.version }}
# 발행 전 게이트: 태그된 커밋이 green이 아니면 PyPI로 나가지 않는다(무회귀 보증).
verify:
runs-on: ubuntu-latest
# 체크아웃만 필요하다
permissions:
contents: read
defaults: { run: { working-directory: python } }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
with: { python-version: '3.12' }
- run: 'python -m pip install --only-binary :all: -e ".[dev]"'
- run: ruff check src tests examples
- run: ruff format --check src tests examples
- run: mypy src
- run: pytest -m "not integration" --cov=keycloak_sdk
# 발행 전 게이트(2): 단위테스트만으로는 admin/auth 네트워크 경계를 검증할 수 없다 —
# python-ci.yml의 integration 잡과 동일한 실 Keycloak 컨테이너 E2E를 태그 경로에서도 돌린다.
integration:
name: Integration tests (testcontainers, 실제 Keycloak)
runs-on: ubuntu-latest # ubuntu 러너는 Docker 내장 → testcontainers 동작
# 체크아웃만 필요하다
permissions:
contents: read
defaults: { run: { working-directory: python } }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
with: { python-version: '3.12' }
- run: 'python -m pip install --only-binary :all: -e ".[dev]"'
- name: pytest (integration, Docker)
run: pytest -m integration
release:
needs: [version, verify, integration, install-smoke] # 단위 + 통합 + 설치 스모크 전부 통과해야 발행
runs-on: ubuntu-latest
permissions:
contents: read # checkout
id-token: write # PyPI Trusted Publisher(OIDC)에 필요 — 다른 권한 불필요
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
with:
python-version: '3.12'
- name: Install build backend
run: 'python -m pip install --only-binary :all: build twine'
- name: Build sdist + wheel
working-directory: python
run: python -m build
# 업로드 전 메타데이터 검증. install-smoke는 **로컬** pypiserver에 올려보는데 그건 관대해서
# Warehouse(PyPI)의 검증을 대신하지 못한다 — 즉 "설치되고 동작한다"가 증명돼도 "PyPI가
# 받아준다"는 별개다. PyPI 업로드는 재시도가 아니라 **버전을 태우는** 일이므로 거절 사유는
# 업로드 전에 알아야 한다. `--strict`는 경고까지 실패로 올린다(README 렌더링 포함).
- name: Validate package metadata (twine check)
working-directory: python
run: python -m twine check --strict dist/*
- name: Publish to PyPI (Trusted Publisher / OIDC)
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: python/dist