fix: give rpm installs their own key in latest.json #416
12 changed files with 468 additions and 72 deletions
|
|
@ -43,7 +43,8 @@ updated: 2026-10-10
|
||||||
```bash
|
```bash
|
||||||
bash .claude/skills/release/scripts/verify-release.sh vX.Y.Z
|
bash .claude/skills/release/scripts/verify-release.sh vX.Y.Z
|
||||||
```
|
```
|
||||||
Le script (token `~/.forgejo-token`) lit la release par tag, compte les 7 artefacts **par suffixe** (`.exe` NSIS, `.deb`, `.rpm`, leurs 3 signatures `.sig`, `latest.json` — les noms exacts varient selon la version de Tauri), puis télécharge `latest.json` (il pilote l'auto-update des installations existantes) et vérifie : `version`, `pub_date`, signature non vide et URL contenant la version pour `linux-x86_64` et `windows-x86_64`, notes non vides. Toute ligne `MANQUANT`, `VIDE` ou `MAUVAISE VERSION` bloque l'étape 10.
|
Le script (token `~/.forgejo-token`) lit la release par tag, compte les 7 artefacts **par suffixe** (`.exe` NSIS, `.deb`, `.rpm`, leurs 3 signatures `.sig`, `latest.json` — les noms exacts varient selon la version de Tauri), puis télécharge `latest.json` (il pilote l'auto-update des installations existantes) et vérifie `version` (il affiche `pub_date` et la longueur des notes), puis exige **exactement quatre clés** (#320) : `linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64`. Pour chacune : l'extension de l'URL (`.deb`, `.deb`, `.rpm`, `.exe`), une signature non vide dont le `file:` (commentaire de confiance minisign, signature décodée du base64) est le nom de fichier de l'URL, décodé, et la version dans l'URL. **Il sort en 1 sur tout écart** (`MANQUANT`, `VIDE`, `MAUVAISE VERSION`, `MAUVAISE EXTENSION`, `DIFFÉRENT`, `INATTENDUE`) et finit par `VERDICT : OK` ou `VERDICT : ÉCHEC` : pas d'annonce de la release ni d'étape 10 avant un verdict OK. Un second argument (`verify-release.sh vX.Y.Z fichier.json`) contrôle un `latest.json` local au lieu de l'asset, pour éprouver les contrôles sur un fichier fabriqué.
|
||||||
|
**Lire aussi l'étape « Generate latest.json » du journal du run** (même route web que ci-dessous) : `scripts/build-latest-json.sh` y nomme le paquet retenu pour chaque type (`build-latest-json: .rpm <- …`), puis le `latest.json` généré est affiché. Ce script, le délimiteur aléatoire des notes et l'assertion placée avant le `DELETE` du registre ne s'exercent qu'au tag : une assertion en échec arrête le job sur `ERROR: release-assets/latest.json failed its checks`, registre intact, et la release Forgejo reste créée sans mise à jour servie.
|
||||||
Durée observée du run : 22 à 30 min sur les 12 dernières releases — poser le `Monitor` puis passer à autre chose.
|
Durée observée du run : 22 à 30 min sur les 12 dernières releases — poser le `Monitor` puis passer à autre chose.
|
||||||
**Lire aussi le Node.js du run.** `release.yml` n'est exercé qu'au tag : une montée de Node (Node 20 → 24 par #376) ne se prouve que dans ce journal. Il passe par la route web (l'API v1 des journaux répond 404). Le `run_number` vient de `/actions/tasks`, qui rend toute la liste, la plus récente en tête (le paramètre `limit` y est ignoré) :
|
**Lire aussi le Node.js du run.** `release.yml` n'est exercé qu'au tag : une montée de Node (Node 20 → 24 par #376) ne se prouve que dans ce journal. Il passe par la route web (l'API v1 des journaux répond 404). Le `run_number` vient de `/actions/tasks`, qui rend toute la liste, la plus récente en tête (le paramètre `limit` y est ignoré) :
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -81,3 +82,4 @@ updated: 2026-10-10
|
||||||
- 2026-09-13 — Étape 0 (pré-vol) : cross-référence vers `/issue-license` quand la release modifie `FEATURE_TIERS` ou verrouille une fonctionnalité Free — émettre et distribuer les clés testeurs avant de tagger, sinon ils perdent l'auto-update. Source : session 690a6129 via /skills-review-inbox (draft 2026-09-06).
|
- 2026-09-13 — Étape 0 (pré-vol) : cross-référence vers `/issue-license` quand la release modifie `FEATURE_TIERS` ou verrouille une fonctionnalité Free — émettre et distribuer les clés testeurs avant de tagger, sinon ils perdent l'auto-update. Source : session 690a6129 via /skills-review-inbox (draft 2026-09-06).
|
||||||
- 2026-09-13 — Étape 0 : prérequis « clés testeurs saisies avant le tag » précisé (déclencheurs `FEATURE_TIERS`, gating, `auto-update` ; cas réel v0.15.0, deux clés Premium émises le jour même). Étape 9 : script `scripts/verify-release.sh` (7 artefacts par suffixe + contenu de `latest.json`) au lieu d'un script réécrit à chaque release ; durée de run observée 22-30 min. Source : session 690a6129 (release v0.15.0, run 402) via /skills-review-inbox (draft 2026-09-12).
|
- 2026-09-13 — Étape 0 : prérequis « clés testeurs saisies avant le tag » précisé (déclencheurs `FEATURE_TIERS`, gating, `auto-update` ; cas réel v0.15.0, deux clés Premium émises le jour même). Étape 9 : script `scripts/verify-release.sh` (7 artefacts par suffixe + contenu de `latest.json`) au lieu d'un script réécrit à chaque release ; durée de run observée 22-30 min. Source : session 690a6129 (release v0.15.0, run 402) via /skills-review-inbox (draft 2026-09-12).
|
||||||
- 2026-10-10 — Étape 9 (post-CI) : lire `node --version` dans le journal du run de release (route web, seule ligne `vX.Y.Z` du journal). `release.yml` n'est exercé qu'au tag, donc la montée Node 20 → 24 de #376 ne se prouve qu'à la release suivante. Source : caveat de planification de #376 (milestone `planned-2026-10-10-release-deps`).
|
- 2026-10-10 — Étape 9 (post-CI) : lire `node --version` dans le journal du run de release (route web, seule ligne `vX.Y.Z` du journal). `release.yml` n'est exercé qu'au tag, donc la montée Node 20 → 24 de #376 ne se prouve qu'à la release suivante. Source : caveat de planification de #376 (milestone `planned-2026-10-10-release-deps`).
|
||||||
|
- 2026-10-10 — Étape 9 (post-CI) : `verify-release.sh` exige les quatre clés de `latest.json` (`linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm`, `windows-x86_64`), l'extension de chaque URL et le `file:` minisign de chaque signature, et sort en 1 sur tout écart (verdict en dernière ligne) ; lire l'étape « Generate latest.json » du journal. Source : #320 (milestone `planned-2026-10-10-release-deps`).
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Step 9 of /release: verify the published Forgejo release for a tag —
|
# Step 9 of /release: verify the published Forgejo release for a tag —
|
||||||
# the 7 expected assets and the content of latest.json (drives auto-update).
|
# the 7 expected assets and the content of latest.json (drives auto-update).
|
||||||
|
# Exits 1 when any check fails (#320), with a VERDICT as last line.
|
||||||
|
#
|
||||||
|
# Usage: verify-release.sh vX.Y.Z [latest.json]
|
||||||
|
# The optional second argument checks that local file instead of the
|
||||||
|
# release's latest.json asset, to prove the checks on crafted files.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
TAG="${1:?usage: verify-release.sh vX.Y.Z}"
|
TAG="${1:?usage: verify-release.sh vX.Y.Z [latest.json]}"
|
||||||
|
LOCAL_LATEST="${2:-}"
|
||||||
VERSION="${TAG#v}"
|
VERSION="${TAG#v}"
|
||||||
TOKEN=$(cat ~/.forgejo-token)
|
TOKEN=$(cat ~/.forgejo-token)
|
||||||
API="https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-resultat"
|
API="https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-resultat"
|
||||||
WORK="${TMPDIR:-/tmp}/verify-release-${TAG}"; mkdir -p "$WORK"
|
WORK="${TMPDIR:-/tmp}/verify-release-${TAG}"; mkdir -p "$WORK"
|
||||||
OUT="$WORK/latest.json"
|
OUT="$WORK/latest.json"
|
||||||
|
STATUS=0
|
||||||
|
|
||||||
echo "=== release ${TAG} ==="
|
echo "=== release ${TAG} ==="
|
||||||
timeout 20 curl -s -H "Authorization: token $TOKEN" "${API}/releases/tags/${TAG}" > "$WORK/release.json"
|
timeout 20 curl -s -H "Authorization: token $TOKEN" "${API}/releases/tags/${TAG}" > "$WORK/release.json"
|
||||||
python3 - "$WORK/release.json" "$VERSION" <<'EOF'
|
python3 - "$WORK/release.json" "$VERSION" <<'EOF' || STATUS=1
|
||||||
import sys, json
|
import sys, json
|
||||||
r = json.load(open(sys.argv[1])); version = sys.argv[2]
|
r = json.load(open(sys.argv[1])); version = sys.argv[2]
|
||||||
if "id" not in r:
|
if "id" not in r:
|
||||||
|
|
@ -34,19 +41,69 @@ for n in names:
|
||||||
missing = [k for k, v in kinds.items() if v == 0]
|
missing = [k for k, v in kinds.items() if v == 0]
|
||||||
print("couverture:", kinds, "| MANQUANT:" if missing else "| complet", missing if missing else "")
|
print("couverture:", kinds, "| MANQUANT:" if missing else "| complet", missing if missing else "")
|
||||||
open(sys.argv[1] + ".latest-url", "w").write(names.get("latest.json") or "")
|
open(sys.argv[1] + ".latest-url", "w").write(names.get("latest.json") or "")
|
||||||
|
sys.exit(1 if missing else 0)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
URL=$(cat "$WORK/release.json.latest-url" 2>/dev/null)
|
if [ -n "$LOCAL_LATEST" ]; then
|
||||||
if [ -z "$URL" ]; then echo "latest.json absent — arrêt"; exit 1; fi
|
OUT="$LOCAL_LATEST"
|
||||||
echo; echo "=== latest.json ($URL) ==="
|
echo; echo "=== latest.json (fichier local $OUT, pas l'asset de la release) ==="
|
||||||
timeout 20 curl -sL "$URL" > "$OUT"
|
else
|
||||||
python3 - "$OUT" "$VERSION" <<'EOF'
|
URL=$(cat "$WORK/release.json.latest-url" 2>/dev/null)
|
||||||
import sys, json
|
if [ -z "$URL" ]; then echo "latest.json absent — arrêt"; echo "VERDICT : ÉCHEC"; exit 1; fi
|
||||||
|
echo; echo "=== latest.json ($URL) ==="
|
||||||
|
timeout 20 curl -sL "$URL" > "$OUT"
|
||||||
|
fi
|
||||||
|
python3 - "$OUT" "$VERSION" <<'EOF' || STATUS=1
|
||||||
|
import sys, json, base64, binascii
|
||||||
|
from urllib.parse import unquote, urlsplit
|
||||||
j = json.load(open(sys.argv[1])); version = sys.argv[2]
|
j = json.load(open(sys.argv[1])); version = sys.argv[2]
|
||||||
|
ok = True
|
||||||
v = j.get("version"); pub = j.get("pub_date"); notes = j.get("notes", "")
|
v = j.get("version"); pub = j.get("pub_date"); notes = j.get("notes", "")
|
||||||
print(f"version={v} {'OK' if v == version else 'MAUVAISE VERSION'} | pub_date={pub}")
|
print(f"version={v} {'OK' if v == version else 'MAUVAISE VERSION'} | pub_date={pub}")
|
||||||
|
ok &= v == version
|
||||||
print(f"notes: {len(notes)} caractères — début: {notes[:120]!r}")
|
print(f"notes: {len(notes)} caractères — début: {notes[:120]!r}")
|
||||||
for plat, p in j.get("platforms", {}).items():
|
|
||||||
sig = p.get("signature", ""); url = p.get("url", "")
|
# The four keys (#320): tauri-plugin-updater looks up "{os}-{arch}-{installer}",
|
||||||
print(f" {plat}: url={url.rsplit('/', 1)[-1]} | signature={'OK (' + str(len(sig)) + ' car.)' if sig else 'VIDE'} | url contient version={'oui' if version in url else 'NON'}")
|
# then "{os}-{arch}". Each with the extension its URL must end with.
|
||||||
|
EXPECTED = {"linux-x86_64": ".deb", "linux-x86_64-deb": ".deb",
|
||||||
|
"linux-x86_64-rpm": ".rpm", "windows-x86_64": ".exe"}
|
||||||
|
|
||||||
|
def signed_file(sig):
|
||||||
|
"""The file: field of the trusted comment, in the base64 minisign signature."""
|
||||||
|
try:
|
||||||
|
text = base64.b64decode(sig, validate=True).decode("utf-8")
|
||||||
|
except (binascii.Error, ValueError):
|
||||||
|
return None
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("trusted comment: ") and "\tfile:" in line:
|
||||||
|
return line.split("\tfile:", 1)[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
platforms = j.get("platforms", {})
|
||||||
|
for plat in sorted(set(platforms) | set(EXPECTED)):
|
||||||
|
if plat not in platforms:
|
||||||
|
print(f" {plat}: MANQUANT"); ok = False; continue
|
||||||
|
if plat not in EXPECTED:
|
||||||
|
print(f" {plat}: clé INATTENDUE"); ok = False; continue
|
||||||
|
p = platforms[plat]
|
||||||
|
sig = p.get("signature") or ""; url = p.get("url") or ""
|
||||||
|
name = unquote(urlsplit(url).path.rsplit("/", 1)[-1]) # file name, decoded
|
||||||
|
ext = EXPECTED[plat]; signed = signed_file(sig) if sig else None
|
||||||
|
checks = {
|
||||||
|
"extension": name.endswith(ext),
|
||||||
|
"signature": bool(sig),
|
||||||
|
"file:": signed == name,
|
||||||
|
"version": version in url,
|
||||||
|
}
|
||||||
|
ok &= all(checks.values())
|
||||||
|
print(f" {plat}: url={name}"
|
||||||
|
f" | extension {ext} {'OK' if checks['extension'] else 'MAUVAISE EXTENSION'}"
|
||||||
|
f" | signature {'OK (' + str(len(sig)) + ' car.)' if sig else 'VIDE'}"
|
||||||
|
f" | file: {'OK' if checks['file:'] else 'DIFFÉRENT (' + repr(signed) + ')'}"
|
||||||
|
f" | url contient version={'oui' if checks['version'] else 'NON'}")
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$STATUS" -eq 0 ]; then echo "VERDICT : OK"; else echo "VERDICT : ÉCHEC"; fi
|
||||||
|
exit "$STATUS"
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,14 @@ jobs:
|
||||||
- name: Install Node.js 24
|
- name: Install Node.js 24
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y --no-install-recommends curl ca-certificates git
|
# jq: scripts/build-latest-json.test.mjs runs the script that writes
|
||||||
|
# the release's latest.json, and that script needs it (#320).
|
||||||
|
apt-get install -y --no-install-recommends curl ca-certificates git jq
|
||||||
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
||||||
apt-get install -y nodejs
|
apt-get install -y nodejs
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
npm --version
|
||||||
|
jq --version
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://github.com/actions/checkout@v4
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
|
||||||
|
|
@ -87,59 +87,30 @@ jobs:
|
||||||
NOTES=$(sed -n "/^## ${TAG}/,/^## /{/^## ${TAG}/d;/^## /d;p}" CHANGELOG.md)
|
NOTES=$(sed -n "/^## ${TAG}/,/^## /{/^## ${TAG}/d;/^## /d;p}" CHANGELOG.md)
|
||||||
fi
|
fi
|
||||||
NOTES=$(echo "$NOTES" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba}')
|
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<<CHANGELOG_EOF"
|
echo "notes<<${DELIM}"
|
||||||
echo "$NOTES"
|
echo "$NOTES"
|
||||||
echo "CHANGELOG_EOF"
|
echo "${DELIM}"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Generate latest.json
|
- name: Generate latest.json
|
||||||
env:
|
env:
|
||||||
CHANGELOG_NOTES: ${{ steps.changelog.outputs.notes }}
|
CHANGELOG_NOTES: ${{ steps.changelog.outputs.notes }}
|
||||||
run: |
|
run: |
|
||||||
TAG="${GITHUB_REF_NAME}"
|
# One key per Linux package, a .deb fallback and the NSIS installer;
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
# fails unless it finds exactly one .deb, .rpm and -setup.exe, each
|
||||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
# with a non-empty .sig (#320). Notes reach jq as a file, as data.
|
||||||
BASE_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${TAG}"
|
# Called as scripts/build-latest-json.test.mjs calls it.
|
||||||
|
printf '%s' "$CHANGELOG_NOTES" > /tmp/release-notes.md
|
||||||
LINUX_SIG=""
|
bash -e -o pipefail scripts/build-latest-json.sh \
|
||||||
LINUX_DEB=""
|
release-assets \
|
||||||
WINDOWS_SIG=""
|
"${GITHUB_REF_NAME#v}" \
|
||||||
WINDOWS_EXE=""
|
/tmp/release-notes.md \
|
||||||
|
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}" \
|
||||||
for f in release-assets/*.deb.sig; do
|
|
||||||
[ -f "$f" ] && LINUX_SIG=$(cat "$f")
|
|
||||||
done
|
|
||||||
for f in release-assets/*.deb; do
|
|
||||||
[ -f "$f" ] && LINUX_DEB=$(basename "$f")
|
|
||||||
done
|
|
||||||
for f in release-assets/*-setup.exe.sig; do
|
|
||||||
[ -f "$f" ] && WINDOWS_SIG=$(cat "$f")
|
|
||||||
done
|
|
||||||
for f in release-assets/*-setup.exe; do
|
|
||||||
[ -f "$f" ] && WINDOWS_EXE=$(basename "$f")
|
|
||||||
done
|
|
||||||
|
|
||||||
PLATFORMS="{}"
|
|
||||||
if [ -n "$LINUX_SIG" ] && [ -n "$LINUX_DEB" ]; then
|
|
||||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
|
||||||
--arg sig "$LINUX_SIG" \
|
|
||||||
--arg url "${BASE_URL}/${LINUX_DEB}" \
|
|
||||||
'. + {"linux-x86_64": {"signature": $sig, "url": $url}}')
|
|
||||||
fi
|
|
||||||
if [ -n "$WINDOWS_SIG" ] && [ -n "$WINDOWS_EXE" ]; then
|
|
||||||
PLATFORMS=$(echo "$PLATFORMS" | jq \
|
|
||||||
--arg sig "$WINDOWS_SIG" \
|
|
||||||
--arg url "${BASE_URL}/${WINDOWS_EXE}" \
|
|
||||||
'. + {"windows-x86_64": {"signature": $sig, "url": $url}}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
jq -n \
|
|
||||||
--arg version "$VERSION" \
|
|
||||||
--arg notes "$CHANGELOG_NOTES" \
|
|
||||||
--arg pub_date "$PUB_DATE" \
|
|
||||||
--argjson platforms "$PLATFORMS" \
|
|
||||||
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}' \
|
|
||||||
> release-assets/latest.json
|
> release-assets/latest.json
|
||||||
|
|
||||||
echo "Generated latest.json:"
|
echo "Generated latest.json:"
|
||||||
|
|
@ -161,7 +132,7 @@ jobs:
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**Windows** : Téléchargez le fichier \`.exe\` ci-dessous.
|
**Windows** : Téléchargez le fichier \`.exe\` ci-dessous.
|
||||||
**Linux** : Téléchargez le fichier \`.deb\` ou \`.AppImage\` 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
|
# Delete existing release for this tag (if any) to allow re-creation
|
||||||
EXISTING=$(curl -s "${API_URL}/repos/${REPO}/releases/tags/${TAG}" \
|
EXISTING=$(curl -s "${API_URL}/repos/${REPO}/releases/tags/${TAG}" \
|
||||||
|
|
@ -220,6 +191,24 @@ jobs:
|
||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
# 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.
|
||||||
|
# Same four keys as scripts/build-latest-json.sh writes.
|
||||||
|
if ! jq -e --arg version "${GITHUB_REF_NAME#v}" '
|
||||||
|
.version == $version
|
||||||
|
and (.platforms | keys) == ["linux-x86_64", "linux-x86_64-deb", "linux-x86_64-rpm", "windows-x86_64"]
|
||||||
|
and all(.platforms[]; (.signature | type) == "string" and (.signature | length) > 0)
|
||||||
|
and (.platforms["linux-x86_64"].url | endswith(".deb"))
|
||||||
|
and (.platforms["linux-x86_64-deb"].url | endswith(".deb"))
|
||||||
|
and (.platforms["linux-x86_64-rpm"].url | endswith(".rpm"))
|
||||||
|
and (.platforms["windows-x86_64"].url | endswith(".exe"))
|
||||||
|
' release-assets/latest.json > /dev/null; then
|
||||||
|
echo "ERROR: release-assets/latest.json failed its checks, registry left as is:"
|
||||||
|
cat release-assets/latest.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# DELETE uses API v1, PUT uses the package upload API
|
# 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"
|
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"
|
UPLOAD_URL="${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/generic/simpl-resultat/latest"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [Non publié]
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- Les installations faites depuis le paquet `.rpm` reçoivent désormais les mises à jour automatiques. Le manifeste de mise à jour ne listait que le `.deb`, qu'une installation `.rpm` téléchargeait puis refusait ; il liste maintenant chaque paquet Linux sous sa propre clé. Les installations `.rpm` existantes n'ont rien à réinstaller : elles recevront la prochaine version (vérifié sur le paquet v0.17.1 publié). Les instructions de téléchargement de la page de release ne mentionnent plus d'AppImage, qui n'est pas construite, et nomment désormais le `.rpm` (#320).
|
||||||
|
|
||||||
## [0.17.1] - 2026-10-08
|
## [0.17.1] - 2026-10-08
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Installs from the `.rpm` package now receive automatic updates. The update manifest only listed the `.deb`, which an `.rpm` install downloaded and then refused; it now lists each Linux package under its own key. Existing `.rpm` installs need no reinstall: they will receive the next version (checked on the published v0.17.1 package). The download instructions of the release page no longer mention an AppImage, which is not built, and now name the `.rpm` (#320).
|
||||||
|
|
||||||
## [0.17.1] - 2026-10-08
|
## [0.17.1] - 2026-10-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,10 @@ Application de bureau 100 % locale pour importer, catégoriser et analyser vos t
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
1. Rendez-vous sur la page [**Releases**](https://github.com/Le-King-Fu/simpl-resultat/releases/latest)
|
1. Rendez-vous sur la page [**Releases**](https://github.com/Le-King-Fu/simpl-resultat/releases/latest)
|
||||||
2. Téléchargez le format adapté à votre distribution :
|
2. Téléchargez le paquet adapté à votre distribution :
|
||||||
- `.deb` pour Debian / Ubuntu
|
- `.deb` pour Debian / Ubuntu
|
||||||
- `.rpm` pour Fedora / openSUSE
|
- `.rpm` pour Fedora / openSUSE
|
||||||
- `.AppImage` pour toute distribution (exécutable universel)
|
3. Installez le paquet
|
||||||
3. Installez le paquet ou lancez l'AppImage directement
|
|
||||||
|
|
||||||
## Démarrage rapide
|
## Démarrage rapide
|
||||||
|
|
||||||
|
|
|
||||||
1
STATE.md
1
STATE.md
|
|
@ -75,6 +75,7 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA
|
||||||
- Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers.
|
- Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers.
|
||||||
- **Vérifications dues à la prochaine release** (après le tag, étape 9 de `/release`) :
|
- **Vérifications dues à la prochaine release** (après le tag, étape 9 de `/release`) :
|
||||||
- `release.yml` sous Node 24 (#376) : `node --version` à `v24.x` dans le journal du run de release, et `verify-release.sh` vert. Le diff touchant `release.yml`, l'étape 10 de `/release` (cycle réel de mise à jour, `docs/qa-update-cycle.md`) s'applique aussi, selon la règle du skill.
|
- `release.yml` sous Node 24 (#376) : `node --version` à `v24.x` dans le journal du run de release, et `verify-release.sh` vert. Le diff touchant `release.yml`, l'étape 10 de `/release` (cycle réel de mise à jour, `docs/qa-update-cycle.md`) s'applique aussi, selon la règle du skill.
|
||||||
|
- `latest.json` à quatre clés (#320) : étape « Generate latest.json » du journal lue (un paquet retenu par type), `verify-release.sh` en `VERDICT : OK` sur `linux-x86_64`, `linux-x86_64-deb`, `linux-x86_64-rpm` et `windows-x86_64` (extensions, `file:` des signatures). Cycle réel depuis une installation `.rpm` (§3 de `docs/qa-update-cycle.md`) dès qu'une machine Fedora ou openSUSE est disponible, sinon « non déroulé » avec la raison dans la trace de la release.
|
||||||
- **Le paiement est live : « aucun tag avant le go-live » n'a plus de raison d'être, et la release desktop qu'attendait le go-live est publiée (v0.16.0, 2026-10-03).** Bascule live le 2026-09-29 (Product et Price live à 19 $ CAD, webhook, variables Stripe de production, redeploy `3673d09` de maximus-api, URL `cs_live_` vérifiée), puis achat réel de contrôle le 2026-09-30 : clé sur `/merci` et par courriel, activation dans l'app, remboursement complet et licence révoquée par `charge.refunded`. **Volet serveur clos le 2026-10-03** (derniers commentaires relus le 2026-10-04) : maximus-api#62 fermée, tous critères cochés (maximus-api 0.4.2 en prod, secrets en runtime seul), avec la milestone `spec-paiements` de maximus-api (12/12) ; la-compagnie-maximus#150 (Link dans les textes légaux) livrée et fermée le 2026-10-01. **Go-live clos : la-compagnie-maximus#141** fermée le 2026-10-10 sur décision de Max, l'updater vérifié (Free refuse et invite à acheter, Base accepte ; confirmé le même jour). Restent pour mémoire, sans issue : le Dashboard Stripe (logo, paramètres Checkout, reçus automatiques, alertes d'échec de webhook) et les relevés du commentaire de fermeture. la-compagnie-maximus#184 (politique de confidentialité : données d'activation, contrôle au démarrage) est en `status:review` ; sa PR reste en brouillon jusqu'au déploiement de maximus-api#85 (commentaire du 2026-10-03). Les deux textes des suites de licence (contrôle à la saisie, retrait de la clé) y sont postés depuis le 2026-10-05 (commentaire 19724). **Vérifications manuelles sur la release publiée, toutes faites** : #365 le 2026-10-03, #346 et #350 le 2026-10-04 (voir Décisions). L'étape 10 sous Windows est faite (Michel, confirmée le 2026-10-10).
|
- **Le paiement est live : « aucun tag avant le go-live » n'a plus de raison d'être, et la release desktop qu'attendait le go-live est publiée (v0.16.0, 2026-10-03).** Bascule live le 2026-09-29 (Product et Price live à 19 $ CAD, webhook, variables Stripe de production, redeploy `3673d09` de maximus-api, URL `cs_live_` vérifiée), puis achat réel de contrôle le 2026-09-30 : clé sur `/merci` et par courriel, activation dans l'app, remboursement complet et licence révoquée par `charge.refunded`. **Volet serveur clos le 2026-10-03** (derniers commentaires relus le 2026-10-04) : maximus-api#62 fermée, tous critères cochés (maximus-api 0.4.2 en prod, secrets en runtime seul), avec la milestone `spec-paiements` de maximus-api (12/12) ; la-compagnie-maximus#150 (Link dans les textes légaux) livrée et fermée le 2026-10-01. **Go-live clos : la-compagnie-maximus#141** fermée le 2026-10-10 sur décision de Max, l'updater vérifié (Free refuse et invite à acheter, Base accepte ; confirmé le même jour). Restent pour mémoire, sans issue : le Dashboard Stripe (logo, paramètres Checkout, reçus automatiques, alertes d'échec de webhook) et les relevés du commentaire de fermeture. la-compagnie-maximus#184 (politique de confidentialité : données d'activation, contrôle au démarrage) est en `status:review` ; sa PR reste en brouillon jusqu'au déploiement de maximus-api#85 (commentaire du 2026-10-03). Les deux textes des suites de licence (contrôle à la saisie, retrait de la clé) y sont postés depuis le 2026-10-05 (commentaire 19724). **Vérifications manuelles sur la release publiée, toutes faites** : #365 le 2026-10-03, #346 et #350 le 2026-10-04 (voir Décisions). L'étape 10 sous Windows est faite (Michel, confirmée le 2026-10-10).
|
||||||
- Backlog ouvert : **vague `planned-2026-09-24-paiements` close 3/3** (milestone fermée le 2026-09-30) : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, #347 (PR #364, ferme aussi #360) mergée le 2026-09-30 ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). **#365 fermée le 2026-10-03** après sa vérification manuelle. **Milestone `planned-2026-10-03-suites-licence` fermée 3/3 le 2026-10-05** (#373, #370, #367 mergées, voir Décisions ; suivi #389 en `status:triage`). Hors milestone, en `status:ready` : **#369** (réserver le fractionnement de transactions à l'édition Base) et **#372** (la liste complète des versions repousse le Journal en bas de Paramètres → Systèmes). **#350 fermée le 2026-10-04** (étape 3 vérifiée en production, voir Décisions) ; #363 fermée le 2026-10-10 sur décision de Max (ses tests, absorbés par #347, sont livrés dans la v0.16.0) ; réglés côté serveur et site : maximus-api#63 (clés de 100 ans) et maximus-api#70 (réactivation d'une machine désactivée), fermées et contenues dans `3673d09`, le commit déployé à la bascule du 2026-09-29, et la-compagnie-maximus#157, fermée, dont les CGV sont publiées en tronc commun plus annexe Simpl-Résultat (annexe relue en production le 2026-09-30, plus aucune durée de 10 ans) ; côté serveur, `/v1/prices` ne vérifie pas qu'une machine est encore active (une machine désactivée garde les cours jusqu'à l'expiration de son jeton), issue maximus-api à ouvrir ; suites de #350 ouvertes le 2026-10-01 : maximus-api#82 (`status:triage` : identifiants machine bruts déjà stockés, lignes désactivées comprises ; à rehacher avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne) et la-compagnie-maximus#184 (`status:review`, voir ci-dessus) ; go-live clos (la-compagnie-maximus#141 fermée le 2026-10-10, maximus-api#62 le 2026-10-03) ; `spec-paiements` fermée ici 5/5 ; onboarding phase 2 (parcours guidé) **mergée** : milestone `planned-2026-10-06-onboarding-parcours-guide` fermée 6/6 le 2026-10-08 (#396 à #401, `ee17f31`), suivis **#408** et **#409** (`status:triage`) ; onboarding phase 1 **livrée** : milestone `planned-2026-10-03-onboarding-phase1` fermée 7/7 le 2026-10-04 (#351 à #357, mergées en avance rapide, `667e058`) ; suivis ouverts : **#384** (`status:ready`, suivis non bloquants des revues) et **#371** (`status:blocked`, hors `/autopilot` : en-têtes CSV BMO à fournir par Max depuis un compte réel, un par type de compte, chèques/épargne et Mastercard, avec des valeurs fictives) ; analyse d'origine dans `docs/analyse-onboarding-2026-09.md`, plan détaillé dans les `spec-*-onboarding-phase1.md` locaux, ignorés par git ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **fermée 4/4** le 2026-10-05 (#232 livrée le 2026-07-27 ; #234 close le 2026-10-05 avec ses quatre preuves : dispatch run 464, PR #386 run 480, run planifié 488, `actcache` à 999 Mo) ; **#376** (`status:triage` : Node 20, en fin de vie depuis le 2026-04-30, encore installé par `check-frontend.yml` et `audit.yml`, et commentaire `concurrency` de `check-rust.yml` démenti par `forgejo-ops.md` pour les runs de PR). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist.
|
- Backlog ouvert : **vague `planned-2026-09-24-paiements` close 3/3** (milestone fermée le 2026-09-30) : #270 (PR #359) et #346 (PR #362) mergées le 2026-09-27, #347 (PR #364, ferme aussi #360) mergée le 2026-09-30 ; volet site **clos** (maximus/la-compagnie-maximus#153, redirection de `/simpl-resultat` en prod). **#365 fermée le 2026-10-03** après sa vérification manuelle. **Milestone `planned-2026-10-03-suites-licence` fermée 3/3 le 2026-10-05** (#373, #370, #367 mergées, voir Décisions ; suivi #389 en `status:triage`). Hors milestone, en `status:ready` : **#369** (réserver le fractionnement de transactions à l'édition Base) et **#372** (la liste complète des versions repousse le Journal en bas de Paramètres → Systèmes). **#350 fermée le 2026-10-04** (étape 3 vérifiée en production, voir Décisions) ; #363 fermée le 2026-10-10 sur décision de Max (ses tests, absorbés par #347, sont livrés dans la v0.16.0) ; réglés côté serveur et site : maximus-api#63 (clés de 100 ans) et maximus-api#70 (réactivation d'une machine désactivée), fermées et contenues dans `3673d09`, le commit déployé à la bascule du 2026-09-29, et la-compagnie-maximus#157, fermée, dont les CGV sont publiées en tronc commun plus annexe Simpl-Résultat (annexe relue en production le 2026-09-30, plus aucune durée de 10 ans) ; côté serveur, `/v1/prices` ne vérifie pas qu'une machine est encore active (une machine désactivée garde les cours jusqu'à l'expiration de son jeton), issue maximus-api à ouvrir ; suites de #350 ouvertes le 2026-10-01 : maximus-api#82 (`status:triage` : identifiants machine bruts déjà stockés, lignes désactivées comprises ; à rehacher avant l'expiration des premiers jetons bruts, sinon une réactivation crée une seconde ligne) et la-compagnie-maximus#184 (`status:review`, voir ci-dessus) ; go-live clos (la-compagnie-maximus#141 fermée le 2026-10-10, maximus-api#62 le 2026-10-03) ; `spec-paiements` fermée ici 5/5 ; onboarding phase 2 (parcours guidé) **mergée** : milestone `planned-2026-10-06-onboarding-parcours-guide` fermée 6/6 le 2026-10-08 (#396 à #401, `ee17f31`), suivis **#408** et **#409** (`status:triage`) ; onboarding phase 1 **livrée** : milestone `planned-2026-10-03-onboarding-phase1` fermée 7/7 le 2026-10-04 (#351 à #357, mergées en avance rapide, `667e058`) ; suivis ouverts : **#384** (`status:ready`, suivis non bloquants des revues) et **#371** (`status:blocked`, hors `/autopilot` : en-têtes CSV BMO à fournir par Max depuis un compte réel, un par type de compte, chèques/épargne et Mastercard, avec des valeurs fictives) ; analyse d'origine dans `docs/analyse-onboarding-2026-09.md`, plan détaillé dans les `spec-*-onboarding-phase1.md` locaux, ignorés par git ; #271 absorbé par #301, livré ; `spec-ci-build-optimization` **fermée 4/4** le 2026-10-05 (#232 livrée le 2026-07-27 ; #234 close le 2026-10-05 avec ses quatre preuves : dispatch run 464, PR #386 run 480, run planifié 488, `actcache` à 999 Mo) ; **#376** (`status:triage` : Node 20, en fin de vie depuis le 2026-04-30, encore installé par `check-frontend.yml` et `audit.yml`, et commentaire `concurrency` de `check-rust.yml` démenti par `forgejo-ops.md` pour les runs de PR). Le suivi « glob `src-tauri/**` non prouvé » ouvert avec #232 est **levé** : la PR #344 est le cas test isolé attendu (`Cargo.lock` + 2 `.md` seulement) — `check-rust` a bien tourné, `check-frontend` a bien été sauté par sa denylist.
|
||||||
- Suivis des advisories : **#312, #313 et #315 fermées le 2026-08-15** (PRs #321/#322 mergées). **#314 fermée le 2026-08-15, prémisse infirmée** — elle affirmait « zéro run `schedule` », or `audit.yml` tire tous les jours à 06:00 UTC (**19 runs `schedule` verts** du 07-28 au 08-15, sans trou) ; elle a été écrite le 07-27 vers 23:56Z, ~6 h avant le premier tir programmé possible, et décrivait donc une fenêtre trop courte, pas un scheduler en panne. Ces 19 runs confirment au passage que `.cargo/audit.toml` est bien lu en conteneur **sur `main`** — l'ADR 0018 ne l'avait vérifié que par un `workflow_dispatch` sur une branche. **#343 fermée le 2026-09-04** (PR #344) — RUSTSEC-2026-0258 sur `h2`, la seule vulnérabilité qui rougissait l'audit depuis le 08-19 ; l'audit est de nouveau vert sur `main`, prouvé par `workflow_dispatch` (run 393) et non déduit du run de PR. **#348 fermée le 2026-09-21** (PR #349) — RUSTSEC-2026-0285 sur `rustls`, seule vulnérabilité rougissant l'audit depuis le 09-15 (7 runs, 405→411) ; vert reprouvé par `workflow_dispatch` sur `main` (run 414 : 666 paquets, zéro vulnérabilité, 12 warnings autorisés). Deux advisories en cinq semaines sur le même chemin, celui de la mise à jour automatique. Restent **#317** (prémisse caduque depuis le 2026-09-26 : `react-router-dom` 7.18.2 et suivantes corrigent l'advisory par un bump de patch dans `^7.18.1`, sans migration v8 ; absorbée par #358, dont la PR la fermera), **#319** (déclencheur de retrait pour `rsa`, désormais **seule** entrée de `.cargo/audit.toml` — le garde-fou est passé à 2 vérifications) et **#345** (domicilier la contrainte `windows-sys 0.61.2` — analysée le 09-04 : le corps d'origine, écrit de mémoire en fin de revue, se trompait sur trois points. Le déclencheur n'est pas « n'importe quel `cargo update` » mais tout bump atteignant le sous-arbre réseau/TLS — mesuré : `-p anyhow` ne bouge rien, `-p hyper` fait descendre les 7 — soit précisément là où tombent les advisories. **Prémisse restreinte le 2026-09-21 par #348** : `-p rustls`, au cœur de ce même sous-arbre, n'en déplace **aucun** — le déclencheur tient à `hyper` et à ce qu'il tire, pas à la couche TLS en général, donc « sous-arbre réseau/TLS » prédit une dérive là où il n'y en a pas et ferait écrire une strophe à la main sans nécessité. D'où la suggestion portée en commentaire sur l'issue : formuler le garde-fou sur la **version résolue de `windows-sys`** plutôt que sur une liste de crates déclencheurs — la liste est ce qui vient d'être trouvé faux, la version est ce qui est protégé. #310 avait **bien** vu la dérive et l'avait jugée inoffensive *sur Linux*, où elle l'est ; son coût tombe sur le build Windows que personne ne mesurait : **3 versions de `windows-sys` compilées contre 5**. Aucun risque de correction — les 7 crates déclarent des bornes larges couvrant les deux états. Et cargo annonce « Locking 1 package » en écrivant 8 changements, donc le silence est le sien. Décision : garde-fou CI sur la version + ADR portant la condition de sortie. Le check croisé Windows depuis Linux a été **testé et écarté** — `cc-rs` s'arrête sur `lib.exe`, il faudrait `cargo-xwin`). Côté npm, **#358** ouverte le 2026-09-26 par `/analyse-vulnerabilite` : `npm audit` remonte 7 findings (4 high, 3 moderate) sur 4 dépendances racines, contre les 2 acceptés au 07-27, et chaque correctif tient dans la borne déjà déclarée ; le lock se régénère avec `npx npm@11` (npm 10 plante sur un bump de vitest 4.1.x).
|
- Suivis des advisories : **#312, #313 et #315 fermées le 2026-08-15** (PRs #321/#322 mergées). **#314 fermée le 2026-08-15, prémisse infirmée** — elle affirmait « zéro run `schedule` », or `audit.yml` tire tous les jours à 06:00 UTC (**19 runs `schedule` verts** du 07-28 au 08-15, sans trou) ; elle a été écrite le 07-27 vers 23:56Z, ~6 h avant le premier tir programmé possible, et décrivait donc une fenêtre trop courte, pas un scheduler en panne. Ces 19 runs confirment au passage que `.cargo/audit.toml` est bien lu en conteneur **sur `main`** — l'ADR 0018 ne l'avait vérifié que par un `workflow_dispatch` sur une branche. **#343 fermée le 2026-09-04** (PR #344) — RUSTSEC-2026-0258 sur `h2`, la seule vulnérabilité qui rougissait l'audit depuis le 08-19 ; l'audit est de nouveau vert sur `main`, prouvé par `workflow_dispatch` (run 393) et non déduit du run de PR. **#348 fermée le 2026-09-21** (PR #349) — RUSTSEC-2026-0285 sur `rustls`, seule vulnérabilité rougissant l'audit depuis le 09-15 (7 runs, 405→411) ; vert reprouvé par `workflow_dispatch` sur `main` (run 414 : 666 paquets, zéro vulnérabilité, 12 warnings autorisés). Deux advisories en cinq semaines sur le même chemin, celui de la mise à jour automatique. Restent **#317** (prémisse caduque depuis le 2026-09-26 : `react-router-dom` 7.18.2 et suivantes corrigent l'advisory par un bump de patch dans `^7.18.1`, sans migration v8 ; absorbée par #358, dont la PR la fermera), **#319** (déclencheur de retrait pour `rsa`, désormais **seule** entrée de `.cargo/audit.toml` — le garde-fou est passé à 2 vérifications) et **#345** (domicilier la contrainte `windows-sys 0.61.2` — analysée le 09-04 : le corps d'origine, écrit de mémoire en fin de revue, se trompait sur trois points. Le déclencheur n'est pas « n'importe quel `cargo update` » mais tout bump atteignant le sous-arbre réseau/TLS — mesuré : `-p anyhow` ne bouge rien, `-p hyper` fait descendre les 7 — soit précisément là où tombent les advisories. **Prémisse restreinte le 2026-09-21 par #348** : `-p rustls`, au cœur de ce même sous-arbre, n'en déplace **aucun** — le déclencheur tient à `hyper` et à ce qu'il tire, pas à la couche TLS en général, donc « sous-arbre réseau/TLS » prédit une dérive là où il n'y en a pas et ferait écrire une strophe à la main sans nécessité. D'où la suggestion portée en commentaire sur l'issue : formuler le garde-fou sur la **version résolue de `windows-sys`** plutôt que sur une liste de crates déclencheurs — la liste est ce qui vient d'être trouvé faux, la version est ce qui est protégé. #310 avait **bien** vu la dérive et l'avait jugée inoffensive *sur Linux*, où elle l'est ; son coût tombe sur le build Windows que personne ne mesurait : **3 versions de `windows-sys` compilées contre 5**. Aucun risque de correction — les 7 crates déclarent des bornes larges couvrant les deux états. Et cargo annonce « Locking 1 package » en écrivant 8 changements, donc le silence est le sien. Décision : garde-fou CI sur la version + ADR portant la condition de sortie. Le check croisé Windows depuis Linux a été **testé et écarté** — `cc-rs` s'arrête sur `lib.exe`, il faudrait `cargo-xwin`). Côté npm, **#358** ouverte le 2026-09-26 par `/analyse-vulnerabilite` : `npm audit` remonte 7 findings (4 high, 3 moderate) sur 4 dépendances racines, contre les 2 acceptés au 07-27, et chaque correctif tient dans la borne déjà déclarée ; le lock se régénère avec `npx npm@11` (npm 10 plante sur un bump de vitest 4.1.x).
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ simpl-resultat/
|
||||||
│ ├── audit.yml
|
│ ├── audit.yml
|
||||||
│ └── release.yml
|
│ └── release.yml
|
||||||
├── .github/workflows/ # Miroir GitHub (dormant)
|
├── .github/workflows/ # Miroir GitHub (dormant)
|
||||||
|
├── scripts/ # build-latest-json.sh (latest.json de l'updater, appelé par release.yml, #320) et son test vitest
|
||||||
├── docs/ # Documentation technique
|
├── docs/ # Documentation technique
|
||||||
└── config/ # Configuration
|
└── config/ # Configuration
|
||||||
```
|
```
|
||||||
|
|
@ -598,7 +599,7 @@ Aucun `branches:` : une PR stackée sur une autre branche de feature — ce que
|
||||||
|
|
||||||
### `check-frontend.yml` — Vérifications frontend sur PR
|
### `check-frontend.yml` — Vérifications frontend sur PR
|
||||||
|
|
||||||
Déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown (`paths-ignore`). Lance `npm ci`, `npm run build` (tsc + vite) et `npm test` (vitest). Pas de cache npm : ce workflow ne tourne que sur des PR, donc un cache ne servirait qu'aux pushes suivants d'une même PR, et `npm ci` y prend 9 s.
|
Déclenché sur les PR, sauf si tous les fichiers modifiés sont du Rust, de la doc ou du markdown (`paths-ignore`). Lance `npm ci`, `npm run build` (tsc + vite) et `npm test` (vitest). Installe aussi `jq`, dont a besoin `scripts/build-latest-json.test.mjs` : ce test lance le script de génération de `latest.json` de `release.yml` ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). Pas de cache npm : ce workflow ne tourne que sur des PR, donc un cache ne servirait qu'aux pushes suivants d'une même PR, et `npm ci` y prend 9 s.
|
||||||
|
|
||||||
**Aucune étape `npm audit`** — contrairement au Rust, les advisories npm ne sont donc pas un gate de CI. Conséquence à connaître : `npm audit` remonte **2 high en permanence**, les deux clés d'une même advisory `react-router` ([GHSA-qwww-vcr4-c8h2](https://github.com/advisories/GHSA-qwww-vcr4-c8h2), mode React Server Components). Elle est acceptée : l'app est un client de bureau sans serveur — `App.tsx` monte un `BrowserRouter` client-only — et `react-router-dom` étant figé en 7.18.1, sortir de la plage vulnérable demanderait une migration vers react-router v8, pas un bump. Déclencheur de re-évaluation : [#317](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/317). `npm audit` n'a pas de mécanisme d'exclusion natif, il n'y a donc pas d'équivalent du `.cargo/audit.toml` côté npm.
|
**Aucune étape `npm audit`** — contrairement au Rust, les advisories npm ne sont donc pas un gate de CI. Conséquence à connaître : `npm audit` remonte **2 high en permanence**, les deux clés d'une même advisory `react-router` ([GHSA-qwww-vcr4-c8h2](https://github.com/advisories/GHSA-qwww-vcr4-c8h2), mode React Server Components). Elle est acceptée : l'app est un client de bureau sans serveur — `App.tsx` monte un `BrowserRouter` client-only — et `react-router-dom` étant figé en 7.18.1, sortir de la plage vulnérable demanderait une migration vers react-router v8, pas un bump. Déclencheur de re-évaluation : [#317](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/317). `npm audit` n'a pas de mécanisme d'exclusion natif, il n'y a donc pas d'équivalent du `.cargo/audit.toml` côté npm.
|
||||||
|
|
||||||
|
|
@ -620,8 +621,9 @@ Déclenché par les tags `v*`. Un seul job, `build-and-release`, dans un contene
|
||||||
|
|
||||||
Fonctionnalités :
|
Fonctionnalités :
|
||||||
- Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY)
|
- Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY)
|
||||||
- JSON d'updater publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json`
|
- JSON d'updater (`latest.json`) écrit par `scripts/build-latest-json.sh` à partir des assets ([#320](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/320)). `tauri-plugin-updater` cherche `{os}-{arch}-{installer}`, puis `{os}-{arch}` : le fichier porte `linux-x86_64-deb` et `linux-x86_64-rpm` (une clé par paquet Linux), `linux-x86_64` (le `.deb`, repli d'un binaire sans type de paquet connu, condition de retrait dans `docs/qa-update-cycle.md`) et `windows-x86_64` (l'installeur NSIS). Le script échoue s'il ne trouve pas exactement un `.deb`, un `.rpm` et un `-setup.exe`, chacun avec un `.sig` non vide ; le JSON vient de `jq` seul (notes lues comme données par `--rawfile`), avec les noms de fichiers encodés par `@uri`, comme à l'envoi des assets. Testé par `scripts/build-latest-json.test.mjs` (vitest, lancé par `check-frontend`), qui l'exécute sous `bash -e -o pipefail` comme `release.yml`.
|
||||||
- Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md
|
- JSON publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json` (registre de paquets, endpoint de `tauri.conf.json`). Avant le `DELETE` de l'ancienne version, une assertion `jq` (version égale au tag, exactement les quatre clés, signatures non vides, extension de chaque URL) arrête le job si le fichier ne la passe pas : le registre reste intact.
|
||||||
|
- Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md, transmises par `GITHUB_OUTPUT` sous un délimiteur aléatoire (une ligne du CHANGELOG égale à un délimiteur fixe terminait la valeur)
|
||||||
|
|
||||||
**Node.js 24** ([#376](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/376)), installé comme dans les trois autres workflows par le script NodeSource (`curl … | bash`). Ce script tourne en root dans le conteneur du job, et le Node.js qu'il installe lance ensuite `npx tauri build` avec `TAURI_SIGNING_PRIVATE_KEY` dans son environnement : le job confie à NodeSource (script et dépôt apt) la clé de signature de l'updater, et les jetons de publication des étapes suivantes. Risque accepté par Max le 2026-10-10, alternative écartée : un tarball Node.js épinglé. La chaîne Rust, installée de la même façon par `rustup`, est dans le même cas. Ce workflow ne tourne qu'au tag : la première release après #376 est la première sous Node 24, à vérifier par `node --version` dans le journal du run (étape 9 de `/release`).
|
**Node.js 24** ([#376](https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues/376)), installé comme dans les trois autres workflows par le script NodeSource (`curl … | bash`). Ce script tourne en root dans le conteneur du job, et le Node.js qu'il installe lance ensuite `npx tauri build` avec `TAURI_SIGNING_PRIVATE_KEY` dans son environnement : le job confie à NodeSource (script et dépôt apt) la clé de signature de l'updater, et les jetons de publication des étapes suivantes. Risque accepté par Max le 2026-10-10, alternative écartée : un tarball Node.js épinglé. La chaîne Rust, installée de la même façon par `rustup`, est dans le même cas. Ce workflow ne tourne qu'au tag : la première release après #376 est la première sous Node 24, à vérifier par `node --version` dans le journal du run (étape 9 de `/release`).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ Elle existe parce que la CI ne peut rien prouver ici : `cargo check` et `cargo t
|
||||||
|
|
||||||
Ce sont les conditions sans lesquelles le test ne teste rien. Chacune a un mode d'échec où la checklist se coche « OK » sans avoir rien vérifié.
|
Ce sont les conditions sans lesquelles le test ne teste rien. Chacune a un mode d'échec où la checklist se coche « OK » sans avoir rien vérifié.
|
||||||
|
|
||||||
- [ ] **Une machine Windows** (hôte ou VM) et **une machine Linux de bureau**. Les deux flux sont réellement différents, ils ne se substituent pas l'un à l'autre.
|
- [ ] **Une machine Windows** (hôte ou VM) et **une machine Linux de bureau**, plus, pour le §3, **une machine Fedora ou openSUSE** installée depuis le `.rpm`. Les flux sont réellement différents, ils ne se substituent pas l'un à l'autre.
|
||||||
- [ ] Sur la machine Linux, **un agent polkit qui tourne**. L'installation du `.deb` passe par `pkexec` ; sans agent, l'app retombe en cascade sur `zenity`/`kdialog` puis sur un `sudo` dans un terminal inexistant, et **paraît figée au lieu d'échouer — sur l'écran de téléchargement, pas sur `readyToInstall`** (voir §2, c'est le piège qui produit le mauvais rapport de bug). Noter plus bas quelle invite est réellement apparue : si ce n'est pas polkit, c'est un chemin de repli qui a été validé, pas le chemin nominal.
|
- [ ] Sur chaque machine Linux, **un agent polkit qui tourne**. L'installation du `.deb` comme du `.rpm` passe par `pkexec` ; sans agent, l'app retombe en cascade sur `zenity`/`kdialog` puis sur un `sudo` dans un terminal inexistant, et **paraît figée au lieu d'échouer — sur l'écran de téléchargement, pas sur `readyToInstall`** (voir §2, c'est le piège qui produit le mauvais rapport de bug). Noter plus bas quelle invite est réellement apparue : si ce n'est pas polkit, c'est un chemin de repli qui a été validé, pas le chemin nominal.
|
||||||
- [ ] **La version précédente installée sur chaque machine**, depuis les artefacts de la release précédente (`.deb` et `-setup.exe` attachés à la release Forgejo). Ne pas tester sur la machine qui vient de builder la nouvelle version.
|
- [ ] **La version précédente installée sur chaque machine**, depuis les artefacts de la release précédente (`.deb`, `.rpm` et `-setup.exe` attachés à la release Forgejo). Ne pas tester sur la machine qui vient de builder la nouvelle version.
|
||||||
|
|
||||||
> Sans ça, `check()` compare `release.version > version_courante`, trouve faux, et l'UI affiche « à jour ». La checklist se coche alors comme « rien à mettre à jour » — soit exactement le saut silencieux qu'elle est censée éliminer, avec en prime une trace écrite affirmant qu'elle est passée.
|
> Sans ça, `check()` compare `release.version > version_courante`, trouve faux, et l'UI affiche « à jour ». La checklist se coche alors comme « rien à mettre à jour » — soit exactement le saut silencieux qu'elle est censée éliminer, avec en prime une trace écrite affirmant qu'elle est passée.
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ Ce sont les conditions sans lesquelles le test ne teste rien. Chacune a un mode
|
||||||
|
|
||||||
À écrire explicitement, parce que la tentation est de croire qu'un cycle réussi couvre tout le chemin de mise à jour.
|
À écrire explicitement, parce que la tentation est de croire qu'un cycle réussi couvre tout le chemin de mise à jour.
|
||||||
|
|
||||||
**Prouvé** sur les deux cibles : le téléchargement TLS (donc `rustls-webpki`), la vérification de signature minisign, l'exécution de l'installeur, et le redémarrage sur la nouvelle version.
|
**Prouvé** sur chaque cible déroulée : le téléchargement TLS (donc `rustls-webpki`), la vérification de signature minisign, l'exécution de l'installeur, et le redémarrage sur la nouvelle version.
|
||||||
|
|
||||||
**Non prouvé** : l'extraction d'archive, donc `tar`. Dans `tauri-plugin-updater`, `tar` n'est atteint que par `install_appimage` et la branche macOS `.app.tar.gz`. Aucun de nos bundles ne passe par là — le `.deb` fait `pkexec dpkg -i`, le NSIS écrit le `.exe` dans un répertoire temporaire et le lance. **Tant qu'on ne livre pas d'AppImage, le chemin `tar` reste mort** et aucune checklist manuelle ne le couvrira.
|
**Non prouvé** : l'extraction d'archive, donc `tar`. Dans `tauri-plugin-updater`, `tar` n'est atteint que par `install_appimage` et la branche macOS `.app.tar.gz`. Aucun de nos bundles ne passe par là — le `.deb` fait `pkexec dpkg -i`, le NSIS écrit le `.exe` dans un répertoire temporaire et le lance. **Tant qu'on ne livre pas d'AppImage, le chemin `tar` reste mort** et aucune checklist manuelle ne le couvrira.
|
||||||
|
|
||||||
|
|
@ -62,11 +62,36 @@ Point d'entrée : **Paramètres → Systèmes → carte Mises à jour → « Vé
|
||||||
- [ ] Cliquer → état `installing`, l'app redémarre.
|
- [ ] Cliquer → état `installing`, l'app redémarre.
|
||||||
- [ ] Vérifier la version, re-vérifier → `upToDate`.
|
- [ ] Vérifier la version, re-vérifier → `upToDate`.
|
||||||
|
|
||||||
## 3. RPM — cassé, connu
|
## 3. Linux (.rpm)
|
||||||
|
|
||||||
- [ ] **Ne pas tester** : ce chemin est cassé, pas seulement non vérifié.
|
Même parcours que le §2, sur la machine Fedora ou openSUSE. L'updater y lance `pkexec rpm -U` au lieu de `pkexec dpkg -i` (`install_rpm`, même cascade polkit → zenity/kdialog → `sudo`) : mêmes états, même moment pour l'invite, même piège.
|
||||||
|
|
||||||
`latest.json` ne porte qu'une entrée `linux-x86_64`, construite à partir du `.deb` (`release.yml:104-122`). Une installation rpm reçoit donc des octets deb, que `Installer::Rpm` rejette après vérification de la signature magique du payload. Les artefacts `.rpm` continuent pourtant d'être publiés comme assets de release. Suivi en **#320**.
|
- [ ] Même point d'entrée ; la carte affiche `available` avec le **numéro de la nouvelle version**.
|
||||||
|
- [ ] **Pendant que la carte affiche encore « Téléchargement en cours… »**, une invite d'authentification apparaît. **Noter laquelle.**
|
||||||
|
|
||||||
|
Invite observée : `________________`
|
||||||
|
|
||||||
|
- [ ] Authentifier → `readyToInstall`, puis cliquer → `installing`, l'app redémarre.
|
||||||
|
- [ ] Vérifier la version, re-vérifier → `upToDate`.
|
||||||
|
|
||||||
|
> Un refus juste après le téléchargement (« format de mise à jour invalide ») est le défaut d'avant #320 : l'installation a reçu le `.deb`. Vérifier que le `latest.json` publié porte la clé `linux-x86_64-rpm` (`verify-release.sh`, étape 9 de `/release`).
|
||||||
|
|
||||||
|
**Pas encore déroulé** : aucune machine Fedora ou openSUSE n'était disponible quand #320 a ajouté la clé `linux-x86_64-rpm`. Le dérouler dès qu'il y en a une ; d'ici là, l'écrire « non déroulé », avec la raison, dans la trace de la release.
|
||||||
|
|
||||||
|
Les installations `.rpm` existantes n'ont rien à réinstaller : le binaire du `.rpm` v0.17.1 publié porte le marqueur `__TAURI_BUNDLE_TYPE_VAR_RPM` (vérifié le 2026-10-10), donc il cherche `linux-x86_64-rpm` dès qu'une release publie cette clé. La première release qui la publie est la première que ces installations peuvent recevoir.
|
||||||
|
|
||||||
|
### Les clés de `latest.json`
|
||||||
|
|
||||||
|
`tauri-plugin-updater` cherche la clé `{os}-{arch}-{installer}`, puis `{os}-{arch}` (`get_urls`) ; l'installeur est le type de paquet que le bundler grave dans le binaire de chaque paquet. `scripts/build-latest-json.sh`, appelé par `release.yml`, écrit donc :
|
||||||
|
|
||||||
|
| Clé | Fichier | Lue par |
|
||||||
|
|---|---|---|
|
||||||
|
| `linux-x86_64-deb` | `.deb` | les installations `.deb` |
|
||||||
|
| `linux-x86_64-rpm` | `.rpm` | les installations `.rpm` (#320) |
|
||||||
|
| `linux-x86_64` | `.deb` | un binaire Linux sans marqueur de type de paquet (repli) |
|
||||||
|
| `windows-x86_64` | `-setup.exe` | les installations NSIS, qui cherchent d'abord `windows-x86_64-nsis` |
|
||||||
|
|
||||||
|
**Repli `linux-x86_64`.** Raison : un binaire sans marqueur de type de paquet ne cherche que cette clé ; sans clé à son nom, sa vérification échoue (`TargetsNotFound`) au lieu de répondre. Condition de retrait : quand aucune version supportée n'est antérieure au marqueur. Relevé le 2026-10-10 : toutes les versions qui embarquent l'updater (v0.2.1 et suivantes) ont été empaquetées par `@tauri-apps/cli` 2.10.0, d'après le `package-lock.json` de chaque tag, comme la v0.17.1 dont le `.rpm` porte le marqueur. Un binaire sans marqueur ne saurait d'ailleurs pas installer le `.deb` : faute de type connu, l'updater le traite comme une AppImage.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
114
scripts/build-latest-json.sh
Executable file
114
scripts/build-latest-json.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build latest.json, the updater manifest of a release, from its assets (#320).
|
||||||
|
#
|
||||||
|
# Usage: build-latest-json.sh <assets-dir> <version> <notes-file> <base-url>
|
||||||
|
#
|
||||||
|
# assets-dir directory holding the bundles and their .sig files
|
||||||
|
# version release version without the leading v, e.g. 0.17.1
|
||||||
|
# notes-file release notes, embedded verbatim as data (jq --rawfile)
|
||||||
|
# base-url download URL of the release, without a trailing slash, e.g.
|
||||||
|
# https://<forgejo>/<owner>/<repo>/releases/download/v0.17.1
|
||||||
|
#
|
||||||
|
# Writes the JSON on stdout and its log on stderr. release.yml redirects
|
||||||
|
# stdout to release-assets/latest.json; scripts/build-latest-json.test.mjs
|
||||||
|
# runs it under bash -e -o pipefail, as the CI runner does.
|
||||||
|
#
|
||||||
|
# Fails closed: unless the assets dir holds exactly one .deb, one .rpm and
|
||||||
|
# one -setup.exe, each with a non-empty "<asset>.sig" next to it, the script
|
||||||
|
# exits non-zero before writing anything on stdout. The JSON is built by jq
|
||||||
|
# alone: notes, file names and signatures enter it as jq values, never as
|
||||||
|
# text spliced into JSON.
|
||||||
|
#
|
||||||
|
# Keys. tauri-plugin-updater 2.x looks up "{os}-{arch}-{installer}" first,
|
||||||
|
# then "{os}-{arch}" (get_urls in its updater.rs). The installer is the
|
||||||
|
# bundle type that the bundler writes into the binary of each package.
|
||||||
|
#
|
||||||
|
# linux-x86_64-deb the .deb, for .deb installs.
|
||||||
|
# linux-x86_64-rpm the .rpm, for .rpm installs. Without this key they fell
|
||||||
|
# back on linux-x86_64, downloaded the .deb and refused it.
|
||||||
|
# linux-x86_64 the .deb again: the fallback of a binary without a
|
||||||
|
# bundle-type marker, which looks up this key alone.
|
||||||
|
# Remove it once no supported version predates the
|
||||||
|
# marker (docs/qa-update-cycle.md, "Les clés de
|
||||||
|
# latest.json").
|
||||||
|
# windows-x86_64 the NSIS installer. An NSIS install looks up
|
||||||
|
# windows-x86_64-nsis first, then this key.
|
||||||
|
#
|
||||||
|
# Each URL ends with the file name percent-encoded by jq's @uri, as the upload
|
||||||
|
# step of release.yml encodes it: the bundle names contain a space.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf 'build-latest-json: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -eq 4 ] || die "usage: build-latest-json.sh <assets-dir> <version> <notes-file> <base-url>"
|
||||||
|
assets_dir=$1
|
||||||
|
version=$2
|
||||||
|
notes_file=$3
|
||||||
|
base_url=$4
|
||||||
|
|
||||||
|
[ -d "$assets_dir" ] || die "assets dir not found: $assets_dir"
|
||||||
|
[ -n "$version" ] || die "empty version"
|
||||||
|
[ -f "$notes_file" ] || die "notes file not found: $notes_file"
|
||||||
|
[ -n "$base_url" ] || die "empty base URL"
|
||||||
|
|
||||||
|
# read_asset <kind> <matches...>: the single bundle of a kind and its
|
||||||
|
# signature, in asset_name and asset_sig. Any other count of matches, or a
|
||||||
|
# missing or blank "<asset>.sig", stops the script.
|
||||||
|
asset_name=""
|
||||||
|
asset_sig=""
|
||||||
|
read_asset() {
|
||||||
|
local kind=$1
|
||||||
|
shift
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
printf 'build-latest-json: expected exactly one %s in %s, found %d\n' "$kind" "$assets_dir" "$#" >&2
|
||||||
|
[ "$#" -eq 0 ] || printf ' %s\n' "$@" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local file=$1
|
||||||
|
[ -s "$file.sig" ] || die "missing or empty signature: $file.sig"
|
||||||
|
asset_name=${file##*/}
|
||||||
|
asset_sig=$(cat "$file.sig")
|
||||||
|
[ -n "$asset_sig" ] || die "blank signature: $file.sig"
|
||||||
|
printf 'build-latest-json: %s <- %s\n' "$kind" "$asset_name" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
read_asset .deb "$assets_dir"/*.deb
|
||||||
|
deb_name=$asset_name
|
||||||
|
deb_sig=$asset_sig
|
||||||
|
|
||||||
|
read_asset .rpm "$assets_dir"/*.rpm
|
||||||
|
rpm_name=$asset_name
|
||||||
|
rpm_sig=$asset_sig
|
||||||
|
|
||||||
|
read_asset -setup.exe "$assets_dir"/*-setup.exe
|
||||||
|
exe_name=$asset_name
|
||||||
|
exe_sig=$asset_sig
|
||||||
|
|
||||||
|
pub_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg version "$version" \
|
||||||
|
--rawfile notes "$notes_file" \
|
||||||
|
--arg pub_date "$pub_date" \
|
||||||
|
--arg base "$base_url" \
|
||||||
|
--arg deb "$deb_name" --arg deb_sig "$deb_sig" \
|
||||||
|
--arg rpm "$rpm_name" --arg rpm_sig "$rpm_sig" \
|
||||||
|
--arg exe "$exe_name" --arg exe_sig "$exe_sig" \
|
||||||
|
'def asset($name; $sig): {signature: $sig, url: ($base + "/" + ($name | @uri))};
|
||||||
|
{
|
||||||
|
version: $version,
|
||||||
|
notes: $notes,
|
||||||
|
pub_date: $pub_date,
|
||||||
|
platforms: {
|
||||||
|
"linux-x86_64": asset($deb; $deb_sig),
|
||||||
|
"linux-x86_64-deb": asset($deb; $deb_sig),
|
||||||
|
"linux-x86_64-rpm": asset($rpm; $rpm_sig),
|
||||||
|
"windows-x86_64": asset($exe; $exe_sig)
|
||||||
|
}
|
||||||
|
}'
|
||||||
196
scripts/build-latest-json.test.mjs
Normal file
196
scripts/build-latest-json.test.mjs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
/**
|
||||||
|
* scripts/build-latest-json.sh — the updater manifest of a release (#320).
|
||||||
|
*
|
||||||
|
* release.yml runs this script to write latest.json, which every installed
|
||||||
|
* app reads to find its update. tauri-plugin-updater looks up
|
||||||
|
* "{os}-{arch}-{installer}" first, then "{os}-{arch}". Before #320 the
|
||||||
|
* manifest only had linux-x86_64 (the .deb), so an .rpm install downloaded
|
||||||
|
* the .deb and refused it.
|
||||||
|
*
|
||||||
|
* Each test runs the script as release.yml does, under bash -e -o pipefail
|
||||||
|
* (the shell of the runner's steps), on a temporary assets directory. The
|
||||||
|
* bundle names are the real ones of v0.17.1: they contain a space, which the
|
||||||
|
* URLs must percent-encode. Expected values are literals, never rebuilt from
|
||||||
|
* the inputs.
|
||||||
|
*
|
||||||
|
* Plain .mjs on purpose: tsconfig only covers src/, so a .ts file here would
|
||||||
|
* look type-checked without being so. Needs bash and jq on PATH;
|
||||||
|
* check-frontend.yml installs jq for this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "build-latest-json.sh");
|
||||||
|
const BASE = "https://git.example.test/owner/Repo/releases/download/v0.17.1";
|
||||||
|
|
||||||
|
const DEB = "Simpl Resultat_0.17.1_amd64.deb";
|
||||||
|
const RPM = "Simpl Resultat-0.17.1-1.x86_64.rpm";
|
||||||
|
const EXE = "Simpl Resultat_0.17.1_x64-setup.exe";
|
||||||
|
|
||||||
|
// One distinct signature per bundle, to tell which .sig each key carries.
|
||||||
|
const SIG = {
|
||||||
|
[DEB]: "RWRkZWItc2lnbmF0dXJl",
|
||||||
|
[RPM]: "RWRycG0tc2lnbmF0dXJl",
|
||||||
|
[EXE]: "RWRleGUtc2lnbmF0dXJl",
|
||||||
|
};
|
||||||
|
|
||||||
|
let work;
|
||||||
|
let assets;
|
||||||
|
let notesFile;
|
||||||
|
|
||||||
|
function writeAsset(name, sig = SIG[name]) {
|
||||||
|
writeFileSync(join(assets, name), `bytes of ${name}\n`);
|
||||||
|
if (sig !== null) writeFileSync(join(assets, `${name}.sig`), sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAllAssets() {
|
||||||
|
for (const name of [DEB, RPM, EXE]) writeAsset(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(args = [assets, "0.17.1", notesFile, BASE]) {
|
||||||
|
return spawnSync("bash", ["-e", "-o", "pipefail", SCRIPT, ...args], { encoding: "utf8" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectFailure(result, message) {
|
||||||
|
expect(result.status).not.toBe(0);
|
||||||
|
expect(result.stdout).toBe("");
|
||||||
|
expect(result.stderr).toContain(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const jq = spawnSync("jq", ["--version"], { encoding: "utf8" });
|
||||||
|
if (jq.status !== 0) {
|
||||||
|
throw new Error("jq not found: build-latest-json.sh needs it (check-frontend.yml installs it)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
work = mkdtempSync(join(tmpdir(), "build-latest-json-"));
|
||||||
|
assets = join(work, "release-assets");
|
||||||
|
mkdirSync(assets);
|
||||||
|
notesFile = join(work, "notes.md");
|
||||||
|
writeFileSync(notesFile, "### Fixed\n\n- Something (#320).");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(work, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("build-latest-json.sh", () => {
|
||||||
|
it("writes the four keys, each with its own URL and its own asset's signature", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
|
||||||
|
const result = run();
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
const manifest = JSON.parse(result.stdout);
|
||||||
|
expect(manifest.version).toBe("0.17.1");
|
||||||
|
expect(manifest.notes).toBe("### Fixed\n\n- Something (#320).");
|
||||||
|
expect(manifest.pub_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
||||||
|
expect(manifest.platforms).toStrictEqual({
|
||||||
|
"linux-x86_64": {
|
||||||
|
signature: "RWRkZWItc2lnbmF0dXJl",
|
||||||
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
|
||||||
|
},
|
||||||
|
"linux-x86_64-deb": {
|
||||||
|
signature: "RWRkZWItc2lnbmF0dXJl",
|
||||||
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_amd64.deb",
|
||||||
|
},
|
||||||
|
"linux-x86_64-rpm": {
|
||||||
|
signature: "RWRycG0tc2lnbmF0dXJl",
|
||||||
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat-0.17.1-1.x86_64.rpm",
|
||||||
|
},
|
||||||
|
"windows-x86_64": {
|
||||||
|
signature: "RWRleGUtc2lnbmF0dXJl",
|
||||||
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_x64-setup.exe",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// The step log names the bundle picked for each kind.
|
||||||
|
expect(result.stderr).toContain("build-latest-json: .rpm <- Simpl Resultat-0.17.1-1.x86_64.rpm");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[".deb", DEB],
|
||||||
|
[".rpm", RPM],
|
||||||
|
["-setup.exe", EXE],
|
||||||
|
])("fails without a %s bundle, writing nothing on stdout", (kind, missing) => {
|
||||||
|
for (const name of [DEB, RPM, EXE]) {
|
||||||
|
if (name !== missing) writeAsset(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
expectFailure(run(), `expected exactly one ${kind} in ${assets}, found 0`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails on two .deb bundles", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
writeAsset("Simpl Resultat_0.17.0_amd64.deb", "RWRvbGQtZGViLXNpZ25hdHVyZQ==");
|
||||||
|
|
||||||
|
expectFailure(run(), `expected exactly one .deb in ${assets}, found 2`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serves Windows the NSIS -setup.exe only, not another .exe", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
writeAsset("simpl-result.exe", "RWRhcHAtZXhlLXNpZ25hdHVyZQ==");
|
||||||
|
|
||||||
|
const result = run();
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(JSON.parse(result.stdout).platforms["windows-x86_64"]).toStrictEqual({
|
||||||
|
signature: "RWRleGUtc2lnbmF0dXJl",
|
||||||
|
url: "https://git.example.test/owner/Repo/releases/download/v0.17.1/Simpl%20Resultat_0.17.1_x64-setup.exe",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails on an empty .sig", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
writeFileSync(join(assets, `${RPM}.sig`), "");
|
||||||
|
|
||||||
|
expectFailure(run(), `missing or empty signature: ${assets}/${RPM}.sig`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails on a .sig holding only line breaks", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
writeFileSync(join(assets, `${EXE}.sig`), "\n\n");
|
||||||
|
|
||||||
|
expectFailure(run(), `blank signature: ${assets}/${EXE}.sig`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails on a missing .sig", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
unlinkSync(join(assets, `${DEB}.sig`));
|
||||||
|
|
||||||
|
expectFailure(run(), `missing or empty signature: ${assets}/${DEB}.sig`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps notes with quotes, backslashes and a CHANGELOG_EOF line intact", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
const notes =
|
||||||
|
'### Fixed\n\n- A "quoted" word, a back\\slash and C:\\Users\\path.\n' +
|
||||||
|
"CHANGELOG_EOF\n" +
|
||||||
|
'"version": "9.9.9", "platforms": {}\n';
|
||||||
|
writeFileSync(notesFile, notes);
|
||||||
|
|
||||||
|
const result = run();
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
const manifest = JSON.parse(result.stdout);
|
||||||
|
expect(manifest.notes).toBe(
|
||||||
|
'### Fixed\n\n- A "quoted" word, a back\\slash and C:\\Users\\path.\n' +
|
||||||
|
"CHANGELOG_EOF\n" +
|
||||||
|
'"version": "9.9.9", "platforms": {}\n',
|
||||||
|
);
|
||||||
|
expect(manifest.version).toBe("0.17.1");
|
||||||
|
expect(Object.keys(manifest.platforms)).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails without its four arguments", () => {
|
||||||
|
writeAllAssets();
|
||||||
|
|
||||||
|
expectFailure(run([assets, "0.17.1", notesFile]), "usage: build-latest-json.sh");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue