Compare commits
10
Commits
06254db3d4
...
v0.4.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b1cfba09 | ||
|
|
ca6028bbb3 | ||
|
|
b3d07bda09 | ||
|
|
e025a7441a | ||
|
|
8f62949902 | ||
|
|
6354cb42b2 | ||
|
|
9b55a12b32 | ||
|
|
049232099b | ||
|
|
945883bb9d | ||
|
|
b71e15c2c0 |
@@ -43,7 +43,18 @@ name: Build App (Preview)
|
|||||||
# prunes previous previews itself, keeping the newest few. Bundles are ~130 MB a
|
# prunes previous previews itself, keeping the newest few. Bundles are ~130 MB a
|
||||||
# release; the point of a preview is the build you are testing now.
|
# release; the point of a preview is the build you are testing now.
|
||||||
#
|
#
|
||||||
# `sync-release.yml` is workflow_dispatch-only, so nothing here reaches GitHub.
|
# A preview release is not meant to reach GitHub. `build-app.yml`'s inline
|
||||||
|
# mirror never sees one (it only runs for its own `push`-triggered release),
|
||||||
|
# but `backfill-releases.yml` pulls every Gitea release unfiltered and would
|
||||||
|
# faithfully forward a preview's `prerelease: true` if it were ever dispatched
|
||||||
|
# while one existed — so `GitHubRelease::prerelease` in `update_commands.rs`
|
||||||
|
# is real defence, not a no-op, even though the `preview-<sha>` tag shape
|
||||||
|
# (never valid semver) already blocks it independently. (The previous
|
||||||
|
# mechanism here, `sync-release.yml`, was `workflow_dispatch`-only and read
|
||||||
|
# `gitea.event.release.*` fields that are only ever populated by a `release`
|
||||||
|
# trigger, so it could never have actually run; deleted rather than fixed,
|
||||||
|
# since build-app.yml's inline mirror already does what it was meant to do
|
||||||
|
# for real releases. See triple-c#32.)
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ gitea.server_url }}
|
GITEA_URL: ${{ gitea.server_url }}
|
||||||
@@ -70,12 +81,23 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.version.outputs.VERSION }}
|
version: ${{ steps.version.outputs.VERSION }}
|
||||||
sha: ${{ steps.version.outputs.SHA }}
|
sha: ${{ steps.version.outputs.SHA }}
|
||||||
|
# Everything after the first `-` in VERSION (e.g. `preview.a1b2c3d`).
|
||||||
|
# The bundle version fields never see this — see "Set app version" in
|
||||||
|
# each build job — but it is baked into the binary as
|
||||||
|
# `TRIPLE_C_BUILD_SUFFIX` so `get_app_version()` can still report it.
|
||||||
|
# An installed preview otherwise reports the same bare number a
|
||||||
|
# production build would, indistinguishable in the About panel and to
|
||||||
|
# `check_for_updates`. See triple-c#32.
|
||||||
|
suffix: ${{ steps.version.outputs.SUFFIX }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch all tags
|
||||||
|
run: git fetch --tags
|
||||||
|
|
||||||
- name: Compute preview version
|
- name: Compute preview version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
@@ -86,21 +108,60 @@ jobs:
|
|||||||
# is testing and not something to hang a tag on.
|
# is testing and not something to hang a tag on.
|
||||||
echo "SHA=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
echo "SHA=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# The patch number is computed exactly as build-app.yml does it, so a
|
# The patch number must be the same "one past the highest patch
|
||||||
# preview is labelled with the version the release it previews would
|
# already used" build-app.yml computes for a real release — not a
|
||||||
# carry. This used to be hard-coded `.0`, which made every preview
|
# distance from the latest tag. It used to be
|
||||||
# installer claim to be x.y.0 no matter what it contained.
|
# `git rev-list --count <latest tag>..HEAD`, which build-app.yml's
|
||||||
LATEST_TAG=$(git tag -l "v${MAJOR_MINOR}.*" --sort=-v:refname | grep -E "^v${MAJOR_MINOR}\.[0-9]+$" | head -1 || true)
|
# own history section documents as broken for exactly this reason:
|
||||||
if [ -n "$LATEST_TAG" ]; then
|
# it resets to zero on every tag cut, so previews went *backwards*
|
||||||
PATCH=$(git rev-list --count "${LATEST_TAG}..HEAD")
|
# (0.4.62 -> 0.4.0) the moment a release landed, and nothing stopped
|
||||||
echo "Latest matching tag: ${LATEST_TAG} (+${PATCH} commits)"
|
# a preview number from later colliding with a real release's.
|
||||||
|
#
|
||||||
|
# Reading the same `v${MAJOR_MINOR}.*` tags (including the `-mac`
|
||||||
|
# / `-win` suffixed ones a partially-published release can leave
|
||||||
|
# behind) means a preview built right before a release computes the
|
||||||
|
# exact number that release is about to take — e.g. `0.4.13` for
|
||||||
|
# both. That makes the two numerically *equal*, not "preview less
|
||||||
|
# than release" — plain semver ordering does not make a
|
||||||
|
# `-preview.<sha>` suffix sort lower on its own here, because
|
||||||
|
# `check_for_updates` compares against the bare, stripped
|
||||||
|
# `CARGO_PKG_VERSION`, never the suffixed display string. What
|
||||||
|
# closes the loop is `update_commands.rs`'s `is_preview_build`
|
||||||
|
# check, which relaxes that one comparison to `>=` specifically so
|
||||||
|
# "a release exists at my own number" reads as an update. See
|
||||||
|
# triple-c#32.
|
||||||
|
HIGHEST=$(git tag -l "v${MAJOR_MINOR}.*" \
|
||||||
|
| grep -E "^v${MAJOR_MINOR}\.[0-9]+(-mac|-win)?$" \
|
||||||
|
| sed -E "s/^v${MAJOR_MINOR}\.([0-9]+).*/\1/" \
|
||||||
|
| sort -n | tail -1 || true)
|
||||||
|
|
||||||
|
# Mirrors build-app.yml's own `EXISTING` guard: this workflow is
|
||||||
|
# also `workflow_dispatch`-able on `main`, not just PR-triggered, so
|
||||||
|
# HEAD can be a commit a release was already cut from. Without this,
|
||||||
|
# dispatching a preview there would compute `HIGHEST + 1` — one past
|
||||||
|
# that release — and produce exactly the "preview outranks
|
||||||
|
# production" failure triple-c#32 was filed over, just reintroduced
|
||||||
|
# through the manual-dispatch door instead of the automatic one.
|
||||||
|
EXISTING=$(git tag --points-at HEAD \
|
||||||
|
| grep -E "^v${MAJOR_MINOR}\.[0-9]+$" \
|
||||||
|
| sed -E "s/^v${MAJOR_MINOR}\.([0-9]+)$/\1/" \
|
||||||
|
| sort -n | tail -1 || true)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
echo "HEAD is already tagged v${MAJOR_MINOR}.${EXISTING} — matching it"
|
||||||
|
PATCH="${EXISTING}"
|
||||||
|
elif [ -n "$HIGHEST" ]; then
|
||||||
|
echo "Highest patch already used on this line: ${HIGHEST}"
|
||||||
|
PATCH=$((HIGHEST + 1))
|
||||||
else
|
else
|
||||||
echo "No v${MAJOR_MINOR}.* tag yet — starting this line at .0"
|
echo "No v${MAJOR_MINOR}.* tag yet — starting this line at .0"
|
||||||
PATCH=0
|
PATCH=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION="${MAJOR_MINOR}.${PATCH}-preview.${SHORT_SHA}"
|
SUFFIX="preview.${SHORT_SHA}"
|
||||||
|
VERSION="${MAJOR_MINOR}.${PATCH}-${SUFFIX}"
|
||||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "SUFFIX=${SUFFIX}" >> $GITHUB_OUTPUT
|
||||||
echo "Computed preview version: ${VERSION}"
|
echo "Computed preview version: ${VERSION}"
|
||||||
|
|
||||||
# One release, created once. The three build jobs run concurrently, so
|
# One release, created once. The three build jobs run concurrently, so
|
||||||
@@ -249,6 +310,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
env:
|
||||||
|
# Baked into the binary via `option_env!` in `get_app_version()` —
|
||||||
|
# the bundle version above stays bare (WiX/MSI's ProductVersion has
|
||||||
|
# no room for a suffix), so this is the only place a preview build
|
||||||
|
# can still tell itself apart from a production one. See
|
||||||
|
# triple-c#32.
|
||||||
|
TRIPLE_C_BUILD_SUFFIX: ${{ needs.compute-version.outputs.suffix }}
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.cargo/bin:$PATH"
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
npx tauri build
|
npx tauri build
|
||||||
@@ -361,6 +429,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Tauri app (universal)
|
- name: Build Tauri app (universal)
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
env:
|
||||||
|
# See the matching comment on the Linux job's "Build Tauri app" step.
|
||||||
|
TRIPLE_C_BUILD_SUFFIX: ${{ needs.compute-version.outputs.suffix }}
|
||||||
run: |
|
run: |
|
||||||
export PATH="$HOME/.cargo/bin:$PATH"
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
npx tauri build --target universal-apple-darwin
|
npx tauri build --target universal-apple-darwin
|
||||||
@@ -489,6 +560,8 @@ jobs:
|
|||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
env:
|
env:
|
||||||
TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}"
|
TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}"
|
||||||
|
# See the matching comment on the Linux job's "Build Tauri app" step.
|
||||||
|
TRIPLE_C_BUILD_SUFFIX: ${{ needs.compute-version.outputs.suffix }}
|
||||||
run: |
|
run: |
|
||||||
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
cargo tauri build
|
cargo tauri build
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
name: Publish AUR Package
|
||||||
|
|
||||||
|
# Builds and pushes the `triple-c-bin` AUR package (packaging/arch/PKGBUILD)
|
||||||
|
# for a given release, or the latest one if none is given. Manual dispatch
|
||||||
|
# only — deliberately not triggered by `release` or `push`, for the same
|
||||||
|
# reason sync-release.yml (removed in triple-c#32) never worked safely as an
|
||||||
|
# automatic trigger: this repo's releases are assembled by build-app.yml
|
||||||
|
# across three separate platform jobs, and there is no single automatic event
|
||||||
|
# that fires only once everything (including the Linux .deb this workflow
|
||||||
|
# needs) is actually uploaded. A human deciding "this release is ready, go
|
||||||
|
# package it" is the correct trigger, the same reasoning
|
||||||
|
# backfill-releases.yml already uses for its own manual-only GitHub sync.
|
||||||
|
#
|
||||||
|
# ## What this does and does not do
|
||||||
|
#
|
||||||
|
# It renders `packaging/arch/PKGBUILD` for one specific version (real
|
||||||
|
# download URL, real sha256sums — never guessed; see the resolve-asset step)
|
||||||
|
# and pushes the rendered PKGBUILD plus a regenerated `.SRCINFO` to AUR. It
|
||||||
|
# does NOT commit anything back to this repo — `packaging/arch/PKGBUILD` stays
|
||||||
|
# a hand-maintained template with a placeholder version, and every real,
|
||||||
|
# published version lives only in AUR's own git history, which is where a
|
||||||
|
# PKGBUILD's revision history is expected to live. A corollary worth knowing:
|
||||||
|
# a hand-edit made directly in the AUR repo (outside this workflow) is
|
||||||
|
# silently overwritten the next time this runs, since every run renders fresh
|
||||||
|
# from this repo's template rather than starting from AUR's current state.
|
||||||
|
#
|
||||||
|
# ## Required secret
|
||||||
|
#
|
||||||
|
# `AUR_SSH_PRIVATE_KEY` — an SSH private key registered against an AUR
|
||||||
|
# account that has already created (or been given co-maintainer access to)
|
||||||
|
# the `triple-c-bin` package. This workflow cannot create that AUR account or
|
||||||
|
# register the key for you — both are manual, one-time steps on
|
||||||
|
# https://aur.archlinux.org. Until this secret exists, every run fails at the
|
||||||
|
# "Push to AUR" step with a clear error rather than silently doing nothing.
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: >-
|
||||||
|
Release version to package, without a leading "v" (e.g. "0.4.14").
|
||||||
|
Leave empty to use the latest published GitHub release.
|
||||||
|
required: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_REPO: shadowdao/triple-c
|
||||||
|
AUR_REPO: ssh://aur@aur.archlinux.org/triple-c-bin.git
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Resolve version and find the Linux asset
|
||||||
|
id: resolve
|
||||||
|
env:
|
||||||
|
VERSION_INPUT: ${{ inputs.version }}
|
||||||
|
GH_PAT: ${{ secrets.GH_PAT }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Authenticated when the secret is available (it is, everywhere
|
||||||
|
# else in this repo's workflows) to avoid the unauthenticated
|
||||||
|
# 60-requests/hour-per-IP cap; still works without it, just at that
|
||||||
|
# lower limit, since this hits nothing but a public repo's public
|
||||||
|
# releases.
|
||||||
|
AUTH=()
|
||||||
|
[ -n "${GH_PAT}" ] && AUTH=(-H "Authorization: Bearer ${GH_PAT}")
|
||||||
|
|
||||||
|
if [ -z "${VERSION_INPUT}" ]; then
|
||||||
|
echo "No version given — resolving the latest GitHub release"
|
||||||
|
RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/latest")
|
||||||
|
else
|
||||||
|
echo "Using requested version ${VERSION_INPUT}"
|
||||||
|
RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${VERSION_INPUT}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name')
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
echo "Resolved to ${TAG}"
|
||||||
|
|
||||||
|
# Discovered from the real release, not assumed: Tauri names the
|
||||||
|
# asset after `productName` verbatim ("Triple-C"), not the
|
||||||
|
# lowercase Cargo binary name, and asset naming is exactly the kind
|
||||||
|
# of thing that silently drifts if a future Tauri upgrade changes
|
||||||
|
# bundler defaults — a hardcoded pattern here would then 404
|
||||||
|
# forever until someone noticed. `head -1` guards against a release
|
||||||
|
# somehow carrying more than one matching asset, which would
|
||||||
|
# otherwise pass the emptiness check below and then break the
|
||||||
|
# download step with two URLs on one line.
|
||||||
|
DEB_URL=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .browser_download_url' | head -1)
|
||||||
|
DEB_NAME=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .name' | head -1)
|
||||||
|
if [ -z "$DEB_URL" ] || [ "$DEB_URL" = "null" ]; then
|
||||||
|
echo "No *_amd64.deb asset found on release ${TAG}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found asset: ${DEB_NAME}"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deb_url=${DEB_URL}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deb_name=${DEB_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download the release asset and compute real checksums
|
||||||
|
id: checksums
|
||||||
|
env:
|
||||||
|
DEB_URL: ${{ steps.resolve.outputs.deb_url }}
|
||||||
|
DEB_NAME: ${{ steps.resolve.outputs.deb_name }}
|
||||||
|
TAG: ${{ steps.resolve.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL -o "${DEB_NAME}" "${DEB_URL}"
|
||||||
|
curl -fsSL -o LICENSE "https://raw.githubusercontent.com/${GITHUB_REPO}/${TAG}/LICENSE"
|
||||||
|
|
||||||
|
echo "deb_sha256=$(sha256sum "${DEB_NAME}" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "license_sha256=$(sha256sum LICENSE | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Render PKGBUILD
|
||||||
|
id: render
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.resolve.outputs.version }}
|
||||||
|
DEB_NAME: ${{ steps.resolve.outputs.deb_name }}
|
||||||
|
DEB_SHA256: ${{ steps.checksums.outputs.deb_sha256 }}
|
||||||
|
LICENSE_SHA256: ${{ steps.checksums.outputs.license_sha256 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p rendered
|
||||||
|
cp packaging/arch/PKGBUILD rendered/PKGBUILD
|
||||||
|
cd rendered
|
||||||
|
|
||||||
|
# Plain string replacement throughout, not sed — the source URL
|
||||||
|
# contains slashes and the repo name does too, and getting a sed
|
||||||
|
# delimiter choice AND its escaping right for that is exactly the
|
||||||
|
# kind of thing that looks correct, passes review, and breaks the
|
||||||
|
# next time someone touches it. `re.sub` with `count=1` and an
|
||||||
|
# exact `.format`-free literal match is boring and that's the
|
||||||
|
# point: every substitution below fails loudly (an assertion /
|
||||||
|
# the checks after) rather than silently no-op'ing if the
|
||||||
|
# template's shape ever drifts from what this expects.
|
||||||
|
#
|
||||||
|
# pkgrel resets to 1 for a new pkgver — a packaging-only fix to the
|
||||||
|
# same upstream version (a dependency bump, say) is what pkgrel is
|
||||||
|
# for, and this workflow always republishes the current PKGBUILD
|
||||||
|
# verbatim rather than incrementing anything, so 1 is always
|
||||||
|
# correct for what this workflow does. It is NOT correct for a
|
||||||
|
# dependency-only fix republished at the *same* pkgver: pkgrel
|
||||||
|
# would be forced back to 1, and no existing installation sees an
|
||||||
|
# upgrade. That case needs a manual pkgrel bump in the template
|
||||||
|
# before dispatching, which this workflow has no input for.
|
||||||
|
python3 - "$VERSION" "$DEB_NAME" "$DEB_SHA256" "$LICENSE_SHA256" "$GITHUB_REPO" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
version, deb_name, deb_sha, license_sha, github_repo = sys.argv[1:6]
|
||||||
|
|
||||||
|
with open("PKGBUILD") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
text, n = re.subn(r"(?m)^pkgver=.*$", f"pkgver={version}", text, count=1)
|
||||||
|
assert n == 1, "pkgver=... line not found"
|
||||||
|
text, n = re.subn(r"(?m)^pkgrel=.*$", "pkgrel=1", text, count=1)
|
||||||
|
assert n == 1, "pkgrel=... line not found"
|
||||||
|
|
||||||
|
old_source = (
|
||||||
|
f'source=("Triple-C_${{pkgver}}_amd64.deb::'
|
||||||
|
f'https://github.com/{github_repo}/releases/download/v${{pkgver}}/'
|
||||||
|
f'Triple-C_${{pkgver}}_amd64.deb"'
|
||||||
|
)
|
||||||
|
new_source = (
|
||||||
|
f'source=("{deb_name}::'
|
||||||
|
f'https://github.com/{github_repo}/releases/download/v{version}/{deb_name}"'
|
||||||
|
)
|
||||||
|
assert old_source in text, "source=() line does not match the expected template shape"
|
||||||
|
text = text.replace(old_source, new_source, 1)
|
||||||
|
|
||||||
|
old_sums = "sha256sums=('SKIP'\n 'SKIP')"
|
||||||
|
assert old_sums in text, "sha256sums=() placeholders not found"
|
||||||
|
text = text.replace(old_sums, f"sha256sums=('{deb_sha}'\n '{license_sha}')", 1)
|
||||||
|
|
||||||
|
with open("PKGBUILD", "w") as f:
|
||||||
|
f.write(text)
|
||||||
|
PY
|
||||||
|
|
||||||
|
grep -q "pkgver=${VERSION}$" PKGBUILD
|
||||||
|
! grep -q "SKIP" PKGBUILD
|
||||||
|
|
||||||
|
- name: Validate with makepkg and namcap
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# A bind mount (`docker run -v "$PWD/...":/work`) is the more
|
||||||
|
# obvious way to write this, and was the first draft — but on a
|
||||||
|
# containerized Gitea act_runner job, `$PWD` is a path inside this
|
||||||
|
# job's own container, which the daemon's host cannot resolve; the
|
||||||
|
# mount would silently attach an empty directory instead of failing
|
||||||
|
# loudly. `docker cp` moves real bytes across that boundary
|
||||||
|
# regardless of where the daemon actually lives, which is what
|
||||||
|
# makes this work under both a bind-mount-capable runner and a
|
||||||
|
# containerized one.
|
||||||
|
docker pull archlinux:latest
|
||||||
|
CID=$(docker create -w /work archlinux:latest bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
pacman -Syu --noconfirm --needed base-devel namcap sudo git openssh >/dev/null
|
||||||
|
useradd -m builder
|
||||||
|
chown -R builder:builder /work
|
||||||
|
echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder
|
||||||
|
sudo -u builder bash -c "cd /work && makepkg --printsrcinfo > .SRCINFO"
|
||||||
|
sudo -u builder bash -c "cd /work && makepkg -s --noconfirm"
|
||||||
|
echo "--- namcap ---"
|
||||||
|
NAMCAP_OUT=$(sudo -u builder bash -c "cd /work && namcap PKGBUILD *.pkg.tar.*" || true)
|
||||||
|
echo "$NAMCAP_OUT"
|
||||||
|
# Matches "triple-c-bin E:", "PKGBUILD (triple-c-bin) E:" and any
|
||||||
|
# split-package variant ("triple-c-bin-debug E:") alike — namcap
|
||||||
|
# uses more than one line shape for its two rule families, and
|
||||||
|
# namcap itself exits 0 regardless of what it reports, so this
|
||||||
|
# grep is the only thing standing between an E: and a green job.
|
||||||
|
if echo "$NAMCAP_OUT" | grep -q " E: "; then
|
||||||
|
echo "namcap reported an error — see above" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
')
|
||||||
|
mkdir -p rendered
|
||||||
|
docker cp rendered/. "${CID}:/work"
|
||||||
|
# `docker start -a` streams output and its exit code is the
|
||||||
|
# container's own — the same failure this would have hit with a
|
||||||
|
# bind mount still fails the job the same way.
|
||||||
|
docker start -a "${CID}"
|
||||||
|
docker cp "${CID}:/work/.SRCINFO" rendered/.SRCINFO
|
||||||
|
docker rm -f "${CID}" >/dev/null
|
||||||
|
|
||||||
|
- name: Push to AUR
|
||||||
|
env:
|
||||||
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
VERSION: ${{ steps.resolve.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then
|
||||||
|
echo "AUR_SSH_PRIVATE_KEY is not set — see this workflow file's header comment for" >&2
|
||||||
|
echo "the one-time AUR account setup this needs before it can publish anything." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
# Created with the final mode before any bytes land in it, rather
|
||||||
|
# than a plain redirect followed by chmod, which leaves the key
|
||||||
|
# world-readable for whatever window falls between the two calls.
|
||||||
|
install -m 600 /dev/null ~/.ssh/aur
|
||||||
|
echo "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur
|
||||||
|
# TOFU, not verification — accepted here because pinning AUR's
|
||||||
|
# actual host key needs a value fetched from somewhere trusted
|
||||||
|
# ahead of time, which this workflow doesn't have, and getting a
|
||||||
|
# pinned value wrong fails every future run rather than just this
|
||||||
|
# one. A keyscan failure below surfaces later as an opaque
|
||||||
|
# "Host key verification failed" rather than a clear one here.
|
||||||
|
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur -o IdentitiesOnly=yes -o UserKnownHostsFile=~/.ssh/known_hosts"
|
||||||
|
|
||||||
|
git clone "${AUR_REPO}" aur-repo
|
||||||
|
cp rendered/PKGBUILD rendered/.SRCINFO aur-repo/
|
||||||
|
cd aur-repo
|
||||||
|
git config user.name "Triple-C CI"
|
||||||
|
git config user.email "noreply@triple-c.invalid"
|
||||||
|
git add PKGBUILD .SRCINFO
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No change from what's already published on AUR for ${VERSION}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "triple-c-bin: update to ${VERSION}"
|
||||||
|
# AUR itself uses `master`, which is what a fresh, not-yet-created
|
||||||
|
# AUR package's empty repo advertises on clone — but the *local*
|
||||||
|
# branch name after cloning an empty repo falls back to whatever
|
||||||
|
# this runner's `init.defaultBranch` is if the server sends no
|
||||||
|
# symref, so naming the destination explicitly is what keeps this
|
||||||
|
# working if that default is ever `main` instead of `master`.
|
||||||
|
git push origin HEAD:master
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: Sync Release to GitHub
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Mirror release to GitHub
|
|
||||||
env:
|
|
||||||
GH_PAT: ${{ secrets.GH_PAT }}
|
|
||||||
GITHUB_REPO: shadowdao/triple-c
|
|
||||||
RELEASE_TAG: ${{ gitea.event.release.tag_name }}
|
|
||||||
RELEASE_NAME: ${{ gitea.event.release.name }}
|
|
||||||
RELEASE_BODY: ${{ gitea.event.release.body }}
|
|
||||||
IS_PRERELEASE: ${{ gitea.event.release.prerelease }}
|
|
||||||
IS_DRAFT: ${{ gitea.event.release.draft }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "==> Creating release $RELEASE_TAG on GitHub..."
|
|
||||||
|
|
||||||
RESPONSE=$(curl -sf -X POST \
|
|
||||||
-H "Authorization: Bearer $GH_PAT" \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
https://api.github.com/repos/$GITHUB_REPO/releases \
|
|
||||||
-d "{
|
|
||||||
\"tag_name\": \"$RELEASE_TAG\",
|
|
||||||
\"name\": \"$RELEASE_NAME\",
|
|
||||||
\"body\": $(echo "$RELEASE_BODY" | jq -Rs .),
|
|
||||||
\"draft\": $IS_DRAFT,
|
|
||||||
\"prerelease\": $IS_PRERELEASE
|
|
||||||
}")
|
|
||||||
|
|
||||||
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
|
||||||
echo "Release created. Upload URL: $UPLOAD_URL"
|
|
||||||
|
|
||||||
echo '${{ toJSON(gitea.event.release.assets) }}' | jq -c '.[]' | while read asset; do
|
|
||||||
ASSET_NAME=$(echo "$asset" | jq -r '.name')
|
|
||||||
ASSET_URL=$(echo "$asset" | jq -r '.browser_download_url')
|
|
||||||
|
|
||||||
echo "==> Downloading asset: $ASSET_NAME"
|
|
||||||
curl -sfL -o "/tmp/$ASSET_NAME" "$ASSET_URL"
|
|
||||||
|
|
||||||
echo "==> Uploading $ASSET_NAME to GitHub..."
|
|
||||||
ENCODED_NAME=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$ASSET_NAME")
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: Bearer $GH_PAT" \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary "@/tmp/$ASSET_NAME" \
|
|
||||||
"$UPLOAD_URL?name=$ENCODED_NAME"
|
|
||||||
|
|
||||||
echo " Uploaded: $ASSET_NAME"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "==> Release sync complete."
|
|
||||||
+12
-7
@@ -412,13 +412,18 @@ triple-c/
|
|||||||
│
|
│
|
||||||
├── .gitea/
|
├── .gitea/
|
||||||
│ └── workflows/
|
│ └── workflows/
|
||||||
│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows)
|
│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows); mirrors releases to GitHub inline
|
||||||
│ ├── build-app-preview.yml # Preview builds
|
│ ├── build-app-preview.yml # Preview builds
|
||||||
│ ├── build.yml # Build container image (multi-arch)
|
│ ├── build.yml # Build container image (multi-arch)
|
||||||
│ ├── build-stt.yml # Build the STT image
|
│ ├── build-stt.yml # Build the STT image
|
||||||
│ ├── sync-release.yml # Mirror releases to GitHub
|
│ ├── backfill-releases.yml # Bulk copy releases to GitHub
|
||||||
│ ├── backfill-releases.yml # Bulk copy releases to GitHub
|
│ ├── cleanup-releases.yml # Prune old releases
|
||||||
│ └── cleanup-releases.yml # Prune old releases
|
│ └── publish-aur-package.yml # Publish triple-c-bin to the AUR (packaging/arch/)
|
||||||
|
│
|
||||||
|
├── packaging/
|
||||||
|
│ └── arch/ # AUR triple-c-bin package — see packaging/arch/README.md
|
||||||
|
│ ├── PKGBUILD
|
||||||
|
│ └── README.md
|
||||||
│
|
│
|
||||||
└── app/ # Tauri v2 desktop application
|
└── app/ # Tauri v2 desktop application
|
||||||
├── package.json # React, xterm.js, zustand, tailwindcss
|
├── package.json # React, xterm.js, zustand, tailwindcss
|
||||||
|
|||||||
@@ -16,9 +16,37 @@ const REGISTRY_API_BASE: &str =
|
|||||||
const GHCR_TOKEN_URL: &str =
|
const GHCR_TOKEN_URL: &str =
|
||||||
"https://ghcr.io/token?scope=repository:shadowdao/triple-c-sandbox:pull";
|
"https://ghcr.io/token?scope=repository:shadowdao/triple-c-sandbox:pull";
|
||||||
|
|
||||||
|
/// The build-time preview suffix, if one was baked in and isn't blank.
|
||||||
|
///
|
||||||
|
/// The bundle version itself (`tauri.conf.json`, `Cargo.toml`, `package.json`)
|
||||||
|
/// is never given a `-preview.<sha>` suffix — `build-app-preview.yml` strips
|
||||||
|
/// it before patching those files, because the Windows MSI's `ProductVersion`
|
||||||
|
/// is a fixed-width numeric field with no room for one, and nothing here can
|
||||||
|
/// verify a change to that without an actual Windows build. `TRIPLE_C_BUILD_SUFFIX`
|
||||||
|
/// is the workaround: set as a build-time env var in the preview workflow
|
||||||
|
/// only, so `option_env!` bakes it into the binary without the bundle version
|
||||||
|
/// ever seeing it. A production build sets nothing, so `option_env!` reads
|
||||||
|
/// `None` here — see triple-c#32.
|
||||||
|
///
|
||||||
|
/// The single source of truth for "is this a preview build": both
|
||||||
|
/// `get_app_version()` (what the About panel shows) and `check_for_updates()`
|
||||||
|
/// (whether a same-numbered release counts as an update — see `pick_update`)
|
||||||
|
/// read this rather than each calling `option_env!` themselves, so the two
|
||||||
|
/// can never silently disagree about which build this is.
|
||||||
|
fn preview_build_suffix() -> Option<&'static str> {
|
||||||
|
option_env!("TRIPLE_C_BUILD_SUFFIX").filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_app_version(base: &str, build_suffix: Option<&str>) -> String {
|
||||||
|
match build_suffix {
|
||||||
|
Some(suffix) if !suffix.is_empty() => format!("{}-{}", base, suffix),
|
||||||
|
_ => base.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_app_version() -> String {
|
pub fn get_app_version() -> String {
|
||||||
env!("CARGO_PKG_VERSION").to_string()
|
format_app_version(env!("CARGO_PKG_VERSION"), preview_build_suffix())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -51,30 +79,20 @@ pub async fn check_for_updates() -> Result<Option<UpdateInfo>, String> {
|
|||||||
&[".AppImage", ".deb", ".rpm"]
|
&[".AppImage", ".deb", ".rpm"]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter releases that have at least one asset matching the current platform
|
// `current_version` above is always the bare, stripped `CARGO_PKG_VERSION`
|
||||||
let platform_releases: Vec<&GitHubRelease> = releases
|
// — the preview workflow patches `Cargo.toml` with that before compiling,
|
||||||
.iter()
|
// never the `-preview.<sha>`-suffixed one `get_app_version()` reports —
|
||||||
.filter(|r| {
|
// so a preview build and the release it precedes compile to the identical
|
||||||
r.assets.iter().any(|a| {
|
// numeric tuple by construction (see `build-app-preview.yml`'s "highest
|
||||||
platform_extensions.iter().any(|ext| a.name.ends_with(ext))
|
// tag used, +1" computation). A strict `>` therefore never fires for the
|
||||||
})
|
// one release a preview most needs to be offered. `is_preview_build`
|
||||||
})
|
// relaxes that one comparison to `>=` so "there is a real release at my
|
||||||
.collect();
|
// own number" reads as an update, without touching the production case
|
||||||
|
// — see `pick_update`.
|
||||||
|
let is_preview_build = preview_build_suffix().is_some();
|
||||||
|
|
||||||
// Find the latest release with a higher semver version
|
match pick_update(&releases, current_semver, platform_extensions, is_preview_build) {
|
||||||
let mut best: Option<(&GitHubRelease, (u32, u32, u32))> = None;
|
Some(release) => {
|
||||||
for release in &platform_releases {
|
|
||||||
if let Some(ver) = parse_semver_from_tag(&release.tag_name) {
|
|
||||||
if ver > current_semver {
|
|
||||||
if best.is_none() || ver > best.unwrap().1 {
|
|
||||||
best = Some((release, ver));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match best {
|
|
||||||
Some((release, _)) => {
|
|
||||||
// Only include assets matching the current platform
|
// Only include assets matching the current platform
|
||||||
let assets = release
|
let assets = release
|
||||||
.assets
|
.assets
|
||||||
@@ -105,6 +123,51 @@ pub async fn check_for_updates() -> Result<Option<UpdateInfo>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pick the newest available update out of a release list, or `None` if
|
||||||
|
/// nothing beats `current_semver`. Pure and synchronous — split out of
|
||||||
|
/// `check_for_updates` so the prerelease/platform/version filtering can be
|
||||||
|
/// tested without a live HTTP call.
|
||||||
|
///
|
||||||
|
/// Three filters, all of which must pass: not a prerelease (see the long
|
||||||
|
/// comment on `GitHubRelease::prerelease`), at least one asset for this
|
||||||
|
/// platform, and a tag that parses as semver *and* beats what is running. A
|
||||||
|
/// tag that does not parse — `preview-<sha>` (the shape
|
||||||
|
/// `build-app-preview.yml` actually creates release tags with), most
|
||||||
|
/// realistically — is skipped rather than erroring, the same as it always
|
||||||
|
/// has been; nothing here changes what an update tag is expected to look
|
||||||
|
/// like, only what channel it is allowed to come from.
|
||||||
|
///
|
||||||
|
/// `is_preview_build` relaxes "beats" from `>` to `>=`. A preview build's
|
||||||
|
/// `current_semver` is the bare number it was compiled with, which is by
|
||||||
|
/// construction identical to the release it precedes — see the comment at
|
||||||
|
/// `check_for_updates`'s call site — so a strict `>` would never fire for
|
||||||
|
/// exactly the release a preview install most needs to be told about.
|
||||||
|
fn pick_update<'a>(
|
||||||
|
releases: &'a [GitHubRelease],
|
||||||
|
current_semver: (u32, u32, u32),
|
||||||
|
platform_extensions: &[&str],
|
||||||
|
is_preview_build: bool,
|
||||||
|
) -> Option<&'a GitHubRelease> {
|
||||||
|
releases
|
||||||
|
.iter()
|
||||||
|
.filter(|r| !r.prerelease)
|
||||||
|
.filter(|r| {
|
||||||
|
r.assets
|
||||||
|
.iter()
|
||||||
|
.any(|a| platform_extensions.iter().any(|ext| a.name.ends_with(ext)))
|
||||||
|
})
|
||||||
|
.filter_map(|r| parse_semver_from_tag(&r.tag_name).map(|ver| (r, ver)))
|
||||||
|
.filter(|(_, ver)| {
|
||||||
|
if is_preview_build {
|
||||||
|
*ver >= current_semver
|
||||||
|
} else {
|
||||||
|
*ver > current_semver
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.max_by_key(|(_, ver)| *ver)
|
||||||
|
.map(|(r, _)| r)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a semver string like "0.2.5" -> (0, 2, 5)
|
/// Parse a semver string like "0.2.5" -> (0, 2, 5)
|
||||||
fn parse_semver(version: &str) -> Option<(u32, u32, u32)> {
|
fn parse_semver(version: &str) -> Option<(u32, u32, u32)> {
|
||||||
let clean = version.trim_start_matches('v');
|
let clean = version.trim_start_matches('v');
|
||||||
@@ -131,6 +194,120 @@ fn extract_version_from_tag(tag: &str) -> Option<String> {
|
|||||||
Some(format!("{}.{}.{}", major, minor, patch))
|
Some(format!("{}.{}.{}", major, minor, patch))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::models::GitHubAsset;
|
||||||
|
|
||||||
|
// ── format_app_version ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_production_build_reports_the_bare_version() {
|
||||||
|
assert_eq!(format_app_version("0.4.12", None), "0.4.12");
|
||||||
|
// An empty env var (set but blank) must not print a trailing dash.
|
||||||
|
assert_eq!(format_app_version("0.4.12", Some("")), "0.4.12");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_preview_build_reports_its_suffix() {
|
||||||
|
assert_eq!(
|
||||||
|
format_app_version("0.4.12", Some("preview.a1b2c3d")),
|
||||||
|
"0.4.12-preview.a1b2c3d"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── pick_update ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn release(tag: &str, prerelease: bool, asset_names: &[&str]) -> GitHubRelease {
|
||||||
|
GitHubRelease {
|
||||||
|
tag_name: tag.to_string(),
|
||||||
|
html_url: format!("https://example.invalid/{}", tag),
|
||||||
|
body: String::new(),
|
||||||
|
assets: asset_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| GitHubAsset {
|
||||||
|
name: name.to_string(),
|
||||||
|
browser_download_url: String::new(),
|
||||||
|
size: 0,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
published_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
prerelease,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINUX_EXTENSIONS: &[&str] = &[".AppImage", ".deb", ".rpm"];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_prerelease_is_never_offered_even_if_its_tag_would_otherwise_win() {
|
||||||
|
let releases = vec![release("v9.9.9", true, &["app-9.9.9.AppImage"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 10), LINUX_EXTENSIONS, false).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_release_with_no_asset_for_this_platform_is_skipped() {
|
||||||
|
let releases = vec![release("v0.4.12", false, &["app-0.4.12.msi"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 10), LINUX_EXTENSIONS, false).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_release_that_is_not_newer_is_not_offered() {
|
||||||
|
let releases = vec![release("v0.4.10", false, &["app.AppImage"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 10), LINUX_EXTENSIONS, false).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_untagged_or_unparseable_release_is_skipped_not_fatal() {
|
||||||
|
// A `-preview.<sha>` tag is exactly the shape this must not choke on
|
||||||
|
// or mistake for an update — it simply never parses as a bare semver.
|
||||||
|
let releases = vec![
|
||||||
|
release("preview-a1b2c3d", false, &["app.AppImage"]),
|
||||||
|
release("v0.4.12", false, &["app.AppImage"]),
|
||||||
|
];
|
||||||
|
let best = pick_update(&releases, (0, 4, 10), LINUX_EXTENSIONS, false).unwrap();
|
||||||
|
assert_eq!(best.tag_name, "v0.4.12");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_highest_qualifying_version_wins_not_the_first_or_last_in_the_list() {
|
||||||
|
let releases = vec![
|
||||||
|
release("v0.4.11", false, &["app.AppImage"]),
|
||||||
|
release("v0.4.13", false, &["app.AppImage"]),
|
||||||
|
release("v0.4.12", false, &["app.AppImage"]),
|
||||||
|
];
|
||||||
|
let best = pick_update(&releases, (0, 4, 10), LINUX_EXTENSIONS, false).unwrap();
|
||||||
|
assert_eq!(best.tag_name, "v0.4.13");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── is_preview_build (>= instead of >) ─────────────────────────────────
|
||||||
|
|
||||||
|
/// The exact scenario triple-c#32 was filed to fix: a preview compiled as
|
||||||
|
/// `0.4.12-preview.<sha>` (bare `CARGO_PKG_VERSION` "0.4.12") must be
|
||||||
|
/// offered the `v0.4.12` release that follows it, even though the two
|
||||||
|
/// compute to the identical numeric tuple.
|
||||||
|
#[test]
|
||||||
|
fn a_preview_build_is_offered_the_release_it_precedes() {
|
||||||
|
let releases = vec![release("v0.4.12", false, &["app.AppImage"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 12), LINUX_EXTENSIONS, false).is_none());
|
||||||
|
let best = pick_update(&releases, (0, 4, 12), LINUX_EXTENSIONS, true).unwrap();
|
||||||
|
assert_eq!(best.tag_name, "v0.4.12");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_preview_build_is_not_offered_an_older_release() {
|
||||||
|
let releases = vec![release("v0.4.11", false, &["app.AppImage"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 12), LINUX_EXTENSIONS, true).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_production_build_still_requires_strictly_newer() {
|
||||||
|
// A production build must never treat "equal" as an update — that
|
||||||
|
// would perpetually re-offer the version already running.
|
||||||
|
let releases = vec![release("v0.4.12", false, &["app.AppImage"])];
|
||||||
|
assert!(pick_update(&releases, (0, 4, 12), LINUX_EXTENSIONS, false).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check whether a newer container image is available in the registry.
|
/// Check whether a newer container image is available in the registry.
|
||||||
///
|
///
|
||||||
/// Compares the local image digest with the remote registry digest using the
|
/// Compares the local image digest with the remote registry digest using the
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
// Prevents additional console window on Windows in release
|
// Prevents additional console window on Windows in release
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
/// WebKitGTK's DMA-BUF renderer (its default accelerated-compositing path
|
||||||
|
/// since 2.42) fails outright on some Mesa/driver/compositor combinations
|
||||||
|
/// under Wayland, printing `Could not create default EGL display:
|
||||||
|
/// EGL_BAD_PARAMETER. Aborting.` straight to stderr from WebKitGTK's own C
|
||||||
|
/// code and killing the webview before Triple-C's own logging even starts —
|
||||||
|
/// see triple-c#34, reported on CachyOS/Arch with Wayland.
|
||||||
|
///
|
||||||
|
/// Set unconditionally on Linux rather than gated on `WAYLAND_DISPLAY`: that
|
||||||
|
/// variable is exported into an XWayland client's environment too, so a
|
||||||
|
/// gate on it wouldn't even cleanly separate "Wayland" from "X11" — and
|
||||||
|
/// there is no reliable heuristic at all for the actual variable that
|
||||||
|
/// matters, which Mesa/driver/compositor combination is affected. This is
|
||||||
|
/// the blunt instrument, chosen deliberately because the fallback is a real
|
||||||
|
/// trade, not a free one: the terminal's `@xterm/addon-webgl` renderer
|
||||||
|
/// (`TerminalView.tsx`) is the one surface in this app actually asking for
|
||||||
|
/// GPU compositing, and it degrades to xterm's canvas renderer under this
|
||||||
|
/// setting — slower on very heavy output, but the addon's own construction
|
||||||
|
/// is already wrapped in a fallback (`WebGL not available` is a handled
|
||||||
|
/// case, not a crash), so this is a real but graceful downgrade, traded
|
||||||
|
/// against a startup abort that has no fallback at all.
|
||||||
|
///
|
||||||
|
/// Must be set before `triple_c_lib::run()` — GTK/WebKitGTK reads it at
|
||||||
|
/// their own init time, which happens inside the Tauri builder that
|
||||||
|
/// function calls into, not at binary load.
|
||||||
|
///
|
||||||
|
/// A user who has already set this themselves is left alone. That includes
|
||||||
|
/// setting it to `0`, on the assumption WebKitGTK treats it as a boolean
|
||||||
|
/// rather than presence-only — not verified against WebKitGTK's own source,
|
||||||
|
/// so if it turns out to be presence-only, `=0` still reads as "set" here
|
||||||
|
/// and disables DMA-BUF the same as any other value, which is at least the
|
||||||
|
/// safe direction to be wrong in.
|
||||||
|
///
|
||||||
|
/// This env var also leaks to whatever the app spawns afterwards — notably
|
||||||
|
/// a cold-launched default browser via the `opener` plugin's `xdg-open`
|
||||||
|
/// call. Narrow in practice (an already-running browser just receives the
|
||||||
|
/// URL; most non-WebKitGTK browsers ignore the variable entirely), but
|
||||||
|
/// worth knowing before chasing the "links don't open" half of triple-c#34
|
||||||
|
/// as a separate, unrelated cause.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn apply_webkit_wayland_workaround() {
|
||||||
|
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
apply_webkit_wayland_workaround();
|
||||||
|
|
||||||
triple_c_lib::run()
|
triple_c_lib::run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ pub struct GitHubRelease {
|
|||||||
pub body: String,
|
pub body: String,
|
||||||
pub assets: Vec<GitHubAsset>,
|
pub assets: Vec<GitHubAsset>,
|
||||||
pub published_at: String,
|
pub published_at: String,
|
||||||
|
/// Whether GitHub itself has this release marked as a prerelease.
|
||||||
|
/// `#[serde(default)]` rather than required: every response GitHub sends
|
||||||
|
/// carries this, but nothing here should refuse to parse the rest of a
|
||||||
|
/// release over one missing field. Defaults to `false` (offered) rather
|
||||||
|
/// than `true` (excluded) — a missing field only happens if GitHub's API
|
||||||
|
/// shape changes, and "API changed, therefore updates silently stop
|
||||||
|
/// working forever" is the worse failure of the two.
|
||||||
|
///
|
||||||
|
/// `build-app.yml`'s own mirror never publishes a prerelease, but
|
||||||
|
/// `.gitea/workflows/backfill-releases.yml` forwards every Gitea release
|
||||||
|
/// unfiltered, `prerelease` included. A preview release's `preview-<sha>`
|
||||||
|
/// tag already fails semver parsing on its own, so this field is not what
|
||||||
|
/// stops *that* case — it is what stops the case tag-parsing can't catch:
|
||||||
|
/// a normally-tagged release (`v0.4.13`) that someone marks as a
|
||||||
|
/// prerelease on Gitea (a hotfix candidate, an RC) and a backfill then
|
||||||
|
/// mirrors as-is. Real defence for that case, not a no-op.
|
||||||
|
#[serde(default)]
|
||||||
|
pub prerelease: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GitHub API asset response (internal).
|
/// GitHub API asset response (internal).
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Maintainer: Triple-C Contributors
|
||||||
|
#
|
||||||
|
# This file is regenerated by .gitea/workflows/publish-aur-package.yml on every
|
||||||
|
# publish — pkgver, the source URL and sha256sums are rewritten from the real,
|
||||||
|
# already-uploaded release asset, never guessed. Editing pkgver/source/
|
||||||
|
# sha256sums by hand here only matters until the next automated run overwrites
|
||||||
|
# them; everything else (depends, pkgdesc, package()) is meant to be hand-
|
||||||
|
# maintained normally.
|
||||||
|
#
|
||||||
|
# "-bin" rather than building from source: this repackages the same .deb
|
||||||
|
# build-app.yml already produces and publishes, so a user gets exactly the
|
||||||
|
# binary the project ships and tests, and `makepkg` never needs a Rust
|
||||||
|
# toolchain, Node, or the dozen -dev packages CLAUDE.md lists for building
|
||||||
|
# Triple-C itself. The trade-off is the one every "-bin" package makes: it
|
||||||
|
# assumes the glibc the CI runner (Ubuntu 24.04) linked against is compatible
|
||||||
|
# with the installing system's — true for essentially every currently
|
||||||
|
# supported Arch install, since Arch tracks glibc newer than Ubuntu 24.04
|
||||||
|
# ships, and forward compatibility is the direction that holds.
|
||||||
|
pkgname=triple-c-bin
|
||||||
|
pkgver=0.4.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Sandbox Claude Code inside Docker containers"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://github.com/shadowdao/triple-c"
|
||||||
|
license=('MIT')
|
||||||
|
# Verified against a real release asset (v0.4.14), not Tauri's generic docs:
|
||||||
|
# downloaded Triple-C_0.4.14_amd64.deb, installed each of these into a real
|
||||||
|
# Arch container, and re-ran `ldd` on the actual binary until nothing came
|
||||||
|
# back "not found". `pango` and `libayatana-appindicator` were both in an
|
||||||
|
# earlier draft — pango isn't directly linked (gtk3 already pulls it in
|
||||||
|
# transitively, and namcap correctly flags declaring it as redundant), and
|
||||||
|
# libayatana-appindicator is in Tauri's own linux dependency list but this
|
||||||
|
# binary never links it at all: there is no tray icon or menu in this app
|
||||||
|
# (see CLAUDE.md's note that `core:menu`/`core:tray` are dropped for the
|
||||||
|
# same reason), so it was never a real dependency to begin with.
|
||||||
|
depends=('cairo' 'desktop-file-utils' 'gdk-pixbuf2' 'glib2' 'gtk3'
|
||||||
|
'hicolor-icon-theme' 'libsoup3' 'webkit2gtk-4.1')
|
||||||
|
optdepends=('docker: to actually run the sandboxed containers'
|
||||||
|
'xdg-utils: opening links from the app in your default browser')
|
||||||
|
provides=('triple-c')
|
||||||
|
conflicts=('triple-c')
|
||||||
|
# !strip: the upstream .deb's binary is already the release build Tauri
|
||||||
|
# produced and tested; re-stripping a prebuilt binary is unnecessary risk for
|
||||||
|
# no benefit. It's also what actually suppresses makepkg's debug-package
|
||||||
|
# machinery here (debug-package extraction requires strip; verified in a
|
||||||
|
# real build — with !strip alone, no debug package is produced at all).
|
||||||
|
# !debug is kept anyway, explicit about intent rather than relying on that
|
||||||
|
# side effect. Without either, makepkg built a usr/src/debug/triple-c-bin
|
||||||
|
# tree containing a dangling .build-id symlink, which is a real namcap
|
||||||
|
# error (not just the empty-directory warning it looks like) — there is no
|
||||||
|
# debug info in this release binary for the machinery to have extracted in
|
||||||
|
# the first place.
|
||||||
|
options=('!strip' '!debug')
|
||||||
|
# Tauri names the asset after `productName` verbatim ("Triple-C"), not the
|
||||||
|
# lowercase Cargo binary name — verified against the real release, not
|
||||||
|
# assumed; a lowercase guess here would 404. The LICENSE fetch is separate
|
||||||
|
# because the .deb itself carries no license file — namcap flags an MIT
|
||||||
|
# package with nothing under /usr/share/licenses/ as an error, correctly.
|
||||||
|
source=("Triple-C_${pkgver}_amd64.deb::https://github.com/shadowdao/triple-c/releases/download/v${pkgver}/Triple-C_${pkgver}_amd64.deb"
|
||||||
|
"LICENSE::https://raw.githubusercontent.com/shadowdao/triple-c/v${pkgver}/LICENSE")
|
||||||
|
sha256sums=('SKIP'
|
||||||
|
'SKIP')
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir"
|
||||||
|
# A .deb is an ar archive of debian-binary, control.tar.*, data.tar.* — `ar`
|
||||||
|
# (part of base-devel's binutils) pulls just the payload out. Extracting
|
||||||
|
# that tar directly into $pkgdir works here with no path rewriting at all:
|
||||||
|
# verified against the real archive, whose entire payload is
|
||||||
|
# usr/bin/triple-c, usr/share/applications/Triple-C.desktop and
|
||||||
|
# usr/share/icons/hicolor/*/apps/triple-c.png — Tauri's Linux bundle for
|
||||||
|
# this app carries no separate resource directory under usr/lib/, so there
|
||||||
|
# is nothing that could disagree between Debian's and Arch's package trees
|
||||||
|
# for it to land in the wrong place.
|
||||||
|
#
|
||||||
|
# Globbed rather than named literally: the publish workflow discovers the
|
||||||
|
# real asset name from the release itself specifically so a Tauri bundler
|
||||||
|
# naming change can't silently break this — naming the file again here
|
||||||
|
# would throw that away and fail this one line with an opaque "No such
|
||||||
|
# file or directory" instead. `source=()` above guarantees exactly one
|
||||||
|
# `*_amd64.deb` entry, so the glob can only ever match that one file.
|
||||||
|
ar x ./*_amd64.deb
|
||||||
|
tar xf data.tar.* -C "$pkgdir"
|
||||||
|
|
||||||
|
install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Arch / CachyOS package
|
||||||
|
|
||||||
|
`PKGBUILD` here is the AUR `triple-c-bin` package's template — see triple-c#34
|
||||||
|
(the "I would like to also have an Arch/CachyOS native version" part of it).
|
||||||
|
|
||||||
|
## Why "-bin"
|
||||||
|
|
||||||
|
It repackages the same `.deb` `build-app.yml` already produces, rather than
|
||||||
|
building from source. That means `makepkg` never needs a Rust toolchain,
|
||||||
|
Node, or the dozen `-dev` packages CLAUDE.md lists for building Triple-C
|
||||||
|
itself — and a user gets exactly the binary the project ships and tests,
|
||||||
|
built on Ubuntu 24.04 in CI. Verified end to end against a real release
|
||||||
|
(v0.4.14): downloaded the actual `.deb`, confirmed every `depends` entry
|
||||||
|
against a real `ldd` of the actual binary (two packages that looked right
|
||||||
|
from Tauri's own docs — `pango`, `libayatana-appindicator` — turned out not
|
||||||
|
to be real dependencies of *this* binary and were dropped), and ran a real
|
||||||
|
`makepkg`/`namcap`/`pacman -U` cycle rather than guessing at the shape.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
`.gitea/workflows/publish-aur-package.yml` does the actual work: given a
|
||||||
|
version (or "latest" if none is given), it finds that release's real Linux
|
||||||
|
asset on GitHub, downloads it, computes real checksums, renders this
|
||||||
|
template into a version-specific PKGBUILD, validates it with `makepkg` and
|
||||||
|
`namcap` inside a real Arch container, and pushes the result to AUR.
|
||||||
|
|
||||||
|
It is `workflow_dispatch`-only, deliberately — see the workflow file's own
|
||||||
|
header comment for why an automatic trigger isn't safe here (the same reason
|
||||||
|
`sync-release.yml` didn't work and was removed in triple-c#32).
|
||||||
|
|
||||||
|
**Before it can push anything**, an AUR account has to exist and the
|
||||||
|
`triple-c-bin` package has to have been created (or you added as a
|
||||||
|
co-maintainer) under it — both are one-time, manual steps on
|
||||||
|
https://aur.archlinux.org, since there's no API to automate creating an
|
||||||
|
account or a new package. Once that's done, add the account's SSH private
|
||||||
|
key as the `AUR_SSH_PRIVATE_KEY` secret on this repo. Until that secret
|
||||||
|
exists, the workflow fails at the "Push to AUR" step with a message saying
|
||||||
|
so, rather than silently doing nothing.
|
||||||
|
|
||||||
|
## What's hand-maintained vs. generated
|
||||||
|
|
||||||
|
`pkgver`/`pkgrel`/`source`/`sha256sums` in this file are placeholders —
|
||||||
|
the workflow rewrites them for every real publish and never commits the
|
||||||
|
result back here, so don't read this file's `pkgver` as "the last published
|
||||||
|
version." Everything else (`depends`, `pkgdesc`, `package()`) is meant to be
|
||||||
|
edited by hand normally, the same as any other PKGBUILD.
|
||||||
|
|
||||||
|
**A hand-edit made directly in the AUR repo is silently overwritten the
|
||||||
|
next time this workflow runs.** Every run renders fresh from *this*
|
||||||
|
repo's template rather than starting from whatever AUR's copy currently
|
||||||
|
looks like, so a quick fix pushed straight to AUR (bumping `pkgrel` for a
|
||||||
|
packaging-only issue, say) survives only until the next dispatch. Make
|
||||||
|
the fix here instead.
|
||||||
Reference in New Issue
Block a user