DefStream-RabbitMQ 0.1.2.6__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of DefStream-RabbitMQ might be problematic. Click here for more details.
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/.github/workflows/ci.yml +210 -107
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3/DefStream_RabbitMQ.egg-info}/PKG-INFO +2 -2
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/DefStream_RabbitMQ.egg-info/SOURCES.txt +1 -0
- defstream_rabbitmq-0.1.3/DefStream_RabbitMQ.egg-info/requires.txt +2 -0
- {defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info → defstream_rabbitmq-0.1.3}/PKG-INFO +2 -2
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/setup.py +1 -1
- defstream_rabbitmq-0.1.3/test/test_release_workflow.py +398 -0
- defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info/requires.txt +0 -2
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/.gitignore +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/DefStream_RabbitMQ.egg-info/dependency_links.txt +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/DefStream_RabbitMQ.egg-info/top_level.txt +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/LICENSE +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/README.en.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/README.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/issues/feature/1/IMPLEMENTS.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/issues/feature/1/README.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/issues/feature/1/REQUIREMENTS.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/issues/feature/1/TASK.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/issues/feature/1/ci.refs.json +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/sample/Sample.RPC.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/sample/Sample.Stream.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/sample/Sample.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/setup.cfg +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Configuration.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Connection.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Consumer.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Event.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Exchange.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Policies.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Queue.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Serializer.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Stream.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/Subscription.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/src/__init__.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_broker_methods.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_priorities.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_rabbitmq_integration.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_rabbitmq_worker.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_rpc.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/test/test_rpc_integration.py +0 -0
|
@@ -27,6 +27,9 @@ jobs:
|
|
|
27
27
|
outputs:
|
|
28
28
|
release: ${{ steps.changes.outputs.release }}
|
|
29
29
|
version: ${{ steps.changes.outputs.version }}
|
|
30
|
+
build: ${{ steps.changes.outputs.build }}
|
|
31
|
+
new_version: ${{ steps.changes.outputs.new_version }}
|
|
32
|
+
tag: ${{ steps.changes.outputs.tag }}
|
|
30
33
|
core_ref: ${{ steps.refs.outputs.core_ref }}
|
|
31
34
|
rabbitmq_ref: ${{ steps.refs.outputs.rabbitmq_ref }}
|
|
32
35
|
redis_ref: ${{ steps.refs.outputs.redis_ref }}
|
|
@@ -39,10 +42,80 @@ jobs:
|
|
|
39
42
|
uses: actions/setup-python@v7
|
|
40
43
|
with:
|
|
41
44
|
python-version: '3.13'
|
|
45
|
+
- name: Check complete push range and plan version
|
|
46
|
+
id: changes
|
|
47
|
+
env:
|
|
48
|
+
BEFORE: ${{ github.event.before }}
|
|
49
|
+
RELEASE_PUSH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
50
|
+
run: |
|
|
51
|
+
python - <<'PY'
|
|
52
|
+
from json import loads
|
|
53
|
+
from os import environ
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from re import fullmatch
|
|
56
|
+
from subprocess import check_output
|
|
57
|
+
|
|
58
|
+
release_push = environ['RELEASE_PUSH'] == 'true'
|
|
59
|
+
series = environ['RELEASE_SERIES']
|
|
60
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)', series) is None:
|
|
61
|
+
raise ValueError(f'Invalid RELEASE_SERIES: {series!r}')
|
|
62
|
+
before = environ['BEFORE']
|
|
63
|
+
if not release_push:
|
|
64
|
+
event = loads(Path(environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8'))
|
|
65
|
+
before = event['pull_request']['base']['sha']
|
|
66
|
+
if not before:
|
|
67
|
+
raise ValueError('Missing push base commit')
|
|
68
|
+
first_push = set(before) == {'0'}
|
|
69
|
+
base = check_output(['git', 'hash-object', '-t', 'tree', '--stdin'], input='', text=True).strip() if first_push else before
|
|
70
|
+
# 재실행 시 이번 실행에서 만든 태그는 제외한다.
|
|
71
|
+
tags = [] if first_push else check_output(['git', 'tag', '--merged', before], text=True).splitlines()
|
|
72
|
+
releases = []
|
|
73
|
+
release_tags = {}
|
|
74
|
+
for tag in tags:
|
|
75
|
+
match = fullmatch(r'v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:\.post(\d+))?', tag)
|
|
76
|
+
if match and f'{int(match[1])}.{int(match[2])}' == series:
|
|
77
|
+
release = tuple(map(int, match.groups()[:3])) + (
|
|
78
|
+
int(match[4] or 0), int(match[5] or 0), match[4] is not None or match[5] is not None,
|
|
79
|
+
)
|
|
80
|
+
releases.append(release)
|
|
81
|
+
release_tags[release] = tag
|
|
82
|
+
latest = max(releases) if releases else None
|
|
83
|
+
if latest is not None:
|
|
84
|
+
base = release_tags[latest]
|
|
85
|
+
changed = check_output(
|
|
86
|
+
[
|
|
87
|
+
'git', 'diff', '--name-only', '--no-renames', base, environ['GITHUB_SHA'],
|
|
88
|
+
'--', ':(glob)src/**/*.py', 'setup.py', 'setup.cfg', 'pyproject.toml', 'MANIFEST.in',
|
|
89
|
+
],
|
|
90
|
+
text=True,
|
|
91
|
+
).strip()
|
|
92
|
+
number = int(environ['GITHUB_RUN_NUMBER'])
|
|
93
|
+
if number < 1:
|
|
94
|
+
raise ValueError('Build number must be positive')
|
|
95
|
+
if latest is None and not changed:
|
|
96
|
+
raise ValueError('No release tag or source/packaging changes; tag the existing release before rebuilding')
|
|
97
|
+
migrate = latest is not None and latest[5]
|
|
98
|
+
new_version = bool(changed) or latest is None or migrate
|
|
99
|
+
if new_version:
|
|
100
|
+
patch = latest[2] + 1 if latest is not None else 0
|
|
101
|
+
print(f'New public version: source/packaging changes or initial/migration release\n{changed}')
|
|
102
|
+
else:
|
|
103
|
+
patch = latest[2]
|
|
104
|
+
print('Keeping MAJOR.MINOR.PATCH; publishing a new wheel build')
|
|
105
|
+
version = f'{series}.{patch}'
|
|
106
|
+
print(f'Planned version: {version}, wheel build: {number}')
|
|
107
|
+
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
108
|
+
output.write(f'release={str(release_push).lower()}\nversion={version}\n')
|
|
109
|
+
output.write(f'build={number}\nnew_version={str(new_version).lower()}\n')
|
|
110
|
+
output.write(f'tag={version if new_version else release_tags[latest]}\n')
|
|
111
|
+
PY
|
|
42
112
|
- name: Validate PR branch changelog
|
|
43
113
|
id: changelog
|
|
44
114
|
env:
|
|
45
115
|
GH_TOKEN: ${{ github.token }}
|
|
116
|
+
BEFORE: ${{ github.event.before }}
|
|
117
|
+
NEW_VERSION: ${{ steps.changes.outputs.new_version }}
|
|
118
|
+
RELEASE_TAG: ${{ steps.changes.outputs.tag }}
|
|
46
119
|
run: |
|
|
47
120
|
python - <<'PY'
|
|
48
121
|
from json import loads
|
|
@@ -67,20 +140,38 @@ jobs:
|
|
|
67
140
|
and pull['base']['ref'] == 'master'
|
|
68
141
|
and pull['base']['repo']['full_name'] == repository
|
|
69
142
|
]
|
|
70
|
-
if len(pulls)
|
|
71
|
-
raise ValueError('
|
|
72
|
-
branch = pulls[0]['head']['ref']
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
143
|
+
if len(pulls) > 1:
|
|
144
|
+
raise ValueError('Multiple merged PRs found for the release commit')
|
|
145
|
+
branch = pulls[0]['head']['ref'] if pulls else 'master'
|
|
146
|
+
if environ['NEW_VERSION'] == 'false':
|
|
147
|
+
tag = environ['RELEASE_TAG']
|
|
148
|
+
release = loads(check_output([
|
|
149
|
+
'gh', 'api', f'repos/{environ["GITHUB_REPOSITORY"]}/releases/tags/{tag}',
|
|
150
|
+
]))
|
|
151
|
+
if release['tag_name'] != tag or not release.get('body', '').strip():
|
|
152
|
+
raise ValueError(f'Existing release notes are missing or empty: {tag}')
|
|
153
|
+
body = release['body'].encode('utf-8')
|
|
154
|
+
print(f'Reusing release notes unchanged: {tag}')
|
|
155
|
+
elif environ['GITHUB_EVENT_NAME'] == 'push' and not pulls:
|
|
156
|
+
before = environ['BEFORE']
|
|
157
|
+
if not before:
|
|
158
|
+
raise ValueError('Missing push base commit')
|
|
159
|
+
revision = head if set(before) == {'0'} else f'{before}..{head}'
|
|
160
|
+
commits = check_output(['git', 'log', '--reverse', '--format=- %s (%h)', revision])
|
|
161
|
+
body = '## master 직접 push\n\n'.encode('utf-8') + commits
|
|
162
|
+
print('Direct master push: generating release notes from commits')
|
|
163
|
+
else:
|
|
164
|
+
path = f'issues/{branch}/README.md'
|
|
165
|
+
entry = check_output(['git', 'ls-tree', head, '--', path]).decode().split()
|
|
166
|
+
if not entry or entry[0] not in ('100644', '100755'):
|
|
167
|
+
raise ValueError(f'Changelog must exist as a regular file: {path}')
|
|
168
|
+
body = check_output(['git', 'show', f'{head}:{path}'])
|
|
169
|
+
if not body.decode('utf-8').strip():
|
|
170
|
+
raise ValueError(f'Customer changelog is empty: {path}')
|
|
171
|
+
print(f'Customer changelog: {path}')
|
|
80
172
|
notes = Path(environ['RUNNER_TEMP'], 'release-notes', 'README.md')
|
|
81
173
|
notes.parent.mkdir(parents=True, exist_ok=True)
|
|
82
174
|
notes.write_bytes(body)
|
|
83
|
-
print(f'Customer changelog: {path}')
|
|
84
175
|
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
85
176
|
output.write(f'branch={branch}\n')
|
|
86
177
|
PY
|
|
@@ -129,63 +220,6 @@ jobs:
|
|
|
129
220
|
if-no-files-found: error
|
|
130
221
|
retention-days: 14
|
|
131
222
|
overwrite: true
|
|
132
|
-
- name: Check complete push range and plan version
|
|
133
|
-
id: changes
|
|
134
|
-
env:
|
|
135
|
-
BEFORE: ${{ github.event.before }}
|
|
136
|
-
RELEASE_PUSH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
137
|
-
run: |
|
|
138
|
-
python - <<'PY'
|
|
139
|
-
from os import environ
|
|
140
|
-
from pathlib import Path
|
|
141
|
-
from re import fullmatch
|
|
142
|
-
from subprocess import check_output
|
|
143
|
-
|
|
144
|
-
if environ['RELEASE_PUSH'] != 'true':
|
|
145
|
-
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
146
|
-
output.write('release=false\n')
|
|
147
|
-
raise SystemExit(0)
|
|
148
|
-
series = environ['RELEASE_SERIES']
|
|
149
|
-
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)', series) is None:
|
|
150
|
-
raise ValueError(f'Invalid RELEASE_SERIES: {series!r}')
|
|
151
|
-
before = environ['BEFORE']
|
|
152
|
-
if not before:
|
|
153
|
-
raise ValueError('Missing push base commit')
|
|
154
|
-
first_push = set(before) == {'0'}
|
|
155
|
-
base = check_output(['git', 'hash-object', '-t', 'tree', '--stdin'], input='', text=True).strip() if first_push else before
|
|
156
|
-
changed = check_output(
|
|
157
|
-
['git', 'diff', '--name-only', '--no-renames', base, environ['GITHUB_SHA'], '--', 'src/'],
|
|
158
|
-
text=True,
|
|
159
|
-
).strip()
|
|
160
|
-
# Anchor to the pre-push history so a rerun ignores its own release tag.
|
|
161
|
-
tags = [] if first_push else check_output(['git', 'tag', '--merged', before], text=True).splitlines()
|
|
162
|
-
releases = []
|
|
163
|
-
for tag in tags:
|
|
164
|
-
match = fullmatch(r'v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:\.post\d+)?', tag)
|
|
165
|
-
if match and f'{int(match[1])}.{int(match[2])}' == series:
|
|
166
|
-
releases.append(tuple(map(int, match.groups()[:3])) + (int(match[4] or 0),))
|
|
167
|
-
latest = max(releases) if releases else None
|
|
168
|
-
number = int(environ['GITHUB_RUN_NUMBER'])
|
|
169
|
-
if number < 1:
|
|
170
|
-
raise ValueError('Build number must be positive')
|
|
171
|
-
if latest is not None and number <= latest[3]:
|
|
172
|
-
raise ValueError('Build number must exceed the previous build; do not reset the workflow counter')
|
|
173
|
-
if changed:
|
|
174
|
-
patch = latest[2] + 1 if latest is not None else 0
|
|
175
|
-
print(f'Source changes:\n{changed}')
|
|
176
|
-
else:
|
|
177
|
-
if latest is None:
|
|
178
|
-
raise ValueError(
|
|
179
|
-
'No release tag in pre-push history for this series. '
|
|
180
|
-
'Tag the exact source commit of the existing PyPI release before retrying.'
|
|
181
|
-
)
|
|
182
|
-
patch = latest[2]
|
|
183
|
-
print('No src changes: keeping MAJOR.MINOR.PATCH and increasing BUILD')
|
|
184
|
-
version = f'{series}.{patch}.{number}'
|
|
185
|
-
print(f'Planned version: {version}')
|
|
186
|
-
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
187
|
-
output.write(f'release=true\nversion={version}\n')
|
|
188
|
-
PY
|
|
189
223
|
|
|
190
224
|
test:
|
|
191
225
|
name: Test Python ${{ matrix.python }}
|
|
@@ -256,7 +290,7 @@ jobs:
|
|
|
256
290
|
with:
|
|
257
291
|
python-version: ${{ matrix.python }}
|
|
258
292
|
|
|
259
|
-
- name: Record source commits and
|
|
293
|
+
- name: Record source commits and configure candidate version
|
|
260
294
|
env:
|
|
261
295
|
RELEASE_BUILD: ${{ needs.changes.outputs.release }}
|
|
262
296
|
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
@@ -269,7 +303,6 @@ jobs:
|
|
|
269
303
|
from os import environ
|
|
270
304
|
from pathlib import Path
|
|
271
305
|
from re import fullmatch
|
|
272
|
-
from subprocess import check_output, run
|
|
273
306
|
|
|
274
307
|
project = environ['PROJECT']
|
|
275
308
|
if project not in ('DefStream', 'DefStream-RabbitMQ', 'DefStream-Redis'):
|
|
@@ -278,37 +311,70 @@ jobs:
|
|
|
278
311
|
print('Test-only build: using the existing Git-derived development version.')
|
|
279
312
|
raise SystemExit(0)
|
|
280
313
|
version = environ['RELEASE_VERSION']
|
|
281
|
-
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)
|
|
314
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)', version) is None:
|
|
282
315
|
raise ValueError(f'Invalid planned version: {version!r}')
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if tags:
|
|
287
|
-
tagged = check_output(['git', '-C', str(path), 'rev-parse', f'{tag}^{{commit}}'], text=True).strip()
|
|
288
|
-
if tagged != environ['GITHUB_SHA']:
|
|
289
|
-
raise RuntimeError(f'{tag} already points at another commit')
|
|
290
|
-
else:
|
|
291
|
-
run(['git', '-C', str(path), 'tag', tag, environ['GITHUB_SHA']], check=True)
|
|
292
|
-
print(f'{project}: local candidate {tag}')
|
|
316
|
+
with Path(environ['GITHUB_ENV']).open('a', encoding='utf-8') as output:
|
|
317
|
+
output.write(f'RELEASE_VERSION={version}\n')
|
|
318
|
+
print(f'{project}: candidate {version}')
|
|
293
319
|
with Path(environ['GITHUB_STEP_SUMMARY']).open('a', encoding='utf-8') as summary:
|
|
294
320
|
summary.write(f'## {project} {version}\n\nSource: `{environ["GITHUB_SHA"]}`\n')
|
|
295
321
|
PY
|
|
296
322
|
|
|
297
323
|
- name: Install build tools
|
|
298
|
-
run: python -m pip install --upgrade build twine
|
|
324
|
+
run: python -m pip install --upgrade build twine wheel
|
|
299
325
|
|
|
300
326
|
- name: Build source distributions and wheels
|
|
327
|
+
env:
|
|
328
|
+
RELEASE_BUILD: ${{ needs.changes.outputs.release }}
|
|
329
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
330
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
301
331
|
run: |
|
|
302
332
|
mkdir -p dist dependency-wheels
|
|
303
333
|
for name in DefStream DefStream-RabbitMQ DefStream-Redis; do
|
|
304
334
|
if [ "$name" = "$PROJECT" ]; then
|
|
305
|
-
|
|
335
|
+
if [ "$RELEASE_BUILD" = "true" ]; then
|
|
336
|
+
if [ "$NEW_VERSION" = "true" ]; then
|
|
337
|
+
SETUPTOOLS_SCM_PRETEND_VERSION="$RELEASE_VERSION" python -m build --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
338
|
+
else
|
|
339
|
+
SETUPTOOLS_SCM_PRETEND_VERSION="$RELEASE_VERSION" python -m build --wheel --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
340
|
+
fi
|
|
341
|
+
python -m wheel tags --build "$WHEEL_BUILD" --remove dist/*.whl
|
|
342
|
+
else
|
|
343
|
+
python -m build --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
344
|
+
fi
|
|
306
345
|
else
|
|
307
346
|
python -m build --outdir "$GITHUB_WORKSPACE/dependency-wheels" "packages/$name"
|
|
308
347
|
fi
|
|
309
348
|
done
|
|
310
349
|
python -m twine check --strict dist/*
|
|
311
350
|
|
|
351
|
+
- name: Normalize source archive timestamps
|
|
352
|
+
run: |
|
|
353
|
+
python - <<'PY'
|
|
354
|
+
from gzip import GzipFile
|
|
355
|
+
from io import BytesIO
|
|
356
|
+
from os import environ
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
from tarfile import open as open_tar
|
|
359
|
+
|
|
360
|
+
epoch = int(environ['SOURCE_DATE_EPOCH'])
|
|
361
|
+
for path in Path('dist').glob('*.tar.gz'):
|
|
362
|
+
with BytesIO() as buffer:
|
|
363
|
+
with open_tar(path, 'r:gz') as source:
|
|
364
|
+
with GzipFile(fileobj=buffer, mode='wb', filename='', mtime=epoch) as compressed:
|
|
365
|
+
with open_tar(fileobj=compressed, mode='w') as target:
|
|
366
|
+
for member in sorted(source.getmembers(), key=lambda item: item.name):
|
|
367
|
+
member.mtime = epoch
|
|
368
|
+
member.uid = member.gid = 0
|
|
369
|
+
member.uname = member.gname = ''
|
|
370
|
+
member.pax_headers = {
|
|
371
|
+
key: value for key, value in member.pax_headers.items()
|
|
372
|
+
if key not in ('mtime', 'atime', 'ctime')
|
|
373
|
+
}
|
|
374
|
+
target.addfile(member, source.extractfile(member) if member.isfile() else None)
|
|
375
|
+
path.write_bytes(buffer.getvalue())
|
|
376
|
+
PY
|
|
377
|
+
|
|
312
378
|
- name: Install built wheels into a clean environment
|
|
313
379
|
run: |
|
|
314
380
|
python -m venv "$RUNNER_TEMP/installed"
|
|
@@ -379,20 +445,27 @@ jobs:
|
|
|
379
445
|
env:
|
|
380
446
|
PROJECT: ${{ github.event.repository.name }}
|
|
381
447
|
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
448
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
449
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
382
450
|
run: |
|
|
383
451
|
python - <<'PY'
|
|
384
452
|
from email.parser import BytesParser
|
|
453
|
+
from hashlib import sha256
|
|
385
454
|
from json import load
|
|
386
455
|
from os import environ
|
|
387
456
|
from pathlib import Path
|
|
457
|
+
from shutil import copy2
|
|
388
458
|
from urllib.error import HTTPError
|
|
389
459
|
from urllib.request import urlopen
|
|
390
460
|
from zipfile import ZipFile
|
|
391
461
|
|
|
392
462
|
wheels = list(Path('dist').glob('*.whl'))
|
|
393
463
|
sources = list(Path('dist').glob('*.tar.gz'))
|
|
394
|
-
|
|
395
|
-
|
|
464
|
+
expected_sources = 1 if environ['NEW_VERSION'] == 'true' else 0
|
|
465
|
+
if len(wheels) != 1 or len(sources) != expected_sources:
|
|
466
|
+
raise ValueError('Unexpected wheel/source distribution count for the release plan')
|
|
467
|
+
if wheels[0].name.split('-')[2] != environ['WHEEL_BUILD']:
|
|
468
|
+
raise ValueError('Wheel build tag does not match the release plan')
|
|
396
469
|
with ZipFile(wheels[0]) as archive:
|
|
397
470
|
metadata_path, = (name for name in archive.namelist() if name.endswith('.dist-info/METADATA'))
|
|
398
471
|
metadata = BytesParser().parsebytes(archive.read(metadata_path))
|
|
@@ -409,34 +482,42 @@ jobs:
|
|
|
409
482
|
except HTTPError as error:
|
|
410
483
|
if error.code != 404:
|
|
411
484
|
raise
|
|
412
|
-
|
|
485
|
+
published = {}
|
|
413
486
|
else:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
487
|
+
published = {file['filename']: file['digests']['sha256'] for file in release['urls']}
|
|
488
|
+
if environ['NEW_VERSION'] != 'true' and not published:
|
|
489
|
+
raise RuntimeError('Cannot rebuild a version that has not been published on PyPI')
|
|
490
|
+
upload = Path('upload')
|
|
491
|
+
upload.mkdir()
|
|
492
|
+
for path in [*wheels, *sources]:
|
|
493
|
+
if path.name in published:
|
|
494
|
+
if sha256(path.read_bytes()).hexdigest() != published[path.name]:
|
|
495
|
+
raise RuntimeError(f'Published file differs; refusing to overwrite {path.name}')
|
|
496
|
+
print(f'Already published: {path.name}')
|
|
497
|
+
else:
|
|
498
|
+
copy2(path, upload / path.name)
|
|
423
499
|
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
424
|
-
output.write(f'publish={str(
|
|
500
|
+
output.write(f'publish={str(any(upload.iterdir())).lower()}\n')
|
|
425
501
|
output.write(f'version={version}\n')
|
|
426
502
|
PY
|
|
427
503
|
|
|
428
504
|
- name: Record verified release tag
|
|
429
505
|
env:
|
|
430
|
-
VERSION: ${{
|
|
506
|
+
VERSION: ${{ needs.changes.outputs.tag }}
|
|
507
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
431
508
|
run: |
|
|
432
509
|
tag="$VERSION"
|
|
433
510
|
cd release-source
|
|
434
511
|
if git show-ref --verify --quiet "refs/tags/$tag"; then
|
|
435
|
-
if [ "$(git rev-parse "$tag^{commit}")" != "$GITHUB_SHA" ]; then
|
|
512
|
+
if [ "$NEW_VERSION" = "true" ] && [ "$(git rev-parse "$tag^{commit}")" != "$GITHUB_SHA" ]; then
|
|
436
513
|
echo "::error::$tag already points at another commit."
|
|
437
514
|
exit 1
|
|
438
515
|
fi
|
|
439
516
|
else
|
|
517
|
+
if [ "$NEW_VERSION" != "true" ]; then
|
|
518
|
+
echo "::error::Existing release tag is missing: $tag"
|
|
519
|
+
exit 1
|
|
520
|
+
fi
|
|
440
521
|
git tag "$tag" "$GITHUB_SHA"
|
|
441
522
|
fi
|
|
442
523
|
git push origin "refs/tags/$tag"
|
|
@@ -455,29 +536,36 @@ jobs:
|
|
|
455
536
|
echo "::error::Configure PYPI_API_TOKEN in the pypi environment or repository Actions secrets."
|
|
456
537
|
exit 1
|
|
457
538
|
fi
|
|
458
|
-
python -m twine upload --non-interactive
|
|
539
|
+
python -m twine upload --non-interactive upload/*
|
|
459
540
|
|
|
460
541
|
- name: Publish GitHub Release with verified distributions
|
|
461
542
|
env:
|
|
462
543
|
GH_TOKEN: ${{ github.token }}
|
|
463
544
|
VERSION: ${{ steps.release.outputs.version }}
|
|
545
|
+
RELEASE_TAG: ${{ needs.changes.outputs.tag }}
|
|
546
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
547
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
464
548
|
run: |
|
|
465
549
|
python - <<'PY'
|
|
466
550
|
from json import load
|
|
551
|
+
from hashlib import sha256
|
|
467
552
|
from os import environ
|
|
468
553
|
from pathlib import Path
|
|
469
554
|
from subprocess import run
|
|
555
|
+
from tempfile import TemporaryDirectory
|
|
470
556
|
from urllib.error import HTTPError
|
|
471
557
|
from urllib.parse import quote
|
|
472
558
|
from urllib.request import Request, urlopen
|
|
473
559
|
|
|
474
560
|
repository = environ['GITHUB_REPOSITORY']
|
|
475
561
|
version = environ['VERSION']
|
|
562
|
+
tag = environ['RELEASE_TAG']
|
|
563
|
+
release = None
|
|
476
564
|
notes = Path('release-notes/README.md')
|
|
477
565
|
if not notes.read_text(encoding='utf-8').strip():
|
|
478
566
|
raise ValueError('Customer release notes are empty')
|
|
479
567
|
request = Request(
|
|
480
|
-
f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(
|
|
568
|
+
f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(tag, safe="")}',
|
|
481
569
|
headers={
|
|
482
570
|
'Authorization': f'Bearer {environ["GH_TOKEN"]}',
|
|
483
571
|
'Accept': 'application/vnd.github+json',
|
|
@@ -490,20 +578,35 @@ jobs:
|
|
|
490
578
|
except HTTPError as error:
|
|
491
579
|
if error.code != 404:
|
|
492
580
|
raise
|
|
581
|
+
if environ['NEW_VERSION'] != 'true':
|
|
582
|
+
raise RuntimeError('Existing GitHub Release is missing') from error
|
|
493
583
|
run([
|
|
494
|
-
'gh', 'release', 'create',
|
|
584
|
+
'gh', 'release', 'create', tag, '--repo', repository,
|
|
495
585
|
'--verify-tag', '--title', version, '--notes-file', str(notes), '--draft',
|
|
496
586
|
], check=True)
|
|
497
587
|
else:
|
|
498
|
-
if release['tag_name'] !=
|
|
588
|
+
if release['tag_name'] != tag:
|
|
499
589
|
raise ValueError('GitHub Release does not match the verified version')
|
|
590
|
+
if environ['NEW_VERSION'] != 'true':
|
|
591
|
+
if not (release['body'] or '').strip():
|
|
592
|
+
raise ValueError('Existing release notes are empty')
|
|
593
|
+
notes.write_text(release['body'], encoding='utf-8')
|
|
500
594
|
artifacts = sorted(str(path) for path in Path('dist').iterdir() if path.is_file())
|
|
595
|
+
existing = {asset['name'] for asset in release['assets']} if release is not None else set()
|
|
596
|
+
for artifact in artifacts:
|
|
597
|
+
path = Path(artifact)
|
|
598
|
+
if path.name in existing:
|
|
599
|
+
with TemporaryDirectory() as directory:
|
|
600
|
+
run([
|
|
601
|
+
'gh', 'release', 'download', tag, '--repo', repository,
|
|
602
|
+
'--pattern', path.name, '--dir', directory,
|
|
603
|
+
], check=True)
|
|
604
|
+
if sha256((Path(directory) / path.name).read_bytes()).digest() != sha256(path.read_bytes()).digest():
|
|
605
|
+
raise RuntimeError(f'GitHub asset differs; refusing to overwrite {path.name}')
|
|
606
|
+
else:
|
|
607
|
+
run(['gh', 'release', 'upload', tag, '--repo', repository, artifact], check=True)
|
|
501
608
|
run([
|
|
502
|
-
'gh', 'release', '
|
|
503
|
-
'--clobber', *artifacts,
|
|
504
|
-
], check=True)
|
|
505
|
-
run([
|
|
506
|
-
'gh', 'release', 'edit', version, '--repo', repository,
|
|
609
|
+
'gh', 'release', 'edit', tag, '--repo', repository,
|
|
507
610
|
'--notes-file', str(notes), '--draft=false',
|
|
508
611
|
], check=True)
|
|
509
612
|
PY
|
{defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3/DefStream_RabbitMQ.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream-RabbitMQ
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: RabbitMQ broker adapter for DefStream
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream-RabbitMQ
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: DefStream>=0.1.
|
|
19
|
+
Requires-Dist: DefStream>=0.1.5
|
|
20
20
|
Requires-Dist: pika>=1.3.2
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: classifier
|
{defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info → defstream_rabbitmq-0.1.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream-RabbitMQ
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: RabbitMQ broker adapter for DefStream
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream-RabbitMQ
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: DefStream>=0.1.
|
|
19
|
+
Requires-Dist: DefStream>=0.1.5
|
|
20
20
|
Requires-Dist: pika>=1.3.2
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: classifier
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from contextlib import (
|
|
2
|
+
closing,
|
|
3
|
+
redirect_stdout,
|
|
4
|
+
)
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from io import (
|
|
7
|
+
BytesIO,
|
|
8
|
+
StringIO,
|
|
9
|
+
)
|
|
10
|
+
from json import dumps
|
|
11
|
+
from os import environ
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from subprocess import check_output
|
|
14
|
+
from tarfile import (
|
|
15
|
+
TarInfo,
|
|
16
|
+
open as open_tar,
|
|
17
|
+
)
|
|
18
|
+
from tempfile import TemporaryDirectory
|
|
19
|
+
from unittest import (
|
|
20
|
+
TestCase,
|
|
21
|
+
main,
|
|
22
|
+
)
|
|
23
|
+
from unittest.mock import patch
|
|
24
|
+
from urllib.error import HTTPError
|
|
25
|
+
from zipfile import ZipFile
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
WORKFLOW = Path(__file__).resolve().parents[1] / '.github' / 'workflows' / 'ci.yml'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def workflow_script(name: str) -> str:
|
|
32
|
+
lines = WORKFLOW.read_text(encoding='utf-8').splitlines()
|
|
33
|
+
start = lines.index(f' - name: {name}')
|
|
34
|
+
start = lines.index(" python - <<'PY'", start) + 1
|
|
35
|
+
end = lines.index(' PY', start)
|
|
36
|
+
return '\n'.join(line[10:] for line in lines[start:end])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReleaseWorkflowTests(TestCase):
|
|
40
|
+
def test_embedded_python_syntax(self) -> None:
|
|
41
|
+
lines = WORKFLOW.read_text(encoding='utf-8').splitlines()
|
|
42
|
+
for index, line in enumerate(lines):
|
|
43
|
+
if line == " python - <<'PY'":
|
|
44
|
+
end = lines.index(' PY', index + 1)
|
|
45
|
+
code = '\n'.join(line[10:] for line in lines[index + 1:end])
|
|
46
|
+
compile(code, str(WORKFLOW), 'exec')
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def plan(
|
|
50
|
+
self,
|
|
51
|
+
tags: str,
|
|
52
|
+
changed: str = '',
|
|
53
|
+
event: str = 'true',
|
|
54
|
+
) -> dict:
|
|
55
|
+
with TemporaryDirectory() as directory:
|
|
56
|
+
output = Path(directory) / 'output'
|
|
57
|
+
event_path = Path(directory) / 'event.json'
|
|
58
|
+
event_path.write_text(dumps({'pull_request': {'base': {'sha': 'pr-base'}}}))
|
|
59
|
+
environment = {
|
|
60
|
+
'GITHUB_OUTPUT': str(output),
|
|
61
|
+
'RELEASE_PUSH': event,
|
|
62
|
+
'RELEASE_SERIES': '0.1',
|
|
63
|
+
'BEFORE': 'before',
|
|
64
|
+
'GITHUB_SHA': 'head',
|
|
65
|
+
'GITHUB_RUN_NUMBER': '42',
|
|
66
|
+
'GITHUB_EVENT_PATH': str(event_path),
|
|
67
|
+
}
|
|
68
|
+
with (
|
|
69
|
+
patch.dict(environ, environment),
|
|
70
|
+
patch('subprocess.check_output', side_effect=[tags, changed]) as git,
|
|
71
|
+
redirect_stdout(StringIO()),
|
|
72
|
+
):
|
|
73
|
+
try:
|
|
74
|
+
exec(workflow_script('Check complete push range and plan version'), {})
|
|
75
|
+
except SystemExit as error:
|
|
76
|
+
self.assertEqual(error.code, 0)
|
|
77
|
+
self.commands = git.call_args_list
|
|
78
|
+
return dict(line.split('=', 1) for line in output.read_text().splitlines())
|
|
79
|
+
|
|
80
|
+
def test_code_and_packaging_changes_increase_patch(self) -> None:
|
|
81
|
+
for path in ['src/__init__.py', 'src/Worker/__init__.py', 'setup.py', 'pyproject.toml']:
|
|
82
|
+
with self.subTest(path=path):
|
|
83
|
+
result = self.plan('0.1.4', path)
|
|
84
|
+
self.assertEqual(result['version'], '0.1.5')
|
|
85
|
+
self.assertEqual(result['new_version'], 'true')
|
|
86
|
+
self.assertEqual(result['build'], '42')
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def test_rebuild_keeps_public_version(self) -> None:
|
|
90
|
+
result = self.plan('0.1.3.7\n0.1.4')
|
|
91
|
+
self.assertEqual(result['version'], '0.1.4')
|
|
92
|
+
self.assertEqual(result['new_version'], 'false')
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
def test_migrating_legacy_versions_increases_patch(self) -> None:
|
|
96
|
+
for tag in ['0.1.4.12', 'v0.1.4.12', '0.1.4.post1', '0.1.4.0', '0.1.4.post0']:
|
|
97
|
+
with self.subTest(tag=tag):
|
|
98
|
+
result = self.plan(tag)
|
|
99
|
+
self.assertEqual(result['version'], '0.1.5')
|
|
100
|
+
self.assertEqual(result['new_version'], 'true')
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
def test_rebuild_preserves_existing_tag(self) -> None:
|
|
104
|
+
self.assertEqual(self.plan('v0.1.4')['tag'], 'v0.1.4')
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def test_source_comparison_includes_previously_failed_pushes(self) -> None:
|
|
108
|
+
self.plan('0.1.4', 'setup.py')
|
|
109
|
+
self.assertIn('0.1.4', self.commands[1].args[0])
|
|
110
|
+
self.assertNotIn('before', self.commands[1].args[0])
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def test_initial_release(self) -> None:
|
|
114
|
+
self.assertEqual(self.plan('', 'src/__init__.py')['version'], '0.1.0')
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def test_rebuild_without_release_history_is_rejected(self) -> None:
|
|
118
|
+
with self.assertRaisesRegex(ValueError, 'No release tag'):
|
|
119
|
+
self.plan('')
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def test_pull_request_does_not_release(self) -> None:
|
|
123
|
+
result = self.plan('0.1.4', event='false')
|
|
124
|
+
self.assertEqual(result['release'], 'false')
|
|
125
|
+
self.assertEqual(result['new_version'], 'false')
|
|
126
|
+
self.assertEqual(result['tag'], '0.1.4')
|
|
127
|
+
self.assertIn('pr-base', self.commands[0].args[0])
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
def test_git_pathspec_distinguishes_resources_and_packaging(self) -> None:
|
|
131
|
+
with TemporaryDirectory() as directory:
|
|
132
|
+
root = Path(directory)
|
|
133
|
+
check_output(['git', 'init', '--quiet', str(root)])
|
|
134
|
+
names = [
|
|
135
|
+
'src/__init__.py',
|
|
136
|
+
'src/nested/code.py',
|
|
137
|
+
'src/nested/view.html',
|
|
138
|
+
'setup.py',
|
|
139
|
+
'setup.cfg',
|
|
140
|
+
'pyproject.toml',
|
|
141
|
+
'MANIFEST.in',
|
|
142
|
+
'README.md',
|
|
143
|
+
'.github/workflows/ci.yml',
|
|
144
|
+
]
|
|
145
|
+
for name in names:
|
|
146
|
+
path = root / name
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
path.write_text('test\n')
|
|
149
|
+
check_output(['git', '-C', str(root), 'add', '.'])
|
|
150
|
+
check_output([
|
|
151
|
+
'git', '-C', str(root),
|
|
152
|
+
'-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
153
|
+
'commit', '--quiet', '-m', 'test',
|
|
154
|
+
])
|
|
155
|
+
base = check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
|
|
156
|
+
for name in names:
|
|
157
|
+
(root / name).write_text('changed\n')
|
|
158
|
+
self.plan('0.1.4')
|
|
159
|
+
diff = self.commands[1].args[0]
|
|
160
|
+
selected = check_output(
|
|
161
|
+
['git', '-C', str(root), 'diff', '--name-only', base, '--', *diff[diff.index('--') + 1:]],
|
|
162
|
+
text=True,
|
|
163
|
+
).splitlines()
|
|
164
|
+
self.assertEqual(set(selected), set(names[:2] + names[3:7]))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
def test_direct_push_generates_notes(self) -> None:
|
|
168
|
+
with TemporaryDirectory() as directory:
|
|
169
|
+
output = Path(directory) / 'output'
|
|
170
|
+
environment = {
|
|
171
|
+
'GITHUB_OUTPUT': str(output),
|
|
172
|
+
'GITHUB_SHA': 'head',
|
|
173
|
+
'GITHUB_EVENT_NAME': 'push',
|
|
174
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
175
|
+
'BEFORE': 'before',
|
|
176
|
+
'RUNNER_TEMP': directory,
|
|
177
|
+
'NEW_VERSION': 'true',
|
|
178
|
+
}
|
|
179
|
+
with (
|
|
180
|
+
patch.dict(environ, environment),
|
|
181
|
+
patch('subprocess.check_output', side_effect=[b'[[]]', b'- setup.py update (abc)\n']),
|
|
182
|
+
redirect_stdout(StringIO()),
|
|
183
|
+
):
|
|
184
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
185
|
+
self.assertEqual(output.read_text().strip(), 'branch=master')
|
|
186
|
+
notes = (Path(directory) / 'release-notes' / 'README.md').read_text(encoding='utf-8')
|
|
187
|
+
self.assertIn('setup.py update', notes)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
def test_pr_still_requires_changelog(self) -> None:
|
|
191
|
+
with TemporaryDirectory() as directory:
|
|
192
|
+
event = Path(directory) / 'event.json'
|
|
193
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'fix/1'}}}))
|
|
194
|
+
with (
|
|
195
|
+
patch.dict(environ, {
|
|
196
|
+
'GITHUB_EVENT_NAME': 'pull_request',
|
|
197
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
198
|
+
'GITHUB_SHA': 'head',
|
|
199
|
+
'NEW_VERSION': 'true',
|
|
200
|
+
}),
|
|
201
|
+
patch('subprocess.check_output', return_value=b''),
|
|
202
|
+
):
|
|
203
|
+
with self.assertRaisesRegex(ValueError, 'Changelog must exist'):
|
|
204
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
def check_upload(
|
|
208
|
+
self,
|
|
209
|
+
published: dict,
|
|
210
|
+
new_version: bool = False,
|
|
211
|
+
) -> tuple:
|
|
212
|
+
with TemporaryDirectory() as directory:
|
|
213
|
+
root = Path(directory)
|
|
214
|
+
dist = root / 'dist'
|
|
215
|
+
dist.mkdir()
|
|
216
|
+
wheel = dist / 'defstream-0.1.5-42-py3-none-any.whl'
|
|
217
|
+
with ZipFile(wheel, 'w') as archive:
|
|
218
|
+
archive.writestr('defstream-0.1.5.dist-info/METADATA', 'Name: DefStream\nVersion: 0.1.5\n')
|
|
219
|
+
if new_version:
|
|
220
|
+
(dist / 'defstream-0.1.5.tar.gz').write_bytes(b'sdist')
|
|
221
|
+
wheel_hash = sha256(wheel.read_bytes()).hexdigest()
|
|
222
|
+
files = {
|
|
223
|
+
name: wheel_hash if digest == 'matching' else digest
|
|
224
|
+
for name, digest in published.items()
|
|
225
|
+
}
|
|
226
|
+
response = StringIO(dumps({
|
|
227
|
+
'urls': [
|
|
228
|
+
{'filename': name, 'digests': {'sha256': digest}}
|
|
229
|
+
for name, digest in files.items()
|
|
230
|
+
],
|
|
231
|
+
}))
|
|
232
|
+
if not published:
|
|
233
|
+
response.close()
|
|
234
|
+
response = HTTPError('https://example.invalid', 404, 'not found', None, BytesIO())
|
|
235
|
+
with (
|
|
236
|
+
closing(response),
|
|
237
|
+
patch.dict(environ, {
|
|
238
|
+
'PROJECT': 'DefStream',
|
|
239
|
+
'RELEASE_VERSION': '0.1.5',
|
|
240
|
+
'WHEEL_BUILD': '42',
|
|
241
|
+
'NEW_VERSION': str(new_version).lower(),
|
|
242
|
+
'GITHUB_OUTPUT': str(root / 'output'),
|
|
243
|
+
}),
|
|
244
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
245
|
+
patch('urllib.request.urlopen', side_effect=[response]),
|
|
246
|
+
redirect_stdout(StringIO()),
|
|
247
|
+
):
|
|
248
|
+
exec(workflow_script('Check release status'), {})
|
|
249
|
+
result = dict(line.split('=', 1) for line in (root / 'output').read_text().splitlines())
|
|
250
|
+
return result, {path.name for path in (root / 'upload').iterdir()}
|
|
251
|
+
|
|
252
|
+
def test_new_release_uploads_wheel_and_sdist(self) -> None:
|
|
253
|
+
result, files = self.check_upload({}, new_version=True)
|
|
254
|
+
self.assertEqual(result['publish'], 'true')
|
|
255
|
+
self.assertEqual(files, {
|
|
256
|
+
'defstream-0.1.5-42-py3-none-any.whl',
|
|
257
|
+
'defstream-0.1.5.tar.gz',
|
|
258
|
+
})
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
def test_missing_pypi_release_does_not_enter_http_error(self) -> None:
|
|
262
|
+
with patch.object(HTTPError, '__enter__', side_effect=AttributeError('fp is None')) as enter:
|
|
263
|
+
result, files = self.check_upload({}, new_version=True)
|
|
264
|
+
enter.assert_not_called()
|
|
265
|
+
self.assertEqual(result['publish'], 'true')
|
|
266
|
+
self.assertEqual(len(files), 2)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def test_rebuild_only_uploads_new_wheel(self) -> None:
|
|
270
|
+
result, files = self.check_upload({'defstream-0.1.5.tar.gz': 'old-sdist-hash'})
|
|
271
|
+
self.assertEqual(result['publish'], 'true')
|
|
272
|
+
self.assertEqual(files, {'defstream-0.1.5-42-py3-none-any.whl'})
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
def test_rerun_skips_identical_published_wheel(self) -> None:
|
|
276
|
+
result, files = self.check_upload({'defstream-0.1.5-42-py3-none-any.whl': 'matching'})
|
|
277
|
+
self.assertEqual(result['publish'], 'false')
|
|
278
|
+
self.assertEqual(files, set())
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
def test_rerun_rejects_different_published_wheel(self) -> None:
|
|
282
|
+
with self.assertRaisesRegex(RuntimeError, 'refusing to overwrite'):
|
|
283
|
+
self.check_upload({'defstream-0.1.5-42-py3-none-any.whl': 'different'})
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
def test_rebuild_requires_existing_pypi_release(self) -> None:
|
|
287
|
+
with self.assertRaisesRegex(RuntimeError, 'has not been published'):
|
|
288
|
+
self.check_upload({})
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
def test_github_rebuild_preserves_original_notes(self) -> None:
|
|
292
|
+
with TemporaryDirectory() as directory:
|
|
293
|
+
root = Path(directory)
|
|
294
|
+
notes = root / 'release-notes' / 'README.md'
|
|
295
|
+
notes.parent.mkdir()
|
|
296
|
+
notes.write_text('new build notes', encoding='utf-8')
|
|
297
|
+
(root / 'dist').mkdir()
|
|
298
|
+
(root / 'dist' / 'defstream-0.1.5-42-py3-none-any.whl').write_bytes(b'wheel')
|
|
299
|
+
release = {'tag_name': '0.1.5', 'body': 'original release notes', 'assets': []}
|
|
300
|
+
for attempt in range(2):
|
|
301
|
+
with (
|
|
302
|
+
patch.dict(environ, {
|
|
303
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
304
|
+
'GITHUB_API_URL': 'https://api.github.com',
|
|
305
|
+
'GITHUB_SERVER_URL': 'https://github.com',
|
|
306
|
+
'GITHUB_SHA': 'source-commit',
|
|
307
|
+
'GH_TOKEN': 'test-token',
|
|
308
|
+
'VERSION': '0.1.5',
|
|
309
|
+
'RELEASE_TAG': '0.1.5',
|
|
310
|
+
'WHEEL_BUILD': '42',
|
|
311
|
+
'NEW_VERSION': 'false',
|
|
312
|
+
}),
|
|
313
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
314
|
+
patch('urllib.request.urlopen', return_value=StringIO(dumps(release))),
|
|
315
|
+
patch('subprocess.run') as commands,
|
|
316
|
+
):
|
|
317
|
+
exec(workflow_script('Publish GitHub Release with verified distributions'), {})
|
|
318
|
+
body = notes.read_text(encoding='utf-8')
|
|
319
|
+
self.assertIn('original release notes', body)
|
|
320
|
+
self.assertEqual(body, 'original release notes')
|
|
321
|
+
self.assertTrue(all('--clobber' not in call.args[0] for call in commands.call_args_list))
|
|
322
|
+
release['body'] = body
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
def test_rebuild_reuses_existing_notes_without_branch_changelog(self) -> None:
|
|
326
|
+
for event_name in ['push', 'pull_request']:
|
|
327
|
+
with self.subTest(event=event_name), TemporaryDirectory() as directory:
|
|
328
|
+
root = Path(directory)
|
|
329
|
+
event = root / 'event.json'
|
|
330
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'docs/1'}}}))
|
|
331
|
+
body = '## 이전 변경로그\n\n기존 내용을 그대로 유지합니다.\n'
|
|
332
|
+
release = {'tag_name': '0.1.4', 'body': body}
|
|
333
|
+
responses = [dumps(release).encode('utf-8')]
|
|
334
|
+
if event_name == 'push':
|
|
335
|
+
responses.insert(0, b'[[]]')
|
|
336
|
+
with (
|
|
337
|
+
patch.dict(environ, {
|
|
338
|
+
'GITHUB_EVENT_NAME': event_name,
|
|
339
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
340
|
+
'GITHUB_SHA': 'head',
|
|
341
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
342
|
+
'NEW_VERSION': 'false',
|
|
343
|
+
'RELEASE_TAG': '0.1.4',
|
|
344
|
+
'GITHUB_OUTPUT': str(root / 'output'),
|
|
345
|
+
'RUNNER_TEMP': directory,
|
|
346
|
+
}),
|
|
347
|
+
patch('subprocess.check_output', side_effect=responses) as commands,
|
|
348
|
+
redirect_stdout(StringIO()),
|
|
349
|
+
):
|
|
350
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
351
|
+
self.assertEqual((root / 'release-notes' / 'README.md').read_bytes(), body.encode('utf-8'))
|
|
352
|
+
self.assertTrue(all(call.args[0][0] == 'gh' for call in commands.call_args_list))
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
def test_rebuild_rejects_empty_existing_notes(self) -> None:
|
|
356
|
+
with TemporaryDirectory() as directory:
|
|
357
|
+
event = Path(directory) / 'event.json'
|
|
358
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'docs/1'}}}))
|
|
359
|
+
with (
|
|
360
|
+
patch.dict(environ, {
|
|
361
|
+
'GITHUB_EVENT_NAME': 'pull_request',
|
|
362
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
363
|
+
'GITHUB_SHA': 'head',
|
|
364
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
365
|
+
'NEW_VERSION': 'false',
|
|
366
|
+
'RELEASE_TAG': '0.1.4',
|
|
367
|
+
}),
|
|
368
|
+
patch('subprocess.check_output', return_value=b'{"tag_name":"0.1.4","body":""}'),
|
|
369
|
+
):
|
|
370
|
+
with self.assertRaisesRegex(ValueError, 'missing or empty'):
|
|
371
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
def test_sdist_normalization_preserves_content_and_is_reproducible(self) -> None:
|
|
375
|
+
with TemporaryDirectory() as directory:
|
|
376
|
+
root = Path(directory)
|
|
377
|
+
dist = root / 'dist'
|
|
378
|
+
dist.mkdir()
|
|
379
|
+
for number in range(2):
|
|
380
|
+
with open_tar(dist / f'{number}.tar.gz', 'w:gz') as archive:
|
|
381
|
+
member = TarInfo('defstream-0.1.5/src/__init__.py')
|
|
382
|
+
member.mtime = 1700000000 + number
|
|
383
|
+
member.size = 4
|
|
384
|
+
member.pax_headers = {'mtime': str(member.mtime)}
|
|
385
|
+
archive.addfile(member, BytesIO(b'code'))
|
|
386
|
+
with (
|
|
387
|
+
patch.dict(environ, {'SOURCE_DATE_EPOCH': '1700000000'}),
|
|
388
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
389
|
+
):
|
|
390
|
+
exec(workflow_script('Normalize source archive timestamps'), {})
|
|
391
|
+
self.assertEqual((dist / '0.tar.gz').read_bytes(), (dist / '1.tar.gz').read_bytes())
|
|
392
|
+
with open_tar(dist / '0.tar.gz', 'r:gz') as archive:
|
|
393
|
+
self.assertEqual(archive.extractfile(archive.getmembers()[0]).read(), b'code')
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if __name__ == '__main__':
|
|
398
|
+
main()
|
|
File without changes
|
|
File without changes
|
{defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.3}/DefStream_RabbitMQ.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|