Skip to content
Open
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
111 changes: 108 additions & 3 deletions dev-tools/scripts/smokeTestRelease.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def unshortenURL(url):
# Set to False to avoid re-downloading the packages...
FORCE_CLEAN = True

# Set to True via --skip-changelog-current to skip checks that verify the changelog
# is current (i.e. that 'logchange release' and 'logchange generate' have been run).
SKIP_CHANGELOG_CURRENT = False


def getHREFs(urlString):

Expand Down Expand Up @@ -390,6 +394,67 @@ def testChangelogMd(dir, version):
if 'v%s' % version not in content and version not in content:
raise RuntimeError('Version %s not found in CHANGELOG.md' % version)


def testChangelogFolder(dir, version):
"Checks changelog folder structure."
changelog_folder = os.path.join(dir, 'changelog')

if not os.path.exists(changelog_folder):
raise RuntimeError('changelog folder not found at %s' % changelog_folder)

print(' check changelog folder structure...')

# Check that 'unreleased' folder exists and is empty
Comment thread
janhoy marked this conversation as resolved.
unreleased_folder = os.path.join(changelog_folder, 'unreleased')
if not os.path.exists(unreleased_folder):
raise RuntimeError('changelog/unreleased folder not found')

unreleased_contents = os.listdir(unreleased_folder)
# Filter out hidden files like .gitkeep
unreleased_contents = [f for f in unreleased_contents if not f.startswith('.')]
if len(unreleased_contents) > 0:
raise RuntimeError('changelog/unreleased folder is not empty, contains: %s' % unreleased_contents)

# Explicitly verify that the version folder for the current release exists
version_folder = os.path.join(changelog_folder, 'v%s' % version)
if not os.path.exists(version_folder):
raise RuntimeError('changelog/v%s folder not found (run logchange release first)' % version)

# Pattern to match version folders (e.g., v9.5.0, v10.0.0, v10.1.0-beta1, v10.1.0-beta-1, v10.1.0-RC1)
# Uses a prefix match to handle any suffix format without silently skipping unusual variants.
version_pattern = re.compile(r'^v\d+\.\d+\.\d+')

# Check all subdirectories in changelog
for entry in os.listdir(changelog_folder):
entry_path = os.path.join(changelog_folder, entry)

# Skip if not a directory
if not os.path.isdir(entry_path):
continue

# Skip the unreleased folder (already checked)
if entry == 'unreleased':
continue

release_date_file = os.path.join(entry_path, 'release-date.txt')

# Check if this is a version folder (vX.Y.Z format)
if version_pattern.match(entry):
# Version folders for the current release should NOT have release-date.txt
# Only check if this is the version being released
if entry == 'v%s' % version:
if os.path.exists(release_date_file):
raise RuntimeError('changelog/%s folder should not contain release-date.txt (version not yet released)' % entry)
else:
# Other version folders (past releases) should have release-date.txt
if not os.path.exists(release_date_file):
raise RuntimeError('changelog/%s folder is missing release-date.txt' % entry)
else:
# Non-version folders (e.g., documentation or support directories) are allowed
# and are not required to contain a release-date.txt file.
# They are intentionally ignored by this check.
pass
Comment thread
janhoy marked this conversation as resolved.

reChangesSectionHREF = re.compile('<a id="(.*?)".*?>(.*?)</a>', re.IGNORECASE)
reUnderbarNotDashHTML = re.compile(r'<li>(\s*(SOLR)_\d\d\d\d+)')
reUnderbarNotDashTXT = re.compile(r'\s+((SOLR)_\d\d\d\d+)', re.MULTILINE)
Expand All @@ -398,9 +463,41 @@ def testChangelogMd(dir, version):
def checkChangesContent(s, version, name, isHTML):
currentVersionTuple = versionToTuple(version, name)

if isHTML and s.find('Release %s' % version) == -1:
if isHTML and not SKIP_CHANGELOG_CURRENT and s.find('Release %s' % version) == -1:
raise RuntimeError('did not see "Release %s" in %s' % (version, name))

# Validate h2 header structure for Changes.html (requires logchange generate to have been run)
if isHTML and not SKIP_CHANGELOG_CURRENT:
print(' validate h2 header structure...')
h2_pattern = re.compile(r'<h2[^>]*>\s*<a[^>]*>(Release\s+[^<]+)</a>\s*</h2>', re.IGNORECASE | re.DOTALL)
headers = h2_pattern.findall(s)

if len(headers) < 2:
raise RuntimeError('Expected at least 2 release h2 headers in %s, found %d' % (name, len(headers)))

# First header should be "Release {version}" without a date
first_header = headers[0].strip()
expected_first = 'Release %s' % version

# Check if first header matches version and does NOT have a date pattern [yyyy-mm-dd]
if not first_header.startswith(expected_first):
raise RuntimeError('First h2 header should start with "%s", but got "%s" in %s' % (expected_first, first_header, name))

date_pattern = re.compile(r'\[\d{4}-\d{2}-\d{2}\]')
if date_pattern.search(first_header):
raise RuntimeError('First h2 header for "%s" should NOT contain a release date, but got "%s" in %s' % (version, first_header, name))

# Second header should be "Release {version} [yyyy-mm-dd]" with a date
second_header = headers[1].strip()
if not second_header.startswith('Release '):
raise RuntimeError('Second h2 header should start with "Release", but got "%s" in %s' % (second_header, name))

if not date_pattern.search(second_header):
raise RuntimeError('Second h2 header should contain a release date [yyyy-mm-dd], but got "%s" in %s' % (second_header, name))
Comment thread
janhoy marked this conversation as resolved.

print(' - First h2: "%s" (no date)' % first_header)
print(' - Second h2: "%s" (with date)' % second_header)
Comment thread
janhoy marked this conversation as resolved.

if isHTML:
r = reUnderbarNotDashHTML
else:
Expand All @@ -410,7 +507,7 @@ def checkChangesContent(s, version, name, isHTML):
if m is not None:
raise RuntimeError('incorrect issue (_ instead of -) in %s: %s' % (name, m.group(1)))

if s.lower().find('not yet released') != -1:
if not SKIP_CHANGELOG_CURRENT and s.lower().find('not yet released') != -1:
raise RuntimeError('saw "not yet released" in %s' % name)

if not isHTML:
Expand Down Expand Up @@ -652,6 +749,8 @@ def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs):
raise RuntimeError('solr: unexpected files/dirs in artifact %s: %s' % (artifact, in_root_folder))

if isSrc:
if not SKIP_CHANGELOG_CURRENT:
testChangelogFolder(unpackPath, version)
print(' make sure no JARs/WARs in src dist...')
lines = os.popen('find . -name \\*.jar').readlines()
if len(lines) != 0:
Expand Down Expand Up @@ -715,7 +814,8 @@ def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs):

os.chdir(unpackPath)

testChangelogMd('.', version)
if not SKIP_CHANGELOG_CURRENT:
testChangelogMd('.', version)


def find_available_port(max_attempts=100):
Expand Down Expand Up @@ -1067,6 +1167,8 @@ def parse_config():
help='Only perform download and sha hash check steps')
parser.add_argument('--dev-mode', action='store_true', default=False,
help='Enable dev mode, will not check branch compatibility')
parser.add_argument('--skip-changelog-current', action='store_true', default=False,
help='Skip checks that verify the changelog is current (i.e. logchange release/generate have been run)')
parser.add_argument('url', help='Url pointing to release to test')
parser.add_argument('test_args', nargs=argparse.REMAINDER,
help='Arguments to pass to gradle for testing, e.g. -Dwhat=ever.')
Expand Down Expand Up @@ -1117,6 +1219,9 @@ def main():
if not c.version.startswith(scriptVersion + '.') and not c.dev_mode:
raise RuntimeError('smokeTestRelease.py for %s.X is incompatible with a %s release.' % (scriptVersion, c.version))

global SKIP_CHANGELOG_CURRENT
SKIP_CHANGELOG_CURRENT = c.skip_changelog_current

print('NOTE: output encoding is %s' % sys.stdout.encoding)
smokeTest(c.java, c.url, c.revision, c.version, c.tmp_dir, c.is_signed, c.local_keys, ' '.join(c.test_args),
downloadOnly=c.download_only)
Expand Down
Loading