diff --git a/dev-tools/scripts/smokeTestRelease.py b/dev-tools/scripts/smokeTestRelease.py index 93c66d740a0..8e60add3dad 100755 --- a/dev-tools/scripts/smokeTestRelease.py +++ b/dev-tools/scripts/smokeTestRelease.py @@ -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): @@ -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 + 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 + reChangesSectionHREF = re.compile('(.*?)', re.IGNORECASE) reUnderbarNotDashHTML = re.compile(r'
  • (\s*(SOLR)_\d\d\d\d+)') reUnderbarNotDashTXT = re.compile(r'\s+((SOLR)_\d\d\d\d+)', re.MULTILINE) @@ -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']*>\s*]*>(Release\s+[^<]+)\s*', 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)) + + print(' - First h2: "%s" (no date)' % first_header) + print(' - Second h2: "%s" (with date)' % second_header) + if isHTML: r = reUnderbarNotDashHTML else: @@ -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: @@ -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: @@ -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): @@ -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.') @@ -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)