Skip to content

Commit 63ec0e8

Browse files
ci: [NGOv1.X] Automation of NGO releases (#3603)
## Jira ticket Same as main PR ## Documentation Same as main PR ## Testing & QA Same as main PR ## Backports This is a backport of #3602 the same description should apply. The only difference is the base branch (develop instead of develop-2.0.0)
1 parent b1a6081 commit 63ec0e8

File tree

10 files changed

+777
-78
lines changed

10 files changed

+777
-78
lines changed

.yamato/ngo-publish.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ngo_release_preparation:
2+
name: "NGO release preparation"
3+
agent: { type: Unity::VM, flavor: b1.small, image: package-ci/ubuntu-22.04:v4 }
4+
triggers:
5+
recurring:
6+
- branch: develop # We make new releases from this branch
7+
frequency: weekly # Run at some point every Saturday. Note that it's restricted to every 4th Saturday inside the script
8+
rerun: always
9+
commands:
10+
- pip install PyGithub
11+
- pip install GitPython
12+
- python Tools/scripts/ReleaseAutomation/run_release_preparation.py
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Netcode configuration for the release process automation."""
2+
3+
import datetime
4+
import sys
5+
import os
6+
from github import Github
7+
from github import GithubException
8+
9+
PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))
10+
sys.path.insert(0, PARENT_DIR)
11+
12+
from Utils.general_utils import get_package_version_from_manifest
13+
from release import make_package_release_ready
14+
15+
class GithubUtils:
16+
def __init__(self, access_token, repo):
17+
self.github = Github(base_url="https://api.github.com",
18+
login_or_token=access_token)
19+
self.repo = self.github.get_repo(repo)
20+
21+
def is_branch_present(self, branch_name):
22+
try:
23+
self.repo.get_branch(branch_name)
24+
return True # Branch exists
25+
26+
except GithubException as ghe:
27+
if ghe.status == 404:
28+
return False # Branch does not exist
29+
raise Exception(f"An error occurred with the GitHub API: {ghe.status}", data=ghe.data)
30+
31+
class ReleaseConfig:
32+
"""A simple class to hold all shared configuration."""
33+
def __init__(self):
34+
self.manifest_path = 'com.unity.netcode.gameobjects/package.json'
35+
self.changelog_path = 'com.unity.netcode.gameobjects/CHANGELOG.md'
36+
self.validation_exceptions_path = './ValidationExceptions.json'
37+
self.github_repo = 'Unity-Technologies/com.unity.netcode.gameobjects'
38+
self.default_repo_branch = 'develop' # Changelog and package version change will be pushed to this branch
39+
self.yamato_project_id = '1201'
40+
self.command_to_run_on_release_branch = make_package_release_ready
41+
42+
self.release_weekday = 5 # Saturday
43+
self.release_week_cycle = 4 # Release every 4 weeks
44+
self.anchor_date = datetime.date(2025, 7, 19) # Anchor date for the release cycle (previous release Saturday)
45+
46+
self.package_version = get_package_version_from_manifest(self.manifest_path)
47+
self.release_branch_name = f"release/{self.package_version}" # Branch from which we want to release
48+
self.commit_message = f"Updated changelog and package version for Netcode in anticipation of v{self.package_version} release"
49+
50+
GITHUB_TOKEN_NAME = "NETCODE_GITHUB_TOKEN"
51+
YAMATO_API_KEY_NAME = "NETCODE_YAMATO_API_KEY"
52+
self.github_token = os.environ.get(GITHUB_TOKEN_NAME)
53+
self.yamato_api_token = os.environ.get(YAMATO_API_KEY_NAME)
54+
self.commiter_name = "netcode-automation"
55+
self.commiter_email = "[email protected]"
56+
57+
self.yamato_samples_to_build = [
58+
{
59+
"name": "BossRoom",
60+
"jobDefinition": f".yamato%2Fproject-builders%2Fproject-builders.yml%23build_BossRoom_project",
61+
}
62+
]
63+
64+
self.yamato_build_automation_configs = [
65+
{
66+
"job_name": "Build Sample for Windows with minimal supported editor (2022.3), burst ON, IL2CPP",
67+
"variables": [
68+
{ "key": "BURST_ON_OFF", "value": "on" },
69+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "win64" },
70+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "il2cpp" },
71+
{ "key": "UNITY_VERSION", "value": "2022.3" } # Minimal supported editor
72+
]
73+
},
74+
{
75+
"job_name": "Build Sample for Windows with latest functional editor (6000.2), burst ON, IL2CPP",
76+
"variables": [
77+
{ "key": "BURST_ON_OFF", "value": "on" },
78+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "win64" },
79+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "il2cpp" },
80+
{ "key": "UNITY_VERSION", "value": "6000.2" } # Editor that most our users will use (not alpha). Sometimes when testing on trunk we have weird editor issues not caused by us so the preference will be to test on latest editor that our users will use.
81+
]
82+
},
83+
{
84+
"job_name": "Build Sample for MacOS with minimal supported editor (2022.3), burst OFF, Mono",
85+
"variables": [
86+
{ "key": "BURST_ON_OFF", "value": "off" },
87+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "mac" },
88+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "mono" },
89+
{ "key": "UNITY_VERSION", "value": "2022.3" } # Minimal supported editor
90+
]
91+
},
92+
{
93+
"job_name": "Build Sample for MacOS with latest functional editor (6000.2), burst OFF, Mono",
94+
"variables": [
95+
{ "key": "BURST_ON_OFF", "value": "off" },
96+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "mac" },
97+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "mono" },
98+
{ "key": "UNITY_VERSION", "value": "6000.2" } # Editor that most our users will use (not alpha). Sometimes when testing on trunk we have weird editor issues not caused by us so the preference will be to test on latest editor that our users will use.
99+
]
100+
},
101+
{
102+
"job_name": "Build Sample for Android with minimal supported editor (2022.3), burst ON, IL2CPP",
103+
"variables": [
104+
{ "key": "BURST_ON_OFF", "value": "on" },
105+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "android" },
106+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "il2cpp" },
107+
{ "key": "UNITY_VERSION", "value": "2022.3" } # Minimal supported editor
108+
]
109+
},
110+
{
111+
"job_name": "Build Sample for Android with latest functional editor (6000.2), burst ON, IL2CPP",
112+
"variables": [
113+
{ "key": "BURST_ON_OFF", "value": "on" },
114+
{ "key": "PLATFORM_WIN64_MAC_ANDROID", "value": "android" },
115+
{ "key": "SCRIPTING_BACKEND_IL2CPP_MONO", "value": "il2cpp" },
116+
{ "key": "UNITY_VERSION", "value": "6000.2" } # Editor that most our users will use (not alpha). Sometimes when testing on trunk we have weird editor issues not caused by us so the preference will be to test on latest editor that our users will use.
117+
]
118+
}
119+
]
120+
121+
error_messages = []
122+
if not os.path.exists(self.manifest_path):
123+
error_messages.append(f" Path does not exist: {self.manifest_path}")
124+
125+
if not os.path.exists(self.changelog_path):
126+
error_messages.append(f" Path does not exist: {self.changelog_path}")
127+
128+
if not os.path.exists(self.validation_exceptions_path):
129+
error_messages.append(f" Path does not exist: {self.validation_exceptions_path}")
130+
131+
if not callable(self.command_to_run_on_release_branch):
132+
error_messages.append("command_to_run_on_release_branch is not a function! Actual value:", self.command_to_run_on_release_branch)
133+
134+
if self.package_version is None:
135+
error_messages.append(f"Package version not found at {self.manifest_path}")
136+
137+
if not self.github_token:
138+
error_messages.append(f"Error: {GITHUB_TOKEN_NAME} environment variable not set.")
139+
140+
if not self.yamato_api_token:
141+
error_messages.append(f"Error: {YAMATO_API_KEY_NAME} environment variable not set.")
142+
143+
# Initialize PyGithub and get the repository object
144+
self.github_manager = GithubUtils(self.github_token, self.github_repo)
145+
146+
if not self.github_manager.is_branch_present(self.default_repo_branch):
147+
error_messages.append(f"Branch '{self.default_repo_branch}' does not exist.")
148+
149+
if self.github_manager.is_branch_present(self.release_branch_name):
150+
error_messages.append(f"Branch '{self.release_branch_name}' is already present in the repo.")
151+
152+
if error_messages:
153+
summary = "Failed to initialize NetcodeReleaseConfig due to invalid setup:\n" + "\n".join(f"- {msg}" for msg in error_messages)
154+
raise ValueError(summary)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Automation for package release process."""
2+
3+
import sys
4+
import os
5+
6+
PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))
7+
sys.path.insert(0, PARENT_DIR)
8+
9+
from ReleaseAutomation.release_config import ReleaseConfig
10+
from Utils.git_utils import create_branch_execute_commands_and_push
11+
from Utils.verifyReleaseConditions import verifyReleaseConditions
12+
from Utils.commitChangelogAndPackageVersionUpdates import commitChangelogAndPackageVersionUpdates
13+
from Utils.triggerYamatoJobsForReleasePreparation import trigger_release_preparation_jobs
14+
15+
def PrepareNetcodePackageForRelease():
16+
try:
17+
config = ReleaseConfig()
18+
19+
print("\nStep 1: Verifying release conditions...")
20+
verifyReleaseConditions(config)
21+
22+
print("\nStep 2: Creating release branch...")
23+
create_branch_execute_commands_and_push(config)
24+
25+
print("\nStep 3: Triggering Yamato validation jobs...")
26+
trigger_release_preparation_jobs(config)
27+
28+
print("\nStep 4: Committing changelog and version updates...")
29+
commitChangelogAndPackageVersionUpdates(config)
30+
31+
except Exception as e:
32+
print("\n--- ERROR: Netcode release process failed ---", file=sys.stderr)
33+
print(f"Reason: {e}", file=sys.stderr)
34+
sys.exit(1)
35+
36+
if __name__ == "__main__":
37+
PrepareNetcodePackageForRelease()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Creates a direct commit to specified branch (in the config) to update the changelog, package version and validation exceptions for a new release using the GitHub API.
3+
Quite often the changelog gets distorted between the time we branch for the release and the time we will branch back.
4+
To mitigate this we want to create changelog update PR straight away and merge it fast while proceeding with the release.
5+
6+
This will also allow us to skip any PRs after releasing, unless, we made some changes on this branch.
7+
8+
"""
9+
#!/usr/bin/env python3
10+
import os
11+
import sys
12+
from github import GithubException
13+
from git import Actor
14+
15+
PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))
16+
sys.path.insert(0, PARENT_DIR)
17+
18+
from ReleaseAutomation.release_config import ReleaseConfig
19+
from Utils.general_utils import get_package_version_from_manifest, update_changelog, update_package_version_by_patch, update_validation_exceptions
20+
from Utils.git_utils import get_local_repo
21+
22+
def commitChangelogAndPackageVersionUpdates(config: ReleaseConfig):
23+
"""
24+
The function updates the changelog and package version of the package in anticipation of a new release.
25+
This means that it will
26+
1) Clean and update the changelog for the current package version.
27+
2) Add new Unreleased section template at the top.
28+
3) Update the package version in the package.json file by incrementing the patch version to signify the current state of the package.
29+
4) Update package version in the validation exceptions to match the new package version.
30+
31+
This assumes that at the same time you already branched off for the release. Otherwise it may be confusing
32+
"""
33+
34+
try:
35+
if not config.github_manager.is_branch_present(config.default_repo_branch):
36+
print(f"Branch '{config.default_repo_branch}' does not exist. Exiting.")
37+
sys.exit(1)
38+
39+
repo = get_local_repo()
40+
repo.git.fetch('--prune', '--prune-tags')
41+
repo.git.checkout(config.default_repo_branch)
42+
repo.git.pull("origin", config.default_repo_branch)
43+
44+
# Update the changelog file with adding new [Unreleased] section
45+
update_changelog(config.changelog_path, config.package_version, add_unreleased_template=True)
46+
# Update the package version by patch to represent the "current package state" after release
47+
updated_package_version = update_package_version_by_patch(config.manifest_path)
48+
update_validation_exceptions(config.validation_exceptions_path, updated_package_version)
49+
50+
repo.git.add(config.changelog_path)
51+
repo.git.add(config.manifest_path)
52+
repo.git.add(config.validation_exceptions_path)
53+
54+
author = Actor(config.commiter_name, config.commiter_email)
55+
committer = Actor(config.commiter_name, config.commiter_email)
56+
57+
repo.index.commit(config.commit_message, author=author, committer=committer, skip_hooks=True)
58+
repo.git.push("origin", config.default_repo_branch)
59+
60+
print(f"Successfully updated and pushed the changelog on branch: {config.default_repo_branch}")
61+
62+
except GithubException as e:
63+
print(f"An error occurred with the GitHub API: {e.status}", file=sys.stderr)
64+
print(f"Error details: {e.data}", file=sys.stderr)
65+
sys.exit(1)
66+
except Exception as e:
67+
print(f"An unexpected error occurred: {e}", file=sys.stderr)
68+
sys.exit(1)

0 commit comments

Comments
 (0)