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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ ci-signoff:
exit $$result
PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-production-image.py
PYTHONDONTWRITEBYTECODE=1 python3 scripts/deploy/test_lease.py
PYTHONDONTWRITEBYTECODE=1 python3 scripts/deploy/test_run.py
python3 scripts/production-image.py signoff

# What CI would run right now, without running any of it.
Expand Down
29 changes: 28 additions & 1 deletion scripts/deploy/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@ def output(*args):
return execute(*args, stdout=subprocess.PIPE, text=True).stdout.strip()


# Paths deploy.yml refuses to deploy for, because they cannot change the image.
# Kept in step with its `paths-ignore`; test_run.py asserts the two agree.
UNDEPLOYABLE = ('docs/', 'bench/')


def only_undeployable(lease, current, master):
"""Whether `master` differs from `current` by nothing that could change the image.

A documentation merge landing seconds after a code merge used to cancel that
code deploy: the rollout saw a newer master and declined, while the docs push
started no rollout of its own because deploy.yml ignores those paths. The
result was a green deploy job and a production box still running the previous
image, with nothing reporting the gap (2026-09-10, PR #251).

Fails CLOSED. If the newer commits cannot be fetched or inspected, the answer
is False and the rollout defers exactly as it did before.
"""
try:
lease.git('fetch', '--quiet', '--depth=50', 'origin', master)
changed = lease.git('diff', '--name-only', f'{current}..{master}').splitlines()
except subprocess.CalledProcessError:
return False
if not changed:
return False
return all(path.startswith(UNDEPLOYABLE) or path.endswith('.md') for path in changed)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('mode', choices=('local', 'ci'))
Expand Down Expand Up @@ -67,7 +94,7 @@ def interrupted(signum, frame):
with lease.hold(image) as guard:
current = lease.git('rev-parse', 'HEAD')
master = lease.remote('refs/heads/master')
if current != master:
if current != master and not only_undeployable(lease, current, master):
if local:
parser.error('The checkout must be the current merged master commit')
print('A newer master commit superseded this rollout; production is unchanged.')
Expand Down
69 changes: 69 additions & 0 deletions scripts/deploy/test_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""A docs-only merge must not cancel the code deploy it lands behind."""
import pathlib
import re
import subprocess
import tempfile
import unittest

from lease import DeploymentLease
from run import UNDEPLOYABLE, only_undeployable

ROOT = pathlib.Path(__file__).resolve().parents[2]


def repo(root, *rounds):
"""A clone whose origin/master carries each round of paths as one commit."""
remote, work = root / 'remote', root / 'work'
run = lambda *args, **kw: subprocess.run(['git', *map(str, args)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kw)
run('init', '--bare', '--initial-branch=master', remote)
run('clone', remote, work)
run('-C', work, 'config', 'user.email', 'test@example.com')
run('-C', work, 'config', 'user.name', 'test')
commits = []
for round_index, paths in enumerate(rounds):
for path in paths:
target = work / path
target.parent.mkdir(parents=True, exist_ok=True)
# Content must differ per round, or a rewritten path is not a diff.
target.write_text(f'{path} round {round_index}')
run('-C', work, 'add', '-A')
run('-C', work, 'commit', '-m', ' '.join(paths))
commits.append(subprocess.run(['git', '-C', work, 'rev-parse', 'HEAD'], check=True, text=True, stdout=subprocess.PIPE).stdout.strip())
run('-C', work, 'push', '--quiet', 'origin', 'master')
return DeploymentLease(work), commits


class SupersedeTest(unittest.TestCase):
def check(self, later):
with tempfile.TemporaryDirectory() as directory:
lease, commits = repo(pathlib.Path(directory), ['src/main.rs'], later)
return only_undeployable(lease, commits[0], commits[1])

def test_docs_only_does_not_supersede(self):
for paths in (['docs/plan.md'], ['README.md'], ['bench/probe.py'], ['docs/a.md', 'bench/b.py']):
with self.subTest(paths=paths):
self.assertTrue(self.check(paths), f'{paths} cannot change the image, so it must not cancel a rollout')

def test_code_still_supersedes(self):
for paths in (['src/main.rs'], ['Cargo.toml'], ['docs/a.md', 'src/main.rs']):
with self.subTest(paths=paths):
self.assertFalse(self.check(paths), f'{paths} can change the image, so it must still defer the rollout')

def test_identical_commits_are_not_treated_as_undeployable(self):
with tempfile.TemporaryDirectory() as directory:
lease, commits = repo(pathlib.Path(directory), ['src/main.rs'])
self.assertFalse(only_undeployable(lease, commits[0], commits[0]), 'an empty diff must not be read as a docs-only difference')

def test_ignore_list_matches_the_workflow(self):
"""Drift here is silent: the workflow would skip a path this still defers on."""
# Parsed rather than yaml-loaded so the check needs no third-party module.
text = (ROOT / '.github/workflows/deploy.yml').read_text()
block = re.search(r'^\s*paths-ignore:\n((?:\s*-\s.*\n)+)', text, re.M)
self.assertIsNotNone(block, 'deploy.yml no longer declares paths-ignore')
ignored = {re.sub(r"^\s*-\s*|['\"]", '', line) for line in block.group(1).splitlines() if line.strip()}
covered = {p.rstrip('/') + '/**' for p in UNDEPLOYABLE} | {'**/*.md'}
self.assertEqual(ignored, covered, 'deploy.yml paths-ignore and run.py UNDEPLOYABLE disagree')


if __name__ == '__main__':
unittest.main()
Loading