From 449626bffabbab0b4467a29392beda589767b5bc Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Thu, 16 Jul 2026 19:40:21 +0300 Subject: [PATCH] docs: add CI/release documentation and ignore dist directory Add `/dist/` to `.gitignore` to exclude locally built distribution artifacts. Document the Gitea Actions workflow, release tagging, and branch protection setup in the README. --- .gitea/workflows/release.yml | 115 ++++++++++++++++++++++++++++++++++ .gitea/workflows/tests.yml | 43 +++++++++++++ .gitignore | 1 + README.md | 15 +++++ scripts/ci/package-release.sh | 51 +++++++++++++++ scripts/ci/publish-release.sh | 80 +++++++++++++++++++++++ scripts/ci/test.sh | 19 ++++++ 7 files changed, 324 insertions(+) create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitea/workflows/tests.yml create mode 100755 scripts/ci/package-release.sh create mode 100755 scripts/ci/publish-release.sh create mode 100755 scripts/ci/test.sh diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..fbdca2b --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 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 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 their Sherpa/ONNX native 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" diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..666ce01 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -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 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 diff --git a/.gitignore b/.gitignore index 59fd219..5aa75d1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ models/silero_vad_v5.onnx # Locally built executable. /captioneer /build/ +/dist/ diff --git a/README.md b/README.md index d7a8627..a3deaf7 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,21 @@ distrobox enter --no-workdir captioneer-dev -- bash -lc ' 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`, the required Sherpa/ONNX shared libraries, the model download script, the README, and the license. The desktop command still requires GTK4 and GTK4 Layer Shell runtime libraries on the destination system. + +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 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. diff --git a/scripts/ci/package-release.sh b/scripts/ci/package-release.sh new file mode 100755 index 0000000..23ec349 --- /dev/null +++ b/scripts/ci/package-release.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)" +cd -- "$repo_root" + +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" + +# The Sherpa Go module embeds an absolute build-machine RPATH. Add a portable +# fallback so the bundled native libraries resolve beside the extracted tools. +# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader. +export CGO_LDFLAGS='-Wl,-rpath,$ORIGIN/lib' +go build -trimpath -o "$package_dir/captioneer" ./src/cmd/captioneer +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" +cp -- "$sherpa_lib_dir/libsherpa-onnx-c-api.so" "$package_dir/lib/" +cp -- "$sherpa_lib_dir/libonnxruntime.so" "$package_dir/lib/" +cp -- README.md LICENSE "$package_dir/" +cp -- scripts/download-models.sh "$package_dir/scripts/" + +for executable in captioneer captioneer-desktop; do + # shellcheck disable=SC2016 # Verify the literal ELF-loader token. + if ! readelf -d "$package_dir/$executable" | grep -Fq '$ORIGIN/lib'; then + printf 'captioneer: %s is missing the portable native-library RPATH\n' "$executable" >&2 + exit 1 + fi +done + +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" diff --git a/scripts/ci/publish-release.sh b/scripts/ci/publish-release.sh new file mode 100755 index 0000000..d2655b7 --- /dev/null +++ b/scripts/ci/publish-release.sh @@ -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 diff --git a/scripts/ci/test.sh b/scripts/ci/test.sh new file mode 100755 index 0000000..081cb12 --- /dev/null +++ b/scripts/ci/test.sh @@ -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