fix: copy patches folder to web docker image before pnpm install (#14… #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Add versioning tag to commit | |
| concurrency: commit | |
| permissions: | |
| contents: write | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - web/** | |
| - pingpong/** | |
| - uv.lock | |
| - pyproject.toml | |
| jobs: | |
| tag: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 11 # Fetch the last 10 commits and the current one | |
| fetch-tags: true | |
| - name: Get changed files | |
| id: changed-files | |
| uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Add versioning tag to commit | |
| env: | |
| CHANGED_FILES_STRING: ${{ steps.changed-files.outputs.all_changed_files }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -e | |
| python << 'EOF' | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import json | |
| LEGACY_TAG_PATTERN = re.compile(r'^(\d+)-srv(\d+)-web(\d+)$') | |
| NEW_TAG_PATTERN = re.compile(r'^v(\d+)\+srv(\d+)\.web(\d+)$') | |
| def is_versioning_tag(tag): | |
| return bool(LEGACY_TAG_PATTERN.match(tag) or NEW_TAG_PATTERN.match(tag)) | |
| def get_last_tag(n=1): | |
| if n > 10: | |
| raise ValueError('No versioning tags found on the last 10 commits') | |
| try: | |
| # Get all tags pointing to the HEAD~n commit | |
| tags = [] | |
| try: | |
| # Get the shorthand Git SHA of HEAD~{n} | |
| sha_output = subprocess.check_output( | |
| ['git', 'rev-parse', '--short', f'HEAD~{n}'], | |
| stderr=subprocess.DEVNULL | |
| ) | |
| sha = sha_output.decode().strip() | |
| print(f"Shorthand Git SHA of HEAD~{n}: {sha}") | |
| output = subprocess.check_output( | |
| ['git', 'tag', '--points-at', f'HEAD~{n}'], | |
| stderr=subprocess.DEVNULL | |
| ) | |
| decoded_output = output.decode().strip() | |
| print("Raw Output:", decoded_output) # Print the raw decoded output | |
| tags = decoded_output.split('\n') # Split the output into a list of tags | |
| print("Tags List:", tags) # Print the list of tags | |
| except subprocess.CalledProcessError as e: | |
| print(f"Error occurred: {e}") | |
| # Filter tags that match our versioning format | |
| matching_tags = [tag for tag in tags if is_versioning_tag(tag)] | |
| print(f'Matching versioning tags on commit HEAD~{n}: {matching_tags}') | |
| if not matching_tags: | |
| print(f'No versioning tags found on commit HEAD~{n}') | |
| return get_last_tag(n + 1) | |
| print(f'Found versioning tags on commit HEAD~{n}: {matching_tags}') | |
| # If multiple matching tags, return the one with the highest build number | |
| return max(matching_tags, key=lambda t: parse_tag(t)['build']) | |
| except subprocess.CalledProcessError as e: | |
| raise ValueError("Failed to retrieve versioning tags from the previous commits", e) | |
| def parse_tag(tag): | |
| print(f'Parsing tag: {tag}') | |
| match = LEGACY_TAG_PATTERN.match(tag) | |
| if not match: | |
| match = NEW_TAG_PATTERN.match(tag) | |
| if not match: | |
| raise ValueError(f"Invalid versioning tag format: {tag}. Expected NNNN-srvNNN-webNNN or vNNNN+srvNNN.webNNN") | |
| try: | |
| return { | |
| 'build': int(match.group(1)), | |
| 'srv': int(match.group(2)), | |
| 'web': int(match.group(3)), | |
| } | |
| except Exception as e: | |
| raise ValueError(f"Failed to parse versioning tag: {tag}", e) | |
| def create_new_tag(last_tag, web_update, server_update): | |
| if last_tag: | |
| last_tag_info = parse_tag(last_tag) | |
| new_build = last_tag_info['build'] + 1 | |
| new_srv = last_tag_info['srv'] + (1 if server_update else 0) | |
| new_web = last_tag_info['web'] + (1 if web_update else 0) | |
| else: | |
| new_build = 1 | |
| new_srv = 1 | |
| new_web = 1 | |
| return f"v{new_build}+srv{new_srv}.web{new_web}", new_srv, new_web | |
| def get_compare_range(): | |
| default_range = 'HEAD~1..HEAD' | |
| event_path = os.environ.get('GITHUB_EVENT_PATH') | |
| if not event_path: | |
| return default_range | |
| try: | |
| with open(event_path, encoding='utf-8') as f: | |
| event = json.load(f) | |
| except Exception as e: | |
| print(f'Failed to parse GitHub event payload ({event_path}): {e}. Falling back to {default_range}') | |
| return default_range | |
| before = event.get('before') | |
| after = event.get('after') or os.environ.get('GITHUB_SHA') | |
| if not before or not after or before == '0' * 40: | |
| return default_range | |
| return f'{before}..{after}' | |
| def get_changed_diff_lines(file_path, compare_range): | |
| try: | |
| patch = subprocess.check_output( | |
| ['git', 'diff', '--unified=0', compare_range, '--', file_path], | |
| stderr=subprocess.DEVNULL, | |
| ).decode() | |
| except subprocess.CalledProcessError: | |
| return [] | |
| changed_lines = [] | |
| for line in patch.splitlines(): | |
| if line.startswith(('diff --git ', 'index ', '@@', '--- ', '+++ ')): | |
| continue | |
| if line.startswith(('+', '-')): | |
| changed_lines.append(line[1:].strip()) | |
| return changed_lines | |
| def is_version_only_change(file_path, compare_range): | |
| patterns = { | |
| 'pyproject.toml': re.compile(r'^version\s*=\s*".*"$'), | |
| 'uv.lock': re.compile(r'^version\s*=\s*".*"$'), | |
| 'web/study/package.json': re.compile(r'^"version"\s*:\s*".*"\s*,?$'), | |
| } | |
| pattern = patterns.get(file_path) | |
| if not pattern: | |
| return False | |
| def uv_lock_has_pingpong_context(): | |
| package_name_pattern = re.compile(r'^name\s*=\s*"pingpong"\s*$') | |
| try: | |
| patch = subprocess.check_output( | |
| ['git', 'diff', '--unified=1', compare_range, '--', 'uv.lock'], | |
| stderr=subprocess.DEVNULL, | |
| ).decode() | |
| except subprocess.CalledProcessError: | |
| return False | |
| hunk_has_pingpong_name = False | |
| hunk_has_version_change = False | |
| for line in patch.splitlines(): | |
| if line.startswith(('diff --git ', 'index ', '--- ', '+++ ')): | |
| continue | |
| if line.startswith('@@'): | |
| if hunk_has_pingpong_name and hunk_has_version_change: | |
| return True | |
| hunk_has_pingpong_name = False | |
| hunk_has_version_change = False | |
| continue | |
| if not line or line.startswith('\\'): | |
| continue | |
| if line[0] not in {' ', '+', '-'}: | |
| continue | |
| content = line[1:].strip() | |
| if package_name_pattern.match(content): | |
| hunk_has_pingpong_name = True | |
| if line[0] in {'+', '-'} and pattern.match(content): | |
| hunk_has_version_change = True | |
| return hunk_has_pingpong_name and hunk_has_version_change | |
| changed_lines = get_changed_diff_lines(file_path, compare_range) | |
| if not changed_lines: | |
| return False | |
| if file_path == 'uv.lock': | |
| only_version = ( | |
| len(changed_lines) == 2 | |
| and all(pattern.match(line) for line in changed_lines) | |
| and uv_lock_has_pingpong_context() | |
| ) | |
| else: | |
| only_version = all(pattern.match(line) for line in changed_lines) | |
| print(f'Checking version-only change for {file_path} in {compare_range}: {only_version}') | |
| return only_version | |
| def detect_component_updates(changed_files, compare_range): | |
| web_files = [file for file in changed_files if file.startswith('web/')] | |
| server_files = [ | |
| file for file in changed_files | |
| if file.startswith(('pingpong/', 'saml/', 'alembic/')) or file in ['uv.lock', 'pyproject.toml'] | |
| ] | |
| web_update = any(file != 'web/study/package.json' for file in web_files) | |
| if not web_update and 'web/study/package.json' in web_files: | |
| web_update = not is_version_only_change('web/study/package.json', compare_range) | |
| server_update = any(file not in {'pyproject.toml', 'uv.lock'} for file in server_files) | |
| if not server_update and 'pyproject.toml' in server_files: | |
| server_update = not is_version_only_change('pyproject.toml', compare_range) | |
| if not server_update and 'uv.lock' in server_files: | |
| server_update = not is_version_only_change('uv.lock', compare_range) | |
| return web_update, server_update | |
| try: | |
| subprocess.run(['git', 'fetch', 'origin', '--tags', '--depth=11', 'HEAD'], check=True) | |
| changed_files = [file for file in os.environ['CHANGED_FILES_STRING'].split() if file] | |
| compare_range = get_compare_range() | |
| web_update, server_update = detect_component_updates(changed_files, compare_range) | |
| print(f'Changed files: {changed_files}') | |
| print(f'Component updates -> server: {server_update}, web: {web_update}') | |
| last_tag = get_last_tag() | |
| new_tag, new_srv, new_web = create_new_tag(last_tag, web_update, server_update) | |
| subprocess.run(['git', 'tag', new_tag], check=True) | |
| subprocess.run(['git', 'push', 'origin', new_tag], check=True) | |
| print(f"Successfully pushed new versioning tag: {new_tag}") | |
| except Exception as e: | |
| print(f"Error: {str(e)}", file=sys.stderr) | |
| sys.exit(1) | |
| EOF | |
| shell: bash |