3 Commits
Author SHA1 Message Date
kato b3ed538820 ci(release): add patchelf to CI workflows and update release documentation
Tests / Go and GTK tests (push) Canceled after 40s
Release / Tests before release (push) Successful in 1m44s
Release / Build and publish release (push) Successful in 3m2s
- Install patchelf in both build and release jobs to support runtime library relocation.
- Update release notes to describe the private desktop runtime archive.
- Clarify in README that development builds require GTK4 dev libraries, while release archives are self-contained with lib/ directory.
- Adjust troubleshooting and release procedure to reflect new offline runtime packaging.
2026-07-16 22:09:38 +03:00
kato 76bc3d8185 ci: add nodejs24 and nodejs24-bin to CI dependencies
Tests / Go and GTK tests (push) Successful in 1m38s
Release / Tests before release (push) Successful in 2m5s
Release / Build and publish release (push) Successful in 3m23s
2026-07-16 20:48:42 +03:00
kato 449626bffa docs: add CI/release documentation and ignore dist directory
Tests / Go and GTK tests (push) Failing after 1m8s
Add `/dist/` to `.gitignore` to exclude locally built distribution
artifacts. Document the Gitea Actions workflow, release tagging, and
branch protection setup in the README.
2026-07-16 19:40:21 +03:00
7 changed files with 494 additions and 3 deletions
+115
View File
@@ -0,0 +1,115 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
code: read
jobs:
test:
name: Tests before release
runs-on: ubuntu-latest
container: registry.fedoraproject.org/fedora:44
steps:
- name: Validate release tag
shell: bash
run: |
set -Eeuo pipefail
[[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
printf 'Tag must match vMAJOR.MINOR.BUG; got %s\n' "${GITHUB_REF_NAME}" >&2
exit 1
}
- name: Install system dependencies
run: >-
dnf install -y
bzip2 curl gcc gcc-c++ git gtk4-devel gtk4-layer-shell-devel
jq libX11-devel libXfixes-devel nodejs24 nodejs24-bin patchelf pkgconf-pkg-config
pulseaudio-utils ShellCheck tar gzip binutils
- name: Check out repository
uses: https://github.com/actions/checkout@v4
- name: Install the Go version from go.mod
shell: bash
run: |
set -Eeuo pipefail
go_version="$(awk '$1 == "go" { print $2 }' go.mod)"
curl --fail --location --silent --show-error \
"https://go.dev/dl/go${go_version}.linux-amd64.tar.gz" \
--output /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
go version
- name: Run tests and static checks
run: ./scripts/ci/test.sh
publish:
name: Build and publish release
needs: test
permissions:
code: read
releases: write
runs-on: ubuntu-latest
container: registry.fedoraproject.org/fedora:44
steps:
- name: Install system dependencies
run: >-
dnf install -y
bzip2 curl gcc gcc-c++ git gtk4-devel gtk4-layer-shell-devel
jq libX11-devel libXfixes-devel nodejs24 nodejs24-bin patchelf pkgconf-pkg-config
pulseaudio-utils ShellCheck tar gzip binutils
- name: Check out repository and tags
uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Go version from go.mod
shell: bash
run: |
set -Eeuo pipefail
go_version="$(awk '$1 == "go" { print $2 }' go.mod)"
curl --fail --location --silent --show-error \
"https://go.dev/dl/go${go_version}.linux-amd64.tar.gz" \
--output /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
go version
- name: Build portable release archive
run: ./scripts/ci/package-release.sh "${GITHUB_REF_NAME}"
- name: Generate release notes
shell: bash
run: |
set -Eeuo pipefail
previous_tag="$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" --match 'v[0-9]*' 2>/dev/null || true)"
{
printf 'Automated Captioneer release for `%s`.\n\n' "${GITHUB_REF_NAME}"
printf 'The Linux x86_64 archive contains both Captioneer commands and a private desktop runtime with GTK4, GTK4 Layer Shell, and Sherpa/ONNX libraries. Models are downloaded separately with `scripts/download-models.sh`.\n'
if [[ -n "$previous_tag" ]]; then
printf '\nChanges since `%s`:\n\n' "$previous_tag"
git log --no-merges --pretty='- %s (`%h`)' "${previous_tag}..${GITHUB_REF_NAME}"
fi
} > dist/release-notes.md
- name: Publish Gitea release
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_TAG: ${{ gitea.ref_name }}
RELEASE_NOTES: dist/release-notes.md
run: >-
./scripts/ci/publish-release.sh
"dist/captioneer-${GITHUB_REF_NAME}-linux-amd64.tar.gz"
"dist/captioneer-${GITHUB_REF_NAME}-linux-amd64.tar.gz.sha256"
+43
View File
@@ -0,0 +1,43 @@
name: Tests
on:
pull_request:
push:
branches:
- '**'
permissions:
code: read
jobs:
test:
name: Go and GTK tests
runs-on: ubuntu-latest
container: registry.fedoraproject.org/fedora:44
steps:
- name: Install system dependencies
run: >-
dnf install -y
bzip2 curl gcc gcc-c++ git gtk4-devel gtk4-layer-shell-devel
jq libX11-devel libXfixes-devel nodejs24 nodejs24-bin pkgconf-pkg-config
pulseaudio-utils ShellCheck tar gzip binutils
- name: Check out repository
uses: https://github.com/actions/checkout@v4
- name: Install the Go version from go.mod
shell: bash
run: |
set -Eeuo pipefail
go_version="$(awk '$1 == "go" { print $2 }' go.mod)"
curl --fail --location --silent --show-error \
"https://go.dev/dl/go${go_version}.linux-amd64.tar.gz" \
--output /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
go version
- name: Run tests and static checks
run: ./scripts/ci/test.sh
+1
View File
@@ -5,3 +5,4 @@ models/silero_vad_v5.onnx
# Locally built executable. # Locally built executable.
/captioneer /captioneer
/build/ /build/
/dist/
+18 -3
View File
@@ -11,7 +11,7 @@ Captioneer provides two commands:
## Setup ## Setup
The terminal command requires Linux x86_64, Go 1.26.2 or newer, `pactl`, and `parec`. The desktop command additionally needs GTK4 and GTK4 Layer Shell development/runtime libraries. PipeWire works through its PulseAudio compatibility layer. The terminal command requires Linux x86_64, Go 1.26.2 or newer, `pactl`, and `parec`. Building the desktop command from source additionally needs GTK4 and GTK4 Layer Shell development libraries. Published release archives carry the desktop runtime libraries. PipeWire works through its PulseAudio compatibility layer.
Download the large recognition models once: Download the large recognition models once:
@@ -165,7 +165,7 @@ Captioneer itself has no environment-based application configuration; use the co
- **Processing overload warning:** inference fell behind and the oldest 100 ms packet was dropped. Try more `--threads` or a longer `--chunk-duration`. - **Processing overload warning:** inference fell behind and the oldest 100 ms packet was dropped. Try more `--threads` or a longer `--chunk-duration`.
- **Overlay is not always above other windows:** read the backend warning. On Wayland this usually means the compositor lacks Layer Shell; add a compositor rule for the `Captioneer Overlay` window if supported. - **Overlay is not always above other windows:** read the backend warning. On Wayland this usually means the compositor lacks Layer Shell; add a compositor rule for the `Captioneer Overlay` window if supported.
- **Overlay initialization fails:** `--output=overlay` exits with the error. `--output=both` warns and continues terminal-only. - **Overlay initialization fails:** `--output=overlay` exits with the error. `--output=both` warns and continues terminal-only.
- **Desktop binary misses shared libraries on Bazzite:** use `scripts/distrobox/run.sh`; container-built GTK binaries are not self-contained. - **A locally built desktop binary misses shared libraries:** use `scripts/distrobox/run.sh` or install the matching GTK4 and GTK4 Layer Shell runtime packages. The desktop command in a published release carries these application libraries in its private `lib/` directory; keep the extracted directory together.
## Development ## Development
@@ -201,8 +201,23 @@ distrobox enter --no-workdir captioneer-dev -- bash -lc '
Run this from the repository root. The build and run helpers resolve that path automatically. Run this from the repository root. The build and run helpers resolve that path automatically.
## Gitea Actions and releases
The `Tests` workflow runs the normal and GTK-tagged Go tests, `go vet`, and ShellCheck for every pull request and branch push. To make it mandatory for pull requests, open the repository's **Settings → Branches**, edit the protection rule for `master`, enable required status checks, and select **Tests / Go and GTK tests** after its first run. Workflow files can report the check, but the branch-protection rule is what prevents merging when it fails.
Pushing a tag that exactly follows `vMAJOR.MINOR.BUG` runs the same test suite first, then builds and publishes a Gitea release:
```bash
git tag v1.3.519
git push origin v1.3.519
```
The release contains a checksummed Linux x86_64 archive with `captioneer`, `captioneer-desktop`, and a private runtime containing GTK4, GTK4 Layer Shell, Sherpa/ONNX, and their linked non-glibc dependencies. Extract the whole archive and run `./captioneer-desktop`; do not move the executable away from its adjacent `lib/` directory. Models, `pactl`, `parec`, host fonts, the display server, graphics drivers, and glibc remain host-provided.
The release workflow uses Gitea's built-in `GITEA_TOKEN`; no personal token is required. In **Settings → Actions → General**, allow the workflow token `Read` access to code and `Write` access to releases. Gitea clamps workflow permissions to those repository settings, so a more restrictive maximum produces an actionable HTTP 403 during publishing.
## License ## License
Captioneer is licensed under the [Apache License 2.0](LICENSE). Sherpa-Onnx is Apache-2.0 licensed, GTK4 is LGPL-2.1-or-later, and GTK4 Layer Shell is MIT licensed. Captioneer links to the system GTK libraries rather than copying their source into the project. Captioneer is licensed under the [Apache License 2.0](LICENSE). Sherpa-Onnx is Apache-2.0 licensed, GTK4 is LGPL-2.1-or-later, and GTK4 Layer Shell is MIT licensed. Release archives dynamically link and redistribute the required shared libraries; `RUNTIME-LIBRARIES.tsv` records their source packages and `third-party-licenses/` contains the installed license notices supplied by those packages. Local source builds continue to link against the system libraries.
Version 1 intentionally has no animation, persisted caption history, runtime dragging, background-free mode, capture exclusion, Flatpak, desktop entry, or autostart integration. Version 1 intentionally has no animation, persisted caption history, runtime dragging, background-free mode, capture exclusion, Flatpak, desktop entry, or autostart integration.
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
cd -- "$repo_root"
for command in go ldd patchelf readelf rpm; do
command -v "$command" >/dev/null 2>&1 || {
printf 'captioneer: %s is required to build a release archive\n' "$command" >&2
exit 1
}
done
tag=${1:-}
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf 'captioneer: release tag must match vMAJOR.MINOR.BUG, got %q\n' "$tag" >&2
exit 1
fi
platform=linux-amd64
archive_name="captioneer-${tag}-${platform}.tar.gz"
package_name="captioneer-${tag}-${platform}"
package_dir="dist/$package_name"
rm -rf -- "$package_dir"
rm -f -- "dist/$archive_name" "dist/$archive_name.sha256"
mkdir -p -- \
"$package_dir/lib" \
"$package_dir/scripts" \
"$package_dir/third-party-licenses"
# The Sherpa Go module embeds an absolute build-machine RPATH. Add a portable
# build-time fallback; the final paths are normalized after dependency copying.
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
CGO_LDFLAGS='-Wl,-rpath,$ORIGIN/lib' \
go build -trimpath -o "$package_dir/captioneer" ./src/cmd/captioneer
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
CGO_LDFLAGS='-Wl,-rpath,$ORIGIN/lib' \
go build -trimpath -tags gtk \
-o "$package_dir/captioneer-desktop" ./src/cmd/captioneer-desktop
sherpa_dir="$(go list -m -f '{{.Dir}}' github.com/k2-fsa/sherpa-onnx-go-linux)"
sherpa_lib_dir="$sherpa_dir/lib/x86_64-unknown-linux-gnu"
declare -A bundled_libraries=()
declare -A copied_license_packages=()
manifest_entries="$(mktemp)"
trap 'rm -f -- "$manifest_entries"' EXIT
is_host_runtime_library() {
case "$1" in
ld-linux*.so* | libc.so.* | libm.so.* | libmvec.so.* | libdl.so.* | \
libpthread.so.* | librt.so.* | libresolv.so.* | libanl.so.* | \
libutil.so.* | libnss_*.so.* | libthread_db.so.* | libBrokenLocale.so.*)
return 0
;;
esac
return 1
}
copy_package_licenses() {
local package=$1
local license_path relative_path destination
[[ -z "${copied_license_packages[$package]:-}" ]] || return 0
copied_license_packages["$package"]=1
while IFS= read -r license_path; do
[[ -f "$license_path" && "$license_path" == /usr/share/licenses/* ]] || continue
relative_path="${license_path#/usr/share/licenses/}"
destination="$package_dir/third-party-licenses/$relative_path"
mkdir -p -- "$(dirname -- "$destination")"
cp -L -- "$license_path" "$destination"
done < <(rpm -ql "$package" 2>/dev/null || true)
}
bundle_library() {
local source=$1
local provider=${2:-}
local library_name resolved_source destination package
library_name="$(basename -- "$source")"
is_host_runtime_library "$library_name" && return
resolved_source="$(readlink -f -- "$source")"
[[ -f "$resolved_source" ]] || {
printf 'captioneer: runtime library not found: %s\n' "$source" >&2
exit 1
}
if [[ -n "${bundled_libraries[$library_name]:-}" ]]; then
if ! cmp -s -- "$resolved_source" "${bundled_libraries[$library_name]}"; then
printf 'captioneer: conflicting runtime libraries named %s\n' "$library_name" >&2
exit 1
fi
return
fi
destination="$package_dir/lib/$library_name"
cp -L -- "$resolved_source" "$destination"
bundled_libraries["$library_name"]="$resolved_source"
if [[ -z "$provider" ]] && command -v rpm >/dev/null 2>&1; then
package="$(rpm -qf --queryformat '%{NAME}\n' "$resolved_source" 2>/dev/null | head -n 1 || true)"
if [[ -n "$package" ]]; then
provider="$(rpm -q --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "$package")"
copy_package_licenses "$package"
fi
fi
provider=${provider:-unidentified-system-package}
printf '%s\t%s\n' "$library_name" "$provider" >> "$manifest_entries"
}
bundle_library "$sherpa_lib_dir/libsherpa-onnx-c-api.so" \
'github.com/k2-fsa/sherpa-onnx-go-linux'
bundle_library "$sherpa_lib_dir/libonnxruntime.so" \
'github.com/k2-fsa/sherpa-onnx-go-linux'
# ldd reports the complete linked dependency closure. Copy everything needed by
# either command except glibc and its loader, which must match the host system.
for executable in \
"$package_dir/captioneer" \
"$package_dir/captioneer-desktop"; do
ldd_output="$(ldd "$executable")"
if grep -Fq 'not found' <<< "$ldd_output"; then
printf 'captioneer: unresolved release dependencies for %s:\n%s\n' \
"$executable" "$ldd_output" >&2
exit 1
fi
while IFS= read -r library_path; do
[[ -n "$library_path" ]] || continue
if [[ "$library_path" == "$repo_root/$package_dir/lib/"* ]]; then
continue
fi
bundle_library "$library_path"
done < <(
awk '
$2 == "=>" && $3 ~ /^\// { print $3 }
$1 ~ /^\// { print $1 }
' <<< "$ldd_output"
)
done
{
printf 'Library\tProvider\n'
sort -u -- "$manifest_entries"
} > "$package_dir/RUNTIME-LIBRARIES.tsv"
# Give every object a location-relative path. Patching each library is required
# because an upstream RUNPATH (for example, Tinysparql's Fedora path) otherwise
# prevents the executable's path from resolving that library's own children.
# This avoids LD_LIBRARY_PATH, which would leak into spawned pactl/parec tools.
for library in "$package_dir"/lib/*; do
chmod u+w -- "$library"
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
patchelf --set-rpath '$ORIGIN' "$library"
chmod 0755 -- "$library"
done
for executable in captioneer captioneer-desktop; do
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
patchelf --set-rpath '$ORIGIN/lib' "$package_dir/$executable"
done
cp -- README.md LICENSE "$package_dir/"
cp -- scripts/download-models.sh "$package_dir/scripts/"
for executable in captioneer captioneer-desktop; do
dynamic_section="$(readelf -d "$package_dir/$executable")"
# shellcheck disable=SC2016 # Verify the literal ELF-loader token.
if [[ "$dynamic_section" != *'$ORIGIN/lib'* ]]; then
printf 'captioneer: %s is missing its private-runtime RUNPATH\n' \
"$executable" >&2
exit 1
fi
done
[[ -f "$package_dir/lib/libgtk4-layer-shell.so.0" ]] || {
printf 'captioneer: GTK4 Layer Shell was not included in the release runtime\n' >&2
exit 1
}
for library in "$package_dir"/lib/*; do
# shellcheck disable=SC2016 # Verify the literal ELF-loader token.
[[ "$(patchelf --print-rpath "$library")" == '$ORIGIN' ]] || {
printf 'captioneer: %s is missing its private dependency RUNPATH\n' \
"$(basename -- "$library")" >&2
exit 1
}
done
# Verify the archive itself contains every linked non-glibc dependency instead
# of allowing the build container's installed libraries to mask an omission.
for object in \
"$package_dir/captioneer" \
"$package_dir/captioneer-desktop" \
"$package_dir"/lib/*; do
while IFS= read -r needed_library; do
is_host_runtime_library "$needed_library" && continue
[[ -f "$package_dir/lib/$needed_library" ]] || {
printf 'captioneer: %s requires unbundled library %s\n' \
"$(basename -- "$object")" "$needed_library" >&2
exit 1
}
done < <(patchelf --print-needed "$object")
done
# --help exits before display/model initialization while still exercising the
# dynamic loader through the same executable release users invoke.
"$package_dir/captioneer-desktop" --help >/dev/null 2>&1
tar -C dist -czf "dist/$archive_name" "$package_name"
(
cd -- dist
sha256sum "$archive_name" > "$archive_name.sha256"
)
printf '%s\n' "$repo_root/dist/$archive_name"
printf '%s\n' "$repo_root/dist/$archive_name.sha256"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${GITEA_API_URL:?GITEA_API_URL is required}"
: "${GITEA_REPOSITORY:?GITEA_REPOSITORY is required}"
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
: "${RELEASE_TAG:?RELEASE_TAG is required}"
: "${RELEASE_NOTES:?RELEASE_NOTES is required}"
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf 'captioneer: release tag must match vMAJOR.MINOR.BUG, got %q\n' "$RELEASE_TAG" >&2
exit 1
fi
if (( $# == 0 )); then
printf 'captioneer: at least one release asset is required\n' >&2
exit 1
fi
for asset in "$@"; do
[[ -f "$asset" ]] || { printf 'captioneer: release asset not found: %s\n' "$asset" >&2; exit 1; }
done
api_base="${GITEA_API_URL%/}/repos/$GITEA_REPOSITORY"
authorization="Authorization: token $GITEA_TOKEN"
release_response="$(mktemp)"
trap 'rm -f -- "$release_response"' EXIT
status="$(curl --silent --show-error --output "$release_response" --write-out '%{http_code}' \
--header "$authorization" \
"$api_base/releases/tags/$RELEASE_TAG")"
release_payload="$(jq -n \
--arg tag "$RELEASE_TAG" \
--arg name "Captioneer $RELEASE_TAG" \
--rawfile body "$RELEASE_NOTES" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
case "$status" in
200)
release_id="$(jq -er '.id' "$release_response")"
curl --fail-with-body --silent --show-error \
--request PATCH \
--header "$authorization" \
--header 'Content-Type: application/json' \
--data "$release_payload" \
"$api_base/releases/$release_id" > "$release_response"
;;
404)
curl --fail-with-body --silent --show-error \
--request POST \
--header "$authorization" \
--header 'Content-Type: application/json' \
--data "$release_payload" \
"$api_base/releases" > "$release_response"
release_id="$(jq -er '.id' "$release_response")"
;;
*)
printf 'captioneer: Gitea release lookup failed with HTTP %s\n' "$status" >&2
cat "$release_response" >&2
exit 1
;;
esac
for asset in "$@"; do
asset_name="$(basename -- "$asset")"
while IFS= read -r attachment_id; do
[[ -n "$attachment_id" ]] || continue
curl --fail-with-body --silent --show-error \
--request DELETE \
--header "$authorization" \
"$api_base/releases/$release_id/assets/$attachment_id" >/dev/null
done < <(jq -r --arg name "$asset_name" '.assets[]? | select(.name == $name) | .id' "$release_response")
curl --fail-with-body --silent --show-error \
--request POST \
--header "$authorization" \
--form "attachment=@$asset" \
"$api_base/releases/$release_id/assets?name=$asset_name" >/dev/null
printf 'Uploaded %s\n' "$asset_name"
done
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
cd -- "$repo_root"
go test ./...
go test -tags gtk ./...
go vet ./...
go vet -tags gtk ./...
shellcheck \
scripts/download-models.sh \
scripts/distrobox/common.sh \
scripts/distrobox/build.sh \
scripts/distrobox/run.sh \
scripts/ci/test.sh \
scripts/ci/package-release.sh \
scripts/ci/publish-release.sh