check-rust.yml ran cargo check and cargo test without --locked: a Cargo.lock out of step with Cargo.toml was rewritten in the job's checkout, and the suppression guard, the windows-sys guard and cargo audit then read the rewritten lockfile instead of the committed one. Both now pass --locked, and the windows-sys guard comment says the lockfile it reads is the committed one. release.yml built the shipped binaries the same way. Both npx tauri build calls now end with `-- --locked`, which the Tauri CLI hands to its runner (cargo, or cargo-xwin for Windows, which passes it to cargo build). cargo-xwin, the runner of the Windows build, runs with the updater signing key in its environment and was installed at whatever version crates.io served on the day of the tag. It is pinned at 0.23.1, the version the v0.17.2 release installed (run 570), and its version is printed in the job log. Refs #449 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
255 lines
11 KiB
YAML
255 lines
11 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
jobs:
|
|
build-and-release:
|
|
runs-on: ubuntu
|
|
container: ubuntu:22.04
|
|
env:
|
|
PATH: /root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
steps:
|
|
- name: Install base tools and runtimes
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y curl wget git sudo ca-certificates gnupg
|
|
# Install Node.js 24 (#376). Trust note, kept on purpose without a
|
|
# pinned tarball: NodeSource's script runs as root in this
|
|
# container, and the Node.js it installs then runs `npx tauri build`
|
|
# with TAURI_SIGNING_PRIVATE_KEY in its environment. A compromised
|
|
# script or package would reach the updater signing key, and the
|
|
# publishing tokens of the later steps. See docs/architecture.md
|
|
# (CI/CD, release.yml).
|
|
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
|
apt-get install -y nodejs
|
|
# Install Rust
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
rustc --version
|
|
cargo --version
|
|
node --version
|
|
npm --version
|
|
|
|
- name: Checkout
|
|
uses: https://github.com/actions/checkout@v4
|
|
|
|
- name: Install Linux dependencies
|
|
run: |
|
|
apt-get install -y build-essential libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf jq libssl-dev xdg-utils libdbus-1-dev
|
|
|
|
- name: Install Windows cross-compile dependencies
|
|
run: |
|
|
apt-get install -y lld llvm clang nsis
|
|
rustup target add x86_64-pc-windows-msvc
|
|
# Pinned (#449): cargo-xwin is the runner of the Windows build below
|
|
# and runs with TAURI_SIGNING_PRIVATE_KEY in its environment, so a
|
|
# new release of it must be a decision, not whatever crates.io serves
|
|
# on the day of the tag. 0.23.1 is the version the v0.17.2 release
|
|
# installed unpinned (run 570). A version without an operator makes
|
|
# cargo install exactly that one. Bump it on purpose, after reading
|
|
# what the new version changes.
|
|
cargo install --locked cargo-xwin --version 0.23.1
|
|
cargo-xwin --version
|
|
|
|
- name: Install frontend dependencies
|
|
run: npm ci
|
|
|
|
- name: Build Tauri
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
# Arguments after -- go to the runner (#449): cargo here, cargo-xwin
|
|
# in the Windows step, which hands them to cargo build. --locked
|
|
# builds the shipped binaries from the committed Cargo.lock, or fails
|
|
# rather than rewrite it. The log never prints the cargo command
|
|
# line: only a build that fails on a lockfile out of step with
|
|
# Cargo.toml proves the flag gets there.
|
|
npx tauri build --bundles deb,rpm -- --locked
|
|
|
|
- name: Build Tauri Windows
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
# -- --locked: see the Linux build above (#449).
|
|
npx tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc -- --locked
|
|
|
|
- name: Collect release files
|
|
run: |
|
|
mkdir -p release-assets
|
|
cp src-tauri/target/release/bundle/deb/*.deb release-assets/ 2>/dev/null || true
|
|
cp src-tauri/target/release/bundle/deb/*.deb.sig release-assets/ 2>/dev/null || true
|
|
cp src-tauri/target/release/bundle/rpm/*.rpm release-assets/ 2>/dev/null || true
|
|
cp src-tauri/target/release/bundle/rpm/*.rpm.sig release-assets/ 2>/dev/null || true
|
|
cp src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe release-assets/ 2>/dev/null || true
|
|
cp src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig release-assets/ 2>/dev/null || true
|
|
ls -la release-assets/
|
|
|
|
- name: Copy changelogs to public
|
|
run: |
|
|
cp CHANGELOG.md public/CHANGELOG.md
|
|
cp CHANGELOG.fr.md public/CHANGELOG.fr.md
|
|
|
|
- name: Extract changelog
|
|
id: changelog
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME#v}"
|
|
NOTES=$(sed -n "/^## \[${TAG}\]/,/^## /{/^## \[${TAG}\]/d;/^## /d;p}" CHANGELOG.md)
|
|
if [ -z "$NOTES" ]; then
|
|
NOTES=$(sed -n "/^## ${TAG}/,/^## /{/^## ${TAG}/d;/^## /d;p}" CHANGELOG.md)
|
|
fi
|
|
NOTES=$(echo "$NOTES" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba}')
|
|
# Random delimiter (#320): a changelog line equal to a fixed one
|
|
# would end the value there and turn the lines after it into step
|
|
# outputs.
|
|
DELIM="CHANGELOG_EOF_$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')"
|
|
{
|
|
echo "notes<<${DELIM}"
|
|
echo "$NOTES"
|
|
echo "${DELIM}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Generate latest.json
|
|
env:
|
|
CHANGELOG_NOTES: ${{ steps.changelog.outputs.notes }}
|
|
run: |
|
|
# Three keys: one per Linux package (.deb, .rpm) and the NSIS
|
|
# installer, no generic linux-x86_64 key (#320); fails unless it finds
|
|
# exactly one .deb, .rpm and -setup.exe, each with a non-empty .sig.
|
|
# Notes reach jq as a file, as data.
|
|
# Called as scripts/build-latest-json.test.mjs calls it.
|
|
printf '%s' "$CHANGELOG_NOTES" > /tmp/release-notes.md
|
|
bash -e -o pipefail scripts/build-latest-json.sh \
|
|
release-assets \
|
|
"${GITHUB_REF_NAME#v}" \
|
|
/tmp/release-notes.md \
|
|
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}" \
|
|
> release-assets/latest.json
|
|
|
|
echo "Generated latest.json:"
|
|
cat release-assets/latest.json
|
|
|
|
- name: Create release and upload assets
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
CHANGELOG_NOTES: ${{ steps.changelog.outputs.notes }}
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME}"
|
|
API_URL="${GITHUB_SERVER_URL}/api/v1"
|
|
REPO="${GITHUB_REPOSITORY}"
|
|
|
|
BODY="${CHANGELOG_NOTES}
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
**Windows** : Téléchargez le fichier \`.exe\` ci-dessous.
|
|
**Linux** : Téléchargez le fichier \`.deb\` (Debian, Ubuntu) ou \`.rpm\` (Fedora, openSUSE) ci-dessous."
|
|
|
|
# Delete existing release for this tag (if any) to allow re-creation
|
|
EXISTING=$(curl -s "${API_URL}/repos/${REPO}/releases/tags/${TAG}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}")
|
|
EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
|
|
if [ -n "$EXISTING_ID" ]; then
|
|
echo "Deleting existing release ID: $EXISTING_ID"
|
|
curl -s -X DELETE \
|
|
"${API_URL}/repos/${REPO}/releases/${EXISTING_ID}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}"
|
|
fi
|
|
|
|
# Create release
|
|
RELEASE_RESPONSE=$(curl -s -X POST \
|
|
"${API_URL}/repos/${REPO}/releases" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n \
|
|
--arg tag "$TAG" \
|
|
--arg name "Simpl'Résultat ${TAG}" \
|
|
--arg body "$BODY" \
|
|
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')")
|
|
|
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
|
echo "Created release ID: $RELEASE_ID"
|
|
|
|
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
|
|
echo "ERROR: Failed to create release"
|
|
echo "$RELEASE_RESPONSE"
|
|
exit 1
|
|
fi
|
|
|
|
# Upload all assets
|
|
for file in release-assets/*; do
|
|
FILENAME=$(basename "$file")
|
|
ENCODED_FILENAME=$(printf '%s' "$FILENAME" | jq -sRr @uri)
|
|
FILESIZE=$(stat -c%s "$file")
|
|
echo "Uploading: $FILENAME (${FILESIZE} bytes)"
|
|
HTTP_CODE=$(curl -w "%{http_code}" --max-time 300 -X POST \
|
|
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ENCODED_FILENAME}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@${file}" \
|
|
-o /tmp/upload_response.json)
|
|
echo "HTTP $HTTP_CODE"
|
|
if [ "$HTTP_CODE" != "201" ]; then
|
|
echo "Upload failed:"
|
|
cat /tmp/upload_response.json
|
|
echo ""
|
|
fi
|
|
done
|
|
|
|
echo "Release created: ${GITHUB_SERVER_URL}/${REPO}/releases/tag/${TAG}"
|
|
|
|
- name: Publish latest.json to package registry
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
|
run: |
|
|
# This text is run by scripts/build-latest-json.test.mjs, with a fake
|
|
# curl: keep it self-contained, no workflow expressions (dollar,
|
|
# double brace) in run:, pass values through env: (#419).
|
|
#
|
|
# Check the manifest before the DELETE below (#320): once the old
|
|
# version is deleted, installed apps find no update until a valid
|
|
# one is uploaded. A failed check stops the job, registry untouched.
|
|
# The filter lives in scripts/check-latest-json.jq; -s makes it
|
|
# refuse an empty file or one with two documents (#419).
|
|
if ! jq -e -s --arg version "${GITHUB_REF_NAME#v}" \
|
|
-f scripts/check-latest-json.jq release-assets/latest.json > /dev/null; then
|
|
echo "ERROR: release-assets/latest.json failed scripts/check-latest-json.jq, registry left as is:"
|
|
cat release-assets/latest.json
|
|
exit 1
|
|
fi
|
|
|
|
# DELETE uses API v1, PUT uses the package upload API
|
|
DELETE_URL="${GITHUB_SERVER_URL}/api/v1/packages/${GITHUB_REPOSITORY_OWNER}/generic/simpl-resultat/latest"
|
|
UPLOAD_URL="${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/generic/simpl-resultat/latest"
|
|
|
|
# Delete the old package version to avoid 409 conflicts
|
|
echo "Deleting old package version (if any)..."
|
|
DEL_CODE=$(curl -s -w "%{http_code}" -X DELETE \
|
|
"${DELETE_URL}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-o /tmp/del_response.json)
|
|
echo "Delete HTTP $DEL_CODE"
|
|
# 204 = deleted, 404 = didn't exist (both OK)
|
|
if [ "$DEL_CODE" != "204" ] && [ "$DEL_CODE" != "404" ]; then
|
|
echo "WARNING: Unexpected delete response:"
|
|
cat /tmp/del_response.json
|
|
fi
|
|
|
|
echo "Uploading latest.json to package registry..."
|
|
HTTP_CODE=$(curl -w "%{http_code}" -X PUT \
|
|
"${UPLOAD_URL}/latest.json" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
--data-binary "@release-assets/latest.json" \
|
|
-o /tmp/pkg_response.json)
|
|
echo "Upload HTTP $HTTP_CODE"
|
|
if [ "$HTTP_CODE" != "201" ]; then
|
|
echo "ERROR: Failed to publish latest.json:"
|
|
cat /tmp/pkg_response.json
|
|
exit 1
|
|
fi
|