Skip to content

v0.11.0

v0.11.0 #114

Workflow file for this run

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"