viewer: pause writes while text is selected — restores copy #74
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish apt repo | |
| # Two channels: | |
| # * `stable` — populated when a `v*` tag is pushed. The package | |
| # version is whatever Cargo.toml says (the `release.yml` tag flow | |
| # is expected to bump that to the release version). | |
| # * `nightly` — populated on every push to `main`. The package | |
| # version is rewritten on the fly to | |
| # <base>~nightly<UTC YYYYMMDD>.<UTC HHMMSS>-1 | |
| # `~` is the Debian pre-release separator (Debian Versioning | |
| # wiki) so e.g. `0.4.0~nightly20260527.123000` sorts strictly | |
| # BEFORE `0.4.0`. That gives users a "track main" channel that | |
| # auto-downgrades-and-upgrades when a stable lands. | |
| # | |
| # Single output: a Debian "pool" repo on the `gh-pages` branch, | |
| # served from https://deb.tab-atelier.wdes.eu/ (CNAME → GH Pages). | |
| # Signed with the key whose private half lives in the | |
| # `APT_SIGNING_KEY` repo secret. | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| permissions: | |
| contents: write | |
| actions: read # download the latest windows-msi artifact for /windows/ | |
| env: | |
| RUST_VERSION: "1.95.0" | |
| jobs: | |
| publish: | |
| name: Build .debs + publish to gh-pages | |
| runs-on: ubuntu-latest | |
| # Skip if the commit is a `[skip apt]` chore (e.g. README-only | |
| # tweaks from the release workflow itself). | |
| if: ${{ !contains(github.event.head_commit.message, '[skip apt]') }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Decide channel + version | |
| id: ver | |
| run: | | |
| if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then | |
| CHANNEL=stable | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| else | |
| CHANNEL=nightly | |
| BASE=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2 | sed 's/-dev$//') | |
| DATE=$(date -u +%Y%m%d.%H%M%S) | |
| VERSION="${BASE}~nightly${DATE}-1" | |
| fi | |
| echo "channel=$CHANNEL" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "→ publishing $VERSION to $CHANNEL" | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| libvulkan-dev libwayland-dev libxkbcommon-dev \ | |
| libxkbcommon-x11-dev libx11-dev libxcb1-dev \ | |
| libxcb-render0-dev libxcb-shm0-dev libxcb-xkb-dev \ | |
| libfontconfig-dev libfreetype-dev \ | |
| apt-utils gnupg | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_VERSION }} | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Build .deb (both variants) | |
| run: | | |
| cargo install cargo-deb --locked | |
| cargo deb -p tab-atelier --locked --deb-version "${{ steps.ver.outputs.version }}" | |
| cargo deb -p tab-atelier --variant headless --locked --deb-version "${{ steps.ver.outputs.version }}" | |
| ls -la target/debian/ | |
| # Pull the existing gh-pages tree into site/ so the | |
| # next steps can prune + regenerate against it. First | |
| # run on a fresh repo: the branch doesn't exist; we | |
| # init an empty site/ instead. peaceiris/actions-gh-pages | |
| # at the end handles the orphan-branch case for the | |
| # actual push, so this step just gets the prior state | |
| # in front of us. | |
| - name: Fetch existing gh-pages state | |
| env: | |
| GH_TOKEN_X: ${{ github.token }} | |
| run: | | |
| REMOTE="https://x-access-token:${GH_TOKEN_X}@github.com/${{ github.repository }}.git" | |
| rm -rf site | |
| if git ls-remote --heads "$REMOTE" gh-pages | grep -q '\<gh-pages$'; then | |
| echo "→ gh-pages exists upstream — clone shallow" | |
| git clone --depth=1 -b gh-pages --single-branch "$REMOTE" site | |
| else | |
| echo "→ gh-pages does NOT exist — start with empty site/" | |
| mkdir -p site | |
| fi | |
| - name: Drop .debs into pool/ | |
| env: | |
| CHANNEL: ${{ steps.ver.outputs.channel }} | |
| run: | | |
| cd site | |
| mkdir -p pool/$CHANNEL/main/t/tab-atelier | |
| cp ../target/debian/*.deb pool/$CHANNEL/main/t/tab-atelier/ | |
| # Cap the pool at the 10 most recent .debs per package | |
| # so the repo doesn't balloon forever. Sort by mtime | |
| # then prune the tail. | |
| for pkg in tab-atelier tab-atelier-headless; do | |
| ls -1t pool/$CHANNEL/main/t/tab-atelier/${pkg}_*.deb 2>/dev/null \ | |
| | tail -n +11 | xargs -r rm -f | |
| done | |
| # Windows MSIs are built on a windows runner by | |
| # windows-msi.yml and can't be produced here. Pull the .msi | |
| # from the most recent successful run into site/windows/ and | |
| # regenerate that folder's index. The gh-pages clone above | |
| # already preserved any previously-published MSIs, so if no | |
| # fresh artifact is found the existing set just carries over. | |
| - name: Add Windows MSI builds + index | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| cd site | |
| mkdir -p windows | |
| RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" \ | |
| --workflow=windows-msi.yml --status=success \ | |
| -L 1 --json databaseId --jq '.[0].databaseId // empty') | |
| if [ -n "$RUN_ID" ]; then | |
| echo "→ pulling MSI from windows-msi run $RUN_ID" | |
| rm -rf ../win-tmp && mkdir -p ../win-tmp | |
| if gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" -D ../win-tmp; then | |
| find ../win-tmp -name '*.msi' -exec cp -f {} windows/ \; | |
| fi | |
| rm -rf ../win-tmp | |
| else | |
| echo "→ no successful windows-msi run yet; keeping existing MSIs" | |
| fi | |
| # Cap at the 10 most recent MSIs. | |
| ls -1t windows/*.msi 2>/dev/null | tail -n +11 | xargs -r rm -f | |
| # Regenerate the folder index (newest first). | |
| { | |
| echo '<!DOCTYPE html><meta charset=utf-8><title>tab-atelier Windows builds</title>' | |
| echo '<style>body{font-family:system-ui;max-width:42em;margin:3em auto;padding:0 1em}' | |
| echo 'code{background:#f4f4f4;padding:.1em .3em;border-radius:3px}</style>' | |
| echo '<h1>tab-atelier Windows builds</h1>' | |
| echo '<p>Headless daemon MSI installers (<code>tab-atelier-headless</code>), newest first. Each filename carries the commit short SHA.</p>' | |
| echo '<ul>' | |
| for f in $(ls -1t windows/*.msi 2>/dev/null); do | |
| bn=$(basename "$f") | |
| sz=$(du -h "$f" | cut -f1) | |
| echo "<li><a href=\"$bn\">$bn</a> — $sz</li>" | |
| done | |
| echo '</ul><p><a href="../">← back to the apt repo</a></p>' | |
| } > windows/index.html | |
| - name: Regenerate Packages + Release | |
| env: | |
| CHANNEL: ${{ steps.ver.outputs.channel }} | |
| run: | | |
| cd site | |
| mkdir -p dists/$CHANNEL/main/binary-amd64 | |
| apt-ftparchive --arch amd64 packages pool/$CHANNEL/ \ | |
| > dists/$CHANNEL/main/binary-amd64/Packages | |
| gzip -9 --force --keep dists/$CHANNEL/main/binary-amd64/Packages | |
| cat > apt-ftparchive-$CHANNEL.conf <<EOF | |
| APT::FTPArchive::Release::Origin "tab-atelier"; | |
| APT::FTPArchive::Release::Label "tab-atelier"; | |
| APT::FTPArchive::Release::Suite "$CHANNEL"; | |
| APT::FTPArchive::Release::Codename "$CHANNEL"; | |
| APT::FTPArchive::Release::Architectures "amd64"; | |
| APT::FTPArchive::Release::Components "main"; | |
| APT::FTPArchive::Release::Description "tab-atelier $CHANNEL channel"; | |
| EOF | |
| apt-ftparchive -c apt-ftparchive-$CHANNEL.conf release \ | |
| dists/$CHANNEL/ > dists/$CHANNEL/Release | |
| rm apt-ftparchive-$CHANNEL.conf | |
| # Import the apt signing key into the runner's GPG | |
| # keyring AND configure git to use it for commit | |
| # signatures. The same key signs both the apt Release | |
| # file AND the gh-pages commits — anyone with the | |
| # public key (curlable from deb.tab-atelier.wdes.eu) | |
| # can verify both ends with one trust anchor. | |
| - name: Import GPG signing key | |
| id: gpg | |
| uses: crazy-max/ghaction-import-gpg@v7 | |
| with: | |
| gpg_private_key: ${{ secrets.APT_SIGNING_KEY }} | |
| fingerprint: ${{ secrets.APT_SIGNING_KEY_ID }} | |
| git_user_signingkey: true | |
| git_commit_gpgsign: true | |
| git_committer_name: 'Wdes Bot' | |
| git_committer_email: 'williamdes+wdes-bot@wdes.fr' | |
| - name: Sign Release | |
| env: | |
| CHANNEL: ${{ steps.ver.outputs.channel }} | |
| GPG_KEY_ID: ${{ steps.gpg.outputs.fingerprint }} | |
| run: | | |
| cd site | |
| gpg --batch --yes --local-user "$GPG_KEY_ID" \ | |
| --output dists/$CHANNEL/Release.gpg \ | |
| --detach-sign dists/$CHANNEL/Release | |
| gpg --batch --yes --local-user "$GPG_KEY_ID" --clearsign \ | |
| --output dists/$CHANNEL/InRelease \ | |
| dists/$CHANNEL/Release | |
| # Public key is republished on every run so a | |
| # re-key propagates automatically to users on | |
| # their next `apt update`. | |
| gpg --batch --yes --armor --export "$GPG_KEY_ID" \ | |
| > tab-atelier.gpg | |
| - name: Write CNAME + landing page | |
| run: | | |
| cd site | |
| echo "deb.tab-atelier.wdes.eu" > CNAME | |
| cat > index.html <<'HTML' | |
| <!DOCTYPE html><meta charset=utf-8><title>tab-atelier apt repo</title> | |
| <style>body{font-family:system-ui;max-width:42em;margin:3em auto;padding:0 1em} | |
| pre{background:#f4f4f4;padding:1em;border-radius:4px;overflow-x:auto} | |
| code{background:#f4f4f4;padding:.1em .3em;border-radius:3px}</style> | |
| <h1>tab-atelier apt repo</h1> | |
| <p>Debian / Ubuntu install:</p> | |
| <pre>curl -fsSL https://deb.tab-atelier.wdes.eu/tab-atelier.gpg \ | |
| | sudo tee /usr/share/keyrings/tab-atelier.gpg > /dev/null | |
| echo "deb [signed-by=/usr/share/keyrings/tab-atelier.gpg] https://deb.tab-atelier.wdes.eu stable main" \ | |
| | sudo tee /etc/apt/sources.list.d/tab-atelier.list > /dev/null | |
| sudo apt update | |
| sudo apt install tab-atelier # or tab-atelier-headless</pre> | |
| <p>Replace <code>stable</code> with <code>nightly</code> to track <code>main</code>.</p> | |
| <h2>Windows</h2> | |
| <p>Headless MSI installers for Windows are at <a href="/windows/">/windows/</a>.</p> | |
| <p>Source: <a href="https://github.com/wdes/tab-atelier">github.com/wdes/tab-atelier</a>.</p> | |
| HTML | |
| # peaceiris/actions-gh-pages handles the orphan branch | |
| # case (first run) and the push. The commit it creates | |
| # picks up the global git config crazy-max set above — | |
| # so `git commit` runs with commit.gpgsign=true and | |
| # user.signingkey = the apt key fingerprint. Result: | |
| # every gh-pages commit shows up as Verified on GitHub, | |
| # signed by the same key that signs the apt Release. | |
| # `keep_files: false` means the action REPLACES gh-pages | |
| # with our `site/` tree wholesale — pruning + the prior | |
| # checkout above gave us exactly the desired final | |
| # state, so wipe-and-replace is the right behaviour. | |
| - name: Publish to gh-pages | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./site | |
| publish_branch: gh-pages | |
| user_name: 'Wdes Bot' | |
| user_email: 'williamdes+wdes-bot@wdes.fr' | |
| commit_message: 'Publish ${{ steps.ver.outputs.version }} to ${{ steps.ver.outputs.channel }}' | |
| keep_files: false |