WSL/tools/devops/create-release.py
WSL Team 697572d664 Initial open source commit for WSL.
Many Microsoft employees have contributed to the Windows Subsystem for Linux, this commit is the result of their work since 2016.

The entire history of the Windows Subsystem for Linux can't be shared here, but here's an overview of WSL's history after it moved to it own repository in 2021:

Number of commits on the main branch: 2930
Number of contributors: 31

Head over https://github.com/microsoft/WSL/releases for a more detailed history of the features added to WSL since 2021.
2025-05-15 12:09:45 -07:00

187 lines
7.0 KiB
Python

# pip install click requests gitpython
import click
import requests
import sys
import re
import os
import backoff
from git import Repo
from urllib.parse import urlparse
@click.command()
@click.argument('version', required=True)
@click.argument('assets', default=None, nargs=-1)
@click.option('--previous', default=None)
@click.option('--max-message-lines', default=1)
@click.option('--publish', is_flag=True, default=False)
@click.option('--no-fetch', is_flag=True, default=False)
@click.option('--github-token', default=None)
@click.option('--use-current-ref', is_flag=True, default=False)
def main(version: str, previous: str, max_message_lines: int, publish: bool, assets: list, no_fetch: bool, github_token: str, use_current_ref: bool):
if publish:
if assets is None:
raise RuntimeError('--publish requires at least one asset')
if github_token is None:
raise RuntimeError('--publish requires --github_token')
for e in assets:
if not os.path.exists(e):
raise RuntimeError(f'Asset not found: {e}')
if previous is None:
previous = get_previous_release(parse_tag(version))
current_ref = '<current-commit>' if use_current_ref else version
print(f'Creating release notes for: {previous} -> {current_ref}', file=sys.stderr)
changes = ''
for e in get_change_list(None if use_current_ref else version, previous, not no_fetch):
# Detect attached github issues
issues = find_github_issues(e.message)
pr_description, pr_number = get_github_pr_message(github_token, e.message)
if pr_description is not None:
issues = issues.union(find_github_issues(pr_description))
message = e.message[:-1] if e.message.endswith('\n') else e.message
# Shrink the message if it's too long
lines = message.split('\n')
message = '\n'.join([e for e in lines if e][:max_message_lines])
# Get rid of the github PR #
if pr_number is not None:
message = message.replace(f'(#{pr_number})', '')
# Append to the changes (chr(92) == '\n')
message = f'{message.replace(chr(92), "")} (solves {",".join(issues)})' if issues else message
changes += f'* {message}\n'
if publish:
publish_release(version, changes, assets, github_token)
else:
print(f'\n{changes}')
@backoff.on_exception(backoff.expo, (requests.exceptions.Timeout, requests.exceptions.ConnectionError, requests.exceptions.RequestException), max_time=600)
def get_github_pr_message(token: str, message: str) -> str:
match = re.search(r'\(#([0-9]+)\)', message)
if match is None:
print(f'Warning: failed to extract Github PR number from message: {message}')
return None, None
pr_number = match.group(1)
headers = {'Accept': 'application/vnd.github+json',
'Authorization': 'Bearer ' + token,
'X-GitHub-Api-Version': '2022-11-28'}
# OSSTODO: Update repo once open source
response = requests.get(f'https://api.github.com/repos/microsoft/wsl-staging/pulls/{pr_number}', timeout=30, headers=headers)
response.raise_for_status()
return response.json()['body'], pr_number
def parse_tag(tag: str) -> list:
version = tag.split('.')
if len(version) != 3:
raise RuntimeError(f'Unexpected tag: {version}')
return tuple(int(e) for e in version)
def get_previous_release(version: tuple) -> str:
response = requests.get('https://api.github.com/repos/Microsoft/WSL/releases');
response.raise_for_status()
# Find the most recent release with a lower version number than this one
versions = [parse_tag(e['tag_name']) for e in response.json()]
previous_versions = [e for e in versions if e < version]
if not previous_versions:
raise RuntimeError(f'No previous found on Github. Response: {response.json()}')
return '.'.join(str(e) for e in max(previous_versions))
def find_github_issues(message: str):
urls = [urlparse(e) for e in re.findall(r"https?://[^\s^\)]+", message)]
issue_urls = [e for e in urls if e.hostname == 'github.com' and e.path.lower().startswith('/microsoft/wsl/issues/')]
issues = set(['#' + e.path.split('/')[-1] for e in issue_urls])
if len(issues) > 1:
print(f'WARNING: found more than 1 github issues in message: {message}. Issues: {issues}', file=sys.stderr)
return issues
def get_change_list(version: str, previous: str, fetch: bool) -> list:
repo = Repo('.')
# Fetch origin first
if fetch and version is not None:
repo.remote('origin').fetch(previous)
# Find both current and previous version tags
previous_tag = repo.tag(previous)
# Set current ref
current_ref = repo.tag(version) if version is not None else repo.head
# Find common root between tags
merge_bases = repo.merge_base(previous_tag.commit, current_ref)
if len(merge_bases) == 0:
raise RuntimeError(f'No merge base found between {version} and {previous}')
elif len(merge_bases) > 1:
raise RuntimeError(f'Multiple merge bases found between {version} and {previous}')
# List commits between tags
for e in repo.iter_commits(rev=current_ref):
if e == merge_bases[0]:
return
yield e
raise RuntimeError(f'Tag {previous} is not an ancestor of {version}')
@backoff.on_exception(backoff.expo, (requests.exceptions.Timeout, requests.exceptions.ConnectionError, requests.exceptions.RequestException), max_time=600)
def publish_release(version: str, changes: str, assets: list, token: str):
print(f'Creating private Github release for: {version}', file=sys.stderr)
# First create the release
headers = {'Accept': 'application/vnd.github+json',
'Authorization': 'Bearer ' + token,
'X-GitHub-Api-Version': '2022-11-28'}
content = {'tag_name': version,
'target_commitish': 'master',
'name': version,
'body': changes,
"draft":True ,
'prerelease':True ,
'generate_release_notes': False}
response = requests.post('https://api.github.com/repos/microsoft/wsl/releases', json=content, headers=headers)
response.raise_for_status()
release = response.json()
print(f'Created release: {release["url"]}', file=sys.stderr)
for asset in assets:
with open(asset, 'rb') as asset_content:
asset_size = os.path.getsize(asset)
# Append asset to the release assets
headers['Content-Type'] = 'application/octet-stream'
response = requests.post(f'https://uploads.github.com/repos/microsoft/wsl/releases/{release["id"]}/assets?name={os.path.basename(asset)}', headers=headers, data=asset_content)
response.raise_for_status()
print(f'Attached asset: {asset} to release: {response.json()["url"]}', file=sys.stderr)
print(f'The release has been created. Navigate to {release["html_url"]} to edit the release notes and publish it', file=sys.stderr)
if __name__ == '__main__':
main()