v0.11.0 #114
Workflow file for this run
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: Build Bundles | |
| permissions: | |
| contents: write | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Git tag/ref to build (leave empty for branch HEAD)' | |
| type: string | |
| default: '' | |
| publish-obs: | |
| description: 'Publish to OBS (override release-only gating)' | |
| type: boolean | |
| default: false | |
| release: | |
| types: | |
| - created | |
| jobs: | |
| linux-bundles: | |
| name: Linux (deb, rpm, appimage, aur) | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.tag || github.event.release.tag_name || github.ref }} | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust artifacts | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| src-tauri -> src-tauri/target | |
| - name: Install Linux build dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libgtk-3-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| rpm \ | |
| zip | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| - name: Generate icons | |
| run: npm run generate:icons | |
| - name: Install cargo-about | |
| shell: bash | |
| run: | | |
| cargo install cargo-about --locked --features cli | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| PATH="$HOME/.cargo/bin:$PATH" cargo-about --version | |
| - name: Generate attribution file | |
| run: bash ./scripts/generate-attributions.sh | |
| - name: Resolve build version | |
| run: | | |
| RAW_VERSION="${{ github.event.release.tag_name || github.ref_name }}" | |
| if [[ -z "$RAW_VERSION" || "$RAW_VERSION" == refs/* ]]; then | |
| RAW_VERSION="${GITHUB_REF_NAME}" | |
| fi | |
| if [[ "$RAW_VERSION" =~ ^v[0-9] ]]; then | |
| BUNDLE_VERSION="${RAW_VERSION#v}" | |
| RELEASE_TAG="${RAW_VERSION}" | |
| else | |
| BUNDLE_VERSION="0.1.0" | |
| RELEASE_TAG="v${BUNDLE_VERSION}" | |
| fi | |
| echo "BUNDLE_VERSION=${BUNDLE_VERSION}" >> "$GITHUB_ENV" | |
| echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_ENV" | |
| echo "LINUX_BUNDLES=deb,rpm" >> "$GITHUB_ENV" | |
| echo "Using BUNDLE_VERSION=${BUNDLE_VERSION}" | |
| - name: Setup Python for OBS tooling | |
| if: ${{ github.event_name == 'release' || inputs.publish-obs }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install OBS CLI | |
| if: ${{ github.event_name == 'release' || inputs.publish-obs }} | |
| run: python -m pip install --upgrade pip osc | |
| - name: Generate OBS source inputs | |
| if: ${{ github.event_name == 'release' || inputs.publish-obs }} | |
| run: bash ./scripts/obs/build-offline-source-inputs.sh ./obs-release-inputs | |
| - name: Configure OBS CLI | |
| if: ${{ github.event_name == 'release' || inputs.publish-obs }} | |
| env: | |
| OBS_APIURL: ${{ secrets.OBS_APIURL }} | |
| OBS_USERNAME: ${{ secrets.OBS_USERNAME }} | |
| OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }} | |
| run: bash ./scripts/obs/configure-osc.sh | |
| - name: Build Ubuntu deb package in OBS for AUR | |
| if: ${{ github.event_name == 'release' || inputs.publish-obs }} | |
| env: | |
| OBS_PROJECT: home:cst8t:gitmun | |
| OBS_PACKAGE: gitmun | |
| OBS_DEB_REPOSITORY: xUbuntu_26.04 | |
| OBS_DEB_ARCH: x86_64 | |
| run: bash ./scripts/obs/publish-release-package.sh "${BUNDLE_VERSION}" ./obs-release-inputs | |
| - name: Download Ubuntu deb package from OBS | |
| if: ${{ github.event_name == 'release' }} | |
| run: | | |
| DEB_NAME="gitmun_${BUNDLE_VERSION}-1_amd64.deb" | |
| DEB_URL="https://download.opensuse.org/repositories/home:/cst8t:/gitmun/xUbuntu_26.04/amd64/${DEB_NAME}" | |
| mkdir -p src-tauri/target/release/bundle/deb | |
| curl -fsSL -o "src-tauri/target/release/bundle/deb/${DEB_NAME}" "${DEB_URL}" | |
| - name: Build Tauri Linux bundles and updater metadata | |
| if: ${{ github.event_name != 'release' }} | |
| uses: tauri-apps/tauri-action@v0.6.2 | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| releaseId: ${{ github.event_name == 'release' && github.event.release.id || '' }} | |
| tagName: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} | |
| projectPath: ./ | |
| tauriScript: npm run tauri | |
| includeUpdaterJson: true | |
| args: >- | |
| --bundles ${{ env.LINUX_BUNDLES }} | |
| --config '{"version":"${{ env.BUNDLE_VERSION }}"}' | |
| - name: Prepare AUR files | |
| env: | |
| REPO_SLUG: ${{ github.repository }} | |
| SERVER_URL: ${{ github.server_url }} | |
| RELEASE_TAG: ${{ env.RELEASE_TAG }} | |
| run: bash ./scripts/generate-aur-package.sh | |
| - name: Validate generated AUR metadata | |
| run: | | |
| if grep -q "owner/repo" packaging/aur/PKGBUILD packaging/aur/.SRCINFO; then | |
| echo "AUR metadata still contains owner/repo placeholder." >&2 | |
| exit 1 | |
| fi | |
| expected_prefix="${{ github.server_url }}/${{ github.repository }}/releases/download/${{ env.RELEASE_TAG }}" | |
| if ! grep -q "$expected_prefix" packaging/aur/PKGBUILD; then | |
| echo "PKGBUILD source URL does not match expected repository release path." >&2 | |
| exit 1 | |
| fi | |
| if ! grep -q "$expected_prefix" packaging/aur/.SRCINFO; then | |
| echo ".SRCINFO source URL does not match expected repository release path." >&2 | |
| exit 1 | |
| fi | |
| - name: Package AUR files | |
| run: | | |
| mkdir -p src-tauri/target/release/bundle/aur | |
| ZIP_NAME="gitmun-bin-aur-${BUNDLE_VERSION}.zip" | |
| ( | |
| cd packaging/aur | |
| zip -9 -j "../../src-tauri/target/release/bundle/aur/${ZIP_NAME}" \ | |
| PKGBUILD \ | |
| .SRCINFO \ | |
| *.install \ | |
| LICENSE \ | |
| LICENSE.* \ | |
| REUSE.toml | |
| ) | |
| - name: Upload Linux artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-bundles | |
| path: | | |
| src-tauri/target/release/bundle/deb/*.deb | |
| src-tauri/target/release/bundle/rpm/*.rpm | |
| src-tauri/target/release/bundle/aur/*.zip | |
| public/ATTRIBUTIONS.html | |
| - name: Attach Linux bundles to release | |
| if: ${{ github.event_name == 'release' }} | |
| env: | |
| TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| TAG_NAME="${RELEASE_TAG_NAME:-${REF_NAME:-}}" | |
| if [ -z "${RELEASE_ID:-}" ]; then | |
| echo "No release found for tag '${TAG_NAME}'. Skipping asset attachment." | |
| exit 0 | |
| fi | |
| upload_asset() { | |
| local file="$1" | |
| local name | |
| name="$(basename "$file")" | |
| local encoded | |
| encoded="$(python -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$name")" | |
| local upload_url="https://uploads.github.com/repos/${REPOSITORY}/releases/${RELEASE_ID}/assets?name=${encoded}" | |
| curl -fsS -X POST \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary "@${file}" \ | |
| "$upload_url" | |
| } | |
| shopt -s nullglob | |
| FILES=( | |
| src-tauri/target/release/bundle/deb/*.deb | |
| src-tauri/target/release/bundle/aur/*.zip | |
| public/ATTRIBUTIONS.html | |
| ) | |
| for file in "${FILES[@]}"; do | |
| echo "Uploading ${file}" | |
| upload_asset "$file" | |
| done | |
| flatpak-bundle: | |
| name: Linux Flatpak bundle | |
| needs: [linux-bundles] | |
| runs-on: ubuntu-22.04 | |
| if: false | |
| container: | |
| image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 | |
| options: --privileged | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download Linux artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: linux-bundles | |
| path: ./linux-bundles | |
| - name: Prepare Flatpak rootfs from .deb | |
| run: | | |
| set -euo pipefail | |
| shopt -s globstar nullglob | |
| DEB_FILES=(linux-bundles/**/*.deb) | |
| if [ ${#DEB_FILES[@]} -eq 0 ]; then | |
| echo "No .deb files found in downloaded linux-bundles artifact." | |
| echo "Downloaded artifact contents:" | |
| ls -R linux-bundles | |
| exit 1 | |
| fi | |
| DEB_FILE="${DEB_FILES[0]}" | |
| echo "Using .deb artifact: ${DEB_FILE}" | |
| rm -rf packaging/flatpak/rootfs | |
| mkdir -p packaging/flatpak/rootfs | |
| if command -v dpkg-deb >/dev/null 2>&1; then | |
| dpkg-deb -x "$DEB_FILE" packaging/flatpak/rootfs | |
| else | |
| echo "dpkg-deb not found; extracting .deb payload with ar/tar fallback." | |
| DATA_MEMBER="$(ar t "$DEB_FILE" | grep '^data.tar' | head -n1 || true)" | |
| if [ -z "$DATA_MEMBER" ]; then | |
| echo "Unable to locate data.tar* payload in ${DEB_FILE}" >&2 | |
| exit 1 | |
| fi | |
| case "$DATA_MEMBER" in | |
| *.tar) | |
| ar p "$DEB_FILE" "$DATA_MEMBER" | tar -x -f - -C packaging/flatpak/rootfs | |
| ;; | |
| *.tar.gz|*.tgz) | |
| ar p "$DEB_FILE" "$DATA_MEMBER" | tar -x -z -f - -C packaging/flatpak/rootfs | |
| ;; | |
| *.tar.xz) | |
| ar p "$DEB_FILE" "$DATA_MEMBER" | tar -x -J -f - -C packaging/flatpak/rootfs | |
| ;; | |
| *.tar.bz2) | |
| ar p "$DEB_FILE" "$DATA_MEMBER" | tar -x -j -f - -C packaging/flatpak/rootfs | |
| ;; | |
| *.tar.zst) | |
| if command -v zstd >/dev/null 2>&1; then | |
| ar p "$DEB_FILE" "$DATA_MEMBER" | zstd -d -c | tar -x -f - -C packaging/flatpak/rootfs | |
| else | |
| echo "data.tar.zst found, but zstd is unavailable in Flatpak container." >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| *) | |
| echo "Unsupported .deb payload member: ${DATA_MEMBER}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| - name: Build Flatpak bundle | |
| uses: flatpak/flatpak-github-actions/flatpak-builder@v6 | |
| with: | |
| manifest-path: packaging/flatpak/com.cst8t.gitmun.yml | |
| bundle: com.cst8t.gitmun.flatpak | |
| cache-key: flatpak-builder-v1-${{ hashFiles('packaging/flatpak/com.cst8t.gitmun.yml') }} | |
| upload-artifact: true | |
| - name: Stage Flatpak bundle for release upload | |
| run: | | |
| set -euo pipefail | |
| mkdir -p src-tauri/target/release/bundle/flatpak | |
| mv -f com.cst8t.gitmun.flatpak src-tauri/target/release/bundle/flatpak/com.cst8t.gitmun.flatpak | |
| - name: Upload Flatpak artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-flatpak | |
| path: src-tauri/target/release/bundle/flatpak/*.flatpak | |
| - name: Attach Flatpak bundle to release | |
| if: ${{ github.event_name == 'release' }} | |
| env: | |
| TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| TAG_NAME="${RELEASE_TAG_NAME:-${REF_NAME:-}}" | |
| if [ -z "${RELEASE_ID:-}" ]; then | |
| echo "No release found for tag '${TAG_NAME}'. Skipping asset attachment." | |
| exit 0 | |
| fi | |
| shopt -s nullglob | |
| FILES=(src-tauri/target/release/bundle/flatpak/*.flatpak) | |
| for file in "${FILES[@]}"; do | |
| name="$(basename "$file")" | |
| upload_url="https://uploads.github.com/repos/${REPOSITORY}/releases/${RELEASE_ID}/assets?name=${name}" | |
| echo "Uploading ${file}" | |
| curl -fsS -X POST \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary "@${file}" \ | |
| "$upload_url" | |
| done | |
| windows-bundles: | |
| name: Windows (NSIS, MSI) | |
| runs-on: windows-latest | |
| if: ${{ github.event_name == 'release' || !inputs.publish-obs }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust artifacts | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| src-tauri -> src-tauri/target | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| - name: Generate icons | |
| run: npm run generate:icons | |
| - name: Install cargo-about | |
| shell: bash | |
| run: | | |
| cargo install cargo-about --locked --features cli | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| PATH="$HOME/.cargo/bin:$PATH" cargo-about --version | |
| - name: Generate attribution file | |
| shell: bash | |
| run: bash ./scripts/generate-attributions.sh | |
| - name: Resolve build version | |
| shell: bash | |
| run: | | |
| RAW_VERSION="${{ github.event.release.tag_name || github.ref_name }}" | |
| if [[ -z "$RAW_VERSION" || "$RAW_VERSION" == refs/* ]]; then | |
| RAW_VERSION="${GITHUB_REF_NAME}" | |
| fi | |
| if [[ "$RAW_VERSION" =~ ^v[0-9] ]]; then | |
| BUNDLE_VERSION="${RAW_VERSION#v}" | |
| else | |
| BUNDLE_VERSION="0.1.0" | |
| fi | |
| echo "BUNDLE_VERSION=${BUNDLE_VERSION}" >> "$GITHUB_ENV" | |
| echo "Using BUNDLE_VERSION=${BUNDLE_VERSION}" | |
| - name: Build Windows app without bundling | |
| shell: bash | |
| env: | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: >- | |
| npm run tauri build -- --no-bundle | |
| --config "{\"version\":\"${{ env.BUNDLE_VERSION }}\",\"bundle\":{\"active\":true}}" | |
| - name: Bundle Windows NSIS installer | |
| shell: bash | |
| env: | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: >- | |
| npm run tauri bundle -- --config | |
| "{\"version\":\"${{ env.BUNDLE_VERSION }}\",\"bundle\":{\"active\":true,\"targets\":\"nsis\"}}" | |
| - name: Bundle Windows MSI installer | |
| shell: bash | |
| env: | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: >- | |
| npm run tauri bundle -- --config | |
| "{\"version\":\"${{ env.BUNDLE_VERSION }}\",\"bundle\":{\"active\":true,\"targets\":\"msi\"}}" | |
| - name: Normalise Windows MSI artefact names | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python <<'PY' | |
| import pathlib | |
| import re | |
| bundle_dir = pathlib.Path("src-tauri/target/release/bundle/msi") | |
| locale_suffix = re.compile(r"_[A-Za-z]{2}(?:[-_][A-Za-z]{2})?$") | |
| def normalise_name(path: pathlib.Path) -> pathlib.Path: | |
| if path.suffix == ".sig" and path.name.endswith(".msi.sig"): | |
| stem = path.name[:-8] | |
| suffix = ".msi.sig" | |
| elif path.suffix == ".msi": | |
| stem = path.stem | |
| suffix = ".msi" | |
| else: | |
| return path | |
| normalised = locale_suffix.sub("", stem) | |
| return path.with_name(f"{normalised}{suffix}") | |
| renames = [] | |
| targets = {} | |
| has_conflict = False | |
| for path in sorted(bundle_dir.glob("*")): | |
| target = normalise_name(path) | |
| if target == path: | |
| continue | |
| renames.append((path, target)) | |
| targets.setdefault(target.name, []).append(path.name) | |
| for target_name, source_names in sorted(targets.items()): | |
| if len(source_names) > 1: | |
| has_conflict = True | |
| joined_sources = ", ".join(source_names) | |
| print( | |
| f"Skipping MSI artefact normalisation because {joined_sources} " | |
| f"would all map to {target_name}." | |
| ) | |
| if has_conflict: | |
| raise SystemExit(0) | |
| renamed_any = False | |
| for path, target in renames: | |
| if target.exists(): | |
| raise SystemExit(f"Refusing to overwrite existing artefact: {target}") | |
| path.rename(target) | |
| renamed_any = True | |
| print(f"Renamed {path.name} -> {target.name}") | |
| if not renamed_any: | |
| print("No locale-qualified MSI artefacts needed renaming.") | |
| PY | |
| - name: Upload Windows release assets | |
| if: ${{ github.event_name == 'release' }} | |
| shell: bash | |
| env: | |
| TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| python <<'PY' | |
| import json | |
| import os | |
| import pathlib | |
| import urllib.parse | |
| import urllib.request | |
| token = os.environ["TOKEN"] | |
| release_id = os.environ["RELEASE_ID"] | |
| repository = os.environ["REPOSITORY"] | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| } | |
| def request(method: str, url: str, *, data: bytes | None = None, extra_headers: dict[str, str] | None = None): | |
| req = urllib.request.Request(url, data=data, method=method) | |
| for key, value in headers.items(): | |
| req.add_header(key, value) | |
| for key, value in (extra_headers or {}).items(): | |
| req.add_header(key, value) | |
| with urllib.request.urlopen(req) as response: | |
| return response.read() | |
| assets_url = f"https://api.github.com/repos/{repository}/releases/{release_id}/assets?per_page=100" | |
| existing_assets = { | |
| asset["name"]: asset["url"] | |
| for asset in json.loads(request("GET", assets_url).decode()) | |
| } | |
| files = [] | |
| for pattern in ( | |
| "src-tauri/target/release/bundle/nsis/*.exe", | |
| "src-tauri/target/release/bundle/nsis/*.sig", | |
| "src-tauri/target/release/bundle/msi/*.msi", | |
| "src-tauri/target/release/bundle/msi/*.sig", | |
| ): | |
| files.extend(pathlib.Path().glob(pattern)) | |
| if not files: | |
| raise SystemExit("No Windows bundle assets found to upload.") | |
| for path in files: | |
| name = path.name | |
| if name in existing_assets: | |
| request("DELETE", existing_assets[name]) | |
| upload_url = ( | |
| f"https://uploads.github.com/repos/{repository}/releases/{release_id}/assets" | |
| f"?name={urllib.parse.quote(name)}" | |
| ) | |
| request( | |
| "POST", | |
| upload_url, | |
| data=path.read_bytes(), | |
| extra_headers={"Content-Type": "application/octet-stream"}, | |
| ) | |
| PY | |
| - name: Upload Windows workflow artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: windows-bundles | |
| path: | | |
| src-tauri/target/release/bundle/nsis/*.exe | |
| src-tauri/target/release/bundle/nsis/*.sig | |
| src-tauri/target/release/bundle/msi/*.msi | |
| src-tauri/target/release/bundle/msi/*.sig | |
| update-updater-json: | |
| name: Patch updater JSON | |
| if: ${{ github.event_name == 'release' }} | |
| needs: | |
| - linux-bundles | |
| - windows-bundles | |
| - macos-bundles | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Add Windows installer targets to latest.json | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| GITHUB_API_URL: ${{ github.api_url }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' | |
| import json | |
| import os | |
| import sys | |
| import urllib.parse | |
| import urllib.request | |
| token = os.environ["GITHUB_TOKEN"] | |
| api_base = os.environ["GITHUB_API_URL"].rstrip("/") | |
| release_id = os.environ["RELEASE_ID"] | |
| repository = os.environ["REPOSITORY"] | |
| default_headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| } | |
| def request(method: str, url: str, *, data: bytes | None = None, extra_headers: dict[str, str] | None = None): | |
| req = urllib.request.Request(url, data=data, method=method) | |
| for key, value in default_headers.items(): | |
| req.add_header(key, value) | |
| for key, value in (extra_headers or {}).items(): | |
| req.add_header(key, value) | |
| with urllib.request.urlopen(req) as response: | |
| return response.read() | |
| assets_url = f"{api_base}/repos/{repository}/releases/{release_id}/assets?per_page=100" | |
| assets = json.loads(request("GET", assets_url).decode()) | |
| latest_asset = next((asset for asset in assets if asset["name"] == "latest.json"), None) | |
| if latest_asset is None: | |
| print("latest.json release asset was not found", file=sys.stderr) | |
| sys.exit(1) | |
| nsis_asset = next( | |
| ( | |
| asset | |
| for asset in assets | |
| if asset["name"].lower().endswith("-setup.exe") | |
| ), | |
| None, | |
| ) | |
| if nsis_asset is None: | |
| print("NSIS release asset was not found", file=sys.stderr) | |
| sys.exit(1) | |
| nsis_sig_asset = next( | |
| ( | |
| asset | |
| for asset in assets | |
| if asset["name"].lower().endswith("-setup.exe.sig") | |
| ), | |
| None, | |
| ) | |
| if nsis_sig_asset is None: | |
| print("NSIS signature release asset was not found", file=sys.stderr) | |
| sys.exit(1) | |
| msi_asset = next( | |
| (asset for asset in assets if asset["name"].lower().endswith(".msi")), | |
| None, | |
| ) | |
| if msi_asset is None: | |
| print("MSI release asset was not found", file=sys.stderr) | |
| sys.exit(1) | |
| msi_sig_asset = next( | |
| (asset for asset in assets if asset["name"].lower().endswith(".msi.sig")), | |
| None, | |
| ) | |
| if msi_sig_asset is None: | |
| print("MSI signature release asset was not found", file=sys.stderr) | |
| sys.exit(1) | |
| latest_json = json.loads( | |
| request( | |
| "GET", | |
| latest_asset["url"], | |
| extra_headers={"Accept": "application/octet-stream"}, | |
| ).decode() | |
| ) | |
| nsis_signature = request( | |
| "GET", | |
| nsis_sig_asset["url"], | |
| extra_headers={"Accept": "application/octet-stream"}, | |
| ).decode().strip() | |
| msi_signature = request( | |
| "GET", | |
| msi_sig_asset["url"], | |
| extra_headers={"Accept": "application/octet-stream"}, | |
| ).decode().strip() | |
| platforms = latest_json.setdefault("platforms", {}) | |
| platforms["windows-x86_64"] = { | |
| "signature": nsis_signature, | |
| "url": nsis_asset["browser_download_url"], | |
| } | |
| platforms["windows-x86_64-nsis"] = { | |
| "signature": nsis_signature, | |
| "url": nsis_asset["browser_download_url"], | |
| } | |
| platforms["windows-x86_64-msi"] = { | |
| "signature": msi_signature, | |
| "url": msi_asset["browser_download_url"], | |
| } | |
| payload = (json.dumps(latest_json, indent=2) + "\n").encode() | |
| request("DELETE", latest_asset["url"]) | |
| upload_url = ( | |
| f"https://uploads.github.com/repos/{repository}/releases/{release_id}/assets" | |
| f"?name={urllib.parse.quote('latest.json')}" | |
| ) | |
| request( | |
| "POST", | |
| upload_url, | |
| data=payload, | |
| extra_headers={"Content-Type": "application/json"}, | |
| ) | |
| PY | |
| windows-msix: | |
| name: Windows MSIX (native Windows packaging) | |
| runs-on: windows-latest | |
| if: ${{ github.event_name == 'release' || !inputs.publish-obs }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust artifacts | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| src-tauri -> src-tauri/target | |
| - name: Setup WinApp CLI | |
| uses: microsoft/setup-WinAppCli@v0.1 | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| - name: Generate icons | |
| run: npm run generate:icons | |
| - name: Install cargo-about | |
| shell: bash | |
| run: | | |
| cargo install cargo-about --locked --features cli | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| PATH="$HOME/.cargo/bin:$PATH" cargo-about --version | |
| - name: Generate attribution file | |
| shell: bash | |
| run: bash ./scripts/generate-attributions.sh | |
| - name: Resolve build version | |
| shell: pwsh | |
| run: | | |
| $rawVersion = "${{ github.event.release.tag_name || github.ref_name }}" | |
| if ([string]::IsNullOrWhiteSpace($rawVersion) -or $rawVersion.StartsWith("refs/")) { | |
| $rawVersion = $env:GITHUB_REF_NAME | |
| } | |
| if ($rawVersion -match '^v[0-9]') { | |
| $bundleVersion = $rawVersion.Substring(1) | |
| } else { | |
| $bundleVersion = "0.1.0" | |
| } | |
| $msixVersion = "$bundleVersion.0" | |
| "BUNDLE_VERSION=$bundleVersion" >> $env:GITHUB_ENV | |
| "MSIX_VERSION=$msixVersion" >> $env:GITHUB_ENV | |
| Write-Host "Using BUNDLE_VERSION=$bundleVersion" | |
| Write-Host "Using MSIX_VERSION=$msixVersion" | |
| - name: Build frontend assets | |
| run: npm run build | |
| - name: Build Windows binary for MSIX | |
| shell: bash | |
| env: | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| GITMUN_MSIX: "1" | |
| run: >- | |
| npm run tauri build -- --no-bundle | |
| --config "{\"version\":\"${{ env.BUNDLE_VERSION }}\",\"bundle\":{\"active\":true}}" | |
| - name: Download MinGit for MSIX | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $release = gh release view --repo git-for-windows/git --json assets,body,tagName | | |
| ConvertFrom-Json | |
| $asset = $release.assets | | |
| Where-Object { $_.name -match '^MinGit-[0-9].*-64-bit\.zip$' -and $_.name -notmatch 'busybox' } | | |
| Select-Object -First 1 | |
| if ($null -eq $asset) { | |
| throw "Could not find a 64-bit MinGit zip in the latest Git for Windows release." | |
| } | |
| $checksumPattern = "^$([regex]::Escape($asset.name))(?:\s+|\s*\|\s*)([0-9a-fA-F]{64})$" | |
| $checksumLine = $release.body -split "`n" | | |
| ForEach-Object { $_.Trim() } | | |
| Where-Object { $_ -match $checksumPattern } | | |
| Select-Object -First 1 | |
| if ($null -eq $checksumLine) { | |
| throw "Could not find the SHA-256 for $($asset.name) in the release notes." | |
| } | |
| $expectedHash = ([regex]::Match($checksumLine, $checksumPattern).Groups[1].Value).ToLowerInvariant() | |
| $archivePath = "packaging/msix/$($asset.name)" | |
| $mingitPath = "packaging/msix/mingit" | |
| Remove-Item $archivePath -Force -ErrorAction SilentlyContinue | |
| Remove-Item $mingitPath -Recurse -Force -ErrorAction SilentlyContinue | |
| gh release download ` | |
| --repo git-for-windows/git ` | |
| --pattern $asset.name ` | |
| --dir "packaging/msix" ` | |
| --clobber | |
| $actualHash = (Get-FileHash $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actualHash -ne $expectedHash) { | |
| throw "SHA-256 mismatch for $($asset.name). Expected $expectedHash but got $actualHash." | |
| } | |
| Expand-Archive -Path $archivePath -DestinationPath $mingitPath | |
| if (-not (Test-Path "$mingitPath/cmd/git.exe")) { | |
| throw "Expanded MinGit archive does not contain cmd/git.exe." | |
| } | |
| - name: Stage MSIX layout | |
| shell: pwsh | |
| env: | |
| MSIX_PUBLISHER: ${{ vars.MSIX_PUBLISHER }} | |
| run: | | |
| $publisher = $env:MSIX_PUBLISHER | |
| if ([string]::IsNullOrWhiteSpace($publisher)) { | |
| $publisher = "CN=B450DE9A-E656-447D-9443-B325D0101D73" | |
| } | |
| $layout = "packaging/msix/layout" | |
| Remove-Item $layout -Recurse -Force -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path "$layout/Assets" | Out-Null | |
| Copy-Item "src-tauri/target/release/gitmun.exe" "$layout/gitmun.exe" | |
| Copy-Item "packaging/msix/mingit" "$layout/mingit" -Recurse | |
| Copy-Item "packaging/msix/assets/Square300x300Logo.png" "$layout/Assets/Square300x300Logo.png" | |
| Copy-Item "packaging/msix/assets/Square150x150Logo.png" "$layout/Assets/Square150x150Logo.png" | |
| Copy-Item "packaging/msix/assets/Square71x71Logo.png" "$layout/Assets/Square71x71Logo.png" | |
| Copy-Item "packaging/msix/assets/Square44x44Logo.png" "$layout/Assets/Square44x44Logo.png" | |
| $manifest = Get-Content "packaging/msix/Package.appxmanifest" -Raw | |
| $manifest = $manifest.Replace("__APP_VERSION__", $env:MSIX_VERSION) | |
| $manifest = $manifest.Replace("__MSIX_PUBLISHER__", $publisher) | |
| Set-Content "$layout/Package.appxmanifest" $manifest -Encoding utf8 | |
| - name: Generate development certificate | |
| run: winapp cert generate --if-exists skip --manifest packaging/msix/layout/Package.appxmanifest | |
| - name: Pack MSIX | |
| run: winapp pack packaging/msix/layout --manifest packaging/msix/layout/Package.appxmanifest --cert devcert.pfx | |
| - name: Upload MSIX release asset | |
| if: ${{ github.event_name == 'release' }} | |
| shell: bash | |
| env: | |
| TOKEN: ${{ github.token }} | |
| RELEASE_ID: ${{ github.event.release.id }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| python <<'PY' | |
| import json | |
| import os | |
| import pathlib | |
| import urllib.parse | |
| import urllib.request | |
| token = os.environ["TOKEN"] | |
| release_id = os.environ["RELEASE_ID"] | |
| repository = os.environ["REPOSITORY"] | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| } | |
| def request(method: str, url: str, *, data: bytes | None = None, extra_headers: dict[str, str] | None = None): | |
| req = urllib.request.Request(url, data=data, method=method) | |
| for key, value in headers.items(): | |
| req.add_header(key, value) | |
| for key, value in (extra_headers or {}).items(): | |
| req.add_header(key, value) | |
| with urllib.request.urlopen(req) as response: | |
| return response.read() | |
| assets_url = f"https://api.github.com/repos/{repository}/releases/{release_id}/assets?per_page=100" | |
| existing_assets = { | |
| asset["name"]: asset["url"] | |
| for asset in json.loads(request("GET", assets_url).decode()) | |
| } | |
| files = sorted(pathlib.Path().glob("*.msix")) | |
| if not files: | |
| raise SystemExit("No MSIX release asset found to upload.") | |
| for path in files: | |
| name = path.name | |
| if name in existing_assets: | |
| request("DELETE", existing_assets[name]) | |
| upload_url = ( | |
| f"https://uploads.github.com/repos/{repository}/releases/{release_id}/assets" | |
| f"?name={urllib.parse.quote(name)}" | |
| ) | |
| request( | |
| "POST", | |
| upload_url, | |
| data=path.read_bytes(), | |
| extra_headers={"Content-Type": "application/octet-stream"}, | |
| ) | |
| PY | |
| - name: Upload MSIX artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: windows-msix | |
| path: | | |
| *.msix | |
| packaging/msix/layout/Package.appxmanifest | |
| macos-bundles: | |
| name: macOS (dmg, app updater) | |
| runs-on: macos-latest | |
| if: ${{ github.event_name == 'release' || !inputs.publish-obs }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust artifacts | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| src-tauri -> src-tauri/target | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| - name: Generate icons | |
| run: npm run generate:icons | |
| - name: Install cargo-about | |
| shell: bash | |
| run: | | |
| cargo install cargo-about --locked --features cli | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| PATH="$HOME/.cargo/bin:$PATH" cargo-about --version | |
| - name: Generate attribution file | |
| run: bash ./scripts/generate-attributions.sh | |
| - name: Resolve build version | |
| run: | | |
| RAW_VERSION="${{ github.event.release.tag_name || github.ref_name }}" | |
| if [[ -z "$RAW_VERSION" || "$RAW_VERSION" == refs/* ]]; then | |
| RAW_VERSION="${GITHUB_REF_NAME}" | |
| fi | |
| if [[ "$RAW_VERSION" =~ ^v[0-9] ]]; then | |
| BUNDLE_VERSION="${RAW_VERSION#v}" | |
| else | |
| BUNDLE_VERSION="0.1.0" | |
| fi | |
| echo "BUNDLE_VERSION=${BUNDLE_VERSION}" >> "$GITHUB_ENV" | |
| echo "Using BUNDLE_VERSION=${BUNDLE_VERSION}" | |
| - name: Build macOS bundles and updater metadata | |
| uses: tauri-apps/tauri-action@v0.6.2 | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| APP_VERSION: ${{ env.BUNDLE_VERSION }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| releaseId: ${{ github.event_name == 'release' && github.event.release.id || '' }} | |
| tagName: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} | |
| projectPath: ./ | |
| tauriScript: npm run tauri | |
| includeUpdaterJson: true | |
| args: >- | |
| --bundles app,dmg | |
| --config '{"version":"${{ env.BUNDLE_VERSION }}"}' | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: macos-bundles | |
| path: | | |
| src-tauri/target/release/bundle/dmg/*.dmg | |
| src-tauri/target/release/bundle/macos/*.app.tar.gz | |
| src-tauri/target/release/bundle/macos/*.sig | |
| notify-r2-release-sync: | |
| name: Notify R2 release sync | |
| if: ${{ github.event_name == 'release' }} | |
| needs: | |
| - linux-bundles | |
| - windows-bundles | |
| - update-updater-json | |
| - macos-bundles | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Notify R2 sync worker | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| CI_TRIGGER_SECRET: ${{ secrets.CI_TRIGGER_SECRET }} | |
| WORKER_URL: ${{ vars.WORKER_URL || secrets.WORKER_URL }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CI_TRIGGER_SECRET:-}" ]; then | |
| echo "CI_TRIGGER_SECRET is not configured." >&2 | |
| exit 1 | |
| fi | |
| if [ -z "${WORKER_URL:-}" ]; then | |
| echo "WORKER_URL is not configured." >&2 | |
| exit 1 | |
| fi | |
| PAYLOAD="$(jq -cn --arg tag "$TAG" '{tag: $tag}')" | |
| SIGNATURE="$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$CI_TRIGGER_SECRET" -binary | xxd -p -c 256)" | |
| curl --fail-with-body \ | |
| --request POST \ | |
| --header "Content-Type: application/json" \ | |
| --header "x-ci-signature: sha256=$SIGNATURE" \ | |
| --data "$PAYLOAD" \ | |
| "${WORKER_URL%/}/webhook" | |
| notify-website-release-metadata: | |
| name: Notify website release metadata | |
| if: ${{ github.event_name == 'release' }} | |
| needs: | |
| - linux-bundles | |
| - windows-bundles | |
| - update-updater-json | |
| - windows-msix | |
| - macos-bundles | |
| - notify-r2-release-sync | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Dispatch website metadata update | |
| env: | |
| DISPATCH_TOKEN: ${{ secrets.GITMUN_WEBSITE_DISPATCH_TOKEN }} | |
| GITHUB_API_URL: ${{ github.api_url }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| RELEASE_URL: ${{ github.event.release.html_url }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${DISPATCH_TOKEN:-}" ]; then | |
| echo "GITMUN_WEBSITE_DISPATCH_TOKEN is not configured." >&2 | |
| exit 1 | |
| fi | |
| payload="$( | |
| jq -cn \ | |
| --arg event_type "gitmun-release-assets-ready" \ | |
| --arg tag "$RELEASE_TAG" \ | |
| --arg release_url "$RELEASE_URL" \ | |
| '{ | |
| event_type: $event_type, | |
| client_payload: { | |
| tag: $tag, | |
| release_url: $release_url | |
| } | |
| }' | |
| )" | |
| curl --fail-with-body \ | |
| --request POST \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "Authorization: Bearer ${DISPATCH_TOKEN}" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| --data "$payload" \ | |
| "${GITHUB_API_URL%/}/repos/cst8t/gitmun-org-website/dispatches" |