DefStream 0.1.4.12__tar.gz → 0.1.6__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.
- {defstream-0.1.4.12 → defstream-0.1.6}/.github/workflows/ci.yml +210 -107
- {defstream-0.1.4.12 → defstream-0.1.6}/DefStream.egg-info/PKG-INFO +8 -3
- {defstream-0.1.4.12 → defstream-0.1.6}/DefStream.egg-info/SOURCES.txt +4 -0
- defstream-0.1.6/DefStream.egg-info/requires.txt +6 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/PKG-INFO +8 -3
- {defstream-0.1.4.12 → defstream-0.1.6}/README.en.md +5 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/README.md +5 -0
- defstream-0.1.6/issues/fix/3/IMPLEMENTS.md +133 -0
- defstream-0.1.6/issues/fix/3/README.md +47 -0
- defstream-0.1.6/issues/fix/3/REQUIREMENTS.md +52 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/setup.py +2 -2
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Connection.py +1 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Worker.py +3 -11
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/__init__.py +0 -2
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_inspector.py +1 -2
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_lifecycle_hooks.py +1 -1
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_protocol_contracts.py +60 -10
- defstream-0.1.6/test/test_release_workflow.py +398 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_runner_hooks.py +1 -2
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_worker.py +1 -2
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_worker_startup.py +1 -1
- defstream-0.1.4.12/DefStream.egg-info/requires.txt +0 -6
- {defstream-0.1.4.12 → defstream-0.1.6}/.gitignore +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/DefStream.egg-info/dependency_links.txt +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/DefStream.egg-info/top_level.txt +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/LICENSE +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/issues/feature/1/IMPLEMENTS.md +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/issues/feature/1/README.md +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/issues/feature/1/REQUIREMENTS.md +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/issues/feature/1/TASK.md +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/issues/feature/1/ci.refs.json +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Broker/Sample.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Worker/RabbitMQ/Client.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Worker/RabbitMQ/Tasks.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Worker/RabbitMQ/Worker.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Worker/Redis/Sample.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/sample/Worker/Sample.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/setup.cfg +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Configuration.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Consumer.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Errors.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Event.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Implements/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Policies.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Priority.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/Protocol.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Broker/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Descriptor/DataObject.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Descriptor/Types.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Descriptor/Value.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Descriptor/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extension.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Console.html +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Console.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Dashboard.html +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Dashboard.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/HealthCheck.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Metrics.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/Specifications.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Extensions/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Inspector.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/Response.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Inspector/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Pattern.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Array.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Common.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Compare.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Condition.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/DataObject.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/DateTime.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Number.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Object.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Size.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/String.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/Type.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Patterns/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Validate.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/Validator.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Validator/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Executor.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Message.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Pool.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Run.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Specification.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/Worker/Startup.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/src/__init__.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_dashboard.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_dashboard_ui.cjs +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_inspector_console.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_pool_callbacks.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_process_signals.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_specifications.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_task_messages.py +0 -0
- {defstream-0.1.4.12 → defstream-0.1.6}/test/test_task_priorities.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -17,9 +17,9 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Provides-Extra: rabbitmq
|
|
20
|
-
Requires-Dist: DefStream-RabbitMQ
|
|
20
|
+
Requires-Dist: DefStream-RabbitMQ; extra == "rabbitmq"
|
|
21
21
|
Provides-Extra: redis
|
|
22
|
-
Requires-Dist: DefStream-Redis
|
|
22
|
+
Requires-Dist: DefStream-Redis; extra == "redis"
|
|
23
23
|
Dynamic: author
|
|
24
24
|
Dynamic: classifier
|
|
25
25
|
Dynamic: description
|
|
@@ -124,6 +124,11 @@ class Add(
|
|
|
124
124
|
|
|
125
125
|
### 2. Worker 실행: `worker.py`
|
|
126
126
|
|
|
127
|
+
`Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
|
|
128
|
+
연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
|
|
129
|
+
`GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
|
|
130
|
+
워커 전용 연결 타입은 필요하지 않습니다.
|
|
131
|
+
|
|
127
132
|
```python
|
|
128
133
|
from signal import SIGINT, signal
|
|
129
134
|
|
|
@@ -14,6 +14,9 @@ issues/feature/1/README.md
|
|
|
14
14
|
issues/feature/1/REQUIREMENTS.md
|
|
15
15
|
issues/feature/1/TASK.md
|
|
16
16
|
issues/feature/1/ci.refs.json
|
|
17
|
+
issues/fix/3/IMPLEMENTS.md
|
|
18
|
+
issues/fix/3/README.md
|
|
19
|
+
issues/fix/3/REQUIREMENTS.md
|
|
17
20
|
sample/Broker/Sample.py
|
|
18
21
|
sample/Worker/Sample.py
|
|
19
22
|
sample/Worker/RabbitMQ/Client.py
|
|
@@ -79,6 +82,7 @@ test/test_lifecycle_hooks.py
|
|
|
79
82
|
test/test_pool_callbacks.py
|
|
80
83
|
test/test_process_signals.py
|
|
81
84
|
test/test_protocol_contracts.py
|
|
85
|
+
test/test_release_workflow.py
|
|
82
86
|
test/test_runner_hooks.py
|
|
83
87
|
test/test_specifications.py
|
|
84
88
|
test/test_task_messages.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -17,9 +17,9 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Provides-Extra: rabbitmq
|
|
20
|
-
Requires-Dist: DefStream-RabbitMQ
|
|
20
|
+
Requires-Dist: DefStream-RabbitMQ; extra == "rabbitmq"
|
|
21
21
|
Provides-Extra: redis
|
|
22
|
-
Requires-Dist: DefStream-Redis
|
|
22
|
+
Requires-Dist: DefStream-Redis; extra == "redis"
|
|
23
23
|
Dynamic: author
|
|
24
24
|
Dynamic: classifier
|
|
25
25
|
Dynamic: description
|
|
@@ -124,6 +124,11 @@ class Add(
|
|
|
124
124
|
|
|
125
125
|
### 2. Worker 실행: `worker.py`
|
|
126
126
|
|
|
127
|
+
`Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
|
|
128
|
+
연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
|
|
129
|
+
`GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
|
|
130
|
+
워커 전용 연결 타입은 필요하지 않습니다.
|
|
131
|
+
|
|
127
132
|
```python
|
|
128
133
|
from signal import SIGINT, signal
|
|
129
134
|
|
|
@@ -93,6 +93,11 @@ declares an error queue to receive rejected messages.
|
|
|
93
93
|
|
|
94
94
|
### 2. Run the Worker: `worker.py`
|
|
95
95
|
|
|
96
|
+
`Worker` accepts `DefStream.Broker.Connection` directly.
|
|
97
|
+
The connection contract only requires `connect()` and `close()`. The worker checks
|
|
98
|
+
for `GetConsumer` support internally when it runs. A broker that cannot create
|
|
99
|
+
a consumer raises `TypeError`; no worker-specific connection type is needed.
|
|
100
|
+
|
|
96
101
|
```python
|
|
97
102
|
from signal import SIGINT, signal
|
|
98
103
|
|
|
@@ -91,6 +91,11 @@ class Add(
|
|
|
91
91
|
|
|
92
92
|
### 2. Worker 실행: `worker.py`
|
|
93
93
|
|
|
94
|
+
`Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
|
|
95
|
+
연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
|
|
96
|
+
`GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
|
|
97
|
+
워커 전용 연결 타입은 필요하지 않습니다.
|
|
98
|
+
|
|
94
99
|
```python
|
|
95
100
|
from signal import SIGINT, signal
|
|
96
101
|
|