name: Release
on:
push:
tags: ['v*']
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
validate:
name: Validate tag matches versions
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Compare tag, Cargo.toml, snapcraft.yaml versions
run: |
TAG_VER="${GITHUB_REF#refs/tags/v}"
CARGO_VER=$(awk -F'"' '/^version[[:space:]]*=[[:space:]]*"/{print $2; exit}' Cargo.toml)
SNAP_VER=$(sed -n "s/^version:[[:space:]]*['\"]\\?\\([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\)['\"]\\?.*/\\1/p" snap/snapcraft.yaml)
echo "tag=$TAG_VER cargo=$CARGO_VER snap=$SNAP_VER"
if [ "$TAG_VER" != "$CARGO_VER" ] || [ "$CARGO_VER" != "$SNAP_VER" ]; then
echo "::error::Version mismatch: tag=v$TAG_VER cargo=$CARGO_VER snap=$SNAP_VER"
exit 1
fi
- name: Vendored fork is publishable to crates.io
run: bash scripts/check-fork-publishable.sh
build:
name: Build (${{ matrix.asset_name }})
runs-on: ${{ matrix.os }}
needs: validate
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: linux-x86_64
- os: macos-14
target: aarch64-apple-darwin
asset_name: macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
asset_name: windows-x86_64
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install system dependencies (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y clang pkg-config libxcb1-dev libxcb-render0-dev \
libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev \
libgtk-3-dev libfontconfig1-dev libdbus-1-dev
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Remove local-only MCP dependency
shell: python
run: |
import pathlib, re
p = pathlib.Path("Cargo.toml")
t = p.read_text()
t = re.sub(r'(?m)^mcp\s*=\s*\["dep:egui-mcp-bridge"\]', 'mcp = []', t)
t = re.sub(r'(?m)^egui-mcp-bridge\s*=\s*.*\n', '', t)
p.write_text(t)
- name: Install git-cliff
if: runner.os == 'Linux'
run: cargo install git-cliff
- name: Generate changelog
if: runner.os == 'Linux'
run: git-cliff --latest --strip header > RELEASE_NOTES.md
- name: Build release
run: cargo build --release --target ${{ matrix.target }}
- name: Package (Linux/macOS)
if: runner.os != 'Windows'
run: |
VERSION=${GITHUB_REF#refs/tags/v}
ASSET="md-viewer-${VERSION}-${{ matrix.asset_name }}.tar.gz"
tar -czf "$ASSET" -C "target/${{ matrix.target }}/release" md-viewer
shasum -a 256 "$ASSET" > "$ASSET.sha256"
- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$VERSION = "${env:GITHUB_REF}" -replace '^refs/tags/v',''
$ASSET = "md-viewer-$VERSION-${{ matrix.asset_name }}.zip"
Compress-Archive -Path "target/${{ matrix.target }}/release/md-viewer.exe" -DestinationPath $ASSET
$Hash = (Get-FileHash -Algorithm SHA256 $ASSET).Hash.ToLower()
"$Hash $ASSET" | Out-File -Encoding ascii "$ASSET.sha256"
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.asset_name }}
if-no-files-found: error
path: |
md-viewer-*.tar.gz
md-viewer-*.zip
md-viewer-*.sha256
RELEASE_NOTES.md
publish-crates:
name: Publish to crates.io
runs-on: ubuntu-latest
needs: [validate, build]
timeout-minutes: 20
permissions:
contents: read
env:
CARGO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
steps:
- name: Check crates.io token
id: check
run: |
if [ -z "$CARGO_TOKEN" ]; then
echo "::notice::CARGO_REGISTRY_TOKEN not set; skipping crates.io publish (see PUBLISHING.md for setup)."
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.check.outputs.proceed == 'true'
- name: Install system dependencies
if: steps.check.outputs.proceed == 'true'
run: |
sudo apt-get update
sudo apt-get install -y clang pkg-config libxcb1-dev libxcb-render0-dev \
libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev \
libgtk-3-dev libfontconfig1-dev libdbus-1-dev
- uses: dtolnay/rust-toolchain@stable
if: steps.check.outputs.proceed == 'true'
- uses: Swatinem/rust-cache@v2
if: steps.check.outputs.proceed == 'true'
with:
key: publish-crates
- name: Remove local-only MCP dependency
if: steps.check.outputs.proceed == 'true'
shell: python
run: |
import pathlib, re
p = pathlib.Path("Cargo.toml")
t = p.read_text()
t = re.sub(r'(?m)^mcp\s*=\s*\["dep:egui-mcp-bridge"\]', 'mcp = []', t)
t = re.sub(r'(?m)^egui-mcp-bridge\s*=\s*.*\n', '', t)
p.write_text(t)
- name: Publish fork crates and md-viewer
if: steps.check.outputs.proceed == 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: bash scripts/publish-crates.sh
publish-snap:
name: Publish to Snap Store
runs-on: ubuntu-latest
needs: [validate, build]
timeout-minutes: 30
permissions:
contents: read
env:
SNAP_CREDS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- name: Check Snap Store credentials
id: check
run: |
if [ -z "$SNAP_CREDS" ]; then
echo "::notice::SNAPCRAFT_STORE_CREDENTIALS not set; skipping snap publish (see PUBLISHING.md for setup)."
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.check.outputs.proceed == 'true'
- uses: snapcore/action-build@v1
if: steps.check.outputs.proceed == 'true'
id: build
- uses: snapcore/action-publish@v1
if: steps.check.outputs.proceed == 'true'
with:
snap: ${{ steps.build.outputs.snap }}
release: stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
- name: Push snap store listing metadata
if: steps.check.outputs.proceed == 'true'
run: |
sudo snap install snapcraft --classic
snapcraft push-metadata "${SNAP_FILE}" --force
env:
SNAP_FILE: ${{ steps.build.outputs.snap }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
publish-aur:
name: Publish to AUR
runs-on: ubuntu-latest
needs: [validate, build]
timeout-minutes: 10
permissions:
contents: read
env:
AUR_SSH_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
steps:
- name: Check AUR secret
id: check
run: |
if [ -z "$AUR_SSH_KEY" ]; then
echo "::notice::AUR_SSH_PRIVATE_KEY not set; skipping AUR publish (see PUBLISHING.md for setup)."
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.check.outputs.proceed == 'true'
- name: Set version
if: steps.check.outputs.proceed == 'true'
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV"
- name: Set up SSH for AUR
if: steps.check.outputs.proceed == 'true'
run: |
mkdir -p ~/.ssh
printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org > ~/.ssh/known_hosts 2>/dev/null
cat > ~/.ssh/config <<'EOF'
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
StrictHostKeyChecking yes
EOF
chmod 600 ~/.ssh/config
- name: Clone AUR repo and update PKGBUILD
if: steps.check.outputs.proceed == 'true'
run: |
git clone ssh://aur@aur.archlinux.org/md-viewer-git.git aur-repo
cp aur/PKGBUILD aur-repo/PKGBUILD
# Bump the literal pkgver line so the file reflects the latest tag.
# (Build-time pkgver() still overrides at install, but this keeps the file readable.)
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" aur-repo/PKGBUILD
- name: Regenerate .SRCINFO
if: steps.check.outputs.proceed == 'true'
run: |
# makepkg refuses to run as root; spin up an Arch container with an unprivileged user.
docker run --rm -v "$PWD/aur-repo:/pkg" -w /pkg archlinux/archlinux:base-devel bash -c '
useradd -m builder &&
chown -R builder /pkg &&
sudo -u builder makepkg --printsrcinfo
' > aur-repo/.SRCINFO
- name: Restore aur-repo ownership to runner
if: steps.check.outputs.proceed == 'true'
run: sudo chown -R "$(id -u):$(id -g)" aur-repo
- name: Commit and push to AUR
if: steps.check.outputs.proceed == 'true'
working-directory: aur-repo
run: |
git config user.name "md-viewer CI"
git config user.email "ci@noreply.aydiler"
git add PKGBUILD .SRCINFO
if git diff --cached --quiet; then
echo "No changes to push to AUR."
exit 0
fi
git commit -m "Update to ${VERSION}"
git push
publish-aur-bin:
name: Publish to AUR (-bin)
runs-on: ubuntu-latest
needs: [validate, build]
timeout-minutes: 10
permissions:
contents: read
env:
AUR_SSH_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
steps:
- name: Check AUR secret
id: check
run: |
if [ -z "$AUR_SSH_KEY" ]; then
echo "::notice::AUR_SSH_PRIVATE_KEY not set; skipping AUR -bin publish (see PUBLISHING.md for setup)."
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.check.outputs.proceed == 'true'
- name: Set version
if: steps.check.outputs.proceed == 'true'
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV"
- name: Set up SSH for AUR
if: steps.check.outputs.proceed == 'true'
run: |
mkdir -p ~/.ssh
printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org > ~/.ssh/known_hosts 2>/dev/null
cat > ~/.ssh/config <<'EOF'
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
StrictHostKeyChecking yes
EOF
chmod 600 ~/.ssh/config
- name: Download linux build artifact (for tarball sha256)
if: steps.check.outputs.proceed == 'true'
uses: actions/download-artifact@v4
with:
name: release-linux-x86_64
path: dist/
- name: Clone AUR repo and update PKGBUILD
if: steps.check.outputs.proceed == 'true'
run: |
# On first run for this package, the repo doesn't exist yet — AUR
# creates it on first push. `git clone` of a not-yet-existing repo
# returns "warning: You appear to have cloned an empty repository."
# which is fine; we initialize it from the staged PKGBUILD.
git clone ssh://aur@aur.archlinux.org/md-viewer-bin.git aur-repo || true
if [ ! -d aur-repo ]; then
mkdir aur-repo
(cd aur-repo && git init -b master && \
git remote add origin ssh://aur@aur.archlinux.org/md-viewer-bin.git)
fi
cp aur-bin/PKGBUILD aur-repo/PKGBUILD
# Tarball sha256 comes from the downloaded artifact (see comment above).
# The three aux files (.desktop, icon, LICENSE) come from raw.github
# at the tagged commit — those URLs are valid as soon as the tag exists.
mkdir -p sums
cp "dist/md-viewer-${VERSION}-linux-x86_64.tar.gz.sha256" sums/tarball.sha256
curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/data/md-viewer.desktop" \
| sha256sum > sums/desktop.sha256
curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/data/io.github.aydiler.md-viewer.png" \
| sha256sum > sums/icon.sha256
curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/LICENSE" \
| sha256sum > sums/license.sha256
# Rewrite pkgver and the four-element sha256sums=( ... ) array.
# sed on a multi-line array is fragile, so use Python.
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("aur-repo/PKGBUILD")
sums = [open(f"sums/{n}.sha256").read().split()[0]
for n in ("tarball", "desktop", "icon", "license")]
import os
version = os.environ["VERSION"]
text = p.read_text()
text = re.sub(r"^pkgver=.*$", f"pkgver={version}", text, count=1, flags=re.M)
block = "sha256sums=(\n '" + "'\n '".join(sums) + "'\n)"
text = re.sub(r"sha256sums=\([^)]*\)", block, text, count=1, flags=re.S)
p.write_text(text)
PY
- name: Regenerate .SRCINFO
if: steps.check.outputs.proceed == 'true'
run: |
docker run --rm -v "$PWD/aur-repo:/pkg" -w /pkg archlinux/archlinux:base-devel bash -c '
useradd -m builder &&
chown -R builder /pkg &&
sudo -u builder makepkg --printsrcinfo
' > aur-repo/.SRCINFO
- name: Restore aur-repo ownership to runner
if: steps.check.outputs.proceed == 'true'
run: sudo chown -R "$(id -u):$(id -g)" aur-repo
- name: Commit and push to AUR
if: steps.check.outputs.proceed == 'true'
working-directory: aur-repo
run: |
git config user.name "md-viewer CI"
git config user.email "ci@noreply.aydiler"
git add PKGBUILD .SRCINFO
if git diff --cached --quiet; then
echo "No changes to push to AUR."
exit 0
fi
git commit -m "Update to ${VERSION}"
# `-u origin HEAD:refs/heads/master` works for both first push
# (brand-new AUR repo without an upstream) and subsequent pushes.
git push -u origin HEAD:refs/heads/master
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [validate, build]
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: release-*
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTES.md
files: |
md-viewer-*.tar.gz
md-viewer-*.zip
md-viewer-*.sha256