fix(relay): restore commandline bundle CI #16
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: Relay CI | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| paths: | |
| - "relay/**" | |
| pull_request: | |
| paths: | |
| - "relay/**" | |
| permissions: | |
| contents: write | |
| env: | |
| BUNDLED_NODE_VERSION: "24.14.1" | |
| CHROME_DEVTOOLS_MCP_VERSION: "0.20.0" | |
| BUNDLED_PYTHON_VERSION: "3.13.10" | |
| BUNDLED_PYTHON_STANDALONE_RELEASE: "20251202" | |
| BUNDLED_WINDOWS_GIT_VERSION: "2.49.0.windows.1" | |
| BUNDLED_FFMPEG_RELEASE_TAG: "n7.1-2" | |
| COMMANDLINE_PACKAGE_PROFILE: "default-data-v5" | |
| PIP_INDEX_URL: "https://pypi.org/simple" | |
| ENABLE_COS_UPLOAD: ${{ vars.ENABLE_COS_UPLOAD || '' }} | |
| jobs: | |
| prepare-cli-matrix: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.select.outputs.matrix }} | |
| selected_platforms: ${{ steps.select.outputs.selected_platforms }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Select CLI target platforms | |
| id: select | |
| env: | |
| MATRIX_JSON: >- | |
| [ | |
| {"goos":"linux","goarch":"amd64","suffix":"linux-amd64"}, | |
| {"goos":"linux","goarch":"arm64","suffix":"linux-arm64"}, | |
| {"goos":"darwin","goarch":"amd64","suffix":"darwin-amd64"}, | |
| {"goos":"darwin","goarch":"arm64","suffix":"darwin-arm64"}, | |
| {"goos":"windows","goarch":"amd64","suffix":"windows-amd64","ext":".exe"} | |
| ] | |
| REQUESTED_PLATFORMS: ${{ vars.RELAY_CLI_BUILD_PLATFORMS || 'all' }} | |
| run: python3 .github/scripts/filter-platform-matrix.py | |
| prepare-gui-matrix: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.select.outputs.matrix }} | |
| selected_platforms: ${{ steps.select.outputs.selected_platforms }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Select GUI target platforms | |
| id: select | |
| env: | |
| MATRIX_JSON: >- | |
| [ | |
| {"runner":"ubuntu-latest","goos":"linux","goarch":"amd64","suffix":"linux-amd64","wails_tags":"desktop_cua,webkit2_41","cgo_ldflags_allow":"","macos_deployment_target":"","wails_extra_args":"","setup":"sudo apt-get update -qq\nsudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev desktop-file-utils patchelf squashfs-tools"}, | |
| {"runner":"macos-15","goos":"darwin","goarch":"arm64","suffix":"darwin-arm64","wails_tags":"desktop_cua","cgo_ldflags_allow":"^(-weak_framework|ScreenCaptureKit)$","macos_deployment_target":"14.4","wails_extra_args":"","setup":""}, | |
| {"runner":"macos-15-intel","goos":"darwin","goarch":"amd64","suffix":"darwin-amd64","wails_tags":"desktop_cua","cgo_ldflags_allow":"^(-weak_framework|ScreenCaptureKit)$","macos_deployment_target":"14.4","wails_extra_args":"","setup":""}, | |
| {"runner":"windows-latest","goos":"windows","goarch":"amd64","suffix":"windows-amd64","ext":".exe","wails_tags":"desktop_cua","cgo_ldflags_allow":"","macos_deployment_target":"","wails_extra_args":"-nsis","setup":"pwsh -File .github/scripts/setup-windows-gui-build.ps1"} | |
| ] | |
| REQUESTED_PLATFORMS: ${{ vars.RELAY_GUI_BUILD_PLATFORMS || 'all' }} | |
| run: python3 .github/scripts/filter-platform-matrix.py | |
| prepare-bundle-matrix: | |
| needs: | |
| - prepare-cli-matrix | |
| - prepare-gui-matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.union.outputs.matrix }} | |
| selected_platforms: ${{ steps.union.outputs.selected_platforms }} | |
| steps: | |
| - name: Combine bundle target platforms | |
| id: union | |
| env: | |
| CLI_MATRIX: ${{ needs.prepare-cli-matrix.outputs.matrix }} | |
| GUI_MATRIX: ${{ needs.prepare-gui-matrix.outputs.matrix }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| cli = json.loads(os.environ["CLI_MATRIX"]).get("include", []) | |
| gui = json.loads(os.environ["GUI_MATRIX"]).get("include", []) | |
| selected = [] | |
| seen = set() | |
| for item in cli + gui: | |
| suffix = item["suffix"] | |
| if suffix in seen: | |
| continue | |
| seen.add(suffix) | |
| selected.append( | |
| { | |
| "goos": item["goos"], | |
| "goarch": item["goarch"], | |
| "suffix": suffix, | |
| } | |
| ) | |
| outputs = { | |
| "matrix": json.dumps({"include": selected}, separators=(",", ":")), | |
| "selected_platforms": ",".join(item["suffix"] for item in selected), | |
| } | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle: | |
| for key, value in outputs.items(): | |
| handle.write(f"{key}={value}\n") | |
| PY | |
| prepare-bundles: | |
| needs: | |
| - prepare-bundle-matrix | |
| runs-on: ubuntu-latest | |
| if: needs.prepare-bundle-matrix.outputs.selected_platforms != '' | |
| strategy: | |
| matrix: ${{ fromJSON(needs.prepare-bundle-matrix.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Restore shared runtime bundle cache | |
| id: runtime-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| relay/internal/nodebundle/assets | |
| relay/internal/chromemcpbundle/assets | |
| relay/internal/commandlinebundle/assets | |
| key: relay-runtime-bundle-${{ runner.os }}-${{ matrix.suffix }}-${{ env.BUNDLED_NODE_VERSION }}-${{ env.CHROME_DEVTOOLS_MCP_VERSION }}-${{ env.BUNDLED_PYTHON_VERSION }}-${{ env.BUNDLED_PYTHON_STANDALONE_RELEASE }}-${{ env.BUNDLED_WINDOWS_GIT_VERSION }}-${{ env.BUNDLED_FFMPEG_RELEASE_TAG }}-${{ env.COMMANDLINE_PACKAGE_PROFILE }}-${{ hashFiles('.gitmodules', 'relay/managed-command-providers.json', 'relay/scripts/prepare-node-bundle.mjs', 'relay/scripts/prepare-chrome-devtools-bundle.mjs', 'relay/scripts/prepare-commandline-bundle.mjs', 'relay/scripts/lib/subprojects.mjs', 'subprojects/cli-anything/**/agent-harness/setup.py', 'subprojects/cli-anything/**/agent-harness/cli_anything/**') }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.BUNDLED_NODE_VERSION }} | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.BUNDLED_PYTHON_VERSION }} | |
| - name: Install bundle prerequisites | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq p7zip-full | |
| - name: Show shared bundle platforms | |
| run: | | |
| echo "Preparing shared bundles for: ${{ needs.prepare-bundle-matrix.outputs.selected_platforms }}" | |
| - name: Prepare shared Node runtime | |
| if: steps.runtime-cache.outputs.cache-hit != 'true' | |
| run: node relay/scripts/prepare-node-bundle.mjs --target-platform=${{ matrix.goos }}-${{ matrix.goarch }} --node-version=${{ env.BUNDLED_NODE_VERSION }} | |
| - name: Prepare bundled Chrome DevTools runtime | |
| if: steps.runtime-cache.outputs.cache-hit != 'true' | |
| run: node relay/scripts/prepare-chrome-devtools-bundle.mjs --target-platform=${{ matrix.goos }}-${{ matrix.goarch }} --node-version=${{ env.BUNDLED_NODE_VERSION }} --package-version=${{ env.CHROME_DEVTOOLS_MCP_VERSION }} | |
| - name: Prepare bundled command line runtime | |
| if: steps.runtime-cache.outputs.cache-hit != 'true' | |
| run: node relay/scripts/prepare-commandline-bundle.mjs --target-platform=${{ matrix.goos }}-${{ matrix.goarch }} --node-version=${{ env.BUNDLED_NODE_VERSION }} --python-version=${{ env.BUNDLED_PYTHON_VERSION }} --python-standalone-release=${{ env.BUNDLED_PYTHON_STANDALONE_RELEASE }} --windows-git-version=${{ env.BUNDLED_WINDOWS_GIT_VERSION }} --ffmpeg-release-tag=${{ env.BUNDLED_FFMPEG_RELEASE_TAG }} --package-profile=${{ env.COMMANDLINE_PACKAGE_PROFILE }} | |
| - name: Archive shared runtime bundle | |
| run: | | |
| tar -czf relay-runtime-bundle-${{ matrix.suffix }}.tar.gz \ | |
| relay/internal/nodebundle/assets \ | |
| relay/internal/chromemcpbundle/assets \ | |
| relay/internal/commandlinebundle/assets | |
| - name: Upload shared runtime bundle artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: relay-runtime-bundle-${{ matrix.suffix }} | |
| path: relay-runtime-bundle-${{ matrix.suffix }}.tar.gz | |
| build-cli: | |
| needs: | |
| - prepare-cli-matrix | |
| - prepare-bundles | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: ${{ fromJSON(needs.prepare-cli-matrix.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.22" | |
| - name: Determine version | |
| id: version | |
| shell: bash | |
| run: | | |
| echo "version=dev-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT | |
| - name: Show selected platforms | |
| run: | | |
| echo "Selected CLI platforms: ${{ needs.prepare-cli-matrix.outputs.selected_platforms }}" | |
| - name: Download shared runtime bundle artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: relay-runtime-bundle-${{ matrix.suffix }} | |
| path: . | |
| - name: Extract shared runtime bundle | |
| shell: bash | |
| run: | | |
| tar -xzf relay-runtime-bundle-${{ matrix.suffix }}.tar.gz | |
| - name: Install Linux FUSE build dependencies | |
| if: matrix.goos == 'linux' && matrix.goarch == 'amd64' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq libfuse-dev libfuse2t64 libx11-dev libxtst-dev libxinerama-dev libxrandr-dev | |
| - name: Build | |
| working-directory: relay | |
| env: | |
| CGO_ENABLED: "0" | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| run: | | |
| go build -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \ | |
| -o ../synapse-relay-${{ matrix.suffix }}${{ matrix.ext }} ./cmd/synapse-relay | |
| - name: Run Relay VFS unit/build validation | |
| if: matrix.goos == 'linux' && matrix.goarch == 'amd64' | |
| working-directory: relay | |
| run: | | |
| bash scripts/validate-vfs-unit.sh | |
| - name: Build Linux mount helper | |
| if: matrix.goos == 'linux' && matrix.goarch == 'amd64' | |
| working-directory: relay | |
| env: | |
| CGO_ENABLED: "1" | |
| GOOS: linux | |
| GOARCH: amd64 | |
| run: | | |
| go build -tags 'relay_fuse,desktop_cua' -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \ | |
| -o ../synapse-relay-mount-linux-amd64 ./cmd/synapse-relay-mount | |
| - name: Upload artifacts to Tencent COS | |
| if: startsWith(github.ref, 'refs/tags/') || env.ENABLE_COS_UPLOAD == 'true' | |
| continue-on-error: true | |
| env: | |
| ARTIFACT_PATH: synapse-relay-${{ matrix.suffix }}${{ matrix.ext }} | |
| COS_BUCKET: ${{ secrets.COS_BUCKET }} | |
| COS_PREFIX: ${{ github.sha }} | |
| COS_REGION: ${{ secrets.COS_REGION }} | |
| COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }} | |
| COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }} | |
| shell: bash | |
| run: | | |
| if [ -z "$COS_SECRET_ID" ] || [ -z "$COS_SECRET_KEY" ] || [ -z "$COS_BUCKET" ] || [ -z "$COS_REGION" ]; then | |
| echo "COS secrets are not configured; skipping backup upload." | |
| exit 0 | |
| fi | |
| python3 - <<'PY' | |
| from datetime import UTC, datetime | |
| from email.utils import format_datetime | |
| from pathlib import Path | |
| import hashlib | |
| import hmac | |
| import mimetypes | |
| import os | |
| import sys | |
| import urllib.parse | |
| import urllib.request | |
| artifact_path = Path(os.environ["ARTIFACT_PATH"].strip()) | |
| if not artifact_path.is_file(): | |
| print(f"Artifact not found: {artifact_path}", file=sys.stderr) | |
| sys.exit(1) | |
| bucket = os.environ["COS_BUCKET"].strip() | |
| prefix = os.environ["COS_PREFIX"].strip().strip("/") | |
| region = os.environ["COS_REGION"].strip() | |
| secret_id = os.environ["COS_SECRET_ID"].strip() | |
| secret_key = os.environ["COS_SECRET_KEY"].strip() | |
| host = f"{bucket}.cos.{region}.myqcloud.com" | |
| def hmac_sha1(key: bytes, msg: str) -> str: | |
| return hmac.new(key, msg.encode("utf-8"), hashlib.sha1).hexdigest() | |
| def urlencode(value: str) -> str: | |
| return urllib.parse.quote(value, safe="-_.~") | |
| def make_auth(method: str, uri_path: str, date_value: str) -> str: | |
| start = int(datetime.now(UTC).timestamp()) - 60 | |
| end = start + 3600 | |
| key_time = f"{start};{end}" | |
| sign_key = hmac_sha1(secret_key.encode("utf-8"), key_time) | |
| headers = {"Host": host, "Date": date_value} | |
| header_items = sorted((urlencode(k.lower()), urlencode(v.strip())) for k, v in headers.items()) | |
| header_list = ";".join(k for k, _ in header_items) | |
| http_headers = "&".join(f"{k}={v}" for k, v in header_items) | |
| http_string = "\n".join([method.lower(), uri_path, "", http_headers, ""]) | |
| string_to_sign = "\n".join([ | |
| "sha1", | |
| key_time, | |
| hashlib.sha1(http_string.encode()).hexdigest(), | |
| "", | |
| ]) | |
| signature = hmac_sha1(sign_key.encode("utf-8"), string_to_sign) | |
| return "&".join([ | |
| "q-sign-algorithm=sha1", | |
| f"q-ak={secret_id}", | |
| f"q-sign-time={key_time}", | |
| f"q-key-time={key_time}", | |
| f"q-header-list={header_list}", | |
| "q-url-param-list=", | |
| f"q-signature={signature}", | |
| ]) | |
| object_key = f"{prefix}/{artifact_path.name}" if prefix else artifact_path.name | |
| uri_path = "/" + "/".join(urlencode(part) for part in object_key.split("/")) | |
| date_value = format_datetime(datetime.now(UTC), usegmt=True) | |
| content_type = mimetypes.guess_type(artifact_path.name)[0] or "application/octet-stream" | |
| request = urllib.request.Request( | |
| f"https://{host}{uri_path}", | |
| data=artifact_path.read_bytes(), | |
| method="PUT", | |
| headers={ | |
| "Authorization": make_auth("PUT", uri_path, date_value), | |
| "Content-Type": content_type, | |
| "Date": date_value, | |
| "Host": host, | |
| }, | |
| ) | |
| with urllib.request.urlopen(request, timeout=300) as response: | |
| print(f"Uploaded {object_key} -> {response.status}") | |
| PY | |
| - name: Upload artifact | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: synapse-relay-${{ matrix.suffix }} | |
| path: synapse-relay-${{ matrix.suffix }}${{ matrix.ext }} | |
| - name: Upload Linux mount helper artifact | |
| if: matrix.goos == 'linux' && matrix.goarch == 'amd64' | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: synapse-relay-mount-linux-amd64 | |
| path: synapse-relay-mount-linux-amd64 | |
| build-gui: | |
| needs: | |
| - prepare-gui-matrix | |
| - prepare-bundles | |
| strategy: | |
| matrix: ${{ fromJSON(needs.prepare-gui-matrix.outputs.matrix) }} | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.22" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.BUNDLED_NODE_VERSION }} | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.BUNDLED_PYTHON_VERSION }} | |
| - name: Platform setup | |
| if: matrix.setup != '' | |
| run: ${{ matrix.setup }} | |
| - name: Install Wails | |
| run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0 | |
| - name: Determine version | |
| id: version | |
| shell: bash | |
| run: | | |
| echo "version=dev-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT | |
| - name: Resolve Windows package variants | |
| id: windows_package_variants | |
| if: matrix.goos == 'windows' | |
| shell: bash | |
| env: | |
| REQUESTED_WINDOWS_VARIANTS: ${{ vars.RELAY_GUI_WINDOWS_PACKAGE_VARIANTS || 'all' }} | |
| run: | | |
| raw="$(printf '%s' "${REQUESTED_WINDOWS_VARIANTS:-all}" | tr '[:upper:]' '[:lower:]' | xargs)" | |
| if [ -z "$raw" ]; then | |
| raw="all" | |
| fi | |
| case "$raw" in | |
| all) | |
| want_portable=true | |
| want_setup=true | |
| ;; | |
| portable) | |
| want_portable=true | |
| want_setup=false | |
| ;; | |
| setup) | |
| want_portable=false | |
| want_setup=true | |
| ;; | |
| *) | |
| echo "::error::Unsupported Windows package variant '$raw'. Use all, portable, or setup." | |
| exit 1 | |
| ;; | |
| esac | |
| if [ "$want_setup" = true ]; then | |
| wails_extra_args="-nsis" | |
| else | |
| wails_extra_args="" | |
| fi | |
| echo "value=$raw" >> "$GITHUB_OUTPUT" | |
| echo "want_portable=$want_portable" >> "$GITHUB_OUTPUT" | |
| echo "want_setup=$want_setup" >> "$GITHUB_OUTPUT" | |
| echo "wails_extra_args=$wails_extra_args" >> "$GITHUB_OUTPUT" | |
| echo "Resolved Windows package variants: $raw" | |
| - name: Show selected platforms | |
| run: | | |
| echo "Selected GUI platforms: ${{ needs.prepare-gui-matrix.outputs.selected_platforms }}" | |
| - name: Download shared runtime bundle artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: relay-runtime-bundle-${{ matrix.suffix }} | |
| path: . | |
| - name: Extract shared runtime bundle | |
| shell: bash | |
| run: | | |
| tar -xzf relay-runtime-bundle-${{ matrix.suffix }}.tar.gz | |
| - name: Prepare GUI build assets | |
| shell: bash | |
| run: | | |
| mode="portable" | |
| if [ "${{ matrix.goos }}" = "windows" ]; then | |
| mode="packaged" | |
| fi | |
| node relay/scripts/prepare-gui-build-assets.mjs --goos=${{ matrix.goos }} --runtime-mode="$mode" | |
| - name: Build GUI binary | |
| shell: bash | |
| working-directory: relay/cmd/synapse-relay-gui | |
| env: | |
| CGO_LDFLAGS_ALLOW: ${{ matrix.cgo_ldflags_allow }} | |
| run: | | |
| if [ -n "${{ matrix.macos_deployment_target }}" ]; then | |
| export MACOSX_DEPLOYMENT_TARGET="${{ matrix.macos_deployment_target }}" | |
| fi | |
| extra_args="${{ matrix.wails_extra_args }}" | |
| if [ "${{ matrix.goos }}" = "windows" ]; then | |
| extra_args="${{ steps.windows_package_variants.outputs.wails_extra_args }}" | |
| fi | |
| if [ -n "$extra_args" ]; then | |
| wails build "$extra_args" -tags "${{ matrix.wails_tags }},relay_packaged_runtime" -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" | |
| else | |
| wails build -tags "${{ matrix.wails_tags }},relay_packaged_runtime" -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" | |
| fi | |
| - name: Rename artifact | |
| shell: bash | |
| run: | | |
| if [ "${{ matrix.goos }}" = "windows" ]; then | |
| find relay/cmd/synapse-relay-gui/build/bin -maxdepth 1 -type f -print | |
| main_exe="$(find relay/cmd/synapse-relay-gui/build/bin -maxdepth 1 -type f \( -name 'synapse-relay-gui.exe' -o -name 'synapse-relay-gui-windows-*.exe' \) ! -name '*-setup.exe' | head -n 1)" | |
| test -n "$main_exe" | |
| if [ "${{ steps.windows_package_variants.outputs.want_portable }}" = "true" ]; then | |
| portable_root="synapse-relay-gui-${{ matrix.suffix }}-portable" | |
| mkdir -p "$portable_root" | |
| cp "$main_exe" "$portable_root/synapse-relay-gui.exe" | |
| runtime_stage="$(tr -d '\r\n' < relay/cmd/synapse-relay-gui/build/windows/runtime-path.txt)" | |
| runtime_stage_unix="$(cygpath -u "$runtime_stage")" | |
| mkdir -p "$portable_root/runtime" | |
| cp -R "$runtime_stage_unix"/. "$portable_root/runtime"/ | |
| portable_zip="synapse-relay-gui-${{ matrix.suffix }}-portable.zip" | |
| portable_root_win="$(cygpath -w "$portable_root")" | |
| portable_zip_win="$(cygpath -w "$portable_zip")" | |
| powershell.exe -NoLogo -NoProfile -Command "\$ErrorActionPreference='Stop'; Add-Type -AssemblyName 'System.IO.Compression.FileSystem'; if (Test-Path -LiteralPath '$portable_zip_win') { Remove-Item -LiteralPath '$portable_zip_win' -Force }; [System.IO.Compression.ZipFile]::CreateFromDirectory('$portable_root_win', '$portable_zip_win', [System.IO.Compression.CompressionLevel]::Optimal, \$false)" | |
| fi | |
| if [ "${{ steps.windows_package_variants.outputs.want_setup }}" = "true" ]; then | |
| setup_exe="$(find relay/cmd/synapse-relay-gui/build/bin -maxdepth 1 -type f \( -name 'synapse-relay-gui*-setup.exe' -o -name 'synapse-relay-gui*-installer.exe' -o -name 'Synapse Relay*-installer.exe' \) | head -n 1)" | |
| test -n "$setup_exe" | |
| mv "$setup_exe" synapse-relay-gui-${{ matrix.suffix }}-setup.exe | |
| fi | |
| elif [ "${{ matrix.goos }}" = "darwin" ]; then | |
| app_path="relay/cmd/synapse-relay-gui/build/bin/Synapse Relay.app" | |
| test -d "$app_path" | |
| bash relay/scripts/package-gui-macos.sh --app-path="$app_path" --suffix="${{ matrix.suffix }}" | |
| else | |
| bash relay/scripts/package-gui-linux.sh \ | |
| --binary-path="relay/cmd/synapse-relay-gui/build/bin/synapse-relay-gui" \ | |
| --suffix="${{ matrix.suffix }}" \ | |
| --version="${{ steps.version.outputs.version }}" | |
| fi | |
| - name: Upload artifacts to Tencent COS | |
| if: startsWith(github.ref, 'refs/tags/') || env.ENABLE_COS_UPLOAD == 'true' | |
| continue-on-error: true | |
| env: | |
| ARTIFACT_GLOB: synapse-relay-gui-${{ matrix.suffix }}* | |
| COS_BUCKET: ${{ secrets.COS_BUCKET }} | |
| COS_PREFIX: ${{ github.sha }} | |
| COS_REGION: ${{ secrets.COS_REGION }} | |
| COS_SECRET_ID: ${{ secrets.COS_SECRET_ID }} | |
| COS_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }} | |
| shell: bash | |
| run: | | |
| if [ -z "$COS_SECRET_ID" ] || [ -z "$COS_SECRET_KEY" ] || [ -z "$COS_BUCKET" ] || [ -z "$COS_REGION" ]; then | |
| echo "COS secrets are not configured; skipping backup upload." | |
| exit 0 | |
| fi | |
| python3 - <<'PY' | |
| from datetime import UTC, datetime | |
| from email.utils import format_datetime | |
| from pathlib import Path | |
| import glob | |
| import hashlib | |
| import hmac | |
| import mimetypes | |
| import os | |
| import sys | |
| import urllib.parse | |
| import urllib.request | |
| bucket = os.environ["COS_BUCKET"].strip() | |
| prefix = os.environ["COS_PREFIX"].strip().strip("/") | |
| region = os.environ["COS_REGION"].strip() | |
| secret_id = os.environ["COS_SECRET_ID"].strip() | |
| secret_key = os.environ["COS_SECRET_KEY"].strip() | |
| artifact_glob = os.environ["ARTIFACT_GLOB"].strip() | |
| host = f"{bucket}.cos.{region}.myqcloud.com" | |
| def hmac_sha1(key: bytes, msg: str) -> str: | |
| return hmac.new(key, msg.encode("utf-8"), hashlib.sha1).hexdigest() | |
| def urlencode(value: str) -> str: | |
| return urllib.parse.quote(value, safe="-_.~") | |
| def make_auth(method: str, uri_path: str, date_value: str) -> str: | |
| start = int(datetime.now(UTC).timestamp()) - 60 | |
| end = start + 3600 | |
| key_time = f"{start};{end}" | |
| sign_key = hmac_sha1(secret_key.encode("utf-8"), key_time) | |
| headers = {"Host": host, "Date": date_value} | |
| header_items = sorted((urlencode(k.lower()), urlencode(v.strip())) for k, v in headers.items()) | |
| header_list = ";".join(k for k, _ in header_items) | |
| http_headers = "&".join(f"{k}={v}" for k, v in header_items) | |
| http_string = "\n".join([method.lower(), uri_path, "", http_headers, ""]) | |
| string_to_sign = "\n".join([ | |
| "sha1", | |
| key_time, | |
| hashlib.sha1(http_string.encode()).hexdigest(), | |
| "", | |
| ]) | |
| signature = hmac_sha1(sign_key.encode("utf-8"), string_to_sign) | |
| return "&".join([ | |
| "q-sign-algorithm=sha1", | |
| f"q-ak={secret_id}", | |
| f"q-sign-time={key_time}", | |
| f"q-key-time={key_time}", | |
| f"q-header-list={header_list}", | |
| "q-url-param-list=", | |
| f"q-signature={signature}", | |
| ]) | |
| files = sorted(Path(path) for path in glob.glob(artifact_glob) if Path(path).is_file()) | |
| if not files: | |
| print(f"No artifact files matched {artifact_glob}; skipping COS upload.") | |
| sys.exit(0) | |
| failures = [] | |
| for path in files: | |
| object_key = f"{prefix}/{path.name}" if prefix else path.name | |
| uri_path = "/" + "/".join(urlencode(part) for part in object_key.split("/")) | |
| date_value = format_datetime(datetime.now(UTC), usegmt=True) | |
| content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" | |
| request = urllib.request.Request( | |
| f"https://{host}{uri_path}", | |
| data=path.read_bytes(), | |
| method="PUT", | |
| headers={ | |
| "Authorization": make_auth("PUT", uri_path, date_value), | |
| "Content-Type": content_type, | |
| "Date": date_value, | |
| "Host": host, | |
| }, | |
| ) | |
| try: | |
| with urllib.request.urlopen(request, timeout=300) as response: | |
| print(f"Uploaded {object_key} -> {response.status}") | |
| except Exception as exc: | |
| failures.append((object_key, str(exc))) | |
| if failures: | |
| for object_key, error in failures: | |
| print(f"Failed {object_key}: {error}", file=sys.stderr) | |
| sys.exit(1) | |
| PY | |
| - name: Upload Windows Portable Artifact | |
| if: matrix.goos == 'windows' && steps.windows_package_variants.outputs.want_portable == 'true' | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: synapse-relay-gui-${{ matrix.suffix }}-portable | |
| path: synapse-relay-gui-${{ matrix.suffix }}-portable.zip | |
| - name: Upload Windows Setup Artifact | |
| if: matrix.goos == 'windows' && steps.windows_package_variants.outputs.want_setup == 'true' | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: synapse-relay-gui-${{ matrix.suffix }}-setup | |
| path: synapse-relay-gui-${{ matrix.suffix }}-setup.exe | |
| - name: Upload artifact | |
| if: matrix.goos != 'windows' | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: synapse-relay-gui-${{ matrix.suffix }} | |
| path: synapse-relay-gui-${{ matrix.suffix }}* |