This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
@@ -0,0 +1,47 @@
name: 'Add version to release channel'
description: |
Appends the specified version to the end of the specified release channel if
it wasn't already part of that channel.
inputs:
release_type:
description: 'Release type (firmware, gui, docs, internal-docs).'
required: true
channel:
description: 'The channel on which to register this version'
required: true
version:
description: 'The version name (commit hash or semantic version name)'
required: true
odrive_api_key:
description: 'Key to our release index server'
required: true
runs:
using: "composite"
steps:
- name: Install Prerequisites
shell: bash
run: |
pip install aiohttp cryptography
pip install odrive --pre
- name: Register on release server
shell: python
run: |
import asyncio
import sys
import aiohttp
sys.path.insert(0, '${{ github.workspace }}/.github/actions/upload-release')
from odrive.api_client import ApiClient
from private_release_api import PrivateReleaseApi
async def main():
async with aiohttp.ClientSession() as session:
api_client = ApiClient(session, key='${{ inputs.odrive_api_key }}')
release_api = PrivateReleaseApi(api_client)
await release_api.append_to_channel('${{ inputs.release_type }}', "${{ inputs.channel }}", "${{ inputs.version }}")
await release_api.refresh_routes('${{ inputs.release_type }}')
asyncio.run(main())
@@ -0,0 +1,135 @@
name: 'Upload (Pre)release'
description: |
Pushes the specified local directory to our release file server and registers
the new content on our release index server.
The version will not yet be associated with a channel.
inputs:
release_type:
description: 'Release type (firmware, gui, docs, internal-docs).' # TODO: Currently only firmware and docs is supported.
required: true
version:
description: 'The version name (commit hash or semantic version name)'
required: true
src_dir:
description: 'The source directory on the local system.'
required: true
do_access_key:
description: 'DigitalOcean access key'
required: true
do_secret_key:
description: 'DigitalOcean secret key'
required: true
odrive_api_key:
description: 'Key to our release index server'
required: true
board:
description: 'ODrive board version triplet ("PRODUCT_LINE.VERSION.VARIANT") (for firmware releases only).'
required: false
app:
description: 'Firmware app name (default, bootloader) (for firmware releases only).'
required: false
variant:
description: 'Variant (public or internal).'
required: false
runs:
using: "composite"
steps:
- name: Install Prerequisites
shell: bash
run: pip install aiohttp cryptography
- name: Install odrivetool
shell: bash
run: pip install odrive --pre
- name: Load Content Key
id: load-content-key
shell: python
run: |
import asyncio
import sys
import aiohttp
sys.path.insert(0, '${{ github.workspace }}/.github/actions/upload-release')
from odrive.api_client import ApiClient
from private_release_api import PrivateReleaseApi # well not so private anymore
from odrive.crypto import safe_b64encode
content_key = PrivateReleaseApi.get_content_key('${{ inputs.src_dir }}', "${{ github.sha }}")
async def main():
async with aiohttp.ClientSession() as session:
api_client = ApiClient(session, key='${{ inputs.odrive_api_key }}')
release_api = PrivateReleaseApi(api_client)
manifest = await release_api.get_manifest('${{ inputs.release_type }}', content_key)
needs_upload = manifest is None
print("::set-output name=content-key::" + safe_b64encode(content_key))
print("::set-output name=needs-upload::" + ('true' if needs_upload else 'false'))
asyncio.run(main())
# This is for debugging only
- name: Dump context
shell: bash
env:
CONTEXT: ${{ toJson(steps) }}
run: |
echo "$CONTEXT"
- name: Upload to DigitalOcean
if: steps.load-content-key.outputs.needs-upload == 'true'
uses: BetaHuhn/do-spaces-action@v2
with:
access_key: ${{ inputs.do_access_key }}
secret_key: ${{ inputs.do_secret_key }}
space_name: odrive-cdn
space_region: nyc3
source: ${{ inputs.src_dir }}
out_dir: releases/${{ inputs.release_type }}/${{ steps.load-content-key.outputs.content-key }}
- name: Register on release server
shell: python
run: |
import asyncio
import re
import sys
import aiohttp
sys.path.insert(0, '${{ github.workspace }}/.github/actions/upload-release')
from odrive.api_client import ApiClient
from private_release_api import PrivateReleaseApi
from odrive.crypto import safe_b64decode
version="${{ inputs.version }}"
print("Version: ", version)
print("Content key: ", '${{ steps.load-content-key.outputs.content-key }}')
async def main():
async with aiohttp.ClientSession() as session:
api_client = ApiClient(session, key='${{ inputs.odrive_api_key }}')
release_api = PrivateReleaseApi(api_client)
qualifiers = {}
if '${{ inputs.board }}':
qualifiers['board'] = tuple(int(i) for i in re.match(r'^([0-9]+)\.([0-9]+).([0-9]+)$', '${{ inputs.board }}').groups())
if '${{ inputs.app }}':
qualifiers['app'] = '${{ inputs.app }}'
if '${{ inputs.variant }}':
qualifiers['variant'] = '${{ inputs.variant }}'
content_key = safe_b64decode('${{ steps.load-content-key.outputs.content-key }}')
# Content can only be registered once, afterwards the indicated_commit cannot be changed
if "${{ steps.load-content-key.outputs.needs-upload}}" == 'true':
await release_api.register_content('${{ inputs.release_type }}', content_key, '${{ github.sha }}')
# Can be called multiple times with same or different parameters. No effect if called twice with the same parameters.
await release_api.register_version('${{ inputs.release_type }}', version, content_key, **qualifiers)
asyncio.run(main())
@@ -0,0 +1,74 @@
import hashlib
import os
from odrive.api_client import ApiClient
from odrive.crypto import b64encode
class PrivateReleaseApi():
BASE_URL = '/releases'
@staticmethod
def get_content_key(path: str, commit_hash: str):
commit_hash_bytes = bytes.fromhex(commit_hash)
def _get_file_names(path: str, prefix: list):
with os.scandir(os.path.join(path, *prefix)) as it:
for entry in it:
if entry.is_file():
yield os.path.join(*prefix, entry.name)
else:
yield from _get_file_names(path, prefix + [entry.name])
filenames = sorted(_get_file_names(path, []))
dir_hasher = hashlib.sha256()
for filename in filenames:
with open(os.path.join(path, filename), 'rb') as fp:
content = fp.read()
# Calculate commit-invariant hash of the file content
# This means that if a new compile differs only by embedded commit hash,
# it is considered equal.
patched_content = content.replace(commit_hash_bytes, bytes(len(commit_hash_bytes)))
file_hasher = hashlib.sha256()
file_hasher.update(patched_content)
dir_hasher.update(filename.encode('utf-8'))
dir_hasher.update(file_hasher.digest())
return dir_hasher.digest()
def __init__(self, api_client: 'ApiClient'):
self._api_client = api_client
async def get_manifest(self, release_type: str, content_key: bytes):
outputs = await self._api_client.call('GET', PrivateReleaseApi.BASE_URL + '/' + release_type + '/manifest', inputs={
'content_key': b64encode(content_key),
})
return outputs
async def register_content(self, release_type: str, content_key: bytes, indicated_commit: str):
outputs = await self._api_client.call('PUT', PrivateReleaseApi.BASE_URL + '/' + release_type + '/content', inputs={
'content_key': b64encode(content_key),
'indicated_commit': indicated_commit,
})
return outputs['created']
async def register_version(self, release_type: str, version: str, content_key: bytes, **qualifiers):
outputs = await self._api_client.call('PUT', PrivateReleaseApi.BASE_URL + '/' + release_type + '/version', inputs={
'version': version,
'content_key': b64encode(content_key),
**qualifiers
})
return outputs['created']
async def append_to_channel(self, release_type: str, channel: str, version: str):
outputs = await self._api_client.call('PUT', PrivateReleaseApi.BASE_URL + '/' + release_type + '/channel', inputs={
'channel': channel,
'version': version,
})
return outputs['published']
async def refresh_routes(self, release_type: str):
await self._api_client.call('PUT', PrivateReleaseApi.BASE_URL + '/' + release_type + '/refresh-routes')