Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions vs-code-terraphim-it/scripts/publish_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"""Publish qualified VSIX bytes through a resumable draft; never replace assets or tags."""
import argparse
import hashlib
from itertools import islice
import json
from pathlib import Path
import re
import subprocess
import sys
import tempfile
import time
from urllib.parse import urlparse
import zipfile
from xml.etree.ElementTree import ParseError
Expand All @@ -17,6 +19,7 @@

REPO_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*")
SHA_RE = re.compile(r"[0-9a-f]{40}")
RELEASE_LOOKUP_DELAYS = (0, 2, 2, 2, 2)


def digest(path):
Expand Down Expand Up @@ -99,6 +102,25 @@ def release(repo, tag):
return select_release(pages, tag)


def recover_visible_release(snapshots):
"""Consume at most five reads; only an absent draft permits another read."""
for current in islice(snapshots, len(RELEASE_LOOKUP_DELAYS)):
if current is not None:
return current
raise ValueError("Created draft is not yet readable; rerun the same qualified artifact")


def recovered_release(repo, tag):
"""Allow eight seconds of backoff for creation/listing consistency."""
def snapshots():
for delay in RELEASE_LOOKUP_DELAYS:
if delay:
time.sleep(delay)
yield release(repo, tag)

return recover_visible_release(snapshots())


def verify_remote_tag(repo, tag, source):
value = api(f"repos/{repo}/git/ref/tags/{tag}")["object"]
for _ in range(5):
Expand Down Expand Up @@ -167,9 +189,7 @@ def publish(repo, tag, files, source):
"Registry availability is verified separately.\n")
gh("release", "create", tag, "--repo", f"github.com/{repo}", "--verify-tag",
"--draft", "--prerelease", "--latest=false", "--title", tag, "--notes-file", str(notes))
current = release(repo, tag)
if current is None:
raise ValueError("Draft release could not be recovered")
current = recovered_release(repo, tag)
missing = resume_plan(current, tag, files)
# Verify every existing asset before mutating an incomplete draft.
for path in files:
Expand Down
56 changes: 56 additions & 0 deletions vs-code-terraphim-it/tests/publish_release_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,62 @@ def test_draft_lookup_is_exact_and_rejects_duplicate_releases(self):
with self.assertRaises(ValueError):
publication.select_release([[draft], [draft]], self.tag)

def test_created_draft_readback_is_bounded_and_stops_at_first_visible_release(self):
draft = {"tag_name": self.tag, "draft": True, "prerelease": True, "assets": []}
observed = []

def snapshots(values):
for value in values:
observed.append(value)
yield value

self.assertEqual(publication.recover_visible_release(snapshots([None, None, draft, None])), draft)
self.assertEqual(observed, [None, None, draft])
observed.clear()
self.assertEqual(publication.recover_visible_release(snapshots([None] * 4 + [draft])), draft)
self.assertEqual(len(observed), 5)
observed.clear()
with self.assertRaisesRegex(ValueError, "same qualified artifact"):
publication.recover_visible_release(snapshots([None] * 5 + [draft]))
self.assertEqual(observed, [None] * 5)
self.assertEqual(len(publication.RELEASE_LOOKUP_DELAYS), 5)
self.assertEqual(sum(publication.RELEASE_LOOKUP_DELAYS), 8)

def test_visible_mismatch_or_ambiguous_readback_is_never_retried(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
vsix, manifest, sums, _ = self.qualified(root)
original = {p.name: p.read_bytes() for p in (vsix, manifest, sums)}
wrong = {"tag_name": "other", "draft": True, "prerelease": True, "assets": []}
values = iter([wrong, None])
current = publication.recover_visible_release(values)
with self.assertRaisesRegex(ValueError, "tag or preview"):
publication.resume_plan(current, self.tag, [vsix, manifest, sums])
self.assertIsNone(next(values))
self.assertEqual(original, {p.name: p.read_bytes() for p in (vsix, manifest, sums)})

def ambiguous_snapshots():
yield None
draft = {"tag_name": self.tag}
yield publication.select_release([[draft, draft]], self.tag)

with self.assertRaisesRegex(ValueError, "Ambiguous"):
publication.recover_visible_release(ambiguous_snapshots())

def test_readback_policy_executes_in_real_subprocess_without_cloud_calls(self):
with tempfile.TemporaryDirectory() as folder:
fixture = Path(folder) / "readbacks.json"
draft = {"tag_name": self.tag, "draft": True, "prerelease": True, "assets": []}
fixture.write_text(json.dumps([None, draft]))
code = ("import json,sys; sys.path.insert(0,sys.argv[1]); import publish_release; "
"print(json.dumps(publish_release.recover_visible_release("
"iter(json.load(open(sys.argv[2]))))))")
result = subprocess.run([sys.executable, "-c", code,
str(Path(publication.__file__).parent), str(fixture)],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout), draft)

def test_anonymous_download_rejects_credentials_and_overwrite_before_network(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "download"
Expand Down
Loading