Simpl-Resultat/.forgejo/workflows/release.yml
le king fu aad85a412d ci: pass --locked to cargo in check-rust and the release builds, pin cargo-xwin (#449)
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>
2026-10-18 17:28:34 -04:00

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