Keep DEV as the sole instance and finish releases after publication
Development CI / image (push) Canceled after 0s
Development CI / deploy-development (push) Canceled after 0s
Main CI / check-and-package (pull_request) Canceled after 0s
Main CI / image (pull_request) Canceled after 0s
Development CI / check-and-package (push) Canceled after 2m25s
GitHub Git mirror / mirror (push) Successful in 6s

This commit is contained in:
eezd committed 2026-10-30 13:29:02 +08:00
1 parent be616668fc
commit ef39f9d120
6 files changed
+70 -117

No files matched your search

+6 -26
View File
@@ -14,7 +14,7 @@ import tempfile
import time
import urllib.request
from release import API, GITEA_DEV_IMAGE, GITHUB_IMAGE, release_receipt, require, run, release_state, save_state
from release import API, GITEA_DEV_IMAGE, require, run
def settings(directory, channel):
@@ -48,16 +48,14 @@ def save(path, data):
def deployment(directory, channel, image, commit, version, client_container=None):
directory = Path(directory).resolve(strict=True)
require(channel == 'development', 'Only the persistent DEV instance supports deployment')
cfg = settings(directory, channel)
registry = GITEA_DEV_IMAGE if channel == 'development' else GITHUB_IMAGE
registry = GITEA_DEV_IMAGE
require(re.fullmatch(re.escape(registry) + r'@sha256:[0-9a-f]{64}', image), 'Expected an immutable registry digest')
require(re.fullmatch(r'[0-9a-f]{40}', commit), 'Expected a full commit SHA')
# Serializes actual deployments, including manually started jobs and CLI use.
with (directory / '.deploy.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
if channel == 'production' and (directory / 'deployment.json').exists():
deployed = json.loads((directory / 'deployment.json').read_text())
run('git', 'merge-base', '--is-ancestor', deployed['commit'], commit)
require(not (directory / 'deployment.pending.json').exists(),
'Resolve the previous pending deployment before attempting another update')
run('docker', 'pull', image)
@@ -143,33 +141,15 @@ def deployment(directory, channel, image, commit, version, client_container=None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('channel', choices=('development', 'production'))
parser.add_argument('channel', choices=('development',))
parser.add_argument('--directory', required=True)
parser.add_argument('--image')
parser.add_argument('--commit')
parser.add_argument('--version', required=True)
parser.add_argument('--client-container')
args = parser.parse_args()
if args.channel == 'production':
require(not args.image and not args.commit, 'Production accepts a completed version, not an arbitrary image')
gitea = API(os.environ['GITEA_API_URL'], os.environ['GITEA_READ_TOKEN'])
github = API('https://api.github.com', '', github=True)
build, receipt = release_receipt(gitea, github, args.version)
image, commit = GITHUB_IMAGE + '@' + receipt['image_digest'], build['commit']
else:
image, commit = args.image, args.commit
require(image and commit, 'Development requires an image digest and commit')
if args.channel == 'production':
with release_state() as state:
if state and state.get('status') != 'complete':
require(state.get('commit') == commit and state.get('version') == args.version,
'Another release is pending')
deployment(args.directory, args.channel, image, commit, args.version, args.client_container)
if state and state.get('commit') == commit:
state['status'] = 'complete'
save_state(state)
else:
deployment(args.directory, args.channel, image, commit, args.version, args.client_container)
require(args.image and args.commit, 'Development requires an image digest and commit')
deployment(args.directory, args.channel, args.image, args.commit, args.version, args.client_container)
if __name__ == '__main__':
+12 -3
View File
@@ -332,8 +332,7 @@ def preflight(version, commit):
'GitHub classic PAT needs repository access, workflow and write:packages')
current = gitea.release(version)
if current and not current['draft']:
build, _ = release_receipt(gitea, github, version)
require(build['commit'] == commit, 'Published version belongs to another commit')
complete_publication(gitea, github, version, commit)
output('completed', 'true')
output('reuse', 'true')
return
@@ -386,6 +385,16 @@ def save_state(state):
temporary.replace(path)
def complete_publication(gitea, github, version, commit):
"""Finish the matching reservation only after both published receipts validate."""
state = selected_state()
require(state['version'] == version and state['commit'] == commit, 'Publication candidate mismatch')
build, _ = release_receipt(gitea, github, version)
require(build['commit'] == commit, 'Published version belongs to another commit')
state['status'] = 'complete'
save_state(state)
def reserve(dev):
require(re.fullmatch(r'[0-9a-f]{40}', dev), 'Expected the accepted dev SHA')
with release_state() as current:
@@ -651,7 +660,7 @@ def publish():
run('docker', 'buildx', 'imagetools', 'create', '--tag', image + ':' + stable_tag, image + '@' + value)
require(registry_digest(image, stable_tag) == value, 'Promoted image digest differs')
gitea.repo(f'/releases/{release["id"]}', method='PATCH', data={'draft': False})
release_receipt(gitea, github, tag)
complete_publication(gitea, github, tag, build['commit'])
print(f'Published RawMark {tag}: {build["commit"]}, {value}')
+50 -1
View File
@@ -239,7 +239,7 @@ class DeploymentTests(unittest.TestCase):
def test_channel_mismatch_cannot_touch_docker(self):
with patch.object(deploy, 'run') as docker:
with self.assertRaisesRegex(ValueError, 'channel'):
deploy.deployment(self.directory, 'production', self.image, SHA, '0.1.1')
deploy.settings(self.directory, 'production')
docker.assert_not_called()
def test_unresolved_pending_deployment_cannot_be_overwritten(self):
@@ -380,3 +380,52 @@ class MainAdmissionTests(unittest.TestCase):
{'head_sha': SHA, 'event': 'push', 'path': 'dev.yml@refs/heads/dev', 'conclusion': 'success'},
]}, {'workflow_runs': []}]
self.assertFalse(release.successful_main_ci(api, SHA))
class PublicationCompletionTests(unittest.TestCase):
def test_success_finishes_reservation_without_deployment(self):
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
with patch.object(release, 'selected_state', return_value=state), \
patch.object(release, 'release_receipt', return_value=({'commit': SHA}, {})), \
patch.object(release, 'save_state') as save:
release.complete_publication(None, None, 'version', SHA)
self.assertEqual(state['status'], 'complete')
save.assert_called_once_with(state)
def test_partial_publication_remains_pending(self):
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
with patch.object(release, 'selected_state', return_value=state), \
patch.object(release, 'release_receipt', side_effect=ValueError('Incomplete mirror')), \
patch.object(release, 'save_state') as save:
with self.assertRaisesRegex(ValueError, 'Incomplete mirror'):
release.complete_publication(None, None, 'version', SHA)
self.assertEqual(state['status'], 'selected')
save.assert_not_called()
def test_published_retry_repairs_pending_reservation(self):
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
gitea, github = Mock(), Mock()
gitea.request.return_value = github.request.return_value = {'login': 'eezd'}
github.repo.return_value = {'permissions': {'push': True}}
github.headers = {'X-OAuth-Scopes': 'repo, workflow, write:packages'}
gitea.release.return_value = {'draft': False}
with patch.dict(os.environ, {'CI_RUN_ID': '42'}), \
patch.object(release, 'selected_state', return_value=state), \
patch.object(release, 'clients', return_value=(gitea, github)), \
patch.object(release, 'validate_identity'), \
patch.object(release, 'release_receipt', return_value=({'commit': SHA}, {})), \
patch.object(release, 'save_state'), patch.object(release, 'run') as run, \
patch.object(release, 'output') as output:
release.preflight('version', SHA)
self.assertEqual(state['status'], 'complete')
output.assert_any_call('completed', 'true')
run.assert_not_called()
def test_wrong_candidate_cannot_finish_reservation(self):
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
with patch.object(release, 'selected_state', return_value=state), \
patch.object(release, 'release_receipt', return_value=({'commit': 'c' * 40}, {})), \
patch.object(release, 'save_state') as save:
with self.assertRaisesRegex(ValueError, 'another commit'):
release.complete_publication(None, None, 'version', SHA)
save.assert_not_called()