name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
binary: mdr
archive: mdr-aarch64-apple-darwin.tar.gz
- os: macos-latest
target: x86_64-apple-darwin
binary: mdr
archive: mdr-x86_64-apple-darwin.tar.gz
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: mdr
archive: mdr-x86_64-unknown-linux-gnu.tar.gz
# Native ARM64 runner: the GTK/WebKit/OpenGL system dependencies make
# cross-compilation from x86_64 impractical (it would need a full
# aarch64 sysroot), so we build on an aarch64 machine instead.
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
binary: mdr
archive: mdr-aarch64-unknown-linux-gnu.tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
binary: mdr.exe
archive: mdr-x86_64-pc-windows-msvc.zip
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libxdo-dev libgl1-mesa-dev
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
# LICENSE travels with the binary: the archives are what Homebrew, Scoop,
# Chocolatey and the AUR package all download, and the AUR `package()`
# installs it from ${srcdir}.
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
cp LICENSE target/${{ matrix.target }}/release/
cd target/${{ matrix.target }}/release
tar czf ../../../${{ matrix.archive }} ${{ matrix.binary }} LICENSE
- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item LICENSE "target/${{ matrix.target }}/release/"
Compress-Archive -Path "target/${{ matrix.target }}/release/${{ matrix.binary }}", "target/${{ matrix.target }}/release/LICENSE" -DestinationPath "${{ matrix.archive }}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.archive }}
path: ${{ matrix.archive }}
build-deb:
name: Build .deb package (${{ matrix.arch }})
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: amd64
- os: ubuntu-24.04-arm
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libxdo-dev libgl1-mesa-dev
- name: Install cargo-deb
run: cargo install cargo-deb
# Built natively on each architecture, so the host target is used and the
# `target/release/...` asset paths of Cargo.toml need no rewriting.
- name: Build .deb
run: cargo deb
# Makes the resolved dependency set visible in the logs, so a missing
# runtime library is caught by reading the release run instead of by a
# user's failed install.
- name: Show .deb dependencies
run: |
for f in target/debian/*.deb; do
echo "== $f"
dpkg-deb -f "$f" Package Version Architecture Depends
done
- name: Upload .deb artifact
uses: actions/upload-artifact@v4
with:
name: mdr-deb-${{ matrix.arch }}
path: target/debian/*.deb
build-rpm:
name: Build .rpm package (${{ matrix.arch }})
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64
- os: ubuntu-24.04-arm
arch: aarch64
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libxdo-dev libgl1-mesa-dev
- name: Build release binary
run: cargo build --release
- name: Install cargo-generate-rpm
run: cargo install cargo-generate-rpm
- name: Build .rpm
run: cargo generate-rpm
# Same rationale as for the .deb: the auto-detected requires (auto-req is
# ldd-based) plus the explicit ones from Cargo.toml are printed here.
# `rpm` is installed *after* `cargo generate-rpm` on purpose: installing it
# first would make /usr/lib/rpm/find-requires exist, which switches
# cargo-generate-rpm from its builtin auto-req to find-requires.
- name: Show .rpm dependencies
run: |
sudo apt-get install -y rpm
for f in target/generate-rpm/*.rpm; do
echo "== $f"
rpm -qp --requires "$f"
done
- name: Upload .rpm artifact
uses: actions/upload-artifact@v4
with:
name: mdr-rpm-${{ matrix.arch }}
path: target/generate-rpm/*.rpm
update-homebrew:
name: Update Homebrew tap
needs: release
runs-on: ubuntu-latest
if: ${{ vars.HOMEBREW_TAP_ENABLED == 'true' }}
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Compute SHA256
id: sha
run: |
echo "sha_aarch64=$(sha256sum artifacts/mdr-aarch64-apple-darwin.tar.gz/mdr-aarch64-apple-darwin.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
echo "sha_x86_64=$(sha256sum artifacts/mdr-x86_64-apple-darwin.tar.gz/mdr-x86_64-apple-darwin.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
echo "sha_linux=$(sha256sum artifacts/mdr-x86_64-unknown-linux-gnu.tar.gz/mdr-x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
echo "sha_linux_arm=$(sha256sum artifacts/mdr-aarch64-unknown-linux-gnu.tar.gz/mdr-aarch64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
- name: Extract version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Generate and push formula
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
SHA_AARCH64: ${{ steps.sha.outputs.sha_aarch64 }}
SHA_X86_64: ${{ steps.sha.outputs.sha_x86_64 }}
SHA_LINUX: ${{ steps.sha.outputs.sha_linux }}
SHA_LINUX_ARM: ${{ steps.sha.outputs.sha_linux_arm }}
run: |
cat > mdr.rb <<EOF
class Mdr < Formula
desc "A lightweight Markdown viewer with Mermaid diagram support"
homepage "https://github.com/CleverCloud/mdr"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/CleverCloud/mdr/releases/download/v${VERSION}/mdr-aarch64-apple-darwin.tar.gz"
sha256 "${SHA_AARCH64}"
else
url "https://github.com/CleverCloud/mdr/releases/download/v${VERSION}/mdr-x86_64-apple-darwin.tar.gz"
sha256 "${SHA_X86_64}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/CleverCloud/mdr/releases/download/v${VERSION}/mdr-aarch64-unknown-linux-gnu.tar.gz"
sha256 "${SHA_LINUX_ARM}"
else
url "https://github.com/CleverCloud/mdr/releases/download/v${VERSION}/mdr-x86_64-unknown-linux-gnu.tar.gz"
sha256 "${SHA_LINUX}"
end
end
def install
bin.install "mdr"
end
test do
system "#{bin}/mdr", "--version"
end
end
EOF
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/CleverCloud/homebrew-misc.git" tap
mkdir -p tap/Formula
cp mdr.rb tap/Formula/mdr.rb
cd tap
git config user.email "opensource@clever-cloud.com"
git config user.name "Clever Cloud CI"
git add Formula/mdr.rb
git diff --cached --quiet || git commit -m "Update mdr to ${VERSION}"
git push
publish-crate:
name: Publish to crates.io
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libxdo-dev libgl1-mesa-dev
- name: Publish to crates.io
run: cargo publish || echo "Crate version already published, skipping"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
update-winget:
name: Update WinGet
needs: release
runs-on: windows-latest
if: ${{ vars.WINGET_ENABLED == 'true' }}
steps:
- uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: CleverCloud.mdr
installers-regex: 'windows'
token: ${{ secrets.WINGET_TOKEN }}
update-aur:
name: Update AUR
needs: release
runs-on: ubuntu-latest
if: ${{ vars.AUR_ENABLED == 'true' }}
steps:
- uses: actions/checkout@v4
- name: Extract version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Download Linux artifact
uses: actions/download-artifact@v4
with:
name: mdr-x86_64-unknown-linux-gnu.tar.gz
path: artifacts
- name: Compute SHA256
id: sha
run: echo "sha256=$(sha256sum artifacts/mdr-x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT
- name: Generate PKGBUILD
run: |
cat > PKGBUILD << 'PKGEOF'
# Maintainer: Clever Cloud <opensource@clever-cloud.com>
pkgname=mdr-bin
pkgver=${{ steps.version.outputs.version }}
pkgrel=1
pkgdesc="A lightweight Markdown viewer with Mermaid diagram support, live reload, and multiple backends"
arch=('x86_64')
url="https://github.com/CleverCloud/mdr"
license=('MIT')
provides=('mdr')
conflicts=('mdr')
source=("https://github.com/CleverCloud/mdr/releases/download/v${pkgver}/mdr-x86_64-unknown-linux-gnu.tar.gz")
sha256sums=('${{ steps.sha.outputs.sha256 }}')
package() {
install -Dm755 "${srcdir}/mdr" "${pkgdir}/usr/bin/mdr"
# MIT is not in /usr/share/licenses/common, so Arch requires the
# text to be installed with the package.
install -Dm644 "${srcdir}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
PKGEOF
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3
with:
pkgname: mdr-bin
pkgbuild: ./PKGBUILD
commit_username: "Clever Cloud CI"
commit_email: "opensource@clever-cloud.com"
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update to ${{ steps.version.outputs.version }}"
update-chocolatey:
name: Update Chocolatey
needs: release
runs-on: windows-latest
if: ${{ vars.CHOCOLATEY_ENABLED == 'true' }}
steps:
- name: Download Windows artifact
uses: actions/download-artifact@v4
with:
name: mdr-x86_64-pc-windows-msvc.zip
path: artifacts
- name: Extract version
id: version
shell: pwsh
run: echo "version=$("$env:GITHUB_REF_NAME" -replace '^v','')" >> $env:GITHUB_OUTPUT
- name: Compute SHA256
id: sha
shell: pwsh
run: |
$hash = (Get-FileHash artifacts/mdr-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
echo "sha256=$hash" >> $env:GITHUB_OUTPUT
- name: Generate nuspec and install script
shell: pwsh
env:
VERSION: ${{ steps.version.outputs.version }}
SHA256: ${{ steps.sha.outputs.sha256 }}
run: |
mkdir choco-pkg/tools
$nuspec = "<?xml version=`"1.0`" encoding=`"utf-8`"?>`n<package xmlns=`"http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd`">`n <metadata>`n <id>mdr</id>`n <version>$env:VERSION</version>`n <title>mdr</title>`n <authors>Clever Cloud</authors>`n <owners>CleverCloud</owners>`n <summary>A lightweight Markdown viewer with live reload</summary>`n <description>A lightweight Markdown viewer with Mermaid diagram support, live reload, and multiple rendering backends (gui, tui, web).</description>`n <projectUrl>https://github.com/CleverCloud/mdr</projectUrl>`n <tags>markdown viewer mermaid tui</tags>`n <licenseUrl>https://github.com/CleverCloud/mdr/blob/main/LICENSE</licenseUrl>`n <requireLicenseAcceptance>false</requireLicenseAcceptance>`n </metadata>`n</package>"
[System.IO.File]::WriteAllText("choco-pkg/mdr.nuspec", $nuspec)
$install = "`$ErrorActionPreference = 'Stop'`n`$url = `"https://github.com/CleverCloud/mdr/releases/download/v$env:VERSION/mdr-x86_64-pc-windows-msvc.zip`"`n`$checksum = `"$env:SHA256`"`nInstall-ChocolateyZipPackage -PackageName 'mdr' -Url64bit `$url -Checksum64 `$checksum -ChecksumType64 'sha256' -UnzipLocation `"`$(Split-Path -Parent `$MyInvocation.MyCommand.Definition)`""
[System.IO.File]::WriteAllText("choco-pkg/tools/chocolateyInstall.ps1", $install)
- name: Pack and push
shell: pwsh
env:
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
run: |
cd choco-pkg
choco pack mdr.nuspec
choco push (Get-Item *.nupkg).Name --source https://push.chocolatey.org/ --api-key $env:CHOCOLATEY_API_KEY
update-scoop:
name: Update Scoop bucket
needs: release
runs-on: ubuntu-latest
if: ${{ vars.SCOOP_ENABLED == 'true' }}
steps:
- name: Download Windows artifact
uses: actions/download-artifact@v4
with:
name: mdr-x86_64-pc-windows-msvc.zip
path: artifacts
- name: Extract version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Compute SHA256
id: sha
run: echo "sha256=$(sha256sum artifacts/mdr-x86_64-pc-windows-msvc.zip | cut -d' ' -f1)" >> $GITHUB_OUTPUT
- name: Generate and push manifest
env:
SCOOP_BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
SHA256: ${{ steps.sha.outputs.sha256 }}
run: |
cat > mdr.json << EOF
{
"version": "${VERSION}",
"architecture": {
"64bit": {
"url": "https://github.com/CleverCloud/mdr/releases/download/v${VERSION}/mdr-x86_64-pc-windows-msvc.zip",
"hash": "${SHA256}"
}
},
"bin": "mdr.exe",
"checkver": {
"github": "https://github.com/CleverCloud/mdr"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/CleverCloud/mdr/releases/download/v\$version/mdr-x86_64-pc-windows-msvc.zip"
}
}
},
"homepage": "https://github.com/CleverCloud/mdr",
"license": "MIT",
"description": "A lightweight Markdown viewer with Mermaid diagram support, live reload, and multiple backends"
}
EOF
git clone "https://x-access-token:${SCOOP_BUCKET_TOKEN}@github.com/CleverCloud/scoop-bucket.git" bucket
mkdir -p bucket/bucket
cp mdr.json bucket/bucket/mdr.json
cd bucket
git config user.email "opensource@clever-cloud.com"
git config user.name "Clever Cloud CI"
git add bucket/mdr.json
git diff --cached --quiet || git commit -m "Update mdr to ${VERSION}"
git push
update-snap:
name: Publish to Snap Store
needs: release
runs-on: ubuntu-latest
if: ${{ vars.SNAP_ENABLED == 'true' }}
steps:
- uses: actions/checkout@v4
- name: Build snap
uses: snapcore/action-build@v1
id: build
- name: Publish to Snap Store
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
with:
snap: ${{ steps.build.outputs.snap }}
release: edge
release:
name: Create Release
needs: [build, build-deb, build-rpm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Needed to find the previous tag for the "Full Changelog" link.
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
# Extracts the "## [x.y.z]" section of CHANGELOG.md matching the tag.
# The notes file is rewritten from scratch on every run, so re-running the
# workflow for the same tag replaces the release body instead of growing it.
- name: Extract release notes from CHANGELOG
id: notes
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${TAG#v}"
notes="${RUNNER_TEMP}/release-notes.md"
awk -v ver="$version" '
index($0, "## [" ver "]") == 1 { found = 1; next }
found && /^## \[/ { exit }
found && /^\[[0-9]/ { exit }
found { print }
' CHANGELOG.md | sed -e '/./,$!d' > "$notes"
if [ -s "$notes" ]; then
prev=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)
if [ -n "$prev" ]; then
printf '\n**Full Changelog**: %s/compare/%s...%s\n' \
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" "$prev" "$TAG" >> "$notes"
fi
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Using CHANGELOG.md section for $version"
else
# No matching section: fall back to GitHub's generated notes rather
# than failing the release.
: > "$notes"
echo "found=false" >> "$GITHUB_OUTPUT"
echo "::warning::No CHANGELOG.md section found for $version, falling back to generated release notes"
fi
echo "path=$notes" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: ${{ steps.notes.outputs.path }}
append_body: false
generate_release_notes: ${{ steps.notes.outputs.found == 'false' }}
files: artifacts/**/*