feat: Allow OTA update to beta releases
This commit is contained in:
@@ -129,7 +129,7 @@ def categorize_commit(subject: str) -> tuple[str, str]:
|
||||
return "other", subject
|
||||
|
||||
commit_type = match.group("type").lower()
|
||||
description = match.group("description")
|
||||
description = f'{commit_type}: {match.group("description")}'
|
||||
if match.group("breaking"):
|
||||
description = f"BREAKING: {description}"
|
||||
|
||||
|
||||
+115
-12
@@ -1,13 +1,18 @@
|
||||
"""
|
||||
PlatformIO pre-build script: inject git branch into CROSSPOINT_VERSION for
|
||||
the default (dev) environment.
|
||||
PlatformIO pre-build script: inject Git metadata into preprocessor defines.
|
||||
|
||||
Results in a version string like: 1.1.0-dev+feat-koysnc-xpath
|
||||
Release environments are unaffected; they set CROSSPOINT_VERSION in the ini.
|
||||
- The default (dev) environment gets CROSSPOINT_VERSION with a branch suffix like:
|
||||
1.1.0-dev+feat-koysnc-xpath
|
||||
- The gh_release_rc environment gets CROSSPOINT_VERSION with an RC tag from CI metadata
|
||||
when available, or a local fallback like: 1.1.0-rc+local
|
||||
- All environments get CROSSPOINT_GIT_REPOSITORY, resolved from CI metadata
|
||||
or local Git remotes. A safe fallback is defined in src/network/OtaUpdater.h in case
|
||||
resolution here fails.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -16,18 +21,26 @@ def warn(msg):
|
||||
print(f'WARNING [git_branch.py]: {msg}', file=sys.stderr)
|
||||
|
||||
|
||||
def get_git_branch(project_dir):
|
||||
def run_git_command(*args: str, project_dir: str) -> str:
|
||||
try:
|
||||
branch = subprocess.check_output(
|
||||
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
return subprocess.check_output(
|
||||
['git', *args],
|
||||
text=True, stderr=subprocess.PIPE, cwd=project_dir
|
||||
).strip()
|
||||
except FileNotFoundError:
|
||||
warn('git not found on PATH')
|
||||
raise
|
||||
except subprocess.CalledProcessError as e:
|
||||
warn(f'git command "git {" ".join(args)}" failed (exit {e.returncode}): {e.stderr.strip()}')
|
||||
raise
|
||||
|
||||
|
||||
def get_git_branch(project_dir):
|
||||
try:
|
||||
branch = run_git_command('rev-parse', '--abbrev-ref', 'HEAD', project_dir=project_dir)
|
||||
# Detached HEAD — show the short SHA instead
|
||||
if branch == 'HEAD':
|
||||
branch = subprocess.check_output(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
text=True, stderr=subprocess.PIPE, cwd=project_dir
|
||||
).strip()
|
||||
branch = run_git_command('rev-parse', '--short', 'HEAD', project_dir=project_dir)
|
||||
# Strip characters that would break a C string literal
|
||||
return ''.join(c for c in branch if c not in '"\\')
|
||||
except FileNotFoundError:
|
||||
@@ -41,6 +54,74 @@ def get_git_branch(project_dir):
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def get_all_remotes(project_dir: str) -> list[str]:
|
||||
try:
|
||||
remotes = run_git_command('remote', project_dir=project_dir)
|
||||
return remotes.splitlines()
|
||||
except FileNotFoundError:
|
||||
warn('git not found on PATH; cannot read git remotes')
|
||||
return []
|
||||
except subprocess.CalledProcessError as e:
|
||||
warn(f'git command failed (exit {e.returncode}): {e.stderr.strip()}; cannot read git remotes')
|
||||
return []
|
||||
except Exception as e:
|
||||
warn(f'Unexpected error reading git remotes: {e}; cannot read git remotes')
|
||||
return []
|
||||
|
||||
|
||||
def parse_git_repository(remote_url: str) -> str | None:
|
||||
# Match strings like:
|
||||
# - https://github.com/owner/repo.git
|
||||
# - https://code.example.com/owner/repo
|
||||
# - git+ssh://vcs.example.org:owner/repo.git
|
||||
# - codeberg.org:owner/repo.git
|
||||
match = re.search(r'^(?:.+)?(?:://)?[^:/]+[:/]([^/]+)/([^/]+?)(?:\.git)?$', remote_url.strip())
|
||||
if not match:
|
||||
return None
|
||||
owner = match.group(1)
|
||||
repo = match.group(2)
|
||||
if not owner or not repo:
|
||||
return None
|
||||
return f'{owner}/{repo}'
|
||||
|
||||
|
||||
def get_git_remote_url(project_dir, remote_name):
|
||||
try:
|
||||
return run_git_command('remote', 'get-url', remote_name, project_dir=project_dir)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
return None
|
||||
|
||||
|
||||
def get_git_repository(project_dir):
|
||||
# Other CI systems (Forgejo, Codeberg) may set GITHUB_REPOSITORY for compatibility
|
||||
# with GHA. We could also check for other CI-specific env vars to expand support
|
||||
# later, such as:
|
||||
# - FORGEJO_REPOSITORY
|
||||
# - CI_REPOSITORY_URL (this one is a full URL, likely will work with parse_git_repository)
|
||||
# - BITBUCKET_REPO_FULL_NAME
|
||||
ci_repository = os.environ.get('GITHUB_REPOSITORY')
|
||||
if ci_repository:
|
||||
return ci_repository
|
||||
|
||||
remotes = get_all_remotes(project_dir)
|
||||
# 'origin' is most likely to be the primary remote, so always check for it first
|
||||
if 'origin' in remotes:
|
||||
remotes = ['origin'] + [r for r in remotes if r != 'origin']
|
||||
for remote_name in remotes:
|
||||
remote_url = get_git_remote_url(project_dir, remote_name)
|
||||
if not remote_url:
|
||||
continue
|
||||
repository = parse_git_repository(remote_url)
|
||||
if repository:
|
||||
return repository
|
||||
|
||||
warn(
|
||||
'Could not resolve a repository from CI metadata or git remotes; '
|
||||
'falling back to compile-time default.'
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_base_version(project_dir):
|
||||
ini_path = os.path.join(project_dir, 'platformio.ini')
|
||||
if not os.path.isfile(ini_path):
|
||||
@@ -54,13 +135,35 @@ def get_base_version(project_dir):
|
||||
return config.get('crosspoint', 'version')
|
||||
|
||||
|
||||
def normalize_semver_patch(version: str) -> str:
|
||||
version = version.strip()
|
||||
if version.count('.') == 1:
|
||||
return f'{version}.0'
|
||||
return version
|
||||
|
||||
|
||||
def inject_version(env):
|
||||
project_dir = env['PROJECT_DIR']
|
||||
git_repository = get_git_repository(project_dir)
|
||||
if git_repository:
|
||||
env.Append(CPPDEFINES=[('CROSSPOINT_GIT_REPOSITORY', f'\\"{git_repository}\\"')])
|
||||
print(f'CrossPoint Git repository: {git_repository}')
|
||||
|
||||
# Release candidate builds use the CI-provided RC tag when available, but
|
||||
# keep local gh_release_rc builds identifiable instead of leaving the
|
||||
# firmware version empty.
|
||||
if env['PIOENV'] == 'gh_release_rc':
|
||||
base_version = normalize_semver_patch(get_base_version(project_dir))
|
||||
version_string = os.environ.get('CROSSPOINT_RC_VERSION') or f'{base_version}-rc.0+local'
|
||||
env.Append(CPPDEFINES=[('CROSSPOINT_VERSION', f'\\"{version_string}\\"')])
|
||||
print(f'CrossPoint build version: {version_string}')
|
||||
return
|
||||
|
||||
# Only applies to the dev (default) environment; release envs set the
|
||||
# version via build_flags in platformio.ini and are unaffected.
|
||||
if env['PIOENV'] != 'default':
|
||||
return
|
||||
|
||||
project_dir = env['PROJECT_DIR']
|
||||
base_version = get_base_version(project_dir)
|
||||
branch = get_git_branch(project_dir)
|
||||
version_string = f'{base_version}-dev+{branch}'
|
||||
|
||||
Reference in New Issue
Block a user