diff --git a/.claude/rules/ci.md b/.claude/rules/ci.md index a19fb2af5c..c57df8a9ec 100644 --- a/.claude/rules/ci.md +++ b/.claude/rules/ci.md @@ -32,7 +32,7 @@ paths: - The base is a PR's base SHA, or on a main push the head SHA of the last green CI push run on main. No such run, or a dispatch, runs everything. A dispatch never counts as main's last green run or for a tag. - Concurrency in `ci.yml`: a PR keeps its newest run, main pushes run one at a time and a newer pending run replaces the older, and each `release/**` commit and each dispatch has its own group. `linux-packages.yml` keeps a PR's newest run too; its other runs, a release's call included, each have their own group. - macOS Tests runs the Unit and UI plans in full, with no shard, parallel, iteration or retry flag. -- A `v*` tag runs only `release.yml`, a `linux-v*` tag only `release-linux.yml`, a `windows-v*` tag only `release-windows.yml`, which also runs by dispatch and publishes nothing then (`windows/RELEASING.md`). +- A `v*` tag runs only `release.yml`, a `linux-v*` tag only `release-linux.yml`, a `windows-v*` tag only `release-windows.yml`. The last two also run by dispatch: `release-windows.yml` then publishes nothing (`windows/RELEASING.md`), and `release-linux.yml` publishes nothing with `dry-run` and otherwise uploads a packaging fix (`linux/docs/releasing.md`). - `windows-grid-gate.yml` runs only by dispatch: the Windows data grid's timing and behavior gate on `windows-2025` and `windows-11-arm`, whose numbers vary too much between hosted runners to gate a merge (`windows/docs/decisions/0003-data-grid-control.md`, Gate). `windows.yml` runs the grid's tests on every change. ## Change detector diff --git a/.github/workflows/linux-packages.yml b/.github/workflows/linux-packages.yml index 728d57b6ac..2ec13fcb9c 100644 --- a/.github/workflows/linux-packages.yml +++ b/.github/workflows/linux-packages.yml @@ -9,6 +9,14 @@ name: Linux packages # go into a fresh chroot, then the build runs there with no network, then # rpmlint. Each package is then installed in another fresh container of its # release with only its runtime dependencies and started. +# release-matrix.json names the series and releases. +# The Flatpak is built offline from the same tarballs with the release +# manifest, with every Meson suite, on the nightly and when asked for. It is +# signed with a throwaway key into repositories laid out as +# flatpak.tablepro.app serves them, validated, then installed from a +# .flatpakref and from a bundle, updated, and switched between the release +# and beta branches, in fresh Ubuntu 22.04, 24.04 and 26.04 containers that +# have only their own Flatpak. # release-linux.yml calls this workflow and uploads the source packages. # A run by hand can add arm64, which the nightly and pull requests leave out. # Given a release tag, it packages that release's tarballs instead, with @@ -42,6 +50,10 @@ on: description: Also build, test and install the arm64 .deb and .rpm type: boolean default: false + flatpak: + description: Also build the Flatpak, and install and update it from a repository + type: boolean + default: false workflow_call: inputs: deb-runners: @@ -60,6 +72,14 @@ on: description: The PPA upload number, the N in X.Y.Z-0ppaN. type: string default: "1" + flatpak: + description: Also build the Flatpak once per rpm-runners architecture, and install and update it from a repository. + type: boolean + default: false + flatpak-branch: + description: The Flatpak branch, stable or beta. + type: string + default: stable # A pull request keeps only its newest run; every other run, a release's call included, has its own group. concurrency: @@ -70,6 +90,30 @@ permissions: contents: read jobs: + # The Ubuntu series and Fedora releases come from release-matrix.json, which + # release_assets.py checks a release's assets against. + matrix: + name: Release matrix + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + ubuntu-series: ${{ steps.read.outputs.ubuntu-series }} + fedora: ${{ steps.read.outputs.fedora }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Read release-matrix.json + id: read + run: | + python3 - >> "$GITHUB_OUTPUT" <<'EOF' + import json + with open("linux/build-aux/packaging/release-matrix.json", encoding="utf-8") as file: + matrix = json.load(file) + print("ubuntu-series=" + " ".join(ubuntu["series"] for ubuntu in matrix["ubuntu"])) + print("fedora=" + json.dumps(matrix["fedora"])) + EOF + sources: name: Source tarballs runs-on: ubuntu-24.04 @@ -102,14 +146,14 @@ jobs: source-package: name: Debian source packages - needs: sources + needs: [matrix, sources] runs-on: ubuntu-24.04 timeout-minutes: 30 container: image: ubuntu:26.04 env: # Every Ubuntu series the PPA builds for. - UBUNTU_SERIES: resolute stonking + UBUNTU_SERIES: ${{ needs.matrix.outputs.ubuntu-series }} outputs: series: ${{ steps.build.outputs.series }} steps: @@ -201,14 +245,14 @@ jobs: rpm: name: RPM package (Fedora ${{ matrix.fedora }}, ${{ matrix.runner }}) - needs: sources + needs: [matrix, sources] if: inputs.release-tag == '' strategy: fail-fast: false matrix: runner: ${{ fromJSON(inputs.rpm-runners || (inputs.arm64 && '["ubuntu-24.04", "ubuntu-24.04-arm"]') || '["ubuntu-24.04"]') }} - # Every Fedora release the COPR project builds for. - fedora: ["44", "45"] + # Every Fedora release the COPR project builds for, from release-matrix.json. + fedora: ${{ fromJSON(needs.matrix.outputs.fedora) }} runs-on: ${{ matrix.runner }} timeout-minutes: 300 # On the runner itself, so the build's container can go on the larger @@ -251,12 +295,13 @@ jobs: rpm-install: name: Install the RPM package (Fedora ${{ matrix.fedora }}, ${{ matrix.runner }}) - needs: rpm + needs: [matrix, rpm] strategy: fail-fast: false matrix: runner: ${{ fromJSON(inputs.rpm-runners || (inputs.arm64 && '["ubuntu-24.04", "ubuntu-24.04-arm"]') || '["ubuntu-24.04"]') }} - fedora: ["44", "45"] + # Every Fedora release the COPR project builds for, from release-matrix.json. + fedora: ${{ fromJSON(needs.matrix.outputs.fedora) }} runs-on: ${{ matrix.runner }} timeout-minutes: 20 container: @@ -273,3 +318,107 @@ jobs: run: | linux/build-aux/packaging/smoke-test.sh \ "$(find _packages/rpm -name 'tablepro-[0-9]*.rpm' ! -name '*.src.rpm')" linux/po/LINGUAS + + # From the same tarballs as the packages, offline, with every Meson suite in + # the sandbox (build-flatpak.sh). The unsigned repository goes to + # release-linux.yml, which signs it. + flatpak: + name: Flatpak (${{ matrix.runner }}) + needs: sources + if: inputs.flatpak || github.event_name == 'schedule' + strategy: + fail-fast: false + matrix: + runner: ${{ fromJSON(inputs.rpm-runners || (inputs.arm64 && '["ubuntu-24.04", "ubuntu-24.04-arm"]') || '["ubuntu-24.04"]') }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 300 + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-51 + options: --privileged + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-sources + path: _packages/sources + - name: Build and test + env: + BRANCH: ${{ inputs.flatpak-branch || 'stable' }} + run: | + xvfb-run --auto-servernum linux/build-aux/packaging/build-flatpak.sh --branch "$BRANCH" \ + "$(find _packages/sources -name 'tablepro-*.tar.xz' ! -name '*-vendor.tar.xz')" \ + "$(find _packages/sources -name 'tablepro-*-vendor.tar.xz')" _packages/flatpak + tar -C _packages/flatpak -cf "_packages/flatpak-repo-$(uname -m).tar" repo + # One file, since an OSTree repository is thousands. + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-flatpak-repo-${{ runner.arch }} + path: _packages/flatpak-repo-*.tar + if-no-files-found: error + - name: Lint the manifest + if: runner.arch == 'X64' + run: flatpak-builder-lint manifest linux/build-aux/flatpak/app.tablepro.TablePro.json + + # Signed with a throwaway key, for http://127.0.0.1:8080, by the same + # flatpak-repo.sh release-linux.yml publishes with, on both branches + # whatever the build's own. + flatpak-test-repo: + name: Flatpak test repository + needs: flatpak + runs-on: ubuntu-24.04 + timeout-minutes: 30 + container: + image: ubuntu:26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: linux-flatpak-repo-* + path: _packages/flatpak-repo + merge-multiple: true + - name: Commit twice to both repositories, bundle the first commit and validate + run: | + for tarball in _packages/flatpak-repo/*.tar; do + mkdir -p "${tarball%.tar}" + tar -C "${tarball%.tar}" -xf "$tarball" + set -- "$@" "${tarball%.tar}/repo" + done + linux/build-aux/packaging/flatpak-test-site.sh --install-deps _packages/site "$@" + tar -C _packages -cf _packages/flatpak-test-site.tar site + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-flatpak-test-site + path: _packages/flatpak-test-site.tar + if-no-files-found: error + + # Also on the Ubuntu releases below the GNOME 50 floor, whose users have the + # Flatpak and not the packages, each with its own Flatpak. + flatpak-install: + name: Install the Flatpak (Ubuntu ${{ matrix.ubuntu }}, ${{ matrix.runner }}) + needs: flatpak-test-repo + strategy: + fail-fast: false + matrix: + runner: ${{ fromJSON(inputs.rpm-runners || (inputs.arm64 && '["ubuntu-24.04", "ubuntu-24.04-arm"]') || '["ubuntu-24.04"]') }} + ubuntu: ["22.04", "24.04", "26.04"] + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + container: + image: ubuntu:${{ matrix.ubuntu }} + options: --privileged + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-flatpak-test-site + path: _packages + - name: Install from the repositories and a bundle, start, update and switch branches + run: | + tar -C _packages -xf _packages/flatpak-test-site.tar + linux/build-aux/packaging/smoke-test-flatpak.sh _packages/site linux/po/LINGUAS diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 83a6ed42d8..191e250514 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -1,26 +1,45 @@ name: Release Linux -# A linux-vX.Y.Z tag publishes the Linux release: the source and vendor -# tarballs and the translation template on GitHub Releases, the signed Debian source package for each -# Ubuntu series to the Launchpad PPA, which builds and publishes the .deb, -# and the tag to the COPR project's custom webhook, which builds the -# Fedora packages from the release (linux/build-aux/rpm/copr-sources.sh). -# A pre-release tag such as linux-v0.2.0-rc.1 publishes a GitHub -# pre-release and skips the PPA and COPR. +# A linux-vX.Y.Z tag publishes a release and a linux-vX.Y.Z-beta.N tag a beta +# (linux/docs/decisions/0041-beta-repositories-and-release-packages.md). Both +# publish on GitHub Releases, a beta as a pre-release: the source and vendor +# tarballs, the translation template, the .deb for each Ubuntu series and the +# .rpm for each Fedora release, each built offline, tested and installed in a +# fresh container, a Flatpak bundle, all for amd64 and arm64, and SHA256SUMS. +# The Flatpak is signed into the repository https://flatpak.tablepro.app +# serves, which a bundle names for its updates; CI installed and updated it +# on Ubuntu 22.04, 24.04 and 26.04 first. Then a release uploads that +# repository's new commits, uploads the signed Debian source package for each +# Ubuntu series to the stable PPA, which builds and publishes the .deb, and +# posts the tag to the stable COPR project's custom webhook, which builds the +# Fedora packages from the release (linux/build-aux/rpm/copr-sources.sh). A +# beta goes to the beta Flatpak repository, PPA and COPR project instead. # -# Run by hand on a branch, it uploads a published release to the PPA again -# as X.Y.Z-0ppaN, from that release's tarballs with the branch's .deb -# packaging: a packaging fix that needs no new version. linux/docs/releasing.md -# has the steps. +# Run by hand with dry-run, it builds, tests and gathers everything a tag of +# the branch's version would publish, as the linux-release artifact, signs +# the Flatpak with a throwaway key, and publishes nothing. +# +# Run by hand on a branch with a tag and a revision, it uploads a published +# release to the stable PPA again as X.Y.Z-0ppaN, from that release's +# tarballs with the branch's .deb packaging: a packaging fix that needs no +# new version. linux/docs/releasing.md has the steps. # # Environment linux-release, limited to linux-v* tags and the branch a -# packaging fix runs on, with a required reviewer, holds two secrets. +# packaging fix runs on, with a required reviewer, holds six secrets. A step +# whose secret or variable is unset fails and publishes nothing. # LINUX_PPA_SIGNING_KEY: the ASCII-armored OpenPGP secret key, with no # passphrase, that signs the uploads. Its public key is registered on a -# Launchpad account that may upload to the PPA. -# LINUX_COPR_WEBHOOK: the custom webhook URL from the COPR project's -# Integrations page, ending in the package name: ...//tablepro/. -# Variable LINUX_PPA: the PPA, as owner/name. +# Launchpad account that may upload to both PPAs. +# LINUX_COPR_WEBHOOK and LINUX_COPR_WEBHOOK_BETA: the custom webhook URL +# from the Integrations page of the stable and of the beta COPR project, +# ending in the package name: ...//tablepro/. +# LINUX_FLATPAK_GPG_KEY: the ASCII-armored OpenPGP secret key, with no +# passphrase, that signs the Flatpak repositories, and nothing else. +# LINUX_FLATPAK_R2_ACCESS_KEY_ID and LINUX_FLATPAK_R2_SECRET_ACCESS_KEY: an R2 +# API token that may read and write the tablepro-flatpak bucket. +# Variables LINUX_PPA and LINUX_PPA_BETA: the stable and the beta PPA, as +# owner/name. +# Variable LINUX_FLATPAK_R2_ENDPOINT: https://.r2.cloudflarestorage.com. # Variable LINUX_BRANCH: the branch a release tag must be on, main when unset. on: @@ -28,37 +47,47 @@ on: tags: ["linux-v*"] workflow_dispatch: inputs: + dry-run: + description: Build, test and gather what a tag of this branch would publish, and publish nothing + type: boolean + default: false tag: - description: The published release to upload again, such as linux-v0.1.0 - required: true + description: The published release to upload to the PPA again, such as linux-v0.1.0 type: string + default: "" revision: description: The PPA upload number, 2 or more (X.Y.Z-0ppaN) - required: true type: string + default: "" permissions: contents: read +# Tags run one at a time, so two releases never pull and publish one Flatpak +# repository together; flatpak-publish.sh also refuses a repository that +# changed since its run pulled it. A third tag cancels the one waiting. concurrency: - group: release-linux-${{ github.ref }} + group: release-linux-${{ github.event_name == 'push' && 'tags' || github.ref }} cancel-in-progress: false jobs: verify: name: Tag, version and release notes - if: github.event_name == 'push' + if: github.event_name == 'push' || inputs.dry-run runs-on: ubuntu-24.04 timeout-minutes: 10 container: image: ubuntu:26.04 outputs: prerelease: ${{ steps.check.outputs.prerelease }} + version: ${{ steps.check.outputs.version }} + flatpak-branch: ${{ steps.check.outputs.flatpak-branch }} steps: - - name: Install git and meson + # meson introspect detects the project's compilers, C and Rust, before it reads the version. + - name: Install git, meson and the compilers run: | apt-get update - apt-get install -y --no-install-recommends ca-certificates git meson python3 + apt-get install -y --no-install-recommends ca-certificates git meson python3 gcc libc6-dev rustc - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -67,23 +96,34 @@ jobs: id: check working-directory: linux env: + EVENT: ${{ github.event_name }} TAG: ${{ github.ref_name }} LINUX_BRANCH: ${{ vars.LINUX_BRANCH || 'main' }} run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" - if ! git merge-base --is-ancestor HEAD "origin/$LINUX_BRANCH"; then - echo "::error::$GITHUB_SHA is not on $LINUX_BRANCH" - exit 1 - fi version="$(meson introspect --projectinfo meson.build \ | python3 -c 'import json, sys; print(json.load(sys.stdin)["version"])')" - tag_date="$(TZ=UTC git log -1 --date=format-local:%Y-%m-%d --format=%cd)" - python3 build-aux/packaging/check_packaging.py --source-root . \ - --version "$version" --tag "$TAG" --tag-date "$tag_date" + if [ "$EVENT" = push ]; then + if ! git merge-base --is-ancestor HEAD "origin/$LINUX_BRANCH"; then + echo "::error::$GITHUB_SHA is not on $LINUX_BRANCH" + exit 1 + fi + tag_date="$(TZ=UTC git log -1 --date=format-local:%Y-%m-%d --format=%cd)" + python3 build-aux/packaging/check_packaging.py --source-root . \ + --version "$version" --tag "$TAG" --tag-date "$tag_date" + else + # A dry run has no tag; it checks the branch as the release-metadata test does. + python3 build-aux/packaging/check_packaging.py --source-root . --version "$version" + fi case "$version" in - *-*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;; - *) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;; + *-*) prerelease=true branch=beta ;; + *) prerelease=false branch=stable ;; esac + { + echo "prerelease=$prerelease" + echo "version=$version" + echo "flatpak-branch=$branch" + } >> "$GITHUB_OUTPUT" packages: name: Packages @@ -92,6 +132,8 @@ jobs: with: deb-runners: '["ubuntu-24.04", "ubuntu-24.04-arm"]' rpm-runners: '["ubuntu-24.04", "ubuntu-24.04-arm"]' + flatpak: true + flatpak-branch: ${{ needs.verify.outputs.flatpak-branch }} # The release carries the template translators start from, made with # gettext 1.0 as build-linux.yml's Translations job makes it. @@ -130,27 +172,152 @@ jobs: path: linux/po/tablepro.pot if-no-files-found: error + # The published repository, pulled from R2, takes the new commits and their + # deltas; the bundles are made from it, so an installed bundle updates from + # it. A dry run starts an empty repository and signs it with a throwaway key. + flatpak-repo: + name: Flatpak repository + needs: [verify, packages] + runs-on: ubuntu-24.04 + environment: ${{ github.event_name == 'push' && 'linux-release' || '' }} + timeout-minutes: 60 + container: + image: ubuntu:26.04 + steps: + - name: Install Flatpak, OSTree, GnuPG and curl + run: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl unzip flatpak ostree gnupg + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: linux-flatpak-repo-* + path: _packages/flatpak-repo + merge-multiple: true + - name: Sign the new commits into the repository and bundle them + env: + EVENT: ${{ github.event_name }} + BRANCH: ${{ needs.verify.outputs.flatpak-branch }} + GPG_KEY: ${{ secrets.LINUX_FLATPAK_GPG_KEY }} + RCLONE_CONFIG_R2_TYPE: s3 + RCLONE_CONFIG_R2_PROVIDER: Cloudflare + RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.LINUX_FLATPAK_R2_ACCESS_KEY_ID }} + RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.LINUX_FLATPAK_R2_SECRET_ACCESS_KEY }} + RCLONE_CONFIG_R2_ENDPOINT: ${{ vars.LINUX_FLATPAK_R2_ENDPOINT }} + RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true" + run: | + case "$BRANCH" in + beta) dir=beta-repo ;; + *) dir=repo ;; + esac + GNUPGHOME="$(mktemp -d)" + export GNUPGHOME + trap 'gpgconf --kill all; rm -rf "$GNUPGHOME"' EXIT + if [ "$EVENT" = push ]; then + if [ -z "$GPG_KEY" ] || [ -z "$RCLONE_CONFIG_R2_ACCESS_KEY_ID" ] || \ + [ -z "$RCLONE_CONFIG_R2_SECRET_ACCESS_KEY" ] || [ -z "$RCLONE_CONFIG_R2_ENDPOINT" ]; then + echo "::error::Set the LINUX_FLATPAK_GPG_KEY, LINUX_FLATPAK_R2_ACCESS_KEY_ID and LINUX_FLATPAK_R2_SECRET_ACCESS_KEY secrets and the LINUX_FLATPAK_R2_ENDPOINT variable to publish the Flatpak" + exit 1 + fi + printf '%s\n' "$GPG_KEY" | gpg --batch --import + linux/build-aux/packaging/flatpak-publish.sh install-rclone + linux/build-aux/packaging/flatpak-publish.sh pull "$dir" "_packages/site/$dir" + else + gpg --batch --passphrase '' --quick-generate-key 'TablePro dry run ' rsa4096 sign never + fi + key="$(gpg --batch --with-colons --list-secret-keys | awk -F: '$1 == "fpr" { print $10; exit }')" + for tarball in _packages/flatpak-repo/*.tar; do + mkdir -p "${tarball%.tar}" + tar -C "${tarball%.tar}" -xf "$tarball" + linux/build-aux/packaging/flatpak-repo.sh import "${tarball%.tar}/repo" "_packages/site/$dir" "$key" + done + url="https://flatpak.tablepro.app/$dir/" + linux/build-aux/packaging/flatpak-repo.sh update "_packages/site/$dir" "$BRANCH" "$key" "$url" + linux/build-aux/packaging/flatpak-repo.sh bundle "_packages/site/$dir" "$BRANCH" "$key" "$url" _packages/bundles + tar -C _packages -cf _packages/flatpak-site.tar site + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-flatpak-bundles + path: _packages/bundles + if-no-files-found: error + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-flatpak-site + path: _packages/flatpak-site.tar + if-no-files-found: error + + # GitHub turns the '~' of a package version into '.' in an asset name, so + # release_assets.py names each package after the tag's version. + assets: + name: Release assets + needs: [verify, packages, template, flatpak-repo] + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-sources + path: _artifacts/linux-sources + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-pot + path: _artifacts/linux-pot + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: linux-deb-* + path: _artifacts + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: linux-rpm-* + path: _artifacts + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-flatpak-bundles + path: _artifacts/linux-flatpak-bundles + # Exactly what release-matrix.json declares, each package naming itself as its file does. + - name: Gather the assets and SHA256SUMS, and check them + env: + VERSION: ${{ needs.verify.outputs.version }} + run: | + python3 linux/build-aux/packaging/release_assets.py gather --version "$VERSION" _artifacts release + sudo apt-get update -q && sudo apt-get install -y --no-install-recommends rpm + python3 linux/build-aux/packaging/release_assets.py verify --version "$VERSION" release + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-release + path: release + if-no-files-found: error + github-release: name: GitHub release - needs: [verify, packages, template] + needs: [verify, assets] + if: github.event_name == 'push' runs-on: ubuntu-24.04 environment: linux-release timeout-minutes: 15 permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: linux-sources + name: linux-release path: release - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + # gh attestation verify checks a downloaded file against this provenance (README.md, Install). + - name: Attest the assets + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - name: linux-pot - path: release - # A pre-release has no CHANGELOG.md section; its notes are a draft of the fragments at the tag. + subject-path: release/* + # A beta has no CHANGELOG.md section; its notes are a draft of the fragments at the tag. - name: Install towncrier if: needs.verify.outputs.prerelease == 'true' run: | @@ -161,27 +328,67 @@ jobs: env: GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} + VERSION: ${{ needs.verify.outputs.version }} PRERELEASE: ${{ needs.verify.outputs.prerelease }} run: | - version="${TAG#linux-v}" - (cd release && sha256sum -- * > SHA256SUMS) if [ "$PRERELEASE" = true ]; then - python3 linux/build-aux/packaging/check_packaging.py --source-root linux \ - --version "$version" --tag "$TAG" - "$RUNNER_TEMP/release-venv/bin/towncrier" build --draft --dir linux --version "$version" > draft.md + "$RUNNER_TEMP/release-venv/bin/towncrier" build --draft --dir linux --version "$VERSION" > draft.md sed 1d draft.md > notes.md set -- --prerelease else python3 linux/build-aux/packaging/check_packaging.py --source-root linux \ - --version "$version" --tag "$TAG" --notes notes.md + --version "$VERSION" --tag "$TAG" --notes notes.md fi # The repository's latest release stays the macOS one. gh release create "$TAG" release/* --repo "$GITHUB_REPOSITORY" --verify-tag --latest=false \ - --title "TablePro for Linux $version" --notes-file notes.md "$@" + --title "TablePro for Linux $VERSION" --notes-file notes.md "$@" + - name: Check what the release serves + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + VERSION: ${{ needs.verify.outputs.version }} + run: | + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir published + sudo apt-get update -q && sudo apt-get install -y --no-install-recommends rpm + python3 linux/build-aux/packaging/release_assets.py verify --version "$VERSION" published + + # Objects first and summaries last (flatpak-publish.sh), after the GitHub + # release, whose bundles update from this repository. + flatpak-publish: + name: Publish the Flatpak repository + needs: [verify, flatpak-repo, github-release] + runs-on: ubuntu-24.04 + environment: linux-release + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: linux-flatpak-site + path: _packages + - name: Upload to R2 + env: + BRANCH: ${{ needs.verify.outputs.flatpak-branch }} + RCLONE_CONFIG_R2_TYPE: s3 + RCLONE_CONFIG_R2_PROVIDER: Cloudflare + RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.LINUX_FLATPAK_R2_ACCESS_KEY_ID }} + RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.LINUX_FLATPAK_R2_SECRET_ACCESS_KEY }} + RCLONE_CONFIG_R2_ENDPOINT: ${{ vars.LINUX_FLATPAK_R2_ENDPOINT }} + RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true" + run: | + case "$BRANCH" in + beta) dir=beta-repo ;; + *) dir=repo ;; + esac + tar -C _packages -xf _packages/flatpak-site.tar + sudo linux/build-aux/packaging/flatpak-publish.sh install-rclone + linux/build-aux/packaging/flatpak-publish.sh push "_packages/site/$dir" "$dir" revision: name: Release and revision to upload again - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && !inputs.dry-run runs-on: ubuntu-24.04 timeout-minutes: 5 steps: @@ -194,7 +401,7 @@ jobs: version="${TAG#linux-v}" case "$version" in "$TAG" | "" | *[!0-9.]*) - echo "::error::$TAG is not the tag of a Linux release; a pre-release is not on the PPA" + echo "::error::$TAG is not the tag of a Linux release; a beta's fix is the next beta" exit 1 ;; esac @@ -219,13 +426,12 @@ jobs: release-tag: ${{ inputs.tag }} revision: ${{ inputs.revision }} + # After the GitHub release, so a failed publish uploads nothing anywhere. ppa: name: Upload to the Launchpad PPA - needs: [verify, packages, revision-packages] + needs: [verify, github-release, revision-packages] if: >- - !cancelled() && ( - (needs.packages.result == 'success' && needs.verify.outputs.prerelease == 'false') || - needs.revision-packages.result == 'success') + !cancelled() && (needs.github-release.result == 'success' || needs.revision-packages.result == 'success') runs-on: ubuntu-24.04 environment: linux-release timeout-minutes: 30 @@ -241,10 +447,20 @@ jobs: - name: Sign the source packages and upload them env: SIGNING_KEY: ${{ secrets.LINUX_PPA_SIGNING_KEY }} - PPA: ${{ vars.LINUX_PPA }} + PRERELEASE: ${{ needs.verify.outputs.prerelease }} + STABLE_PPA: ${{ vars.LINUX_PPA }} + BETA_PPA: ${{ vars.LINUX_PPA_BETA }} run: | - if [ -z "$SIGNING_KEY" ] || [ -z "$PPA" ]; then - echo "::error::Set the LINUX_PPA_SIGNING_KEY secret and the LINUX_PPA variable to upload to the PPA" + # A packaging fix uploads a release again, so only a tag is ever a beta. + if [ "$PRERELEASE" = true ]; then + ppa="$BETA_PPA" + variable=LINUX_PPA_BETA + else + ppa="$STABLE_PPA" + variable=LINUX_PPA + fi + if [ -z "$SIGNING_KEY" ] || [ -z "$ppa" ]; then + echo "::error::Set the LINUX_PPA_SIGNING_KEY secret and the $variable variable to upload to the PPA" exit 1 fi apt-get update @@ -259,13 +475,12 @@ jobs: echo "$key:6:" | gpg --batch --import-ownertrust for changes in source/*_source.changes; do debsign -k"$key" "$changes" - dput "ppa:$PPA" "$changes" + dput "ppa:$ppa" "$changes" done copr: name: Start the COPR build needs: [verify, github-release] - if: needs.verify.outputs.prerelease == 'false' runs-on: ubuntu-24.04 environment: linux-release timeout-minutes: 5 @@ -274,13 +489,22 @@ jobs: # project's secret, which lasts until a maintainer regenerates it. - name: Post the tag to the COPR webhook env: - WEBHOOK: ${{ secrets.LINUX_COPR_WEBHOOK }} + PRERELEASE: ${{ needs.verify.outputs.prerelease }} + STABLE_WEBHOOK: ${{ secrets.LINUX_COPR_WEBHOOK }} + BETA_WEBHOOK: ${{ secrets.LINUX_COPR_WEBHOOK_BETA }} TAG: ${{ github.ref_name }} run: | - if [ -z "$WEBHOOK" ]; then - echo "::error::Set the LINUX_COPR_WEBHOOK secret to build the Fedora packages" + if [ "$PRERELEASE" = true ]; then + webhook="$BETA_WEBHOOK" + secret=LINUX_COPR_WEBHOOK_BETA + else + webhook="$STABLE_WEBHOOK" + secret=LINUX_COPR_WEBHOOK + fi + if [ -z "$webhook" ]; then + echo "::error::Set the $secret secret to build the Fedora packages" exit 1 fi build="$(curl --fail-with-body --silent --show-error --proto '=https' --request POST \ - --header 'Content-Type: text/plain' --data-raw "$TAG" "$WEBHOOK")" + --header 'Content-Type: text/plain' --data-raw "$TAG" "$webhook")" echo "COPR build $build: https://copr.fedorainfracloud.org/coprs/build/$build/" diff --git a/SECURITY.md b/SECURITY.md index a6a2ecf002..977020f1d4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,13 +17,14 @@ Write in English or Vietnamese. - The Mac app. - The iPhone and iPad app. -- The Linux app, from the Launchpad PPA, the COPR project or a source build of a release. +- The Linux app, releases and betas (`linux-vX.Y.Z-beta.N`) alike: from the Launchpad PPAs, the COPR projects, the Flatpak repositories at `flatpak.tablepro.app`, the .deb, .rpm or Flatpak bundle on a GitHub release, or a source build of a `linux-v*` tag. - The plugins, bundled with the app or installed from the [plugin registry](https://docs.tablepro.app/features/plugins). - The website, [tablepro.app](https://tablepro.app). ## Supported versions - The Mac app: the newest release line, plus security fixes on the line before it ([apple/RELEASING.md](apple/RELEASING.md#branches)). Plugins ship with the app and follow it. -- The iPhone and iPad app, the Linux app and the website: the latest release only. +- The Linux app: the latest release, and the latest beta while it is newer than that release. +- The iPhone and iPad app and the website: the latest release only. A fix ships in a new release and is listed under **Security** in [apple/CHANGELOG.md](apple/CHANGELOG.md), or in `linux/CHANGELOG.md` for the Linux app. Check that the problem is still in a supported version before you report it. diff --git a/linux/README.md b/linux/README.md index 79d7ec9f77..0d0b68927c 100644 --- a/linux/README.md +++ b/linux/README.md @@ -30,29 +30,90 @@ TablePro supports the server releases their vendors still support. CI runs each ## Install -These commands work from the first release, 0.1.0, on amd64 and arm64. +TablePro runs on amd64 and arm64. A release such as 0.1.0 goes to the stable repositories and a beta such as 0.1.0-beta.1 to the beta ones, and both go to [GitHub Releases](https://github.com/TableProApp/TablePro/releases), a beta as a pre-release. Until 0.1.0 is out, only betas have packages. -Ubuntu 26.04 and 26.10, from the Launchpad PPA: +The .deb and .rpm need the GNOME 50 libraries of Ubuntu 26.04 and Fedora 44 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor.md)). Ubuntu 22.04 and 24.04, Debian, Linux Mint, Pop!_OS and other distributions use the [Flatpak repository](#flatpak-repository). + +### Ubuntu 26.04 and 26.10 ```bash sudo add-apt-repository ppa:tablepro/tablepro sudo apt install tablepro ``` -Fedora 44 and later, from COPR: +### Fedora 44 and later ```bash sudo dnf copr enable @tablepro/tablepro sudo dnf install tablepro ``` -apt and dnf, and Software Updater and GNOME Software with them, install the updates. Other distributions build from source ([Quickstart](#quickstart)) until Flathub has the app. +apt and dnf, and Software Updater and GNOME Software with them, install the updates. + +### Flatpak repository + +From TablePro's own Flatpak repository, signed, with the GNOME 51 runtime from Flathub: + +```bash +flatpak install --from https://flatpak.tablepro.app/repo/app.tablepro.TablePro.flatpakref +flatpak run app.tablepro.TablePro +``` + +Opening that link in a browser hands it to GNOME Software or KDE Discover. `flatpak update` and the software centre install the updates. TablePro is not on Flathub itself. + +### Betas + +Add the beta repository beside the stable one. apt and dnf install the highest version the two hold, so a release replaces its betas and the next beta replaces that release. + +```bash +# Ubuntu +sudo add-apt-repository ppa:tablepro/tablepro-beta +sudo apt install tablepro + +# Fedora +sudo dnf copr enable @tablepro/tablepro-beta +sudo dnf install tablepro +``` + +Before 0.1.0, add the beta repository alone, then the stable one once 0.1.0 is out; without it the last beta stays installed until the next one. To stop testing, `sudo add-apt-repository --remove ppa:tablepro/tablepro-beta` or `sudo dnf copr disable @tablepro/tablepro-beta`. The installed beta stays until the stable repository has a newer version. + +The Flatpak beta is the branch `beta` of a second repository, and installs beside the release, as Flathub's beta does. The two branches share one set of settings and connections, and Flatpak never moves from one to the other by version: you choose which one the app's name starts. + +```bash +flatpak install --from https://flatpak.tablepro.app/beta-repo/app.tablepro.TablePro.flatpakref +flatpak run --branch=beta app.tablepro.TablePro +flatpak make-current app.tablepro.TablePro beta # the menu and `flatpak run` start the beta +flatpak make-current app.tablepro.TablePro stable # and the release again +flatpak uninstall app.tablepro.TablePro//beta # stop testing, after making the release current +``` + +A beta may store settings the release before it cannot read; [docs/troubleshooting.md](docs/troubleshooting.md#after-a-downgrade) says what that looks like. + +### From GitHub Releases + +Each release and beta on [GitHub Releases](https://github.com/TableProApp/TablePro/releases) carries the packages CI built and installed, the .deb and .rpm also tested, and their `SHA256SUMS`. The .deb and .rpm do not update themselves; an installed Flatpak bundle takes its updates from the [Flatpak repository](#flatpak-repository). + +| File | For | +|---|---| +| `tablepro--ubuntu--.deb` | Ubuntu 26.04 or 26.10, `amd64` or `arm64` | +| `tablepro--fedora--.rpm` | The Fedora release it names, `x86_64` or `aarch64` | +| `tablepro--.flatpak` | Any distribution with Flatpak, `x86_64` or `aarch64` | + +GitHub attests each file's build. Check a download against it, or against `SHA256SUMS`, before installing it: + +```bash +gh attestation verify tablepro-0.1.0-x86_64.flatpak --repo TableProApp/TablePro +sha256sum --check --ignore-missing SHA256SUMS +sudo apt install ./tablepro-0.1.0-ubuntu-26.04-amd64.deb +sudo dnf install ./tablepro-0.1.0-fedora-44-x86_64.rpm +flatpak install ./tablepro-0.1.0-x86_64.flatpak +``` [docs/troubleshooting.md](docs/troubleshooting.md) covers saved passwords, SSH, Kerberos, logs, crash reports and bug reports. ## Privacy -TablePro makes no network requests of its own: no telemetry, no update check and no crash upload. DuckDB fetches no extension unless your own SQL runs `INSTALL`. It connects to the database servers and SSH hosts you set up, and nothing else. With a provider chosen for the [AI assistant](#ai-assistant), it also sends what the assistant asks to that provider: Anthropic (`api.anthropic.com`), OpenAI (`api.openai.com`), or the Ollama or other server whose address you enter. That is your questions, your SQL, the structure of the tables involved and, for Fix with AI, the server's error message, and only from connections whose AI Assistant setting allows it; it reads no rows for it. With the [MCP server](#mcp-server) on, it also listens on `127.0.0.1` for MCP clients on this computer, which send what they read wherever their own settings say. Crash reports stay on your disk until you attach one to a bug report. Updates come from apt or dnf. +TablePro makes no network requests of its own: no telemetry, no update check and no crash upload. DuckDB fetches no extension unless your own SQL runs `INSTALL`. It connects to the database servers and SSH hosts you set up, and nothing else. With a provider chosen for the [AI assistant](#ai-assistant), it also sends what the assistant asks to that provider: Anthropic (`api.anthropic.com`), OpenAI (`api.openai.com`), or the Ollama or other server whose address you enter. That is your questions, your SQL, the structure of the tables involved and, for Fix with AI, the server's error message, and only from connections whose AI Assistant setting allows it; it reads no rows for it. With the [MCP server](#mcp-server) on, it also listens on `127.0.0.1` for MCP clients on this computer, which send what they read wherever their own settings say. Crash reports stay on your disk until you attach one to a bug report. Updates come from apt, dnf or Flatpak, and the Flatpak ones from `flatpak.tablepro.app`. ## Stack @@ -68,7 +129,7 @@ TablePro makes no network requests of its own: no telemetry, no update check and | Secrets | oo7: the Secret portal inside Flatpak, the Secret Service over D-Bus on the host ([ADR 0004](docs/decisions/0004-libsecret-secret-storage.md)) | | Storage | GSettings for preferences and window state ([ADR 0007](docs/decisions/0007-gsettings-preferences.md)), JSON files for connections and workspace state, SQLite for query history ([docs/storage.md](docs/storage.md)) | | Build | Meson driving Cargo ([ADR 0006](docs/decisions/0006-meson-cargo-build.md)) | -| Distribution | A Launchpad PPA for Ubuntu 26.04 and later and a COPR project for Fedora 44 and later, Flathub later ([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md)) | +| Distribution | A Launchpad PPA for Ubuntu 26.04 and later, a COPR project for Fedora 44 and later and a Flatpak repository of our own for every distribution, each with a beta one beside it, and the packages and a Flatpak bundle on GitHub Releases; Flathub later ([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md), [ADR 0041](docs/decisions/0041-beta-repositories-and-release-packages.md)) | ## What this is not @@ -192,9 +253,26 @@ Secret Service, file choosers, notifications and links go through portals. [ADR 0008](docs/decisions/0008-flatpak-sandbox-permissions.md) gives the reason for each grant. -The repository keeps no Flathub manifest. Flathub's policy refuses -manifests and submissions written with AI tools, so maintainers write and -submit that one by hand ([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md)). +`build-aux/flatpak/app.tablepro.TablePro.json` builds what a release +publishes: the release profile under the real app ID, offline from the two +release tarballs. `build-flatpak.sh` writes the unsigned repository and a +bundle to try. From `linux/`, with the version that `meson.build` names: + +```bash +build-aux/packaging/make-sources.sh _packages/sources +build-aux/packaging/build-flatpak.sh _packages/sources/tablepro-0.1.0.tar.xz \ + _packages/sources/tablepro-0.1.0-vendor.tar.xz _packages/flatpak +flatpak install --user _packages/flatpak/tablepro-x86_64.flatpak +``` + +`build-aux/packaging/flatpak-repo.sh` signs that repository's commits into +the one `flatpak.tablepro.app` serves, and `flatpak-publish.sh` uploads it. +`flatpak-test-site.sh` and `smoke-test-flatpak.sh` do the same with a +throwaway key on `127.0.0.1`, install, and update. + +Neither manifest is for Flathub. Flathub's policy refuses manifests and +submissions written with AI tools, so maintainers write and submit that one +by hand ([ADR 0041](docs/decisions/0041-beta-repositories-and-release-packages.md)). ### Screenshots @@ -208,13 +286,17 @@ captions in step with the metainfo. ## Packages -A `linux-vX.Y.Z` tag publishes the source and vendor tarballs on GitHub -Releases, uploads a source package for Ubuntu 26.04 and 26.10 to the -Launchpad PPA, and starts a COPR build of the spec for Fedora 44 and -later. Both build offline from those tarballs, Launchpad with the Ubuntu -Rust team's `rustc-1.97` and COPR with Fedora's Rust, and apt, dnf and -the software centres update the installed packages -([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md)). +A `linux-vX.Y.Z` tag publishes the source and vendor tarballs, the +packages and the Flatpak bundle on GitHub Releases, signs the Flatpak into +the repository at `flatpak.tablepro.app`, uploads a source package for +Ubuntu 26.04 and 26.10 to the Launchpad PPA, and starts a COPR build of +the spec for Fedora 44 and later; a `linux-vX.Y.Z-beta.N` tag goes to the +beta Flatpak repository, PPA and COPR project instead. Both build offline from +those tarballs, Launchpad with the Ubuntu Rust team's `rustc-1.97` and +COPR with Fedora's Rust, and apt, dnf and the software centres update the +installed packages +([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md), +[ADR 0041](docs/decisions/0041-beta-repositories-and-release-packages.md)). The packaging is `build-aux/debian` and `build-aux/rpm/tablepro.spec`, and [docs/releasing.md](docs/releasing.md) has the release steps. diff --git a/linux/ROADMAP.md b/linux/ROADMAP.md index 14f2a3fd87..966c5a482c 100644 --- a/linux/ROADMAP.md +++ b/linux/ROADMAP.md @@ -161,8 +161,8 @@ The app runs on GNOME 50 ([ADR 0015](docs/decisions/0015-gnome-50-platform-floor | Capability | Detail | |---|---| -| Flatpak | The Devel manifest builds on GNOME 51 for x86_64 and aarch64 with every Meson suite in the sandbox, and is linted, in the nightly Linux run | -| .deb and .rpm | Ubuntu 26.04 and 26.10 and Fedora 44 and 45, built offline in CI the way Launchpad and COPR build them, linted, then installed and started in fresh containers; a `linux-v*` tag uploads to the PPA and starts the COPR build ([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md)) | +| Flatpak | The Devel manifest builds on GNOME 51 for x86_64 and aarch64 with every Meson suite in the sandbox, and is linted, in the nightly Linux run; the release manifest builds the Flatpak a release signs into `flatpak.tablepro.app` and bundles for GitHub, offline from the release tarballs; CI tests it, signs it into test repositories, then installs, starts, updates and switches branches on Ubuntu 22.04, 24.04 and 26.04 ([ADR 0041](docs/decisions/0041-beta-repositories-and-release-packages.md)) | +| .deb and .rpm | Ubuntu 26.04 and 26.10 and Fedora 44 and 45, built offline in CI the way Launchpad and COPR build them, linted, then installed and started in fresh containers; a `linux-v*` tag publishes them on GitHub Releases, uploads to the PPA and starts the COPR build, a beta tag to the beta ones ([ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md), [ADR 0041](docs/decisions/0041-beta-repositories-and-release-packages.md)) | | Desktop integration | Desktop file with Open With for SQLite files, D-Bus activation, a man page, metainfo with screenshots, and SVG icons | | Translations | Vietnamese ships. The app's own strings go through gettext, and CI fails when `POTFILES.in` is stale or a translation loses a placeholder. CI builds the template with gettext 1.0 and publishes it from main and with each release; translators send pull requests against `po/*.po` ([po/README.md](po/README.md#where-translators-work)) | | Logs | stderr, which the session journals, with JSON lines on request. A panic also writes a crash report under `$XDG_STATE_HOME`, offered with Show Report on the next launch | @@ -184,7 +184,7 @@ Not built yet, in no order. ### Release and project -- Before the first tag, the release setup [ADR 0018](docs/decisions/0018-ppa-copr-and-flathub-later.md#consequences) lists: the PPA, the COPR project, and the secrets and variable `release-linux.yml` names in its header. +- Before the first tag, the release setup in [docs/releasing.md](docs/releasing.md#setup-before-the-first-release): the Launchpad team and both PPAs, the COPR group and both projects, the R2 bucket, domain, token and key of the Flatpak repository, and the secrets and variables `release-linux.yml` names in its header. - Write the Flathub manifest and the exception request by hand, and submit them. - An AUR PKGBUILD, kept by the community. - More languages: French, Korean, Turkish, and Simplified and Traditional Chinese, which the macOS app ships. diff --git a/linux/build-aux/debian/README.source b/linux/build-aux/debian/README.source index 03203c7930..2674c0f8cf 100644 --- a/linux/build-aux/debian/README.source +++ b/linux/build-aux/debian/README.source @@ -29,8 +29,7 @@ uploads of one upstream version. The series version keeps each series' version unique and sorts a later series higher. It leaves out the word ubuntu, which dpkg-source reads as an Ubuntu upload by a non-Ubuntu maintainer, and a third dotted part, which lintian reads as a binary NMU. -An upstream pre-release such as 0.2.0-rc.1 becomes 0.2.0~rc.1, which sorts -below 0.2.0. +A beta such as 0.2.0-beta.1 becomes 0.2.0~beta.1, which sorts below 0.2.0. debian/copyright is generated from linux/data/licenses/third-party.json by linux/build-aux/update-third-party-licenses.sh. Do not edit it by hand. diff --git a/linux/build-aux/flatpak/app.tablepro.TablePro.json b/linux/build-aux/flatpak/app.tablepro.TablePro.json new file mode 100644 index 0000000000..b9f957b2ae --- /dev/null +++ b/linux/build-aux/flatpak/app.tablepro.TablePro.json @@ -0,0 +1,81 @@ +{ + "id": "app.tablepro.TablePro", + "runtime": "org.gnome.Platform", + "runtime-version": "51", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable", + "org.freedesktop.Sdk.Extension.llvm22" + ], + "command": "tablepro", + "separate-locales": false, + "finish-args": [ + "--share=ipc", + "--share=network", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=ssh-auth", + "--device=dri", + "--filesystem=home", + "--filesystem=/media", + "--filesystem=/run/media", + "--filesystem=/mnt", + "--filesystem=/run/.heim_org.h5l.kcm-socket", + "--env=KRB5_CONFIG=/app/etc/krb5.conf" + ], + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm22/bin", + "prepend-ld-library-path": "/usr/lib/sdk/llvm22/lib", + "cxxflags": "-g0", + "env": { + "LIBCLANG_PATH": "/usr/lib/sdk/llvm22/lib", + "CARGO_NET_OFFLINE": "true", + "CARGO_PROFILE_RELEASE_LTO": "thin", + "CARGO_PROFILE_RELEASE_DEBUG": "line-tables-only" + }, + "test-args": [ + "--socket=x11", + "--share=network" + ], + "arch": { + "aarch64": { + "env": { + "CARGO_ENCODED_RUSTFLAGS": "-Clink-arg=-fuse-ld=lld" + } + } + } + }, + "cleanup": [ + "/share/man" + ], + "modules": [ + { + "name": "tablepro", + "buildsystem": "meson", + "subdir": "linux", + "run-tests": true, + "config-opts": [ + "-Dprofile=default", + "-Dflatpak=true" + ], + "sources": [ + { + "type": "archive", + "path": "tablepro.tar.xz" + }, + { + "type": "archive", + "path": "tablepro-vendor.tar.xz", + "strip-components": 0 + }, + { + "type": "shell", + "commands": [ + "mkdir -p .cargo", + "cp vendor/cargo-config.toml .cargo/config.toml" + ] + } + ] + } + ] +} diff --git a/linux/build-aux/packaging/build-flatpak.sh b/linux/build-aux/packaging/build-flatpak.sh new file mode 100755 index 0000000000..2e87c8f782 --- /dev/null +++ b/linux/build-aux/packaging/build-flatpak.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Builds TablePro as a Flatpak from the two release tarballs, offline, with +# build-aux/flatpak/app.tablepro.TablePro.json, on the branch given (stable +# when none is), and runs every Meson suite in the build sandbox. Writes +# /repo, an unsigned OSTree repository with the app and its +# .Debug extension that flatpak-repo.sh signs into the published one, and +# /tablepro-.flatpak, an unsigned bundle to try the build. +# The manifest reads the tarballs as tablepro.tar.xz and +# tablepro-vendor.tar.xz beside it, so they go next to a copy of it. The SDK +# and its extensions come from Flathub into this user's installation: one +# baked into a container image cannot update in place. +# +# The gtk suite needs a display; without one, run this under xvfb-run. +# Usage: build-flatpak.sh [--branch stable|beta] +set -eu + +usage() { + echo "usage: build-flatpak.sh [--branch stable|beta] " >&2 + exit 2 +} + +branch=stable +if [ "${1:-}" = --branch ]; then + [ $# -ge 2 ] || usage + branch="$2" + shift 2 +fi +case "$branch" in +stable | beta) ;; +*) usage ;; +esac +[ $# -eq 3 ] || usage +here="$(cd "$(dirname "$0")" && pwd)" +mkdir -p "$3" +out="$(cd "$3" && pwd)" + +work="$(mktemp -d "${TMPDIR:-/var/tmp}/tablepro-flatpak.XXXXXX")" +trap 'rm -rf "$work"' EXIT +cp "$here/../flatpak/app.tablepro.TablePro.json" "$work/" +cp "$1" "$work/tablepro.tar.xz" +cp "$2" "$work/tablepro-vendor.tar.xz" + +flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo +rm -rf "$out/repo" +flatpak-builder --user --install-deps-from=flathub --disable-rofiles-fuse --force-clean \ + --default-branch="$branch" --state-dir="$work/state" --repo="$out/repo" \ + "$work/build" "$work/app.tablepro.TablePro.json" +arch="$(flatpak --default-arch)" +flatpak build-bundle --runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo \ + "$out/repo" "$out/tablepro-$arch.flatpak" app.tablepro.TablePro "$branch" +ls -l "$out" diff --git a/linux/build-aux/packaging/build-source.sh b/linux/build-aux/packaging/build-source.sh index c0a1ce9b99..2f218936e0 100755 --- a/linux/build-aux/packaging/build-source.sh +++ b/linux/build-aux/packaging/build-source.sh @@ -52,8 +52,8 @@ shift 3 upstream="$(basename "$dist" .tar.xz)" upstream="${upstream#tablepro-}" -# Debian sorts a pre-release below its release through a tilde, so -# 0.2.0-rc.1 becomes 0.2.0~rc.1. +# Debian sorts a beta below its release through a tilde, so +# 0.2.0-beta.1 becomes 0.2.0~beta.1. debian_upstream="$(printf '%s\n' "$upstream" | sed 's/-/~/')" work="$(mktemp -d)" @@ -70,7 +70,7 @@ tar -xf "$vendor" -C "$src" cp -R "${debian:-$src/linux/build-aux/debian}" "$src/debian" maintainer="$(sed -n 's/^Maintainer: //p' "$src/debian/control")" if [ "$revision" = 1 ]; then - message="New upstream release. Notes: https://github.com/TableProApp/TablePro/blob/linux-v$upstream/linux/CHANGELOG.md" + message="New upstream release. Notes: https://github.com/TableProApp/TablePro/releases/tag/linux-v$upstream" else message="Packaging update of the linux-v$upstream release, upload $revision." fi diff --git a/linux/build-aux/packaging/check_packaging.py b/linux/build-aux/packaging/check_packaging.py index 2d166268b0..f02c3ba5b8 100644 --- a/linux/build-aux/packaging/check_packaging.py +++ b/linux/build-aux/packaging/check_packaging.py @@ -4,12 +4,12 @@ # translations exactly when po/LINGUAS ships one, and that the Debian # package builds with the Rust the workspace names as its rust-version. The # release workflow adds the tag and its date, and then CHANGELOG.md must -# carry that version's notes, unless it is a pre-release, and the spec must -# be the version's first package release. +# carry that version's notes, unless it is a beta, and the spec must be the +# version's first package release. # -# Once CHANGELOG.md has a heading for the version, the release commit has -# set its dates, and the heading, the spec's %changelog entry and the man -# page must carry the metainfo's release date. +# A beta's version exists only from its release commit on, as does a +# release's CHANGELOG.md heading. From then the heading, the spec's +# %changelog entry and the man page must carry the metainfo's release date. import argparse import re @@ -29,9 +29,8 @@ RULES = Path("build-aux/debian/rules") MAN = Path("data/tablepro.1") -# X.Y.Z, or a pre-release such as X.Y.Z-rc.1 with no '-' after the first, -# so the package version below stays a plain substitution. -RELEASE_VERSION = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$") +# X.Y.Z, or the beta X.Y.Z-beta.N, which goes to the beta repositories. +RELEASE_VERSION = re.compile(r"^\d+\.\d+\.\d+(?:-beta\.[1-9]\d*)?$") SPEC_VERSION = re.compile(r"^Version:\s*(\S+)\s*$", re.MULTILINE) SPEC_CHANGELOG = re.compile(r"^%changelog\s*\n\*[^\n]*\s-\s(\S+)\s*$", re.MULTILINE) SPEC_RELEASE = re.compile(r"^Release:\s*(\S+)\s*$", re.MULTILINE) @@ -45,11 +44,15 @@ def package_version(version): - # RPM forbids '-' in Version and git forbids '~' in a tag. Debian and - # Fedora both sort X.Y.Z~rc.1 below X.Y.Z. + # RPM forbids '-' in Version and git forbids '~' in a tag. dpkg, RPM and + # AppStream all sort X.Y.Z~beta.1 below X.Y.Z, and X.Y.Z-beta.1 above it. return version.replace("-", "~", 1) +def is_beta(version): + return "-" in version + + def newest_release(metainfo): releases = ElementTree.parse(metainfo).getroot().find("releases") if releases is None: @@ -164,12 +167,12 @@ def toolchain_problems(root): def problems(root, version, tag=None, tag_date=None): found = [] if not RELEASE_VERSION.match(version): - found.append(f"version {version} is neither X.Y.Z nor a pre-release such as X.Y.Z-rc.1") + found.append(f"version {version} is neither X.Y.Z nor a beta such as X.Y.Z-beta.1") if tag is not None: if tag != f"linux-v{version}": found.append(f"tag {tag} does not name version {version}; expected linux-v{version}") - # A pre-release keeps the fragments; its notes are a towncrier draft of them. - if "-" not in version and not release_notes(root / CHANGELOG, version): + # A beta keeps the fragments; its notes are a towncrier draft of them. + if not is_beta(version) and not release_notes(root / CHANGELOG, version): found.append(f"{CHANGELOG} has no notes under ## [{version}]") packaged = package_version(version) @@ -180,10 +183,14 @@ def problems(root, version, tag=None, tag_date=None): if release is None: found.append(f"{METAINFO} has no ") else: - if release.get("version") != version: + if release.get("version") != packaged: found.append( - f"the newest release in {METAINFO} is {release.get('version')}, not {version}" + f"the newest release in {METAINFO} is {release.get('version')}, not {packaged}" ) + kind = release.get("type", "stable") + expected_kind = "development" if is_beta(version) else "stable" + if kind != expected_kind: + found.append(f'the newest release in {METAINFO} has type="{kind}", not "{expected_kind}"') released = release.get("date", "") dated = True try: @@ -196,8 +203,10 @@ def problems(root, version, tag=None, tag_date=None): f"the newest release in {METAINFO} is dated {released or 'nothing'}, " f"the tagged commit {tag_date}" ) - released_at_commit = tag is not None or release_heading_date(root / CHANGELOG, version) is not None - if dated and release.get("version") == version and released_at_commit: + released_at_commit = ( + tag is not None or is_beta(version) or release_heading_date(root / CHANGELOG, version) is not None + ) + if dated and release.get("version") == packaged and released_at_commit: found.extend(release_date_problems(root, version, spec, released, spec_opens_with_version)) versions = SPEC_VERSION.findall(spec) diff --git a/linux/build-aux/packaging/flatpak-publish.sh b/linux/build-aux/packaging/flatpak-publish.sh new file mode 100755 index 0000000000..e9007a5941 --- /dev/null +++ b/linux/build-aux/packaging/flatpak-publish.sh @@ -0,0 +1,164 @@ +#!/bin/sh +# Copies a TablePro Flatpak repository between this machine and the +# tablepro-flatpak R2 bucket that https://flatpak.tablepro.app serves, with +# rclone and the r2 remote that the RCLONE_CONFIG_R2_* variables configure. +# +# pull records the published summary's SHA-256 beside the local copy, and +# push refuses to publish when the bucket's summary is neither that one nor +# the local one: another run published in between, and its objects would be +# deleted. The local one is this push's own, interrupted, which a rerun of +# the job finishes. A listing that fails stops both, so a failed read never +# looks like an empty repository. +# +# push uploads the new objects and deltas first, then the refs, the +# .flatpakrepo, the .flatpakref and last the summaries, so a client never +# reads a summary that names an object not there yet. Only then does it +# delete what pruning removed, which no summary names any more. +# +# Usage: +# flatpak-publish.sh install-rclone +# Installs rclone 1.75.2, checked against its SHA-256, as root. +# flatpak-publish.sh pull +# flatpak-publish.sh push +# No globbing: the rclone filters are patterns. +set -euf + +bucket=r2:tablepro-flatpak +rclone_version=1.75.2 + +usage() { + sed -n 's/^# //p' "$0" >&2 + exit 2 +} + +check_dir() { + case "$1" in + repo | beta-repo) ;; + *) + echo "flatpak-publish.sh: the repository is repo or beta-repo, not $1" >&2 + exit 2 + ;; + esac +} + +install_rclone() { + case "$(uname -m)" in + x86_64) + platform=linux-amd64 + sum=349ac8fba6ff65d6247043f1750cdcb518ec5d500ef91463a10d37c0ccdf3702 + ;; + aarch64) + platform=linux-arm64 + sum=7e1e8d69654941b7b7df84ee74c5f7fb09cce7d5496947fb7bf16b55ff427d10 + ;; + *) + echo "flatpak-publish.sh: no rclone for $(uname -m)" >&2 + exit 1 + ;; + esac + work="$(mktemp -d)" + name="rclone-v$rclone_version-$platform" + curl --fail --silent --show-error --location --proto '=https' --output "$work/$name.zip" \ + "https://github.com/rclone/rclone/releases/download/v$rclone_version/$name.zip" + echo "$sum $work/$name.zip" | sha256sum --check --strict + unzip -q "$work/$name.zip" -d "$work" + install -m 0755 "$work/$name/rclone" /usr/local/bin/rclone + rm -rf "$work" + rclone version +} + +# ostree's own working state never leaves the machine. +local_only() { + set -- --exclude "/tmp/**" --exclude "/state/**" --exclude "/.lock" + echo "$@" +} + +fail() { + echo "flatpak-publish.sh: $*" >&2 + exit 1 +} + +# The SHA-256 of the published summary, or none before the first publication. +published_summary() { + status=0 + listing="$(rclone lsf --files-only --max-depth 1 "$bucket/$1")" || status=$? + case "$status" in + 0) ;; + # rclone's exit status for a directory that does not exist. + 3) + echo none + return 0 + ;; + *) + echo "flatpak-publish.sh: could not list $bucket/$1, rclone exit status $status" >&2 + return 1 + ;; + esac + if ! printf '%s\n' "$listing" | grep -qx summary; then + echo none + return 0 + fi + copy="$(mktemp)" + rclone copyto "$bucket/$1/summary" "$copy" || return 1 + sha256sum "$copy" | cut -d ' ' -f 1 + rm -f "$copy" +} + +pull() { + check_dir "$1" + mkdir -p "$2" + summary="$(published_summary "$1")" || fail "not pulling $1" + if [ "$summary" != none ]; then + rclone copy --fast-list "$bucket/$1" "$2" + fi + echo "$summary" > "$2.published-summary" +} + +push() { + check_dir "$2" + from="$1" + to="$bucket/$2" + [ -f "$from.published-summary" ] || fail "$from was not pulled with flatpak-publish.sh pull" + summary="$(published_summary "$2")" || fail "not pushing to $2" + own="$(sha256sum "$from/summary" | cut -d ' ' -f 1)" + if [ "$summary" != "$(cat "$from.published-summary")" ] && [ "$summary" != "$own" ]; then + fail "$2 changed since $from was pulled; run the release again" + fi + meta="Cache-Control: no-cache" + # shellcheck disable=SC2046 # one word per option + rclone copy --fast-list $(local_only) --exclude "/refs/**" --exclude "/summaries/**" \ + --exclude "/summary*" --exclude "/config" --exclude "/*.flatpakrepo" --exclude "/*.flatpakref" \ + "$from" "$to" + # shellcheck disable=SC2046 + rclone copy --fast-list $(local_only) --header-upload "$meta" \ + --include "/refs/**" --include "/config" --include "/*.flatpakrepo" --include "/*.flatpakref" \ + "$from" "$to" + # shellcheck disable=SC2046 + rclone copy --fast-list $(local_only) --header-upload "$meta" --include "/summaries/**" "$from" "$to" + for file in summary.idx summary.idx.sig summary summary.sig; do + if [ -f "$from/$file" ]; then + rclone copyto --header-upload "$meta" "$from/$file" "$to/$file" + fi + done + # shellcheck disable=SC2046 + rclone sync --fast-list $(local_only) "$from" "$to" +} + +[ $# -ge 1 ] || usage +command="$1" +shift +case "$command" in +install-rclone) + [ $# -eq 0 ] || usage + install_rclone + ;; +pull) + [ $# -eq 2 ] || usage + pull "$@" + ;; +push) + [ $# -eq 2 ] || usage + push "$@" + ;; +*) usage ;; +esac diff --git a/linux/build-aux/packaging/flatpak-repo.sh b/linux/build-aux/packaging/flatpak-repo.sh new file mode 100755 index 0000000000..aee72146bc --- /dev/null +++ b/linux/build-aux/packaging/flatpak-repo.sh @@ -0,0 +1,149 @@ +#!/bin/sh +# Keeps a TablePro Flatpak repository as flatpak.tablepro.app serves it: +# repo/ holds releases on the branch stable and beta-repo/ betas on the +# branch beta, as Flathub keeps flathub and flathub-beta. Every commit and +# the summary are signed with one GPG key from GNUPGHOME, the summary carries +# static deltas, each ref keeps its last 3 commits, and the repository holds +# a .flatpakrepo that adds it as a remote and a .flatpakref that installs the +# app, with Flathub named for the runtime. release-linux.yml runs this with +# the release key; CI's install test with a throwaway one. +# +# Usage: +# flatpak-repo.sh import [] +# Commits the app and its .Debug extension of each architecture in the +# build repo to , signed, on when given. +# flatpak-repo.sh update +# Writes the signed summary and the deltas, prunes, and writes the +# .flatpakrepo and .flatpakref that name , where is served. +# flatpak-repo.sh bundle +# Writes tablepro-.flatpak for each architecture; an installed +# bundle updates from . +set -eu + +app=app.tablepro.TablePro +flathub=https://dl.flathub.org/repo/flathub.flatpakrepo + +usage() { + sed -n 's/^# //p' "$0" >&2 + exit 2 +} + +check_branch() { + case "$1" in + stable | beta) ;; + *) + echo "flatpak-repo.sh: the branch is stable or beta, not $1" >&2 + exit 2 + ;; + esac +} + +app_refs() { + ostree refs --repo="$1" | grep "^app/$app/[^/]*/$2\$" || true +} + +import() { + src="$1" + repo="$2" + if [ -n "${4:-}" ]; then + check_branch "$4" + fi + if [ ! -d "$repo/objects" ]; then + mkdir -p "$repo" + ostree init --mode=archive-z2 --repo="$repo" + fi + if [ -z "$(ostree refs --repo="$src" | grep "^app/$app/" || true)" ]; then + echo "flatpak-repo.sh: $src holds no $app" >&2 + exit 1 + fi + for ref in $(ostree refs --repo="$src" | grep -E "^(app/$app|runtime/$app\.Debug)/" || true); do + target="$ref" + if [ -n "${4:-}" ]; then + target="${ref%/*}/$4" + fi + flatpak build-commit-from --gpg-sign="$3" --src-repo="$src" --src-ref="$ref" \ + --no-update-summary --force "$repo" "$target" + done +} + +update() { + repo="$1" + branch="$2" + url="$4" + check_branch "$branch" + if [ "$branch" = beta ]; then + title="TablePro beta" + remote=tablepro-beta + else + title=TablePro + remote=tablepro + fi + if [ -z "$(app_refs "$repo" "$branch")" ]; then + echo "flatpak-repo.sh: $repo holds no $app on $branch" >&2 + exit 1 + fi + flatpak build-update-repo --gpg-sign="$3" --generate-static-deltas --prune --prune-depth=3 \ + --title="$title" --homepage=https://tablepro.app --default-branch="$branch" "$repo" + key="$(gpg --batch --export "$3" | base64 --wrap=0)" + cat > "$repo/$remote.flatpakrepo" < "$repo/$app.flatpakref" < "$keys" + refs="$(app_refs "$repo" "$branch")" + if [ -z "$refs" ]; then + echo "flatpak-repo.sh: $repo holds no $app on $branch" >&2 + exit 1 + fi + for ref in $refs; do + arch="$(echo "$ref" | cut -d/ -f3)" + flatpak build-bundle --arch="$arch" --repo-url="$url" --runtime-repo="$flathub" \ + --gpg-keys="$keys" "$repo" "$5/tablepro-$arch.flatpak" "$app" "$branch" + done + rm -f "$keys" +} + +[ $# -ge 1 ] || usage +command="$1" +shift +case "$command" in +import) + [ $# -eq 3 ] || [ $# -eq 4 ] || usage + import "$@" + ;; +update) + [ $# -eq 4 ] || usage + update "$@" + ;; +bundle) + [ $# -eq 5 ] || usage + bundle "$@" + ;; +*) usage ;; +esac diff --git a/linux/build-aux/packaging/flatpak-test-site.sh b/linux/build-aux/packaging/flatpak-test-site.sh new file mode 100755 index 0000000000..9d3b7517df --- /dev/null +++ b/linux/build-aux/packaging/flatpak-test-site.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# Makes the repositories smoke-test-flatpak.sh serves, from the build +# repositories of build-flatpak.sh, with flatpak-repo.sh and a throwaway key, +# for http://127.0.0.1:8080/: repo/ on the branch stable and beta-repo/ on +# the branch beta, as flatpak.tablepro.app lays them out. /one +# holds them after one commit per architecture, /two after a +# second, so an installed app has an update to take, and +# /bundles/ the bundles of the first commit. It also +# validates the desktop entry, the metainfo and the schema each app commit +# installs. +# +# --install-deps first installs, as root in a throwaway Ubuntu, the tools +# this needs. +# Usage: flatpak-test-site.sh [--install-deps] ... +set -eu + +if [ "${1:-}" = --install-deps ]; then + shift + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends ca-certificates flatpak ostree gnupg \ + desktop-file-utils appstream libglib2.0-bin +fi +if [ $# -lt 2 ]; then + echo "usage: flatpak-test-site.sh [--install-deps] ..." >&2 + exit 2 +fi +here="$(cd "$(dirname "$0")" && pwd)" +app=app.tablepro.TablePro +mkdir -p "$1" +out="$(cd "$1" && pwd)" +shift + +GNUPGHOME="$(mktemp -d)" +export GNUPGHOME +work="$(mktemp -d "${TMPDIR:-/var/tmp}/tablepro-site.XXXXXX")" +trap 'gpgconf --kill all; rm -rf "$GNUPGHOME" "$work"' EXIT +gpg --batch --passphrase '' --quick-generate-key 'TablePro CI ' rsa4096 sign never +key="$(gpg --batch --with-colons --list-secret-keys | awk -F: '$1 == "fpr" { print $10; exit }')" + +commit() { + for pair in stable:repo beta:beta-repo; do + branch="${pair%%:*}" + dir="${pair#*:}" + for build in "$@"; do + "$here/flatpak-repo.sh" import "$build" "$work/$dir" "$key" "$branch" + done + "$here/flatpak-repo.sh" update "$work/$dir" "$branch" "$key" "http://127.0.0.1:8080/$dir/" + done +} + +rm -rf "$out/one" "$out/two" "$out/bundles" +mkdir -p "$out/one" "$out/two" +commit "$@" +cp -a "$work/repo" "$work/beta-repo" "$out/one/" +for pair in stable:repo beta:beta-repo; do + "$here/flatpak-repo.sh" bundle "$work/${pair#*:}" "${pair%%:*}" "$key" \ + "http://127.0.0.1:8080/${pair#*:}/" "$out/bundles/${pair%%:*}" +done + +# What each architecture installs, checked with this Ubuntu's validators. +for ref in $(ostree refs --repo="$work/repo" | grep "^app/$app/"); do + tree="$work/tree-$(echo "$ref" | cut -d/ -f3)" + ostree checkout --repo="$work/repo" -U "$ref" "$tree" + desktop-file-validate "$tree/files/share/applications/$app.desktop" + appstreamcli validate --no-net "$tree/files/share/metainfo/$app.metainfo.xml" + glib-compile-schemas --strict --dry-run "$tree/files/share/glib-2.0/schemas" +done + +commit "$@" +cp -a "$work/repo" "$work/beta-repo" "$out/two/" +ls -l "$out" "$out/bundles"/* diff --git a/linux/build-aux/packaging/release-matrix.json b/linux/build-aux/packaging/release-matrix.json new file mode 100644 index 0000000000..c7d706de7d --- /dev/null +++ b/linux/build-aux/packaging/release-matrix.json @@ -0,0 +1,11 @@ +{ + "ubuntu": [ + { "series": "resolute", "version": "26.04" }, + { "series": "stonking", "version": "26.10" } + ], + "fedora": ["44", "45"], + "architectures": [ + { "runner": "ubuntu-24.04", "deb": "amd64", "rpm": "x86_64", "flatpak": "x86_64" }, + { "runner": "ubuntu-24.04-arm", "deb": "arm64", "rpm": "aarch64", "flatpak": "aarch64" } + ] +} diff --git a/linux/build-aux/packaging/release_assets.py b/linux/build-aux/packaging/release_assets.py new file mode 100644 index 0000000000..4dfe88cebf --- /dev/null +++ b/linux/build-aux/packaging/release_assets.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# Gathers the files a Linux release or beta publishes from the release +# workflow's artifacts into one directory and writes SHA256SUMS, then checks +# a directory of them. GitHub turns the '~' of a package version into '.' in +# an asset's name, so each package is renamed after the tag's version, the +# system it was built for and its architecture. release-matrix.json declares +# every Ubuntu series, Fedora release and architecture a release carries, and +# linux-packages.yml builds from the same file; the assets must be exactly +# those. +# +# Usage: +# release_assets.py gather --version X.Y.Z[-beta.N] +# release_assets.py verify --version X.Y.Z[-beta.N] +# Checks the names against the matrix, each file against SHA256SUMS, and +# each package's name, version and architecture against its file name. + +import argparse +import hashlib +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +from check_packaging import RELEASE_VERSION, package_version + +MATRIX = Path(__file__).resolve().parent / "release-matrix.json" +DEB = re.compile(r"^tablepro_(?P[^_]+)-0ppa\d+~(?P\d+\.\d+)_(?P[a-z0-9]+)\.deb$") +RPM = re.compile(r"^tablepro-(?P[0-9][^-]*)-\d+\.fc(?P\d+)\.(?P[a-z0-9_]+)\.rpm$") +FLATPAK = re.compile(r"^tablepro-(?P[a-z0-9_]+)\.flatpak$") + + +class AssetError(Exception): + pass + + +def load_matrix(path=MATRIX): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def expected_assets(version, matrix): + """Every asset name a release of this version carries, mapped to what its package must declare.""" + expected = { + f"tablepro-{version}.tar.xz": None, + f"tablepro-{version}-vendor.tar.xz": None, + "tablepro.pot": None, + } + for arch in matrix["architectures"]: + for ubuntu in matrix["ubuntu"]: + expected[f"tablepro-{version}-ubuntu-{ubuntu['version']}-{arch['deb']}.deb"] = ("deb", arch["deb"]) + for fedora in matrix["fedora"]: + expected[f"tablepro-{version}-fedora-{fedora}-{arch['rpm']}.rpm"] = ("rpm", arch["rpm"]) + expected[f"tablepro-{version}-{arch['flatpak']}.flatpak"] = None + return expected + + +def asset_name(file, version): + """The release asset name of a built file, or None for a file the release does not carry.""" + name = file.name + if name in (f"tablepro-{version}.tar.xz", f"tablepro-{version}-vendor.tar.xz", "tablepro.pot"): + return name + packaged = package_version(version) + for pattern, kind in ((DEB, "deb"), (RPM, "rpm")): + match = pattern.match(name) + if match is None: + continue + if match["arch"] == "src": + return None + if match["version"] != packaged: + raise AssetError(f"{name} packages {match['version']}, not {packaged}") + if kind == "deb": + return f"tablepro-{version}-ubuntu-{match['series']}-{match['arch']}.deb" + return f"tablepro-{version}-fedora-{match['fedora']}-{match['arch']}.rpm" + match = FLATPAK.match(name) + if match: + return f"tablepro-{version}-{match['arch']}.flatpak" + return None + + +def collect(artifacts, version): + """Maps each asset name to the built file it comes from.""" + assets = {} + for file in sorted(path for path in artifacts.rglob("*") if path.is_file()): + name = asset_name(file, version) + if name is None: + continue + if name in assets: + raise AssetError(f"{file} and {assets[name]} would both publish as {name}") + assets[name] = file + return assets + + +def compare(names, expected): + missing = sorted(set(expected) - set(names)) + if missing: + raise AssetError(f"the release has no {', '.join(missing)}") + extra = sorted(set(names) - set(expected)) + if extra: + raise AssetError(f"release-matrix.json declares no {', '.join(extra)}") + + +def sha256(path): + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1 << 20), b""): + digest.update(block) + return digest.hexdigest() + + +def check_version(version): + if not RELEASE_VERSION.match(version): + raise AssetError(f"version {version} is neither X.Y.Z nor a beta such as X.Y.Z-beta.1") + + +def gather(artifacts, out, version, matrix): + check_version(version) + assets = collect(artifacts, version) + compare(assets, expected_assets(version, matrix)) + out.mkdir(parents=True, exist_ok=True) + sums = [] + for name, file in sorted(assets.items()): + shutil.copyfile(file, out / name) + sums.append(f"{sha256(out / name)} {name}\n") + (out / "SHA256SUMS").write_text("".join(sums), encoding="utf-8") + return sorted(assets) + + +def deb_fields(path): + output = subprocess.run( + ["dpkg-deb", "--show", "--showformat=${Package} ${Version} ${Architecture}", str(path)], + check=True, capture_output=True, text=True, + ).stdout + return tuple(output.split()) + + +def rpm_fields(path): + output = subprocess.run( + ["rpm", "--query", "--package", "--nosignature", "--queryformat", "%{NAME} %{VERSION} %{ARCH}", str(path)], + check=True, capture_output=True, text=True, + ).stdout + return tuple(output.split()) + + +def verify(directory, version, matrix, read_deb=deb_fields, read_rpm=rpm_fields): + check_version(version) + expected = expected_assets(version, matrix) + sums_file = directory / "SHA256SUMS" + if not sums_file.is_file(): + raise AssetError(f"{directory} has no SHA256SUMS") + listed = {} + for line in sums_file.read_text(encoding="utf-8").splitlines(): + digest, _, name = line.partition(" ") + listed[name] = digest + compare(listed, expected) + present = sorted(path.name for path in directory.iterdir() if path.is_file() and path.name != "SHA256SUMS") + compare(present, expected) + packaged = package_version(version) + for name, digest in sorted(listed.items()): + if sha256(directory / name) != digest: + raise AssetError(f"{name} does not match its SHA256SUMS line") + kind = expected[name] + if kind is None: + continue + package, declared, arch = (read_deb if kind[0] == "deb" else read_rpm)(directory / name) + if package != "tablepro" or arch != kind[1]: + raise AssetError(f"{name} holds {package} for {arch}") + if kind[0] == "deb" and not declared.startswith(f"{packaged}-0ppa"): + raise AssetError(f"{name} holds version {declared}, not {packaged}-0ppaN") + if kind[0] == "rpm" and declared != packaged: + raise AssetError(f"{name} holds version {declared}, not {packaged}") + return sorted(listed) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Gather or check a Linux release's files.") + commands = parser.add_subparsers(dest="command", required=True) + gather_command = commands.add_parser("gather") + gather_command.add_argument("--version", required=True) + gather_command.add_argument("artifacts", type=Path) + gather_command.add_argument("out", type=Path) + verify_command = commands.add_parser("verify") + verify_command.add_argument("--version", required=True) + verify_command.add_argument("directory", type=Path) + args = parser.parse_args(argv) + try: + matrix = load_matrix() + if args.command == "gather": + names = gather(args.artifacts, args.out, args.version, matrix) + else: + names = verify(args.directory, args.version, matrix) + except (AssetError, OSError, ValueError, subprocess.CalledProcessError) as error: + print(f"release_assets.py: {error}", file=sys.stderr) + return 1 + for name in names: + print(name) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux/build-aux/packaging/smoke-test-flatpak.sh b/linux/build-aux/packaging/smoke-test-flatpak.sh new file mode 100755 index 0000000000..257fefe23a --- /dev/null +++ b/linux/build-aux/packaging/smoke-test-flatpak.sh @@ -0,0 +1,146 @@ +#!/bin/sh +# Installs TablePro from the Flatpak repositories flatpak-test-site.sh made, +# as root in a fresh, privileged Ubuntu container with nothing set up but that +# release's own Flatpak, serving them on http://127.0.0.1:8080. It installs +# the release from its .flatpakref, which brings the GNOME runtime from +# Flathub, checks that it starts and carries the catalogue of each language +# the LINGUAS file names, updates it to the second commit, and installs its +# .Debug extension. It installs the beta beside it and switches which of the +# two the app's name starts, keeping one set of data. Last it installs the +# bundle of the first commit in place of the release, and updates that from +# the repository the bundle names. +# Usage: smoke-test-flatpak.sh [] +set -eu + +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "usage: smoke-test-flatpak.sh []" >&2 + exit 2 +fi +site="$(cd "$1" && pwd)" +languages="" +if [ $# -eq 2 ]; then + languages="$(sed 's/#.*//' "$2")" +fi +app=app.tablepro.TablePro +stable="$app//stable" +beta="$app//beta" +server=http://127.0.0.1:8080 + +fail() { + echo "smoke-test-flatpak.sh: $*" >&2 + exit 1 +} + +export DEBIAN_FRONTEND=noninteractive +apt-get update +# dbus, not dbus-daemon, which Ubuntu 22.04 does not split out. +apt-get install -y --no-install-recommends ca-certificates flatpak python3 xvfb xauth dbus +flatpak --version +export XDG_RUNTIME_DIR=/run/user/0 +mkdir -p "$XDG_RUNTIME_DIR" +chmod 700 "$XDG_RUNTIME_DIR" +# flatpak run asks the system bus for parental controls, and fails with +# "Could not connect" where there is none. +if [ ! -S /run/dbus/system_bus_socket ]; then + mkdir -p /run/dbus + dbus-daemon --system --fork +fi + +# The server follows the link, so pointing it at the second commit publishes +# the update. +serve="$(mktemp -d)" +ln -s "$site/one" "$serve/root" +python3 -m http.server --bind 127.0.0.1 --directory "$serve/root" 8080 > "$serve/http.log" 2>&1 & +pid=$! +trap 'kill "$pid" 2>/dev/null || true' EXIT +python3 - "$server/repo/summary" <<'EOF' +import sys, time, urllib.request +for _ in range(50): + try: + urllib.request.urlopen(sys.argv[1]).close() + sys.exit(0) + except OSError: + time.sleep(0.2) +sys.exit("the repository server did not answer") +EOF + +commit_of() { + flatpak info --user --show-commit "$1" +} + +in_app() { + ref="$1" + shift + dbus-run-session -- flatpak run --command=sh "$ref" -c "$@" +} + +check_app() { + help="$(dbus-run-session -- flatpak run "$1" --help)" + case "$help" in + *--help-gapplication*) ;; + *) fail "$1 --help did not list the GApplication options: $help" ;; + esac + files="$(flatpak info --user --show-location "$1")/files" + for lang in $languages; do + [ -f "$files/share/locale/$lang/LC_MESSAGES/tablepro.mo" ] || fail "$1 has no $lang catalogue" + done +} + +# The branch the app's name starts, from the instance's own record. +current_branch() { + in_app "$app" 'sed -n "s/^branch=//p" /.flatpak-info' +} + +flatpak install --user --noninteractive --from "$server/repo/$app.flatpakref" +remotes="$(flatpak remotes --user --columns=name,url)" +echo "$remotes" +case "$remotes" in +*https://dl.flathub.org/repo/*) ;; +*) fail "the .flatpakref added no Flathub remote for the runtime" ;; +esac +check_app "$stable" +# A window that is still up after the timeout has found its GTK modules, +# icon theme and resources. +status=0 +xvfb-run -a dbus-run-session -- timeout 15 flatpak run "$stable" || status=$? +[ "$status" -eq 124 ] || fail "tablepro exited with status $status before the 15 second timeout" +# shellcheck disable=SC2016 # the sandbox's shell expands it +in_app "$stable" 'touch "$XDG_CONFIG_HOME/smoke-test"' + +first="$(commit_of "$stable")" +ln -sfn "$site/two" "$serve/root" +flatpak update --user --noninteractive "$stable" +second="$(commit_of "$stable")" +[ "$second" != "$first" ] || fail "flatpak update stayed on $first" +check_app "$stable" + +remote="$(flatpak info --user --show-origin "$stable")" +flatpak install --user --noninteractive "$remote" "$app.Debug//stable" +debug="$(flatpak info --user --show-location "$app.Debug//stable")/files" +find "$debug" -name '*.debug' | grep -q . || fail "the .Debug extension holds no debug file" + +flatpak install --user --noninteractive --from "$server/beta-repo/$app.flatpakref" +check_app "$beta" +flatpak make-current --user "$app" beta +[ "$(current_branch)" = beta ] || fail "make-current beta left $(current_branch) current" +# shellcheck disable=SC2016 # the sandbox's shell expands it +in_app "$beta" 'test -f "$XDG_CONFIG_HOME/smoke-test"' || fail "the beta does not see the release's data" +flatpak make-current --user "$app" stable +[ "$(current_branch)" = stable ] || fail "make-current stable left $(current_branch) current" +flatpak uninstall --user --noninteractive "$beta" +[ "$(current_branch)" = stable ] || fail "uninstalling the beta left $(current_branch) current" + +flatpak uninstall --user --noninteractive "$app.Debug//stable" "$stable" +flatpak remote-delete --user --force "$remote" +flatpak install --user --noninteractive "$site/bundles/stable/tablepro-$(flatpak --default-arch).flatpak" +[ "$(commit_of "$stable")" = "$first" ] || fail "the bundle installed $(commit_of "$stable"), not $first" +origin="$(flatpak info --user --show-origin "$stable")" +flatpak remotes --user --columns=name,url | + awk -v name="$origin" -v url="$server/repo/" '$1 == name && $2 == url { found = 1 } END { exit !found }' || + fail "the bundle's remote $origin does not name $server/repo/" +flatpak update --user --noninteractive "$stable" +[ "$(commit_of "$stable")" = "$second" ] || fail "the bundle did not update to $second" +check_app "$stable" +# shellcheck disable=SC2016 # the sandbox's shell expands it +in_app "$stable" 'test -f "$XDG_CONFIG_HOME/smoke-test"' || fail "the bundle does not see the earlier data" +echo "smoke-test-flatpak.sh: $app installs, updates and switches channels from $server" diff --git a/linux/build-aux/rpm/tablepro.spec b/linux/build-aux/rpm/tablepro.spec index 3552e58273..dfb40aa33e 100644 --- a/linux/build-aux/rpm/tablepro.spec +++ b/linux/build-aux/rpm/tablepro.spec @@ -54,7 +54,7 @@ License: %{shrink: Zlib } URL: https://tablepro.app -# A pre-release packages as 0.2.0~rc.1 and is tagged linux-v0.2.0-rc.1. +# A beta packages as 0.2.0~beta.1 and is tagged linux-v0.2.0-beta.1. Source0: https://github.com/TableProApp/TablePro/releases/download/linux-v%{version_no_tilde}/%{name}-%{version_no_tilde}.tar.xz Source1: https://github.com/TableProApp/TablePro/releases/download/linux-v%{version_no_tilde}/%{name}-%{version_no_tilde}-vendor.tar.xz diff --git a/linux/build-aux/tests/test_check_packaging.py b/linux/build-aux/tests/test_check_packaging.py index 85b7a15609..1b37e700ab 100644 --- a/linux/build-aux/tests/test_check_packaging.py +++ b/linux/build-aux/tests/test_check_packaging.py @@ -199,38 +199,103 @@ def test_find_lang_follows_linguas(self): self.assertEqual(len(found), expected) def test_a_version_rpm_or_git_cannot_carry_fails(self): - for version in ("1.2", "1.2.0~rc1", "1.2.0-rc-1", "1.2.0-", "1.2.0-rc..1"): + for version in ("1.2", "1.2.0~beta.1", "1.2.0-beta-1", "1.2.0-", "1.2.0-beta..1"): with self.subTest(version=version): found = check_packaging.problems(self.tree(), version) - self.assertTrue(any("is neither X.Y.Z nor a pre-release" in problem for problem in found)) + self.assertTrue(any("is neither X.Y.Z nor a beta" in problem for problem in found)) - def test_a_pre_release_packages_with_a_tilde(self): - self.assertEqual(check_packaging.package_version("1.2.0-rc.1"), "1.2.0~rc.1") + def test_a_pre_release_other_than_a_beta_fails(self): + for version in ("1.2.0-rc.1", "1.2.0-alpha.1", "1.2.0-beta1", "1.2.0-beta.0", "1.2.0-beta.01"): + with self.subTest(version=version): + found = check_packaging.problems(self.tree(), version) + + self.assertTrue(any("is neither X.Y.Z nor a beta" in problem for problem in found)) + + def test_a_beta_packages_with_a_tilde(self): + self.assertEqual(check_packaging.package_version("1.2.0-beta.1"), "1.2.0~beta.1") self.assertEqual(check_packaging.package_version("1.2.0"), "1.2.0") - def test_a_pre_release_tag_passes_when_the_spec_packages_the_tilde_version(self): - root = self.tree( - releases='', - version="1.2.0~rc.1", - changelog="1.2.0~rc.1", - man_version="1.2.0-rc.1", + def beta_tree(self, **changes): + fields = { + "releases": '', + "version": "1.2.0~beta.1", + "changelog": "1.2.0~beta.1", + "man_version": "1.2.0-beta.1", + "notes": "# Changelog\n", + } + fields.update(changes) + return self.tree(**fields) + + def test_a_beta_tag_passes_when_the_spec_and_metainfo_package_the_tilde_version(self): + root = self.beta_tree() + + self.assertEqual(check_packaging.problems(root, "1.2.0-beta.1", "linux-v1.2.0-beta.1", "2026-10-14"), []) + self.assertEqual(check_packaging.problems(root, "1.2.0-beta.1"), []) + + def test_a_spec_with_the_upstream_beta_fails(self): + root = self.beta_tree(version="1.2.0-beta.1") + + found = check_packaging.problems(root, "1.2.0-beta.1") + + self.assertEqual(len(found), 1) + self.assertIn("packages version 1.2.0-beta.1, not 1.2.0~beta.1", found[0]) + + # AppStream sorts 1.2.0-beta.1 above 1.2.0, so the release's entry would fail validation. + def test_a_metainfo_with_the_upstream_beta_fails(self): + root = self.beta_tree(releases='') + + found = check_packaging.problems(root, "1.2.0-beta.1") + + self.assertEqual(found, [f"the newest release in {check_packaging.METAINFO} is 1.2.0-beta.1, not 1.2.0~beta.1"]) + + def test_a_beta_is_a_development_release(self): + for releases in ( + '', + '', + '', + ): + with self.subTest(releases=releases): + found = check_packaging.problems(self.beta_tree(releases=releases), "1.2.0-beta.1") + + self.assertEqual(len(found), 1) + self.assertIn('not "development"', found[0]) + + def test_a_release_is_not_a_development_release(self): + root = self.tree(releases='') + + found = check_packaging.problems(root, "1.2.0") + + self.assertEqual(found, [f'the newest release in {check_packaging.METAINFO} has type="development", not "stable"']) + + def test_a_release_above_its_betas_passes(self): + releases = ( + '\n' + '\n' + '' ) - self.assertEqual(check_packaging.problems(root, "1.2.0-rc.1", "linux-v1.2.0-rc.1", "2026-10-14"), []) + self.assertEqual(check_packaging.problems(self.tree(releases=releases), "1.2.0", "linux-v1.2.0", "2026-10-14"), []) - def test_a_spec_with_the_upstream_pre_release_fails(self): - root = self.tree( - releases='', - version="1.2.0-rc.1", - changelog="1.2.0~rc.1", - man_version="1.2.0-rc.1", + # A beta has no CHANGELOG.md heading; its version alone marks the release commit. + def test_a_beta_release_commit_carries_its_dates(self): + found = check_packaging.problems( + self.beta_tree(spec_date="Tue Oct 13 2026", man_date="2026-10-13"), "1.2.0-beta.1" ) - found = check_packaging.problems(root, "1.2.0-rc.1") + self.assertEqual( + found, + [ + f"{check_packaging.MAN} is dated 2026-10-13, the metainfo 2026-10-14", + f"the %changelog entry for 1.2.0~beta.1-1 in {check_packaging.SPEC} is dated 2026-10-13, " + "the metainfo 2026-10-14", + ], + ) - self.assertEqual(len(found), 1) - self.assertIn("packages version 1.2.0-rc.1, not 1.2.0~rc.1", found[0]) + def test_a_beta_needs_no_changelog_notes(self): + found = check_packaging.problems(self.beta_tree(), "1.2.0-beta.1", "linux-v1.2.0-beta.1", "2026-10-14") + + self.assertEqual(found, []) def test_the_debian_package_builds_with_the_workspace_rust_version(self): cases = [ diff --git a/linux/build-aux/tests/test_release_assets.py b/linux/build-aux/tests/test_release_assets.py new file mode 100644 index 0000000000..d6d6fbdca8 --- /dev/null +++ b/linux/build-aux/tests/test_release_assets.py @@ -0,0 +1,225 @@ +import contextlib +import hashlib +import io +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "packaging")) + +import release_assets # noqa: E402 + +BETA = "0.1.0-beta.1" +MATRIX = { + "ubuntu": [{"series": "resolute", "version": "26.04"}, {"series": "stonking", "version": "26.10"}], + "fedora": ["44", "45"], + "architectures": [ + {"runner": "ubuntu-24.04", "deb": "amd64", "rpm": "x86_64", "flatpak": "x86_64"}, + {"runner": "ubuntu-24.04-arm", "deb": "arm64", "rpm": "aarch64", "flatpak": "aarch64"}, + ], +} + + +def built_files(version): + packaged = version.replace("-", "~", 1) + files = { + "linux-sources": [f"tablepro-{version}.tar.xz", f"tablepro-{version}-vendor.tar.xz"], + "linux-pot": ["tablepro.pot"], + "linux-flatpak-bundles": ["tablepro-x86_64.flatpak", "tablepro-aarch64.flatpak"], + } + for name in ("26.04", "26.10"): + for arch, runner in (("amd64", "X64"), ("arm64", "ARM64")): + files[f"linux-deb-{name}-{runner}"] = [ + f"tablepro_{packaged}-0ppa1~{name}_{arch}.deb", + f"tablepro-dbgsym_{packaged}-0ppa1~{name}_{arch}.ddeb", + f"tablepro_{packaged}-0ppa1~{name}_{arch}.buildinfo", + f"tablepro_{packaged}-0ppa1~{name}_{arch}.changes", + ] + for fedora in ("44", "45"): + for arch, runner in (("x86_64", "X64"), ("aarch64", "ARM64")): + files[f"linux-rpm-{fedora}-{runner}"] = [ + f"tablepro-{packaged}-1.fc{fedora}.{arch}.rpm", + f"tablepro-debuginfo-{packaged}-1.fc{fedora}.{arch}.rpm", + f"tablepro-debugsource-{packaged}-1.fc{fedora}.{arch}.rpm", + f"tablepro-{packaged}-1.fc{fedora}.src.rpm", + ] + return files + + +def declared(version): + """What each package's own metadata says, read from its asset name as the real tools would.""" + packaged = version.replace("-", "~", 1) + + def read_deb(path): + arch = path.name.rsplit("-", 1)[1].removesuffix(".deb") + return ("tablepro", f"{packaged}-0ppa1~26.04", arch) + + def read_rpm(path): + arch = path.name.rsplit("-", 1)[1].removesuffix(".rpm") + return ("tablepro", packaged, arch) + + return read_deb, read_rpm + + +class GatherTests(unittest.TestCase): + def artifacts(self, files): + workdir = tempfile.TemporaryDirectory() + self.addCleanup(workdir.cleanup) + root = Path(workdir.name) + for directory, names in files.items(): + (root / "artifacts" / directory).mkdir(parents=True) + for name in names: + (root / "artifacts" / directory / name).write_text(f"{directory}/{name}", encoding="utf-8") + return root / "artifacts", root / "release" + + def test_a_beta_publishes_every_declared_package_without_a_tilde(self): + artifacts, out = self.artifacts(built_files(BETA)) + + names = release_assets.gather(artifacts, out, BETA, MATRIX) + + self.assertEqual(len(names), 3 + 2 * 2 + 2 * 2 + 2) + self.assertIn("tablepro-0.1.0-beta.1-ubuntu-26.10-arm64.deb", names) + self.assertIn("tablepro-0.1.0-beta.1-fedora-45-aarch64.rpm", names) + self.assertIn("tablepro-0.1.0-beta.1-x86_64.flatpak", names) + self.assertEqual(sorted(path.name for path in out.iterdir()), sorted([*names, "SHA256SUMS"])) + self.assertFalse(any("~" in name for name in names)) + + def test_sha256sums_lists_each_asset_as_sha256sum_does(self): + artifacts, out = self.artifacts(built_files("0.1.0")) + + names = release_assets.gather(artifacts, out, "0.1.0", MATRIX) + + lines = (out / "SHA256SUMS").read_text(encoding="utf-8").splitlines() + self.assertEqual([line.split(" ", 1)[1] for line in lines], names) + for line in lines: + digest, name = line.split(" ", 1) + self.assertEqual(digest, hashlib.sha256((out / name).read_bytes()).hexdigest()) + + def test_the_debs_keep_their_builds_apart(self): + artifacts, out = self.artifacts(built_files("0.1.0")) + + release_assets.gather(artifacts, out, "0.1.0", MATRIX) + + self.assertEqual( + (out / "tablepro-0.1.0-ubuntu-26.10-arm64.deb").read_text(encoding="utf-8"), + "linux-deb-26.10-ARM64/tablepro_0.1.0-0ppa1~26.10_arm64.deb", + ) + + def test_a_missing_architecture_series_or_release_fails(self): + cases = { + "an architecture": ["linux-deb-26.10-ARM64"], + "a whole Ubuntu series": ["linux-deb-26.10-X64", "linux-deb-26.10-ARM64"], + "a whole Fedora release": ["linux-rpm-45-X64", "linux-rpm-45-ARM64"], + "the bundles": ["linux-flatpak-bundles"], + "the template": ["linux-pot"], + } + for case, directories in cases.items(): + with self.subTest(case=case): + files = built_files(BETA) + for directory in directories: + del files[directory] + artifacts, out = self.artifacts(files) + + with self.assertRaisesRegex(release_assets.AssetError, "the release has no"): + release_assets.gather(artifacts, out, BETA, MATRIX) + self.assertFalse(out.exists()) + + def test_a_package_the_matrix_does_not_declare_fails(self): + files = built_files(BETA) + files["linux-deb-27.04-X64"] = ["tablepro_0.1.0~beta.1-0ppa1~27.04_amd64.deb"] + artifacts, out = self.artifacts(files) + + with self.assertRaisesRegex(release_assets.AssetError, "declares no tablepro-0.1.0-beta.1-ubuntu-27.04"): + release_assets.gather(artifacts, out, BETA, MATRIX) + + def test_a_package_of_another_version_fails(self): + files = built_files(BETA) + files["linux-deb-26.04-X64"][0] = "tablepro_0.1.0~beta.2-0ppa1~26.04_amd64.deb" + artifacts, out = self.artifacts(files) + + with self.assertRaisesRegex(release_assets.AssetError, r"packages 0\.1\.0~beta\.2, not 0\.1\.0~beta\.1"): + release_assets.gather(artifacts, out, BETA, MATRIX) + + def test_two_files_for_one_asset_fail(self): + files = built_files(BETA) + files["linux-deb-26.04-X64-again"] = [files["linux-deb-26.04-X64"][0]] + artifacts, out = self.artifacts(files) + + with self.assertRaisesRegex(release_assets.AssetError, "would both publish"): + release_assets.gather(artifacts, out, BETA, MATRIX) + + def test_a_version_the_release_workflow_refuses_fails(self): + artifacts, out = self.artifacts(built_files("0.1.0-rc.1")) + + with self.assertRaisesRegex(release_assets.AssetError, "nor a beta"): + release_assets.gather(artifacts, out, "0.1.0-rc.1", MATRIX) + + def test_the_committed_matrix_declares_both_architectures(self): + matrix = release_assets.load_matrix() + + self.assertEqual([arch["deb"] for arch in matrix["architectures"]], ["amd64", "arm64"]) + self.assertTrue(matrix["ubuntu"] and matrix["fedora"]) + + +class VerifyTests(unittest.TestCase): + def release(self, version=BETA): + workdir = tempfile.TemporaryDirectory() + self.addCleanup(workdir.cleanup) + root = Path(workdir.name) + for directory, names in built_files(version).items(): + (root / "artifacts" / directory).mkdir(parents=True) + for name in names: + (root / "artifacts" / directory / name).write_text(name, encoding="utf-8") + release_assets.gather(root / "artifacts", root / "release", version, MATRIX) + return root / "release" + + def test_a_gathered_release_verifies(self): + release = self.release() + read_deb, read_rpm = declared(BETA) + + names = release_assets.verify(release, BETA, MATRIX, read_deb, read_rpm) + + self.assertEqual(len(names), 13) + + def test_a_changed_file_fails(self): + release = self.release() + (release / "tablepro-0.1.0-beta.1-aarch64.flatpak").write_text("changed", encoding="utf-8") + + with self.assertRaisesRegex(release_assets.AssetError, "does not match its SHA256SUMS line"): + release_assets.verify(release, BETA, MATRIX, *declared(BETA)) + + def test_a_missing_file_fails(self): + release = self.release() + (release / "tablepro-0.1.0-beta.1-fedora-44-x86_64.rpm").unlink() + + with self.assertRaisesRegex(release_assets.AssetError, "the release has no"): + release_assets.verify(release, BETA, MATRIX, *declared(BETA)) + + def test_a_package_that_declares_another_version_or_architecture_fails(self): + release = self.release() + read_deb, read_rpm = declared(BETA) + cases = { + "version": (read_deb, lambda path: ("tablepro", "0.1.0~beta.2", read_rpm(path)[2])), + "architecture": (lambda path: ("tablepro", read_deb(path)[1], "i386"), read_rpm), + "name": (read_deb, lambda path: ("other", read_rpm(path)[1], read_rpm(path)[2])), + } + for case, readers in cases.items(): + with self.subTest(case=case): + with self.assertRaises(release_assets.AssetError): + release_assets.verify(release, BETA, MATRIX, *readers) + + def test_main_reports_on_stderr_and_exits_1(self): + workdir = tempfile.TemporaryDirectory() + self.addCleanup(workdir.cleanup) + stderr = io.StringIO() + + with contextlib.redirect_stderr(stderr), contextlib.redirect_stdout(io.StringIO()): + failed = release_assets.main(["verify", "--version", BETA, workdir.name]) + + self.assertEqual(failed, 1) + self.assertIn("SHA256SUMS", stderr.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/linux/changelog.d/4046.added.1.md b/linux/changelog.d/4046.added.1.md new file mode 100644 index 0000000000..e34583cb6c --- /dev/null +++ b/linux/changelog.d/4046.added.1.md @@ -0,0 +1 @@ +Searching the app grid or a software centre for MariaDB, Postgres or MSSQL finds TablePro. diff --git a/linux/changelog.d/4046.added.2.md b/linux/changelog.d/4046.added.2.md new file mode 100644 index 0000000000..a480c6200c --- /dev/null +++ b/linux/changelog.d/4046.added.2.md @@ -0,0 +1 @@ +Ubuntu 22.04 and 24.04 and other distributions install TablePro and its updates from a signed Flatpak repository. diff --git a/linux/changelog.d/4046.added.md b/linux/changelog.d/4046.added.md new file mode 100644 index 0000000000..c7e2086b71 --- /dev/null +++ b/linux/changelog.d/4046.added.md @@ -0,0 +1 @@ +Betas install from their own PPA and COPR project, and each GitHub release carries the .deb, .rpm and a Flatpak bundle. diff --git a/linux/crates/app/build.rs b/linux/crates/app/build.rs index 2c67658efd..0e5b6d0218 100644 --- a/linux/crates/app/build.rs +++ b/linux/crates/app/build.rs @@ -64,7 +64,7 @@ fn render_template() -> Result { let rendered = template .replace("@app-id@", &app_id) .replace("@gettext-package@", &package) - .replace("@version@", version); + .replace("@version@", &metainfo_release::tag_version(version)); if let Some(name) = unfilled_placeholder(&rendered) { return Err(format!( "{METAINFO_TEMPLATE} uses @{name}@, which build.rs does not fill" diff --git a/linux/crates/app/src/application/about_dialog.rs b/linux/crates/app/src/application/about_dialog.rs index dd89ec15dc..465f87251b 100644 --- a/linux/crates/app/src/application/about_dialog.rs +++ b/linux/crates/app/src/application/about_dialog.rs @@ -13,7 +13,8 @@ const DEBUG_INFO_FILENAME: &str = "tablepro-debug-info.txt"; /// The name, icon, developer, links and license come from the metainfo /// the GResource embeds, so the dialog and the software centre agree. pub fn build_about_dialog(drivers: &DriverRegistry) -> adw::AboutDialog { - let dialog = adw::AboutDialog::from_appdata(&metainfo_resource(), release_notes_version(config::profile())); + let release = release_notes_version(config::profile()).map(metainfo_version); + let dialog = adw::AboutDialog::from_appdata(&metainfo_resource(), release.as_deref()); // The metainfo gives the latest release, which a development build is // not: its version carries the commit. dialog.set_version(config::VERSION); @@ -43,11 +44,16 @@ pub(super) fn release_notes_version(profile: Profile) -> Option<&'static str> { // Read the way the About dialog reads them, so the update toast never points to a What's New page it lacks. pub(super) fn has_release_notes(version: &str) -> bool { - !adw::AboutDialog::from_appdata(&metainfo_resource(), Some(version)) + !adw::AboutDialog::from_appdata(&metainfo_resource(), Some(metainfo_version(version).as_str())) .release_notes() .is_empty() } +// AppStream sorts 0.1.0-beta.1 above 0.1.0, so the metainfo lists that beta as 0.1.0~beta.1. +fn metainfo_version(version: &str) -> String { + version.replacen('-', "~", 1) +} + #[cfg(test)] pub(super) fn embedded_metainfo() -> String { crate::register_test_resources(); @@ -61,7 +67,7 @@ mod tests { use gtk4 as gtk; use super::*; - use crate::application::metainfo_release::newest_release; + use crate::application::metainfo_release::{newest_release, tag_version}; /// The text between the element `open` starts and the next `close`. fn metainfo_text<'a>(xml: &'a str, open: &str, close: &str) -> &'a str { @@ -118,7 +124,7 @@ mod tests { #[test] fn embedded_metainfo_pins_project_links_to_the_release_tag() { let xml = embedded_metainfo(); - let version = newest_release(&xml).expect("the metainfo lists a release"); + let version = tag_version(newest_release(&xml).expect("the metainfo lists a release")); let tree = format!("https://github.com/TableProApp/TablePro/blob/linux-v{version}/linux"); let screenshots = format!("https://raw.githubusercontent.com/TableProApp/TablePro/linux-v{version}/linux/data/screenshots/"); @@ -202,9 +208,15 @@ mod tests { fn the_newest_release_has_notes_for_whats_new() { adw::init().expect("libadwaita initialises under the test backend"); let xml = embedded_metainfo(); - let version = newest_release(&xml).expect("the metainfo lists a release"); + let version = tag_version(newest_release(&xml).expect("the metainfo lists a release")); - assert!(has_release_notes(version)); + assert!(has_release_notes(&version)); + } + + #[test] + fn a_beta_reads_its_release_by_the_metainfo_spelling() { + assert_eq!(metainfo_version("0.1.0-beta.1"), "0.1.0~beta.1"); + assert_eq!(metainfo_version("0.1.0"), "0.1.0"); } #[test] diff --git a/linux/crates/app/src/application/metainfo_release.rs b/linux/crates/app/src/application/metainfo_release.rs index 3c785a3583..5985b87d36 100644 --- a/linux/crates/app/src/application/metainfo_release.rs +++ b/linux/crates/app/src/application/metainfo_release.rs @@ -11,6 +11,11 @@ pub(crate) fn newest_release(xml: &str) -> Option<&str> { attribute(&tag[..tag.find('>')?], "version") } +// The metainfo writes the beta 0.1.0-beta.1 as 0.1.0~beta.1, which git refuses in a tag. +pub(crate) fn tag_version(release: &str) -> String { + release.replacen('~', "-", 1) +} + fn attribute<'a>(attributes: &'a str, name: &str) -> Option<&'a str> { let mut rest = attributes; loop { @@ -56,6 +61,12 @@ mod tests { ); } + #[test] + fn a_beta_release_is_tagged_with_a_hyphen() { + assert_eq!(tag_version("0.1.0~beta.1"), "0.1.0-beta.1"); + assert_eq!(tag_version("0.1.0"), "0.1.0"); + } + #[test] fn a_release_without_a_version_names_none() { assert_eq!(newest_release(""), None); diff --git a/linux/crates/app/src/application/tests.rs b/linux/crates/app/src/application/tests.rs index 3156cd9a66..f6da4bad0c 100644 --- a/linux/crates/app/src/application/tests.rs +++ b/linux/crates/app/src/application/tests.rs @@ -806,9 +806,9 @@ fn new_window_action_opens_another_window() { fn released_version() -> String { let xml = super::about_dialog::embedded_metainfo(); - super::metainfo_release::newest_release(&xml) - .expect("the metainfo lists a release") - .to_owned() + super::metainfo_release::tag_version( + super::metainfo_release::newest_release(&xml).expect("the metainfo lists a release"), + ) } fn said_at_launch(test: &TestApp) -> Vec { diff --git a/linux/data/app.tablepro.TablePro.desktop.in.in b/linux/data/app.tablepro.TablePro.desktop.in.in index 7b568f3d18..5902fcf4f1 100644 --- a/linux/data/app.tablepro.TablePro.desktop.in.in +++ b/linux/data/app.tablepro.TablePro.desktop.in.in @@ -2,8 +2,8 @@ # Translators: TablePro is the product name and is not translated. Name=TablePro GenericName=Database Client -Comment=Browse and edit PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB databases -Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;DuckDB;SQL Server;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB; +Comment=Browse and edit PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB databases +Keywords=SQL;Database;PostgreSQL;Postgres;MySQL;MariaDB;SQLite;DuckDB;SQL Server;MSSQL;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB; Exec=tablepro %U Icon=@app-id@ Terminal=false diff --git a/linux/data/app.tablepro.TablePro.metainfo.xml.in.in b/linux/data/app.tablepro.TablePro.metainfo.xml.in.in index fbe5359ec0..a87ec6ef8a 100644 --- a/linux/data/app.tablepro.TablePro.metainfo.xml.in.in +++ b/linux/data/app.tablepro.TablePro.metainfo.xml.in.in @@ -8,10 +8,10 @@ Work with your databases

- TablePro connects to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, - ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB. Browse tables, edit - rows in place, run SQL with syntax highlighting, and reach servers behind - an SSH bastion. + TablePro connects to PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL + Server, ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB. Browse tables, + edit rows in place, run SQL with syntax highlighting, and reach servers + behind an SSH bastion.

It can:

    @@ -72,7 +72,7 @@

    The first release of TablePro for Linux.

      -
    • Connect to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB, directly or through an SSH tunnel
    • +
    • Connect to PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB, directly or through an SSH tunnel
    • Browse tables and edit rows in place, with undo until you save
    • Run SQL with completion, find and replace, and a searchable query history
    • Change a table's columns, indexes and foreign keys
    • diff --git a/linux/docs/decisions/0018-ppa-copr-and-flathub-later.md b/linux/docs/decisions/0018-ppa-copr-and-flathub-later.md index 283fdb42d3..73bbbdc796 100644 --- a/linux/docs/decisions/0018-ppa-copr-and-flathub-later.md +++ b/linux/docs/decisions/0018-ppa-copr-and-flathub-later.md @@ -1,6 +1,6 @@ # 0018: A PPA and COPR first, Flathub later -- **Status**: Accepted; the source tarball amended by core 0001 (`core/docs/decisions/0001-shared-core.md`) +- **Status**: Accepted; the source tarball amended by core 0001 (`core/docs/decisions/0001-shared-core.md`), pre-releases, the GitHub release's contents, the Flatpak manifests, other distributions' install and the Snap alternative superseded by 0041 - **Date**: 2026-10-15 ## Context diff --git a/linux/docs/decisions/0041-beta-repositories-and-release-packages.md b/linux/docs/decisions/0041-beta-repositories-and-release-packages.md new file mode 100644 index 0000000000..7f71b333cf --- /dev/null +++ b/linux/docs/decisions/0041-beta-repositories-and-release-packages.md @@ -0,0 +1,176 @@ +# 0041: Beta repositories, a Flatpak repository of our own, and packages on every GitHub release + +- **Status**: Accepted +- **Date**: 2026-11-05 + +## Context + +[ADR 0018](0018-ppa-copr-and-flathub-later.md) made a Launchpad PPA and a +COPR project the channels, sent a pre-release to GitHub Releases alone, +kept binary packages off GitHub, left other distributions to a source +build until Flathub, and kept no Flatpak manifest but the development one. +Before the first tag the maintainer asked for betas that testers install +the way they install a release, for packages on every GitHub release, and +for an updating install on Ubuntu 22.04 and 24.04, Debian, Linux Mint, +Pop!_OS and the rest. Checked on 2026-11-05: + +- Ubuntu 24.04 ships GTK 4.14 and libadwaita 1.5, and 22.04 GTK 4.6 and + libadwaita 1.1, below the GNOME 50 floor of + [ADR 0015](0015-gnome-50-platform-floor.md). Their own Flatpak, 1.12.7 + and 1.14.6, installs the GNOME 51 runtime from Flathub. +- Flatpak documents a repository on any web server (docs.flatpak.org, + Hosting a repository), signed with GPG, with static deltas. +- Flathub's linter documentation no longer refuses the home and host + exceptions "if there are signs of LLM usage" (flathub-infra/documentation + 762f508, 2026-10-26). Flathub moderators now approve each submission's + permissions on the website. +- flatpak-builder-lint stopped reporting home and host access, + `--socket=ssh-auth` and `--socket=gpg-agent` on 2026-10-23 + (flathub-infra/flatpak-builder-lint 1d036a8), leaving them to that + moderation. +- Flathub's generative AI policy still bars AI-generated or AI-assisted + content in a manifest, and AI tools opening or writing a submission pull + request. Flathub beta takes no new application: one reaches it only after + the stable repository accepted it. Both are on docs.flathub.org, + Requirements. +- The Snap Store granted DBeaver Community classic confinement in November + 2025, after a review + ([forum](https://forum.snapcraft.io/t/request-classic-confinement-for-dbeaver-ce/49008)). + 0018 rejected Snap because "a database client does not qualify for + classic confinement"; it needs a store review, which a database client + can pass. +- `rustc-1.98` is published for resolute in `~rust-toolchain/staging` + since 2026-10-20, and COPR has fedora-45-x86_64 and fedora-45-aarch64 + chroots. +- GitHub rewrites a `~` in a release asset's name to `.` + (openpreserve/jpylyzer#217), and every .deb and .rpm file name carries + one. +- AppStream 1.1.2 sorts `0.1.0-beta.1` above `0.1.0`, so `appstreamcli + validate` fails a metainfo that lists 0.1.0 above that beta + (`releases-not-in-order`). `0.1.0~beta.1` sorts below and passes, as + AppStream's version comparison documents for pre-releases. + +## Decision + +`linux-vX.Y.Z-beta.N` is a beta and `linux-vX.Y.Z` a release, with no +other pre-release. Each goes to its own repositories, which testers layer: + +| | Release | Beta | +|---|---|---| +| GitHub Releases | a release | a pre-release | +| Launchpad | `ppa:tablepro/tablepro` | `ppa:tablepro/tablepro-beta` | +| COPR | `@tablepro/tablepro` | `@tablepro/tablepro-beta` | +| Flatpak | `https://flatpak.tablepro.app/repo/`, branch `stable` | `https://flatpak.tablepro.app/beta-repo/`, branch `beta` | + +The Flatpak repositories are OSTree repositories in the Cloudflare R2 +bucket `tablepro-flatpak`, served as `flatpak.tablepro.app`. A key of their +own signs every commit and summary; the summaries carry static deltas, and +each ref keeps its last 3 commits. Each repository holds a `.flatpakrepo` +and a `.flatpakref` that names Flathub for the GNOME runtime. The release +job pulls the published repository, signs the new commits into it, uploads +objects before summaries, and deletes what pruning removed last +(`build-aux/packaging/flatpak-repo.sh`, `flatpak-publish.sh`). + +Every GitHub release and pre-release carries what CI built offline from +that tag's tarballs and installed in fresh containers: the .deb for each +Ubuntu series and the .rpm for each Fedora release CI builds, both tested +as they build, and a single-file Flatpak bundle made from the signed +repository, which it names for its updates, all for amd64 and arm64, with +`SHA256SUMS`, and named after the tag's version. + +The Flatpak comes from `build-aux/flatpak/app.tablepro.TablePro.json`: the +release profile under the real app id, GNOME 51, built offline from the +same tarballs, line tables in its `.Debug` extension, and translations +kept in the app. The `.Devel` manifest +stays for development builds, as GNOME apps keep a `.Devel` id. This +manifest serves our repository only; a Flathub submission stays one a +maintainer writes by hand under Flathub's policy. + +The packages and the metainfo spell a beta `X.Y.Z~beta.N`, and its +metainfo `` has `type="development"`. + +## Rationale + +- **Testing repositories sit beside the stable ones.** Fedora's + updates-testing is enabled next to updates (Fedora QA:Updates Testing), + flathub-beta is a second remote serving the same app ids (Flathub, + Installation), and `ppa:git-core/candidate` ("For stable releases, go to + ~git-core/+archive/ppa instead") and + `ppa:libreoffice/libreoffice-prereleases` ("Defer to + ~libreoffice/+archive/ubuntu/ppa for stable updates!") each pair with a + stable PPA. None copies its releases into the testing repository. +- **One version order for the packages.** dpkg, RPM and AppStream sort + `X.Y.Z~beta.N` below `X.Y.Z` and above the release before it, so a + tester with both apt or dnf repositories gets 0.1.0 over its betas, and + 0.2.0~beta.1 over 0.1.0. Flatpak branches do not compare versions. +- **Flatpak reaches what the floor excludes, with updates.** The GNOME 51 + runtime carries the libraries 22.04 and 24.04 lack, and a remote updates + the app where a bundle alone would not, through `flatpak update` and the + software centres. +- **R2 is a static host outside git.** An OSTree repository grows with + every commit, which does not belong in the history of the GitHub Pages + site `updates.tablepro.app` deploys from. +- **One source for every asset.** The Flatpak builds from the tarballs the + packages build from, so everything `SHA256SUMS` lists comes from them. +- **Our repository is not a Flathub submission.** The linter and + documentation changes remove the refusal 0018 expected for the home + exception, but the AI policy still keeps a manifest from this repository + out of Flathub. + +## Consequences + +- Before the first tag a maintainer sets up a Launchpad team with both + PPAs, a COPR group, which needs a Fedora Accounts group, with both + projects, the R2 bucket, its domain and token, the Flatpak key, and the + secrets and variables `release-linux.yml` names in its header + ([releasing.md](../releasing.md#setup-before-the-first-release)). +- Every installed remote trusts the Flatpak key. A new key makes every + user add the repository again. +- Until 0.1.0 only the beta repositories hold packages. A tester who added + only the beta one keeps the last beta after 0.1.0 until they add the + stable one or the next beta ships. +- The Flatpak's build runs every Meson suite in the release profile. CI + signs it into both repositories with a throwaway key, validates what it + installs, serves them, and installs and updates the app from its + `.flatpakref` and from a bundle, installs its `.Debug` extension, and + switches between the release and beta branches, on Ubuntu 22.04, 24.04 + and 26.04. +- Flatpak keeps the release and the beta as two branches with one set of + data and never moves between them by version: users choose with + `flatpak make-current`, unlike apt and dnf, which take the highest version. +- `release-matrix.json` declares the Ubuntu series, Fedora releases and + architectures; CI builds from it, and a release's assets must be exactly + those, each matching `SHA256SUMS` and its package's own name, version and + architecture. GitHub attests every asset's build. +- `release-linux.yml` gathers every asset in one job, which a dry run by + hand runs too, publishing nothing. +- `check_packaging.py` refuses another pre-release form, a metainfo beta + without the tilde or without `type="development"`, and holds a beta's + dates to its release commit. +- The About dialog and the update notice look a beta's notes up by the + metainfo's spelling. +- A packaging fix goes to the stable PPA only. A beta's fix is the next + beta. + +## Alternatives considered + +- **A release published to the beta repositories too**: testers would + need one repository, but the beta repository would copy the stable one, + which no testing channel above does. +- **No packages on GitHub**: 0018's choice, which left every other + distribution with a source build. +- **Bundles alone**: they install anywhere Flatpak runs, but nothing + updates them. +- **A classic Snap**: possible after a store review, as DBeaver's shows, + but a second confined format to test the database workflows in; one + maintained path comes first. +- **A GNOME 46 floor for native 24.04 packages**: it gives up the widgets + ADR 0015 adopted and still leaves 22.04 out. +- **The `.Devel` id for the Flatpak**: GNOME apps keep it for builds that + install beside a release, and a release under it would never meet the + Flathub app. +- **`X.Y.Z-beta.N` in the metainfo**: the release's own entry fails + validation above it. +- **The Flatpak from the checkout with network access, as the `.Devel` + manifest builds**: it would not come from the tarballs `SHA256SUMS` + lists. diff --git a/linux/docs/decisions/README.md b/linux/docs/decisions/README.md index fc1856d73f..39e4ad0851 100644 --- a/linux/docs/decisions/README.md +++ b/linux/docs/decisions/README.md @@ -65,7 +65,7 @@ A fact the repository keeps elsewhere, such as a workflow trigger, a branch, a v | [0015](0015-gnome-50-platform-floor.md) | GNOME 50 platform floor | Accepted; the runtime row amended by 0018 | GTK 4.22, libadwaita 1.9, GtkSourceView 5.18, GLib 2.88; AdwShortcutsDialog replaces the shortcuts window. | | [0016](0016-driver-contract-v2.md) | Driver contract v2 | Accepted; the no-fork rule for tiberius superseded by core 0005 | Owned calls with engine stops, phase-aware write outcomes, server-side row limits; PostgreSQL on tokio-postgres; no forks. | | [0017](0017-distribution-channels.md) | Distribution channels | Superseded by 0018 | Flathub, a .deb for Ubuntu 26.04 on GitHub Releases built with rustup, and an .rpm for Fedora 44 on COPR, all from `meson install` and one vendor tarball; no AppImage. | -| [0018](0018-ppa-copr-and-flathub-later.md) | A PPA and COPR first, Flathub later | Accepted; the source tarball amended by core 0001 | A Launchpad PPA built with `rustc-1.97` and a COPR project built on each release are the primary channels; Flathub follows as a submission a maintainer writes; Rust 1.97 is the MSRV. | +| [0018](0018-ppa-copr-and-flathub-later.md) | A PPA and COPR first, Flathub later | Accepted; the source tarball amended by core 0001, pre-releases, the GitHub release's contents, the Flatpak manifests, other distributions' install and the Snap alternative superseded by 0041 | A Launchpad PPA built with `rustc-1.97` and a COPR project built on each release are the primary channels; Flathub follows as a submission a maintainer writes; Rust 1.97 is the MSRV. | | [0019](0019-ssh-key-file-identities-only.md) | A chosen SSH key file limits the keys ssh offers | Superseded by 0020 | A key file comes with `IdentitiesOnly=yes`, so the agent's other keys cannot use up `MaxAuthTries` first. | | [0020](0020-pin-only-a-key-file-the-agent-lacks.md) | Pin only a key file the agent does not hold | Accepted | `IdentitiesOnly=yes` only for a key file the agent lacks, so an agent-held certificate or encrypted PEM key still signs. | | [0021](0021-engines-without-sql.md) | Engines without SQL keep the driver contract and say what they hold | Accepted; amended by 0026 | A key-value engine states `DataModel::KeyValue`, lists keys with `scan_keys` and reads them with `read_key`, has its own `SqlGrammar` variant, and is held read-only by the driver's check where the server cannot. | @@ -81,6 +81,7 @@ A fact the repository keeps elsewhere, such as a workflow trigger, a branch, a v | [0031](0031-duckdb-2.md) | DuckDB 2.0, writing files DuckDB 1.x reads | Accepted | duckdb 1.20000.1 compiles DuckDB 2.0.0, and every database opens with `storage_compatibility_version = 'v0.10.2'` so new and attached files stay readable by DuckDB 1.x. | | [0032](0032-connection-list-tree.md) | The connection list is the library's tree in a list view | Accepted | The picker and the window's tree show groups nested and in the library's order from one outline keyed by id, with Favourites and Recent sections in the picker, drag and drop, and a menu item for every move. | | [0033](0033-no-compatibility-with-pre-release-states.md) | No compatibility with pre-release states | Accepted | Every app-owned format starts at version 1 with no upgrade step and the history schema is one migration; nothing reads what a build before the first release wrote. | +| [0041](0041-beta-repositories-and-release-packages.md) | Beta repositories, a Flatpak repository of our own, and packages on every GitHub release | Accepted | A beta tag goes to a beta PPA, COPR project and Flatpak repository that testers add beside the stable ones; a signed Flatpak repository on R2 serves every other distribution with updates; every GitHub release carries the tested .deb and .rpm and a Flatpak bundle that updates from that repository, with SHA256SUMS. | 0011 is not used. It was reserved for the DDL decision, which was recorded as 0010, and docs cite ADRs by number, so the later ones keep theirs. diff --git a/linux/docs/releasing.md b/linux/docs/releasing.md index fcfe44efdc..c9b2d13b04 100644 --- a/linux/docs/releasing.md +++ b/linux/docs/releasing.md @@ -1,39 +1,115 @@ # Releasing -`.github/workflows/release-linux.yml` publishes a release. This page is what a maintainer does around it. [ADR 0018](decisions/0018-ppa-copr-and-flathub-later.md) says why the channels are a Launchpad PPA and a COPR project. +`.github/workflows/release-linux.yml` publishes a release or a beta. This page is what a maintainer does around it. [ADR 0018](decisions/0018-ppa-copr-and-flathub-later.md) says why the channels are a Launchpad PPA and a COPR project, and [ADR 0041](decisions/0041-beta-repositories-and-release-packages.md) why betas have their own, why every other distribution gets a Flatpak repository of ours, and what every GitHub release carries. ## Setup before the first release A repository admin does these once. The workflow's header names the same secrets and variables. -1. **Secrets** in the `linux-release` environment, which takes a required reviewer and deploys from `linux-v*` tags and the `main` branch only. The branch is for a [packaging fix](#a-packaging-fix). - - `LINUX_PPA_SIGNING_KEY`: the ASCII-armored OpenPGP secret key, with no passphrase, that signs the uploads. - - `LINUX_COPR_WEBHOOK`: the custom webhook URL from the COPR project's Integrations page, ending in `/tablepro/`. -2. **Variables**: `LINUX_PPA`, the PPA as `owner/name`. `LINUX_BRANCH` stays unset unless a release comes from a [maintenance branch](#a-fix-release). -3. **Launchpad**: a team that owns the PPA, and the PPA with a dependency on `~rust-toolchain/staging`, amd64 and arm64 turned on under its processors, and debug symbol publishing on (Launchpad's admins turn it on when the PPA's settings do not offer it). Register the signing key's public half on an account in the team. Launchpad mails a failed build to the owner of the key that signed the upload, so that account's mail has to reach someone. -4. **COPR**: a project with the Fedora 44 x86_64 and aarch64 chroots, following Fedora branching, with AppStream metadata generation on, and a package named `tablepro` with the custom source method from `build-aux/rpm/copr-sources.sh` (the command is in its header). Turn on build failure emails for the project's admins. +1. **The signing key**, on a machine you trust, kept for uploads only. Launchpad confirms a key by mail encrypted to it, so it needs an encryption subkey too: + + ```sh + gpg --batch --passphrase '' --quick-generate-key 'TablePro Releases ' rsa4096 sign never + gpg --list-secret-keys --with-colons 'TablePro Releases' | awk -F: '$1 == "fpr" { print $10; exit }' + gpg --batch --passphrase '' --quick-add-key rsa4096 encr never + gpg --keyserver hkps://keyserver.ubuntu.com --send-keys + gpg --armor --export-secret-keys > linux-ppa-signing-key.asc + ``` + +2. **Launchpad**, signed in as the maintainer whose account will hold the key: + 1. Add the fingerprint at [OpenPGP keys](https://launchpad.net/~/+editpgpkeys) and open the link in the encrypted mail Launchpad sends. Launchpad mails a failed build to the owner of the key that signed the upload, so that account's mail has to reach someone. + 2. [Register a team](https://launchpad.net/people/+newteam) named `tablepro`, with that account a member. + 3. On the team's page, Create a new PPA twice: URL `tablepro`, display name `TablePro`, and URL `tablepro-beta`, display name `TablePro beta`. In the beta one's description, tell testers to add `ppa:tablepro/tablepro` as well. + 4. On each PPA, Edit PPA dependencies: add `~rust-toolchain/ubuntu/staging`. Change details: under Processors, amd64 and arm64, and turn on Build debug symbols and Publish debug symbols. +3. **COPR**: + 1. A COPR group is a Fedora Accounts group, which only Fedora Infrastructure creates: [open a ticket](https://forge.fedoraproject.org/infra/tickets/issues/new) asking for the group `tablepro`, then add the maintainers at [Fedora Accounts](https://accounts.fedoraproject.org). + 2. Sign out of COPR and in again, so it reads the group, and choose Activate this group on [My groups](https://copr.fedorainfracloud.org/groups/list/my). + 3. With `copr-cli` and the API token from [COPR's API page](https://copr.fedorainfracloud.org/api/) in `~/.config/copr`, from the repository root: + + ```sh + for project in tablepro tablepro-beta; do + copr-cli create "@tablepro/$project" \ + --chroot fedora-44-x86_64 --chroot fedora-44-aarch64 \ + --chroot fedora-45-x86_64 --chroot fedora-45-aarch64 \ + --follow-fedora-branching on --appstream on + copr-cli add-package-custom "@tablepro/$project" --name tablepro --webhook-rebuild on \ + --script linux/build-aux/rpm/copr-sources.sh --script-builddeps 'curl rpmdevtools' + done + ``` + + 4. In each project's Settings, tell `tablepro-beta`'s users under Instructions to enable `@tablepro/tablepro` as well, and turn on build failure emails for the admins. Integrations shows the custom webhook URL; add the package name to it: `https://copr.fedorainfracloud.org/webhooks/custom///tablepro/`. +4. **The Flatpak repository**, in the Cloudflare account that holds the `tablepro.app` zone: + 1. **R2**: under R2 Object Storage, create the bucket `tablepro-flatpak`. Leave its `r2.dev` public URL off. + 2. **The domain**: in the bucket's Settings, under Custom Domains, connect `flatpak.tablepro.app`. Cloudflare adds the DNS record. The release job uploads `repo/` and `beta-repo/` there. + 3. **The token**: under R2, Manage API tokens, create a token with Object Read & Write on `tablepro-flatpak` only. Keep its Access Key ID, its Secret Access Key and the S3 endpoint it shows, `https://.r2.cloudflarestorage.com`. + 4. **The key**: a key of its own, not the PPA's. Every installed remote trusts it, and a new one makes every user add the repository again, so keep a copy offline: + + ```sh + gpg --batch --passphrase '' --quick-generate-key 'TablePro Flatpak ' rsa4096 sign never + gpg --list-secret-keys --with-colons 'TablePro Flatpak' | awk -F: '$1 == "fpr" { print $10; exit }' + gpg --armor --export-secret-keys > linux-flatpak-key.asc + ``` + +5. **GitHub**: the `linux-release` environment takes a required reviewer and deploys from `linux-v*` tags and the `main` branch only. The branch is for a [packaging fix](#a-packaging-fix). + - Secrets in it: `LINUX_PPA_SIGNING_KEY`, the file the first step exported; `LINUX_COPR_WEBHOOK`, the URL of `@tablepro/tablepro`; `LINUX_COPR_WEBHOOK_BETA`, the URL of `@tablepro/tablepro-beta`; `LINUX_FLATPAK_GPG_KEY`, the Flatpak key's file; `LINUX_FLATPAK_R2_ACCESS_KEY_ID` and `LINUX_FLATPAK_R2_SECRET_ACCESS_KEY`, the R2 token's. Then delete the exported key files. + - Repository variables: `LINUX_PPA` set to `tablepro/tablepro`, `LINUX_PPA_BETA` set to `tablepro/tablepro-beta`, `LINUX_FLATPAK_R2_ENDPOINT` set to the S3 endpoint. `LINUX_BRANCH` stays unset unless a release comes from a [maintenance branch](#a-fix-release). + + ```sh + gh secret set LINUX_PPA_SIGNING_KEY --env linux-release --repo TableProApp/TablePro < linux-ppa-signing-key.asc + gh secret set LINUX_COPR_WEBHOOK --env linux-release --repo TableProApp/TablePro + gh secret set LINUX_COPR_WEBHOOK_BETA --env linux-release --repo TableProApp/TablePro + gh secret set LINUX_FLATPAK_GPG_KEY --env linux-release --repo TableProApp/TablePro < linux-flatpak-key.asc + gh secret set LINUX_FLATPAK_R2_ACCESS_KEY_ID --env linux-release --repo TableProApp/TablePro + gh secret set LINUX_FLATPAK_R2_SECRET_ACCESS_KEY --env linux-release --repo TableProApp/TablePro + gh variable set LINUX_PPA --body tablepro/tablepro --repo TableProApp/TablePro + gh variable set LINUX_PPA_BETA --body tablepro/tablepro-beta --repo TableProApp/TablePro + gh variable set LINUX_FLATPAK_R2_ENDPOINT --body 'https://.r2.cloudflarestorage.com' --repo TableProApp/TablePro + ``` + +Until these exist, a tag still builds and checks everything, and each publishing step that lacks its secret or variable fails and publishes nothing. + +[README.md](../README.md#install) gives users these PPAs, COPR projects and Flatpak repositories; change it if one gets another name. + +## A dry run + +Before a tag, run the workflow by hand on the branch it will be on: -[README.md](../README.md#install) gives users `ppa:tablepro/tablepro` and `@tablepro/tablepro`; change it if the PPA or the COPR project gets another name. +```sh +gh workflow run release-linux.yml --ref main -f dry-run=true +``` + +It checks the version's metadata as the `release-metadata` test does, and builds, tests and installs both packages and the Flatpak on amd64 and arm64. It signs the Flatpak with a throwaway key into both repositories, served on `127.0.0.1`, and on Ubuntu 22.04, 24.04 and 26.04 installs it from there and from a bundle, updates both, and switches between the release and the beta. It keeps every file a tag would publish, `SHA256SUMS` among them, as the run's `linux-release` artifact, checked against `build-aux/packaging/release-matrix.json`. It publishes nothing and needs no reviewer. A new Ubuntu series or Fedora release joins that file, and CI builds it from then on. + +## A release or a beta + +`linux-vX.Y.Z` is a release and `linux-vX.Y.Z-beta.N` a beta; the workflow refuses any other tag. Packages and the metainfo spell a beta `X.Y.Z~beta.N`, which sorts below `X.Y.Z` in dpkg, RPM and AppStream, while the tag and `meson.build` keep the hyphen, since git refuses a tilde. + +1. **The release commit**, a pull request titled `chore(release): linux-vX.Y.Z` or `chore(release): linux-vX.Y.Z-beta.N`, sets one version and one date (UTC) everywhere: + - `version` in `meson.build`: `X.Y.Z` or `X.Y.Z-beta.N`. + - A new first `` in `data/app.tablepro.TablePro.metainfo.xml.in.in`, with the date, a `` to `https://github.com/TableProApp/TablePro/releases/tag/` and the tag, and a short description. A release's is `version="X.Y.Z"`. A beta's is `version="X.Y.Z~beta.N"` with `type="development"`. A release's entry goes above its betas', which stay. + - `Version` (`X.Y.Z` or `X.Y.Z~beta.N`), `Release: 1%{?dist}` and a new first `%changelog` entry in `build-aux/rpm/tablepro.spec`, with the same date. + - A release only: the version's section in `CHANGELOG.md`, from the fragments in `changelog.d/`. From the repository root, with the towncrier venv of the root `RELEASING.md`, Notes: `.venv/bin/towncrier build --dir linux --version X.Y.Z --date YYYY-MM-DD --yes`. It deletes the fragments and stages `CHANGELOG.md`. An X.Y.0 types its lead under the heading, at most 6 plain lines naming what a reader would change their behavior over. A beta adds no section and keeps the fragments, so the release lists them; its notes are a towncrier draft of the fragments at the tag. + - The `.TH` line of `data/tablepro.1`: the date and `TablePro` with the version as `meson.build` has it. + + The pull request changes only these files and `changelog.d/`. The `release-metadata` Meson test checks that they agree from the commit that sets a beta's version, or adds a release's changelog heading, so the pull request fails before the tag does. +2. **The tag**: `linux-vX.Y.Z` or `linux-vX.Y.Z-beta.N` on the merged commit, pushed the same UTC day. The workflow compares the metainfo date with the commit date. Tags run one at a time, and a tag pushed while another waits cancels that one, so push a tag when no other release run is waiting. +3. **The workflow** checks the tag, builds, tests and installs the packages and the Flatpak as the [dry run](#a-dry-run) does, pulls the published Flatpak repository from R2, signs the new commits into it and bundles them. It attests every file, publishes the packages and bundles on GitHub with the tarballs, the translation template `tablepro.pot`, `SHA256SUMS`, and the version's changelog section as notes, a beta as a pre-release, and checks what the release then serves against `SHA256SUMS`. Then it uploads the Flatpak repository to R2, the signed source package to the PPA, and posts the tag to the COPR project: the stable ones for a release, the beta ones for a beta. A reviewer approves each step that uses the environment. +4. **Afterwards**: watch the Launchpad build page of each series and the COPR build the workflow prints. A failed build there does not fail the workflow. `flatpak remote-info` on the TablePro remote shows the new Flatpak commit. -## A release +## The first release -1. **The release commit**, a pull request titled `chore(release): linux-vX.Y.Z`, sets one version and one date (UTC) everywhere: - - `version` in `meson.build`. - - A new first `` in `data/app.tablepro.TablePro.metainfo.xml.in.in`, with the date and a short description. - - `Version`, `Release: 1%{?dist}` and a new first `%changelog` entry in `build-aux/rpm/tablepro.spec`, with the same date. - - The version's section in `CHANGELOG.md`, from the fragments in `changelog.d/`. From the repository root, with the towncrier venv of the root `RELEASING.md`, Notes: `.venv/bin/towncrier build --dir linux --version X.Y.Z --date YYYY-MM-DD --yes`. It deletes the fragments and stages `CHANGELOG.md`. An X.Y.0 types its lead under the heading, at most 6 plain lines naming what a reader would change their behavior over. - - The `.TH` line of `data/tablepro.1`: the date and `TablePro X.Y.Z`. +`main` carries 0.1.0 from before any tag: a metainfo `` and a spec `%changelog` entry for it, and the fragments of every change on the way. - The pull request changes only these files and `changelog.d/`. The `release-metadata` Meson test checks that they agree once the changelog heading exists, so the pull request fails before the tag does. -2. **The tag**: `linux-vX.Y.Z` on the merged commit, pushed the same UTC day. The workflow compares the metainfo date with the commit date. -3. **The workflow** checks the tag, builds and tests both packages on amd64 and arm64, publishes the GitHub release with the tarballs, the translation template `tablepro.pot` and the version's changelog section as notes, uploads the signed source package to the PPA, and posts the tag to COPR. A reviewer approves each step that uses the environment. -4. **Afterwards**: watch the Launchpad build page of each series and the COPR build the workflow prints. A failed build there does not fail the workflow. +- **Placeholders.** The first beta's release commit replaces the metainfo's 0.1.0 entry and the spec's `0.1.0-1` entry with its own: the beta's entries come first, and 0.1.0 listed below its beta fails `appstreamcli validate`. 0.1.0's release commit adds its entries back above the betas'. +- **Notes.** The fragments from before the first beta record work on builds nobody installed. The first beta's release commit curates `changelog.d/` as a release pull request does (`apple/RELEASING.md`, Cutting a stable or patch, step 4), so that beta's notes and 0.1.0's describe the app: + - `git rm` every `fixed`, `changed`, `deprecated`, `removed` and `security` fragment, since no Linux release had what they change. The `+ghsa-*` security fragments mirror advisories that list only the macOS and iOS apps as affected, so there is nothing to list under Security ([SECURITY.md](../../SECURITY.md)). + - Keep each `added` fragment that names something the app does, and `git rm` one a later fragment replaces. -A tag such as `linux-v0.2.0-rc.1` publishes a GitHub pre-release with the tarballs and the template only, and skips the PPA and COPR. Its commit sets the version and the dates but adds no `CHANGELOG.md` section and keeps the fragments, so the stable release lists them; the pre-release notes are a towncrier draft of the fragments at the tag. + Fragments after the first beta are news to testers and go into 0.1.0's section as usual. 0.1.0 types its lead like any X.Y.0, and its notes tell testers who added only the beta repository to add the stable one ([README.md](../README.md#install)). ## A packaging fix -A fix to `build-aux/debian` or `build-aux/rpm` that needs no new version, such as a missing dependency. +A fix to `build-aux/debian` or `build-aux/rpm` that needs no new version, such as a missing dependency. It goes to the stable repositories only; a beta's fix is the next beta. The Flatpak's packaging is its manifest, and a fix to it waits for the next release. **PPA.** Merge the fix to `main`, then run the workflow by hand on `main`: @@ -41,14 +117,14 @@ A fix to `build-aux/debian` or `build-aux/rpm` that needs no new version, such a gh workflow run release-linux.yml --ref main -f tag=linux-v0.1.0 -f revision=2 ``` -It downloads that release's two tarballs, checks them against its `SHA256SUMS`, builds the source package with the packaging from `main` as `0.1.0-0ppa2~YY.MM`, builds and tests the .deb on both architectures, and uploads it once a reviewer approves. Launchpad accepts the release's own orig tarballs again, and would refuse new ones whose bytes differ. Count the revision up for each further upload of the same version. +It downloads that release's two tarballs, checks them against its `SHA256SUMS`, builds the source package with the packaging from `main` as `0.1.0-0ppa2~YY.MM`, builds and tests the .deb on both architectures, and uploads it to the stable PPA once a reviewer approves. Launchpad accepts the release's own orig tarballs again, and would refuse new ones whose bytes differ. Count the revision up for each further upload of the same version. The release's GitHub assets stay as they were. **COPR.** COPR builds the spec at the tag, so a fix to it goes in by hand. Raise `Release` in `build-aux/rpm/tablepro.spec` on `main` with a `%changelog` entry, then, on Fedora with `copr-cli` signed in, build the source RPM from that spec and submit it: ```sh spectool --get-files --sources build-aux/rpm/tablepro.spec rpmbuild -bs --define "_sourcedir $PWD" --define "_srcrpmdir $PWD" build-aux/rpm/tablepro.spec -copr-cli build /tablepro tablepro-0.1.0-2.*.src.rpm +copr-cli build @tablepro/tablepro tablepro-0.1.0-2.*.src.rpm ``` The next version's release commit sets `Release` back to 1. diff --git a/linux/docs/testing.md b/linux/docs/testing.md index 80d77bd784..ffe634e049 100644 --- a/linux/docs/testing.md +++ b/linux/docs/testing.md @@ -517,7 +517,7 @@ Left for a machine with a GPU and a HiDPI screen: the GL and Vulkan renderers on ## CI -`.github/workflows/build-linux.yml` builds and tests the app, `.github/workflows/linux-packages.yml` builds, installs and starts the .deb and .rpm, and `.github/workflows/release-linux.yml` publishes a `linux-v*` tag. Each workflow's `on:` and each job's `if:` say when it runs; its steps say what it runs. To run the same checks locally: the [fast-job commands](../CONTRIBUTING.md#fast-job-commands) and `build-aux/packaging/build-packages.sh`. +`.github/workflows/build-linux.yml` builds and tests the app, `.github/workflows/linux-packages.yml` builds, installs and starts the .deb and the .rpm, and installs and updates the Flatpak from a test repository, and `.github/workflows/release-linux.yml` publishes a `linux-v*` tag. Each workflow's `on:` and each job's `if:` say when it runs; its steps say what it runs. To run the same checks locally: the [fast-job commands](../CONTRIBUTING.md#fast-job-commands) and `build-aux/packaging/build-packages.sh`. `.github/workflows/core.yml` builds the core crates for Windows x64 and Arm64 and macOS, runs the unit tests of `tablepro-core` and the SQL Server driver there, with the driver's `kerberos` feature on macOS, where it links the system's GSS framework, and runs the SSPI login tests above. diff --git a/linux/docs/troubleshooting.md b/linux/docs/troubleshooting.md index 680be38f0b..316a6ec599 100644 --- a/linux/docs/troubleshooting.md +++ b/linux/docs/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -What to check when TablePro for Linux does not behave, and what to send with a bug report. Paths are for the PPA and COPR packages. +What to check when TablePro for Linux does not behave, and what to send with a bug report. Paths are for the .deb and .rpm packages; the Flatpak's are in [Where TablePro keeps its files](#where-tablepro-keeps-its-files). ## Passwords @@ -73,12 +73,14 @@ Read the log before you attach it. It names your hosts, databases and tables. It TablePro writes a report of each crash to `~/.local/state/tablepro/crashes/`. Attach the newest one to the bug report, with the log from the same time. +The Flatpak's line tables, which turn a backtrace's addresses into files and lines, are its `.Debug` extension, from the repository it came from: `flatpak install tablepro app.tablepro.TablePro.Debug//stable`, or `tablepro-beta` and `//beta` for the beta. With flatpak-builder and the GNOME 51 SDK installed, `flatpak-coredumpctl app.tablepro.TablePro` then opens a crash's core dump in gdb with them. + ## Reporting a bug Open an issue at https://github.com/TableProApp/TablePro/issues with: - The text from About TablePro > Troubleshooting > Debugging Information. Copy Text puts it on the clipboard. It holds the versions and no connection details. -- Where TablePro came from (the PPA, COPR or a source build), the distribution and version, and the desktop and session (`echo $XDG_CURRENT_DESKTOP $XDG_SESSION_TYPE`). +- Where TablePro came from (the PPA, COPR, the Flatpak repository, a .deb, .rpm or Flatpak bundle from GitHub Releases, or a source build), the distribution and version, and the desktop and session (`echo $XDG_CURRENT_DESKTOP $XDG_SESSION_TYPE`). - The log and crash report above, when there is one. ## After a downgrade @@ -103,3 +105,5 @@ Installing the newer version again brings all of it back, unless it was reset. C | `~/.local/state/tablepro/crashes/` | Crash reports | Preferences are in GSettings: `gsettings list-recursively app.tablepro.TablePro` lists them. `man tablepro` has the same list. + +The Flatpak keeps the same files under `~/.var/app/app.tablepro.TablePro/`: `config/tablepro/`, `.local/state/tablepro/` and `data/tablepro/` in place of `~/.config/tablepro/`, `~/.local/state/tablepro/` and `~/.local/share/tablepro/`. Its preferences are listed by `flatpak run --command=gsettings app.tablepro.TablePro list-recursively app.tablepro.TablePro`, and a detailed log comes from `flatpak run --env=RUST_LOG=debug app.tablepro.TablePro 2> tablepro.log`. diff --git a/linux/po/vi.po b/linux/po/vi.po index d2f3410e83..898257c20c 100644 --- a/linux/po/vi.po +++ b/linux/po/vi.po @@ -5328,19 +5328,19 @@ msgstr "Trình khách cơ sở dữ liệu" #: data/app.tablepro.TablePro.desktop.in.in msgid "" -"Browse and edit PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, " -"Cassandra, ScyllaDB, Redis and MongoDB databases" +"Browse and edit PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, " +"ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB databases" msgstr "" -"Duyệt và sửa cơ sở dữ liệu PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, " -"ClickHouse, Cassandra, ScyllaDB, Redis và MongoDB" +"Duyệt và sửa cơ sở dữ liệu PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL " +"Server, ClickHouse, Cassandra, ScyllaDB, Redis và MongoDB" #: data/app.tablepro.TablePro.desktop.in.in msgid "" -"SQL;Database;PostgreSQL;MySQL;SQLite;DuckDB;SQL " -"Server;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB;" +"SQL;Database;PostgreSQL;Postgres;MySQL;MariaDB;SQLite;DuckDB;SQL " +"Server;MSSQL;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB;" msgstr "" -"SQL;Cơ sở dữ liệu;CSDL;Database;PostgreSQL;MySQL;SQLite;DuckDB;SQL " -"Server;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB;" +"SQL;Cơ sở dữ liệu;CSDL;Database;PostgreSQL;Postgres;MySQL;MariaDB;SQLite;" +"DuckDB;SQL Server;MSSQL;ClickHouse;Cassandra;ScyllaDB;CQL;Redis;MongoDB;" #: data/app.tablepro.TablePro.desktop.in.in data/resources/gtk/menus.ui #: data/resources/shortcuts-dialog.ui @@ -5570,12 +5570,12 @@ msgstr "Làm việc với cơ sở dữ liệu của bạn" #: data/app.tablepro.TablePro.metainfo.xml.in.in msgid "" -"TablePro connects to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, " +"TablePro connects to PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, " "ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB. Browse tables, edit rows " "in place, run SQL with syntax highlighting, and reach servers behind an SSH " "bastion." msgstr "" -"TablePro kết nối tới PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, " +"TablePro kết nối tới PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, " "ClickHouse, Cassandra, ScyllaDB, Redis và MongoDB. Duyệt bảng, sửa dòng tại chỗ, chạy " "SQL với tô sáng cú pháp, và truy cập các máy chủ phía sau một bastion SSH." @@ -5628,11 +5628,13 @@ msgstr "Bản phát hành đầu tiên của TablePro cho Linux." #: data/app.tablepro.TablePro.metainfo.xml.in.in msgid "" -"Connect to PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, " -"Cassandra, ScyllaDB, Redis and MongoDB, directly or through an SSH tunnel" +"Connect to PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, " +"ClickHouse, Cassandra, ScyllaDB, Redis and MongoDB, directly or through an " +"SSH tunnel" msgstr "" -"Kết nối tới PostgreSQL, MySQL, SQLite, DuckDB, SQL Server, ClickHouse, " -"Cassandra, ScyllaDB, Redis và MongoDB, trực tiếp hoặc qua đường hầm SSH" +"Kết nối tới PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, SQL Server, " +"ClickHouse, Cassandra, ScyllaDB, Redis và MongoDB, trực tiếp hoặc qua đường " +"hầm SSH" #: data/app.tablepro.TablePro.metainfo.xml.in.in msgid "Browse tables and edit rows in place, with undo until you save"