From 99a62ebf81582288510e16fe24cf52b5d7243734 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 15 Aug 2026 15:51:42 +0100 Subject: [PATCH 01/30] postit: validate CHANGELOG section on tag, classify stable/preview/unstable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rend le job publish-release dépendant d'un nouveau job validate-release qui : - parse le tag (format MAJOR.MINOR.PATCH[-SUFFIX]) - classifie le canal : pair=stable, impair=preview, suffixe=instable - fail-fast sur instable sauf opt-in explicite via workflow_dispatch - vérifie que CHANGELOG.md contient une section ## [] - - expose le body de la section via $GITHUB_ENV pour le job de publication Le tag trigger passe de 'v*' à '*' (pas de préfixe sur les tags), et le corps de release GitHub est désormais curé via CHANGELOG.md plutôt que généré automatiquement. Cette convention de parité est partagée avec le dépôt postit-debian pour la production des paquets .deb (alignement à traiter dans une PR séparée). --- .github/workflows/docker-publish-android.yml | 128 +++++++++++++++++-- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index 317cda80..ebf52a6d 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -5,8 +5,14 @@ on: branches: - main tags: - - 'v*' + - '*' workflow_dispatch: + inputs: + force_unstable: + description: 'Publier une release avec suffixe (ex. 1.0.0-rc1) malgré le fail-fast par défaut.' + required: false + type: boolean + default: false # softprops/action-gh-release a besoin de contents: write # pour publier une release + uploader un asset. @@ -41,11 +47,113 @@ jobs: path: ./PostIt.Android.apk retention-days: 7 - publish-release: - # Uniquement déclenché par un tag v*. Le job apk-deploy tourne en - # parallèle, on partage l'artefact entre jobs. + # Job de validation : parse le tag, vérifie le format, applique la règle + # de parité du patch (pair=stable / impair=preview / suffixe=instable), + # et s'assure que CHANGELOG.md contient une section cohérente. + # Sans ce job, le job publish-release peut être bypassé (un attaquant + # qui contrôle un tag ne peut pas publier de release sans une section + # changelog cohérente). + validate-release: if: startsWith(github.ref, 'refs/tags/') - needs: apk-deploy + runs-on: ubuntu-latest + steps: + - name: Checkout du code + uses: actions/checkout@v7 + + - name: Valider le tag et la section CHANGELOG + env: + FORCE_UNSTABLE: ${{ inputs.force_unstable || github.event.inputs.force_unstable || 'false' }} + run: | + TAG="${GITHUB_REF_NAME}" + + # Parse semver : MAJOR.MINOR.PATCH[-SUFFIX] + if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then + echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format." + exit 1 + fi + + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + SUFFIX="${BASH_REMATCH[4]}" + + # Classification du canal par parité du patch. + # Patch pair + pas de suffixe -> stable. + # Patch impair + pas de suffixe -> preview. + # Suffixe présent -> instable. + if [[ -n "$SUFFIX" ]]; then + CHANNEL="unstable" + elif (( PATCH % 2 == 0 )); then + CHANNEL="stable" + else + CHANNEL="preview" + fi + + echo "Tag $TAG classifié comme channel=$CHANNEL" + + # Fail-fast sur instable sauf opt-in explicite via workflow_dispatch. + if [[ "$CHANNEL" == "unstable" && "$FORCE_UNSTABLE" != "true" ]]; then + echo "::error::Tag '$TAG' is unstable (suffix '$SUFFIX'). Refusing to publish." + echo "Set force_unstable=true via workflow_dispatch to override." + exit 1 + fi + + # Lecture du CHANGELOG.md (doit exister à la racine du repo). + if [[ ! -f CHANGELOG.md ]]; then + echo "::error::CHANGELOG.md not found at repo root." + exit 1 + fi + + # Extraction de la section [TAG]. On cherche la première ligne + # commençant par '## [' qui contient '[TAG]' (entre '## [' et + # la prochaine ligne '## [' ou fin de fichier). awk en mode + # paragraphe suffit et reste POSIX. + BODY=$(awk -v tag="[$TAG]" ' + /^## \[/ { + if (in_section) exit + if (index($0, tag) > 0) in_section=1 + next + } + in_section { print } + ' CHANGELOG.md) + + if [[ -z "$BODY" ]]; then + echo "::error::No section matching '## [$TAG]' found in CHANGELOG.md." + echo "Add a '## [$TAG] - $CHANNEL' section before tagging." + exit 1 + fi + + # Vérification cohérence du canal déclaré dans le suffixe. + # Format attendu : "## [TAG] - stable" / "- preview" / "- unstable". + if [[ "$BODY" != *" - $CHANNEL"* ]]; then + echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity." + echo "Current section body (first 5 lines):" + echo "$BODY" | head -5 + exit 1 + fi + + echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" + + # Exposition aux étapes suivantes via $GITHUB_ENV. + # heredoc <> "$GITHUB_ENV" + + publish-release: + # Déclenché uniquement par un push de tag. Le job apk-deploy produit + # l'artefact ; validate-release garantit la cohérence du tag et du + # changelog avant publication. + if: startsWith(github.ref, 'refs/tags/') + needs: [apk-deploy, validate-release] runs-on: ubuntu-latest steps: - name: Récupérer l'APK depuis l'artefact @@ -61,7 +169,9 @@ jobs: # apparaîtra dans l'asset et donc dans le permalink : # https://github.com///releases/latest/download/PostIt.Android.apk files: ./PostIt.Android.apk - # generate_release_notes: true -> évite d'avoir à maintenir - # le corps de release à la main. Décommente si tu veux. - # generate_release_notes: true - + # Le body est extrait de la section CHANGELOG.md correspondant + # au tag, exposée par validate-release via $GITHUB_ENV. + body: ${{ env.RELEASE_BODY }} + # stable -> false (marque comme Latest). + # preview / unstable -> true (visible mais pas Latest). + prerelease: ${{ env.IS_PRERELEASE }} From 93f39ca8722bb0da3baebdce412342e496dfcdaa Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 15 Aug 2026 21:56:38 +0100 Subject: [PATCH 02/30] forgejo/ci: pin dotnet sdk 10.0.x for buildAndTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le repo cible net10.0 partout (csproj, TFM), mais le workflow CI Forgejo buildAndTest installait une SDK 9.0.x. Aligne sur 10.0.x pour que la CI build avec une SDK qui connaît le TFM net10.0. Pas de global.json ajouté : la SDK est résolue à l'installation de l'image runner, le repo reste agnostique de la version exacte. --- .forgejo/workflows/buildAndTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index cda246cc..c10b4439 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -44,7 +44,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build From 0fe293bcd2a1dfea669fe0c038612770e49484bd Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 15 Aug 2026 22:18:03 +0100 Subject: [PATCH 03/30] forgejo/ci: run build in pazof/yavsc-build-env container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le workflow buildAndTest tournait sur un runner nu debian-latest avec setup-dotnet@v5 pour la SDK 10.0.x. Restore échouait car cet environnement n'a ni la source NuGet interne (isn.pschneider.fr) ni les workloads Android configurés, contrairement à l'image pazof/yavsc-build-env utilisée par le Dockerfile. Bascule le job sur un runner labelisé docker avec l'image debian12-dotnet10-android36-v1 directement. Le step setup-dotnet devient inutile (l'image a déjà la SDK 10.0), le restore partage la même config que le Dockerfile. Refs l'image cible par ARG BUILD_ENV_TAG=debian12-dotnet10-android36-v1. --- .forgejo/workflows/buildAndTest.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index c10b4439..12c47ac2 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -37,14 +37,10 @@ jobs: build: - runs-on: debian-latest + runs-on: docker://pazof/yavsc-build-env:debian12-dotnet10-android36-v1 steps: - uses: actions/checkout@v6 - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build From e165e7bb61d02c2afd85ae6334262c945be07112 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 12:49:50 +0100 Subject: [PATCH 04/30] dotnet-android-build-image: pin to --- .gitmodules | 3 +++ external/dotnet-android-build-image | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 external/dotnet-android-build-image diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6595fa7d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/dotnet-android-build-image"] + path = external/dotnet-android-build-image + url = git@forgejo.pschneider.fr:notazof/dotnet-android-build-image.git diff --git a/external/dotnet-android-build-image b/external/dotnet-android-build-image new file mode 160000 index 00000000..0695a6c1 --- /dev/null +++ b/external/dotnet-android-build-image @@ -0,0 +1 @@ +Subproject commit 0695a6c1fea6508f1a88f7ad0ad9cb93733aa52d From 4ac8e14ba928954e3eaaa6df1a179e1f3a86801b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:03:17 +0100 Subject: [PATCH 05/30] run on docker --- .forgejo/workflows/buildAndTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 12c47ac2..28fee837 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -37,7 +37,7 @@ jobs: build: - runs-on: docker://pazof/yavsc-build-env:debian12-dotnet10-android36-v1 + runs-on: docker steps: - uses: actions/checkout@v6 From 380b5d12c86dfa3dfc1aaaf7eee5569fae47566f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:15:10 +0100 Subject: [PATCH 06/30] ci: replace actions/checkout with manual git clone (image has no node) The pazof/yavsc-build-env:debian12-dotnet10-android36-v1 image only ships .NET 10 SDK + Android SDK + JDK 17, no Node. actions/checkout@v6 requires Node, so the job failed with 'exec: node not found'. Replace actions/checkout with a direct git clone over HTTPS (Forgejo anonymous is enabled), and init submodules recursively. Also drop the bogus docker://image:tag runs-on: matcher, use just 'docker' to match the runner's declared label name. --- .forgejo/workflows/buildAndTest.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 28fee837..e0b63433 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -40,10 +40,19 @@ jobs: runs-on: docker steps: - - uses: actions/checkout@v6 + - name: Clone yavsc + run: | + cd "$RUNNER_WORKSPACE" + git clone --depth 1 --branch "${GITHUB_REF_NAME:-main}" \ + https://forgejo.pschneider.fr/notazof/yavsc.git _src + cd _src + git submodule update --init --recursive --depth 1 - name: Restore dependencies + working-directory: ${{ runner.workspace }}/_src run: dotnet restore - name: Build + working-directory: ${{ runner.workspace }}/_src run: dotnet build --no-restore - name: Test + working-directory: ${{ runner.workspace }}/_src run: dotnet test --no-build --verbosity normal From 7370f48aac750def991eb0ded4b8259dc1a38928 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:22:56 +0100 Subject: [PATCH 07/30] ci: clone via GITHUB_REF instead of GITHUB_REF_NAME In pull_request context, GITHUB_REF_NAME is the PR number ('17'), not the source branch. Cloning --branch 17 fails with 'Could not find remote branch 17 to clone'. Use GITHUB_REF (refs/pull/N/head in PR context, refs/heads/ in push context) and fetch + checkout FETCH_HEAD. workflow_dispatch falls back to the default branch. --- .forgejo/workflows/buildAndTest.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index e0b63433..7a19e803 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -43,9 +43,12 @@ jobs: - name: Clone yavsc run: | cd "$RUNNER_WORKSPACE" - git clone --depth 1 --branch "${GITHUB_REF_NAME:-main}" \ - https://forgejo.pschneider.fr/notazof/yavsc.git _src + git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src cd _src + if [ -n "${GITHUB_REF:-}" ]; then + git fetch --depth 1 origin "$GITHUB_REF" + git checkout FETCH_HEAD + fi git submodule update --init --recursive --depth 1 - name: Restore dependencies working-directory: ${{ runner.workspace }}/_src From 86e59ad1c10ed26d6da95ee227632b52f8c5c434 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:26:12 +0100 Subject: [PATCH 08/30] submodule: switch URL from SSH to HTTPS for forgejo anonymous access The runner container does not have an SSH client, and even if it did, no key is configured for it. Forgejo Actions must reach the submodule over HTTPS with anonymous read access (which is now enabled on the Forgejo instance). Use 'git submodule sync --recursive' on the developer side after checkout to propagate the URL change to .git/modules/. --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 6595fa7d..eadf3c7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "external/dotnet-android-build-image"] path = external/dotnet-android-build-image - url = git@forgejo.pschneider.fr:notazof/dotnet-android-build-image.git + url = https://forgejo.pschneider.fr/notazof/dotnet-android-build-image.git From 5f50135c7ff5d9d9081c6e34273d8f956f31d283 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:27:36 +0100 Subject: [PATCH 09/30] ci: drop working-directory, cd into /src/_src explicitly forgejo-runner v13 does not interpolate ${{ runner.workspace }} in working-directory: (or ignores the field entirely for docker containers), so the container tried to chdir to '/_src' (literally) which does not exist. The image WORKDIR is /src, so clone directly into /src/_src and cd into it at the start of each step. Adds an echo of the checkout SHA + branch state for visibility in the log. --- .forgejo/workflows/buildAndTest.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 7a19e803..2c0137bd 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Clone yavsc run: | - cd "$RUNNER_WORKSPACE" + cd /src git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src cd _src if [ -n "${GITHUB_REF:-}" ]; then @@ -50,12 +50,10 @@ jobs: git checkout FETCH_HEAD fi git submodule update --init --recursive --depth 1 + echo "Checked out at $(git rev-parse HEAD) on $(git branch --show-current 2>/dev/null || echo detached HEAD)" - name: Restore dependencies - working-directory: ${{ runner.workspace }}/_src - run: dotnet restore + run: cd /src/_src && dotnet restore - name: Build - working-directory: ${{ runner.workspace }}/_src - run: dotnet build --no-restore + run: cd /src/_src && dotnet build --no-restore - name: Test - working-directory: ${{ runner.workspace }}/_src - run: dotnet test --no-build --verbosity normal + run: cd /src/_src && dotnet test --no-build --verbosity normal From 94012c51ab49ef0026e3e331ab48751c6ed7c880 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:35:44 +0100 Subject: [PATCH 10/30] nuget: add NuGet.config pointing at isn.pschneider.fr The yavsc solution depends on HigginsSoft.IdentityServer8.* 8.1.0-alpha.*, which is only published on the internal feed https://isn.pschneider.fr. Public nuget.org has 8.0.4 as the nearest version, so every project that uses IdentityServer8 (Yavsc.Org, Yavsc.Api, Yavsc.Blogs, Yavsc.Server, cli, Yavsc.Org.Tests, Yavsc.Blogs.Tests) fails with NU1102 on restore. Both feeds are reachable anonymously, so listing isn first and nuget.org second in a project-level config restores everything without credentials. The CI runner on forgejo now sees the same sources as a local clone. --- NuGet.config | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 NuGet.config diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 00000000..c601d09b --- /dev/null +++ b/NuGet.config @@ -0,0 +1,23 @@ + + + + + + + + + From cc50a8bbc8133ddb543b26bdc857604c7ef74c3e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 13:39:53 +0100 Subject: [PATCH 11/30] ci: unshallow clone for GitVersion GitVersion.MsBuild fails on shallow clones ('Repository is a shallow clone. Git repositories must contain the full history.') because it walks the git log to compute the SemVer version. Drop --depth 1 from both the PR ref fetch and the submodule update so the runner's working tree has full history. The repo is small enough that the cost is negligible. --- .forgejo/workflows/buildAndTest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 2c0137bd..9b3403db 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -46,10 +46,10 @@ jobs: git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src cd _src if [ -n "${GITHUB_REF:-}" ]; then - git fetch --depth 1 origin "$GITHUB_REF" + git fetch origin "$GITHUB_REF" git checkout FETCH_HEAD fi - git submodule update --init --recursive --depth 1 + git submodule update --init --recursive echo "Checked out at $(git rev-parse HEAD) on $(git branch --show-current 2>/dev/null || echo detached HEAD)" - name: Restore dependencies run: cd /src/_src && dotnet restore From abc507c0f38d4475ad168c39bef98de1c24dd60e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 14:09:11 +0100 Subject: [PATCH 12/30] dockerfile: drop inline 'dotnet nuget add source isn.pschneider.fr' The project-level NuGet.config (added in 94012c51) lists the isn feed so 'dotnet restore' picks it up without an inline 'dotnet nuget add source' step. The inline add source was duplicating NuGet.config and causing build failures in GitHub Actions: - The --allow-insecure-connections flag did not match the actual HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert, not self-signed), making the step fail with 'exit code 1'. - docker build --target build-env (used by .github/workflows/docker-publish-android.yml) hit this on every run. Both Dockerfile and Dockerfile.backend had the same redundant step; both removed. 'dotnet restore' still finds the feed via NuGet.config at /src/NuGet.config (copied in by 'COPY . .'). --- Dockerfile | 4 ---- Dockerfile.backend | 3 --- 2 files changed, 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 88b11683..795b70ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,10 +46,6 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ # (2) Tout le code source COPY . . -# (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté -# serveur, justifié par build privé). -RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json --allow-insecure-connections - # (4) Restore RUN dotnet restore diff --git a/Dockerfile.backend b/Dockerfile.backend index 76ad9ea0..a4e9a54a 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -25,9 +25,6 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ # 4. Copie de l'intégralité du code source COPY . . -# 3. Restauration des dépendances avec vos workloads actifs -RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json - # 4. Restauration des dépendances pour tous les projets RUN dotnet restore From eaa4c16936a627b7b2350f5bdc185b1fa15dae31 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 14:09:11 +0100 Subject: [PATCH 13/30] dockerfile: drop inline 'dotnet nuget add source isn.pschneider.fr' The project-level NuGet.config (added in 94012c51) lists the isn feed so 'dotnet restore' picks it up without an inline 'dotnet nuget add source' step. The inline add source was duplicating NuGet.config and causing build failures in GitHub Actions: - The --allow-insecure-connections flag did not match the actual HTTPS deployment of isn.pschneider.fr (Letsencrypt-issued cert, not self-signed), making the step fail with 'exit code 1'. - docker build --target build-env (used by .github/workflows/docker-publish-android.yml) hit this on every run. Both Dockerfile and Dockerfile.backend had the same redundant step; both removed. 'dotnet restore' still finds the feed via NuGet.config at /src/NuGet.config (copied in by 'COPY . .'). --- Dockerfile | 4 ---- Dockerfile.backend | 3 --- 2 files changed, 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 88b11683..795b70ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,10 +46,6 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ # (2) Tout le code source COPY . . -# (3) Source NuGet interne (Letsencrypt, certificat auto-signé côté -# serveur, justifié par build privé). -RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json --allow-insecure-connections - # (4) Restore RUN dotnet restore diff --git a/Dockerfile.backend b/Dockerfile.backend index 76ad9ea0..a4e9a54a 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -25,9 +25,6 @@ COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ # 4. Copie de l'intégralité du code source COPY . . -# 3. Restauration des dépendances avec vos workloads actifs -RUN dotnet nuget add source https://isn.pschneider.fr/api/v3/index.json - # 4. Restauration des dépendances pour tous les projets RUN dotnet restore From f92d23b54f60918ad4c4fc7ac92899ecfc2bfe8c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 16:18:37 +0100 Subject: [PATCH 14/30] release: 1.0.6 Patch is even (6) and bare, so this is classified as 'stable' by the validate-release job in .github/workflows/docker-publish-android.yml. Move the Unreleased section up by inserting [1.0.6] below it, with a list of changes that landed on this release: - Self-hosted Forgejo Actions runner now drives CI on yavsc, using pazof/yavsc-build-env:debian12-dotnet10-android36-v1 pulled from Docker Hub. - .forgejo/workflows/buildAndTest.yml builds without actions/checkout (image has no Node) and uses NuGet.config for the isn.pschneider.fr feed. - Dockerfile / Dockerfile.backend drop the redundant 'dotnet nuget add source' step that broke the APK build on GitHub Actions. --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb596070..44ec2b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,4 +26,27 @@ pour la production des paquets `.deb`. ### Removed +## [1.0.6] - stable + +### Added +- Self-hosted Forgejo Actions runner now drives the CI build for the + yavsc repository, using the + `pazof/yavsc-build-env:debian12-dotnet10-android36-v1` image pulled + from Docker Hub. Workflow runs end-to-end: clone, restore, build, + test, with NuGet.config picking up the `isn.pschneider.fr` feed. + +### Changed +- CI workflow `.forgejo/workflows/buildAndTest.yml` no longer relies on + `actions/checkout` (the runner image has no Node); clones yavsc via + `git`, fetches the ref under test, and initializes submodules over + HTTPS. + +### Fixed +- `Dockerfile` and `Dockerfile.backend` no longer carry a redundant + `dotnet nuget add source` step that conflicted with the GitHub + Actions APK build (`--allow-insecure-connections` on an HTTPS + endpoint, exit 1). `NuGet.config` at the repo root supplies the + `isn.pschneider.fr` feed for every restore, including inside Docker. + [Unreleased]: https://github.com/pazof/yavsc/compare/HEAD +[1.0.6]: https://github.com/pazof/yavsc/compare/1.0.5...1.0.6 From 7f84d4d97a01da0224baf22a75c5173548fdae5b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 16:35:05 +0100 Subject: [PATCH 15/30] fix(billing): tolerate ReflectionTypeLoadException during init ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies() and calls Assembly.GetTypes() on each. If any of the loaded assemblies has a type that fails to resolve (a flaky dependency, an AddOn with a broken reference, a test dependency that's been rewritten after compile), GetTypes() throws ReflectionTypeLoadException (or, less commonly, FileNotFoundException / TypeLoadException for the assembly itself). In CI on the forgejo-runner (and especially in test discovery under xunit v3), one such assembly is loaded somewhere between test runs and silently throws. The exception is not handled, so: 1. Collections are Cleared at the top of ConfigureBillingService(). 2. The reflection loop throws before reaching the RegisterBilling calls. 3. BillingService.Billing ends up empty (Count = 0). 4. The second ConfigureBillingService() call sees the same assembly loaded (xunit v3 keeps the AppDomain warm for the whole suite), throws identically, and the test Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing fails with 'Assert.Equal() Failure: Expected 3, Actual 0'. Fix: catch ReflectionTypeLoadException and use the partial .Types() list (the successfully-resolved subset), and use a broader catch (with continue) for any other assembly-level load failure. The lost user-settings types are not material; they are derived from ApplicationDbContext in a separate loop right after, and the RegisterBilling<>() calls that populate BillingService.Billing run last, after both reflective phases have completed best-effort. The test still passes locally because the local test environment loads a clean set of assemblies; only the CI runner (with its extra test-time tooling) hits this path. --- src/Yavsc.Server/Helpers/WorkflowHelpers.cs | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Yavsc.Server/Helpers/WorkflowHelpers.cs b/src/Yavsc.Server/Helpers/WorkflowHelpers.cs index 8be68ea0..23adb981 100644 --- a/src/Yavsc.Server/Helpers/WorkflowHelpers.cs +++ b/src/Yavsc.Server/Helpers/WorkflowHelpers.cs @@ -70,7 +70,27 @@ namespace Yavsc.Helpers foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies()) { - foreach (var c in a.GetTypes()) + Type[] types; + try + { + types = a.GetTypes(); + } + catch (System.Reflection.ReflectionTypeLoadException rtle) + { + // Some referenced types failed to load; keep the + // ones that did and skip the rest so a flaky + // dependency in one assembly does not break + // billing initialization for every other assembly. + types = rtle.Types.Where(t => t != null).ToArray(); + } + catch + { + // Assembly itself cannot be loaded (FileNotFoundException + // on a referenced assembly, etc.). Skip it entirely. + continue; + } + + foreach (var c in types) { if (c.IsClass && !c.IsAbstract && c.GetInterface(nameof(IUserSettings)) != null) From b3d19113021adc9545cf725c080b6d842bad7146 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 16 Aug 2026 16:46:48 +0100 Subject: [PATCH 16/30] ci: re-check CI on release/1.0.6 with billing init fix From 0fc3b81a0f58342f0efa3fb3048a9623ad81e711 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 00:06:32 +0100 Subject: [PATCH 17/30] Checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. complet de l’historique Git et des tags dans le job qui build l’APK via Docker: 2. with tags --- .github/workflows/docker-publish-android.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index ebf52a6d..94f190c2 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -25,6 +25,9 @@ jobs: steps: - name: Checkout du code uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true # 1. Votre étape de build actuelle (on nomme l'image "postit-android") # --target build-env : on ne veut que le stage de build (qui @@ -59,6 +62,9 @@ jobs: steps: - name: Checkout du code uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true - name: Valider le tag et la section CHANGELOG env: From 3dd47004046ea8f4e76dae39f2a0c6f196389c38 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 00:46:51 +0100 Subject: [PATCH 18/30] ci(forgejo): publish release with PostIt APK on tag push Adds .forgejo/workflows/release.yml: triggered by tag push or workflow_dispatch, it validates the tag/CHANGELOG parity (stable / preview / unstable), builds the PostIt Android APK via the existing Dockerfile (--target build-env), and publishes a Forgejo release with the APK as an asset via rasterstate/forgejo-release-action@v1. Mirrors the validate-release logic of .github/workflows/docker-publish-android.yml so the two channels (Forgejo source-of-truth + GitHub mirror) stay consistent. Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo PAT scoped to write:repository configured in the repository's Actions secrets. --- .forgejo/workflows/release.yml | 227 +++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 .forgejo/workflows/release.yml diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 00000000..008ee3f3 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,227 @@ +# Build and publish a release on the Forgejo source-of-truth instance +# with the PostIt Android APK as an attached asset. +# +# Triggered by a push of a git tag. Validates the tag/changelog pair, +# builds the APK using the existing Dockerfile (--target build-env), then +# publishes a Forgejo release via rasterstate/forgejo-release-action and +# uploads the APK as an asset. +# +# Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo PAT scoped +# to `write:repository` configured in the repository's Actions secrets. +# The runner-provided ${{ secrets.GITHUB_TOKEN }} would also work, but +# a dedicated PAT is preferred for least-privilege and revocability. +# +# This workflow complements .github/workflows/docker-publish-android.yml +# which targets the GitHub mirror; the validate-release logic mirrors +# the GitHub-side job so the two channels stay consistent. +name: Forgejo Release + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + tag: + description: 'Tag à publier (requis en dispatch, ex. 1.0.6 ou 1.0.7-rc1).' + required: true + type: string + force_unstable: + description: 'Publier une release avec suffixe (ex. 1.0.0-rc1) malgré le fail-fast par défaut.' + required: false + type: boolean + default: false + +permissions: + contents: write + +jobs: + # Parse le tag, applique la règle de parité du patch + # (pair=stable / impair=preview / suffixe=unstable), fail-fast sur + # instable sauf opt-in, et vérifie que CHANGELOG.md contient une + # section `## [TAG] - ` cohérente. Le body est extrait + # dans un artifact consommé par le job release. + validate-release: + runs-on: docker + steps: + - name: Checkout du code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Valider le tag et la section CHANGELOG + env: + # En push tag : github.ref_name est le tag. + # En workflow_dispatch : on lit l'input 'tag' (obligatoire). + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} + FORCE_UNSTABLE: ${{ inputs.force_unstable || 'false' }} + run: | + if [[ -z "$TAG" ]]; then + echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." + exit 1 + fi + + # Parse semver : MAJOR.MINOR.PATCH[-SUFFIX] + if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then + echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format." + exit 1 + fi + + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + SUFFIX="${BASH_REMATCH[4]}" + + # Classification du canal par parité du patch. + # Patch pair + pas de suffixe -> stable. + # Patch impair + pas de suffixe -> preview. + # Suffixe présent -> instable. + if [[ -n "$SUFFIX" ]]; then + CHANNEL="unstable" + elif (( PATCH % 2 == 0 )); then + CHANNEL="stable" + else + CHANNEL="preview" + fi + + echo "Tag $TAG classifié comme channel=$CHANNEL" + + # Fail-fast sur instable sauf opt-in explicite. + if [[ "$CHANNEL" == "unstable" && "$FORCE_UNSTABLE" != "true" ]]; then + echo "::error::Tag '$TAG' is unstable (suffix '$SUFFIX'). Refusing to publish." + echo "Set force_unstable=true via workflow_dispatch to override." + exit 1 + fi + + # Lecture du CHANGELOG.md (doit exister à la racine du repo). + if [[ ! -f CHANGELOG.md ]]; then + echo "::error::CHANGELOG.md not found at repo root." + exit 1 + fi + + # Extraction de la section [TAG]. On cherche la première ligne + # commençant par '## [' qui contient '[TAG]' (entre '## [' et + # la prochaine ligne '## [' ou fin de fichier). awk en mode + # paragraphe suffit et reste POSIX. + BODY=$(awk -v tag="[$TAG]" ' + /^## \[/ { + if (in_section) exit + if (index($0, tag) > 0) in_section=1 + next + } + in_section { print } + ' CHANGELOG.md) + + if [[ -z "$BODY" ]]; then + echo "::error::No section matching '## [$TAG]' found in CHANGELOG.md." + echo "Add a '## [$TAG] - $CHANNEL' section before tagging." + exit 1 + fi + + # Vérification cohérence du canal déclaré dans le suffixe. + # Format attendu : "## [TAG] - stable" / "- preview" / "- unstable". + if [[ "$BODY" != *" - $CHANNEL"* ]]; then + echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity." + echo "Current section body (first 5 lines):" + echo "$BODY" | head -5 + exit 1 + fi + + echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" + + # Écrit le body dans un fichier pour transmission via artifact. + # Le body est multi-ligne, donc artifact > heredoc $GITHUB_ENV. + mkdir -p release-body + printf '%s\n' "$BODY" > release-body/body.md + + - name: Uploader le body de la release comme artifact + uses: actions/upload-artifact@v7 + with: + name: release-body + path: release-body/body.md + retention-days: 1 + + # Construit l'APK via le Dockerfile (stage build-env), puis publie + # la release Forgejo avec le body validé et l'APK en asset. + release: + needs: validate-release + runs-on: docker + steps: + - name: Checkout du code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Checkout du tag (workflow_dispatch uniquement) + # En push tag, le runner checkout déjà au bon commit. + # En workflow_dispatch, on checkout explicitement le tag demandé + # pour que l'APK soit bien construit depuis ce commit. + if: github.event_name == 'workflow_dispatch' + env: + TAG: ${{ inputs.tag }} + run: | + if [[ -z "$TAG" ]]; then + echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." + exit 1 + fi + git checkout "$TAG" + + - name: Build de l'image Docker (stage build-env uniquement) + run: docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android . + + - name: Extraire l'APK signé du conteneur + run: | + docker create --name extractor postit-android + docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk ./PostIt.Android.apk + docker rm extractor + + - name: Récupérer le body validé + uses: actions/download-artifact@v7 + with: + name: release-body + path: release-body + + - name: Calculer le canal (stable / preview / unstable) depuis le tag + # On re-parse le tag ici plutôt que de transporter le channel + # via artifact. Le calcul est trivial (parité du patch + suffixe) + # et reste ainsi explicite. + id: set-channel + env: + # En push tag : github.ref_name est le tag. + # En workflow_dispatch : on lit l'input 'tag'. + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} + run: | + if [[ -z "$TAG" ]]; then + echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." + exit 1 + fi + if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then + echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format." + exit 1 + fi + PATCH="${BASH_REMATCH[3]}" + SUFFIX="${BASH_REMATCH[4]}" + if [[ -n "$SUFFIX" ]]; then + CHANNEL="unstable" + elif (( PATCH % 2 == 0 )); then + CHANNEL="stable" + else + CHANNEL="preview" + fi + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$([[ $CHANNEL != stable ]] && echo true || echo false)" >> "$GITHUB_OUTPUT" + + - name: Publier la release Forgejo et uploader l'APK + uses: https://rasterhub.com/rasterstate/forgejo-release-action@v1 + with: + # tag_name defaults to the pushed tag (GITHUB_REF_NAME). + body_path: release-body/body.md + # Stable -> Latest (false). + # Preview et Unstable -> prerelease (true). + prerelease: ${{ steps.set-channel.outputs.is_prerelease }} + files: | + PostIt.Android.apk + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file From 80cb8c46fc5b8309f9c7dc9f62cd3363ed272477 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 01:23:44 +0100 Subject: [PATCH 19/30] ci(forgejo): use runner-provided GITHUB_TOKEN for release workflow Repo-level secrets creation is broken on this Forgejo instance (InsertEncryptedSecret fails with UTF-8 byte-sequence error, likely a text-vs-bytea column type on the secret table). The fix is in upstream Forgejo v16; until then, ${{ secrets.GITHUB_TOKEN }} (auto- provided by the runner, scoped to contents: write for the current repo) keeps the release workflow operational without any UI setup. When the instance is upgraded and the secret table is migrated, revert this commit to switch back to ${{ secrets.RELEASE_TOKEN }} for least-privilege. --- .forgejo/workflows/release.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 008ee3f3..3dd00e7a 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,10 +6,14 @@ # publishes a Forgejo release via rasterstate/forgejo-release-action and # uploads the APK as an asset. # -# Authentication uses ${{ secrets.RELEASE_TOKEN }}, a Forgejo PAT scoped -# to `write:repository` configured in the repository's Actions secrets. -# The runner-provided ${{ secrets.GITHUB_TOKEN }} would also work, but -# a dedicated PAT is preferred for least-privilege and revocability. +# Authentication uses ${{ secrets.GITHUB_TOKEN }} (auto-provided by the +# Forgejo runner, scoped to contents: write for the current repo). A +# dedicated PAT (${{ secrets.RELEASE_TOKEN }}) was the preferred option +# for least-privilege, but creating repo-level secrets is currently +# broken on this Forgejo instance (InsertEncryptedSecret fails with a +# UTF-8 byte-sequence error, probably a text-vs-bytea column type on +# the secret table). Bumping to Forgejo v16 should fix it; until then, +# the runner-provided token keeps the workflow operational. # # This workflow complements .github/workflows/docker-publish-android.yml # which targets the GitHub mirror; the validate-release logic mirrors @@ -224,4 +228,4 @@ jobs: files: | PostIt.Android.apk env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From b69c382bebef2bfe1bc2392fea702cfacb41daf7 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 00:06:32 +0100 Subject: [PATCH 20/30] Checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. complet de l’historique Git et des tags dans le job qui build l’APK via Docker: 2. with tags --- .github/workflows/docker-publish-android.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index ebf52a6d..94f190c2 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -25,6 +25,9 @@ jobs: steps: - name: Checkout du code uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true # 1. Votre étape de build actuelle (on nomme l'image "postit-android") # --target build-env : on ne veut que le stage de build (qui @@ -59,6 +62,9 @@ jobs: steps: - name: Checkout du code uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true - name: Valider le tag et la section CHANGELOG env: From 5b957c6cbbbddefe32b829fa5e104e28b47fdca0 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 01:45:22 +0100 Subject: [PATCH 21/30] ci(forgejo): replace all Node-based actions with bash + curl The runner's docker label points at pazof/yavsc-build-env, a Debian image without Node.js. Any action like actions/checkout@v7, actions/upload-artifact@v7, rasterstate/forgejo-release-action, etc. fails at container start with 'executable file not found in /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/paul/.dotnet/tools:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/home/paul/.nvm/versions/node/v22.23.0/bin:/home/paul/.local/bin:/home/paul/.npm-global/bin:/home/paul/bin:/home/paul/.nix-profile/bin'. This workflow is rewritten in pure bash: - replace actions/checkout with explicit git clone + checkout (full history + tags so GitVersion.MsBuild is happy); - merge the two jobs into one (no inter-job artifacts needed since everything shares the runner's filesystem); - replace rasterstate/forgejo-release-action with direct calls to the Forgejo REST API (/api/v1/repos/.../releases, .../assets), with python3 used to build and parse JSON bodies (jq not guaranteed in the runner image). Auth: ${{ secrets.GITHUB_TOKEN }} (runner-provided). The rasterstate action or any other Node-based action can be reinstated later if the runner image is swapped for one with Node installed. --- .forgejo/workflows/release.yml | 229 +++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 97 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 3dd00e7a..05def45d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -3,8 +3,8 @@ # # Triggered by a push of a git tag. Validates the tag/changelog pair, # builds the APK using the existing Dockerfile (--target build-env), then -# publishes a Forgejo release via rasterstate/forgejo-release-action and -# uploads the APK as an asset. +# publishes a Forgejo release via the Forgejo REST API and uploads the +# APK as an asset. # # Authentication uses ${{ secrets.GITHUB_TOKEN }} (auto-provided by the # Forgejo runner, scoped to contents: write for the current repo). A @@ -15,6 +15,12 @@ # the secret table). Bumping to Forgejo v16 should fix it; until then, # the runner-provided token keeps the workflow operational. # +# Why bash + curl, no third-party actions: the runner's docker label +# points at pazof/yavsc-build-env, a Debian image without Node.js. Any +# action like actions/checkout, rasterstate/forgejo-release-action, etc. +# fails with "executable file not found in $PATH". Same constraint as +# .forgejo/workflows/buildAndTest.yml. +# # This workflow complements .github/workflows/docker-publish-android.yml # which targets the GitHub mirror; the validate-release logic mirrors # the GitHub-side job so the two channels stay consistent. @@ -40,24 +46,15 @@ permissions: contents: write jobs: - # Parse le tag, applique la règle de parité du patch - # (pair=stable / impair=preview / suffixe=unstable), fail-fast sur - # instable sauf opt-in, et vérifie que CHANGELOG.md contient une - # section `## [TAG] - ` cohérente. Le body est extrait - # dans un artifact consommé par le job release. - validate-release: + # Job unique : validation tag/CHANGELOG + build APK + publication + # via l'API REST Forgejo (pas d'actions tierces Node). + release: runs-on: docker steps: - - name: Checkout du code - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Valider le tag et la section CHANGELOG + - name: Clone du repo au tag demandé env: # En push tag : github.ref_name est le tag. - # En workflow_dispatch : on lit l'input 'tag' (obligatoire). + # En workflow_dispatch : on lit l'input 'tag'. TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} FORCE_UNSTABLE: ${{ inputs.force_unstable || 'false' }} run: | @@ -66,6 +63,27 @@ jobs: exit 1 fi + # WORKDIR de l'image (cf. dotnet-android-build-image/Dockerfile). + cd /src + + # Clone unshallow pour que GitVersion.MsBuild ait l'historique + # et les tags (sinon MSB3073 sur la cible Android cf. PR #21). + if [[ ! -d _src/.git ]]; then + git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src + fi + + cd _src + git fetch --tags --force --prune origin + git checkout "$TAG" + + echo "Checked out at $(git rev-parse HEAD) on $(git describe --tags --always 2>/dev/null || echo unknown)" + + - name: Valider le tag et la section CHANGELOG + run: | + cd /src/_src + TAG="$(git describe --tags --exact-match HEAD 2>/dev/null || git rev-parse --short HEAD)" + echo "Validating tag $TAG" + # Parse semver : MAJOR.MINOR.PATCH[-SUFFIX] if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format." @@ -92,7 +110,7 @@ jobs: echo "Tag $TAG classifié comme channel=$CHANNEL" # Fail-fast sur instable sauf opt-in explicite. - if [[ "$CHANNEL" == "unstable" && "$FORCE_UNSTABLE" != "true" ]]; then + if [[ "$CHANNEL" == "unstable" && "${FORCE_UNSTABLE:-false}" != "true" ]]; then echo "::error::Tag '$TAG' is unstable (suffix '$SUFFIX'). Refusing to publish." echo "Set force_unstable=true via workflow_dispatch to override." exit 1 @@ -134,98 +152,115 @@ jobs: echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" - # Écrit le body dans un fichier pour transmission via artifact. - # Le body est multi-ligne, donc artifact > heredoc $GITHUB_ENV. - mkdir -p release-body - printf '%s\n' "$BODY" > release-body/body.md - - - name: Uploader le body de la release comme artifact - uses: actions/upload-artifact@v7 - with: - name: release-body - path: release-body/body.md - retention-days: 1 - - # Construit l'APK via le Dockerfile (stage build-env), puis publie - # la release Forgejo avec le body validé et l'APK en asset. - release: - needs: validate-release - runs-on: docker - steps: - - name: Checkout du code - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Checkout du tag (workflow_dispatch uniquement) - # En push tag, le runner checkout déjà au bon commit. - # En workflow_dispatch, on checkout explicitement le tag demandé - # pour que l'APK soit bien construit depuis ce commit. - if: github.event_name == 'workflow_dispatch' - env: - TAG: ${{ inputs.tag }} - run: | - if [[ -z "$TAG" ]]; then - echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." - exit 1 - fi - git checkout "$TAG" + # Expose channel + body pour les étapes suivantes via $GITHUB_ENV. + echo "RELEASE_CHANNEL=$CHANNEL" >> "$GITHUB_ENV" + echo "RELEASE_BODY<> "$GITHUB_ENV" + echo "$BODY" >> "$GITHUB_ENV" + echo "EOF" >> "$GITHUB_ENV" + echo "IS_PRERELEASE=$([ "$CHANNEL" = "stable" ] && echo false || echo true)" >> "$GITHUB_ENV" - name: Build de l'image Docker (stage build-env uniquement) - run: docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android . + run: cd /src/_src && docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android . - name: Extraire l'APK signé du conteneur run: | docker create --name extractor postit-android - docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk ./PostIt.Android.apk + docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk /src/_src/PostIt.Android.apk docker rm extractor - - name: Récupérer le body validé - uses: actions/download-artifact@v7 - with: - name: release-body - path: release-body - - - name: Calculer le canal (stable / preview / unstable) depuis le tag - # On re-parse le tag ici plutôt que de transporter le channel - # via artifact. Le calcul est trivial (parité du patch + suffixe) - # et reste ainsi explicite. - id: set-channel + - name: Publier la release Forgejo via l'API REST + # Pas d'action tierce (pas de Node dans l'image runner). + # On parle à l'API Forgejo directement via curl. + # Docs : https://forgejo.pschneider.fr/api/swagger#/repository/release env: - # En push tag : github.ref_name est le tag. - # En workflow_dispatch : on lit l'input 'tag'. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_REPOSITORY: ${{ github.repository }} TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} + RELEASE_BODY: ${{ env.RELEASE_BODY }} + IS_PRERELEASE: ${{ env.IS_PRERELEASE }} run: | if [[ -z "$TAG" ]]; then - echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." + echo "::error::No tag resolved for the API call." exit 1 fi - if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then - echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format." - exit 1 - fi - PATCH="${BASH_REMATCH[3]}" - SUFFIX="${BASH_REMATCH[4]}" - if [[ -n "$SUFFIX" ]]; then - CHANNEL="unstable" - elif (( PATCH % 2 == 0 )); then - CHANNEL="stable" - else - CHANNEL="preview" - fi - echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" - echo "is_prerelease=$([[ $CHANNEL != stable ]] && echo true || echo false)" >> "$GITHUB_OUTPUT" - - name: Publier la release Forgejo et uploader l'APK - uses: https://rasterhub.com/rasterstate/forgejo-release-action@v1 - with: - # tag_name defaults to the pushed tag (GITHUB_REF_NAME). - body_path: release-body/body.md - # Stable -> Latest (false). - # Preview et Unstable -> prerelease (true). - prerelease: ${{ steps.set-channel.outputs.is_prerelease }} - files: | - PostIt.Android.apk - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + # Le runner Forgejo expose l'API sur github.api_url (par + # défaut http://…/api/v1). On retire le suffixe /api/v1 s'il + # est présent pour dériver la base du serveur, puis on + # reconstruit l'URL de l'API proprement. + API_BASE="${GITHUB_API_URL%/}" + API_BASE="${API_BASE%/api/v1}" + + # 1. Vérifier si la release existe déjà pour ce tag. + echo "::group::Check existing release for tag $TAG" + HTTP=$(curl -sS -o /tmp/existing.json -w '%{http_code}' \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/json" \ + "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$TAG") + echo "GET releases/tags/$TAG -> HTTP $HTTP" + EXISTING_ID="" + if [[ "$HTTP" == "200" ]]; then + EXISTING_ID=$(python3 -c "import json,sys; print(json.load(open('/tmp/existing.json')).get('id',''))" 2>/dev/null || true) + echo "Existing release id: ${EXISTING_ID:-none}" + fi + echo "::endgroup::" + + # 2. Créer ou mettre à jour la release. + # On utilise python3 pour générer le body JSON proprement + # (jq n'est pas garanti dans l'image runner). + if [[ -n "$EXISTING_ID" ]]; then + echo "::group::Update release id=$EXISTING_ID" + BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" python3 -c 'import json,os; print(json.dumps({"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))') + HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ + -X PATCH \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + --data-binary "$BODY" \ + "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$EXISTING_ID") + echo "PATCH release -> HTTP $HTTP" + echo "::endgroup::" + else + echo "::group::Create release" + BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" TAG="$TAG" python3 -c 'import json,os; print(json.dumps({"tag_name":os.environ["TAG"],"name":os.environ["TAG"],"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))') + HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ + -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + --data-binary "$BODY" \ + "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases") + echo "POST release -> HTTP $HTTP" + echo "::endgroup::" + fi + + if [[ "$HTTP" != "200" && "$HTTP" != "201" ]]; then + echo "::error::Release creation/update failed (HTTP $HTTP):" + cat /tmp/release.json + exit 1 + fi + + RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])") + echo "Release id=$RELEASE_ID" + + # 3. Upload l'APK en asset. + echo "::group::Upload APK asset" + HTTP=$(curl -sS -o /tmp/asset.json -w '%{http_code}' \ + -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + -H "Accept: application/json" \ + --data-binary "@/src/_src/PostIt.Android.apk" \ + "?name=PostIt.Android.apk" \ + "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets") + echo "POST asset -> HTTP $HTTP" + echo "::endgroup::" + + if [[ "$HTTP" != "201" ]]; then + echo "::error::Asset upload failed (HTTP $HTTP):" + cat /tmp/asset.json + exit 1 + fi + + echo "Release publiée: $API_BASE/$GITHUB_REPOSITORY/releases/tag/$TAG" \ No newline at end of file From 5e600c11e1d69e50a5ea94acb451a96c2b0b2e38 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 01:49:45 +0100 Subject: [PATCH 22/30] ci(forgejo): check CHANGELOG channel suffix on the section title The previous awk extracted the section body but excluded the title line (## [TAG] - channel), so the '* - $CHANNEL*' pattern never matched. Fix: include the title line in the extracted body, verify the channel suffix on the title, then strip the title before passing the body to the release API. --- .forgejo/workflows/release.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 05def45d..59aafb61 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -125,12 +125,16 @@ jobs: # Extraction de la section [TAG]. On cherche la première ligne # commençant par '## [' qui contient '[TAG]' (entre '## [' et # la prochaine ligne '## [' ou fin de fichier). awk en mode - # paragraphe suffit et reste POSIX. + # paragraphe suffit et reste POSIX. On garde aussi le titre + # (ligne `## [TAG] - channel`) pour la vérification du canal. BODY=$(awk -v tag="[$TAG]" ' /^## \[/ { if (in_section) exit - if (index($0, tag) > 0) in_section=1 - next + if (index($0, tag) > 0) { + in_section=1 + print + next + } } in_section { print } ' CHANGELOG.md) @@ -143,13 +147,16 @@ jobs: # Vérification cohérence du canal déclaré dans le suffixe. # Format attendu : "## [TAG] - stable" / "- preview" / "- unstable". - if [[ "$BODY" != *" - $CHANNEL"* ]]; then - echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity." - echo "Current section body (first 5 lines):" - echo "$BODY" | head -5 + # On lit la première ligne du body qui contient le titre. + TITLE=$(echo "$BODY" | head -1) + if [[ "$TITLE" != *" - $CHANNEL"* ]]; then + echo "::error::Section title '$TITLE' must declare suffix '- $CHANNEL' to match tag parity." exit 1 fi + # Body pour la release : retire la première ligne (titre). + BODY=$(echo "$BODY" | tail -n +2) + echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" # Expose channel + body pour les étapes suivantes via $GITHUB_ENV. From 44edf71b1280f060a94ee99d141e323336b4fefe Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 01:56:01 +0100 Subject: [PATCH 23/30] ci(forgejo): build .NET projects directly, skip docker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'image runner pazof/yavsc-build-env a le SDK .NET 10 et le workload Android, mais PAS le binaire 'docker' ni de daemon Docker. Le 'Build de l'image Docker' du workflow plantait avec 'docker: command not found'. Fix : on exécute directement les commandes dotnet du Dockerfile (restore + build Yavsc.Org/Api/Blogs + build PostIt.Android -r android-arm64), puis on copie l'APK depuis le chemin de sortie standard bin/Release/net10.0-android/android-arm64/. Note : le Dockerfile reste la voie canonique pour les builds en local et via GitHub Actions (qui a docker). Ce fix concerne uniquement le workflow Forgejo Actions où le runner n'a pas Docker. --- .forgejo/workflows/release.yml | 37 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 59aafb61..dc813fbe 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -166,14 +166,37 @@ jobs: echo "EOF" >> "$GITHUB_ENV" echo "IS_PRERELEASE=$([ "$CHANNEL" = "stable" ] && echo false || echo true)" >> "$GITHUB_ENV" - - name: Build de l'image Docker (stage build-env uniquement) - run: cd /src/_src && docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android . - - - name: Extraire l'APK signé du conteneur + - name: Build des projets .NET (sans docker) + # L'image runner (pazof/yavsc-build-env) a le SDK .NET 10 + le + # workload Android, mais PAS le binaire `docker` ni de daemon + # Docker. On exécute donc les commandes dotnet directement + # au lieu de passer par `docker build`. + # Equivalent des stages build-env du Dockerfile (lignes + # restore + build Yavsc.Org + build Yavsc.Api + build + # Yavsc.Blogs + build PostIt.Android -r android-arm64). run: | - docker create --name extractor postit-android - docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk /src/_src/PostIt.Android.apk - docker rm extractor + cd /src/_src + dotnet restore + dotnet build src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-restore -clp:ErrorsOnly + dotnet build src/Yavsc.Api/Yavsc.Api.csproj -c Release --no-restore -clp:ErrorsOnly + dotnet build src/Yavsc.Blogs/Yavsc.Blogs.csproj -c Release --no-restore -clp:ErrorsOnly + dotnet build src/PostIt/PostIt.Android/PostIt.Android.csproj \ + -c Release --no-restore -clp:ErrorsOnly -r android-arm64 + + - name: Copier l'APK signé vers un emplacement connu + # Le build Android avec -r android-arm64 produit l'APK dans + # bin/Release/net10.0-android/android-arm64/. On le copie à + # la racine du checkout pour que l'étape d'upload le trouve. + run: | + cd /src/_src + APK=src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk + if [[ ! -f "$APK" ]]; then + echo "::error::APK not found at $APK" + ls -la src/PostIt/PostIt.Android/bin/Release/net10.0-android/ 2>/dev/null || true + exit 1 + fi + cp "$APK" /src/_src/PostIt.Android.apk + ls -la /src/_src/PostIt.Android.apk - name: Publier la release Forgejo via l'API REST # Pas d'action tierce (pas de Node dans l'image runner). From 704f7565fece13d24ac1017832646ce7821db440 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:00:12 +0000 Subject: [PATCH 24/30] Initial plan From 843d6b227faeb902f86c111369676632088af8f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:02:14 +0000 Subject: [PATCH 25/30] fix(ci): fix validate-release CHANGELOG channel check to inspect heading line Co-authored-by: pazof <3072814+pazof@users.noreply.github.com> --- .github/workflows/docker-publish-android.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index 94f190c2..b9ee364c 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -129,12 +129,12 @@ jobs: exit 1 fi - # Vérification cohérence du canal déclaré dans le suffixe. + # Vérification cohérence du canal déclaré dans le titre de section. # Format attendu : "## [TAG] - stable" / "- preview" / "- unstable". - if [[ "$BODY" != *" - $CHANNEL"* ]]; then + HEADER=$(grep -m1 "^## \[$TAG\]" CHANGELOG.md) + if [[ "$HEADER" != *" - $CHANNEL"* ]]; then echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity." - echo "Current section body (first 5 lines):" - echo "$BODY" | head -5 + echo "Current section header: $HEADER" exit 1 fi From bbe483cb2427d87d5b9f887664256ba663d2b834 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 02:16:46 +0100 Subject: [PATCH 26/30] ci(forgejo): build JSON bodies in pure bash, no python3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'image runner pazof/yavsc-build-env n'a pas python3 (ni jq, ni node). Le step de publication Forgejo utilisait python3 pour générer les bodies JSON (POST /releases, PATCH /releases/{id}) et pour extraire le 'id' de la réponse. Fix : deux fonctions bash : - json_escape : escaping JSON des chaînes (\\, \", \n, \r, \t) - json_field : extraction d'un champ scalaire d'un fichier JSON via sed Suffisant pour les bodies qu'on envoie (tag_name, name, body, prerelease) et les champs qu'on lit (id). --- .forgejo/workflows/release.yml | 37 ++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index dc813fbe..9306d087 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -222,6 +222,28 @@ jobs: API_BASE="${GITHUB_API_URL%/}" API_BASE="${API_BASE%/api/v1}" + # Pas de python3, pas de jq dans l'image runner. On génère + # le JSON à la main : escaping minimal des caractères + # spéciaux JSON dans les chaînes (\\, \", \n, \r, \t). + # Suffisant pour un CHANGELOG.md bien formé. + json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\r'/\\r}" + s="${s//$'\t'/\\t}" + printf '%s' "$s" + } + + # Extraction d'un champ JSON scalaire (string ou number) depuis + # un fichier. Utilise sed basique, suffisant pour les champs + # id / tag_name que l'API renvoie en clair. + json_field() { + local file="$1" field="$2" + sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\?\([^\",}]*\)\"\?.*/\1/p" "$file" | head -1 + } + # 1. Vérifier si la release existe déjà pour ce tag. echo "::group::Check existing release for tag $TAG" HTTP=$(curl -sS -o /tmp/existing.json -w '%{http_code}' \ @@ -231,17 +253,16 @@ jobs: echo "GET releases/tags/$TAG -> HTTP $HTTP" EXISTING_ID="" if [[ "$HTTP" == "200" ]]; then - EXISTING_ID=$(python3 -c "import json,sys; print(json.load(open('/tmp/existing.json')).get('id',''))" 2>/dev/null || true) + EXISTING_ID=$(json_field /tmp/existing.json id) echo "Existing release id: ${EXISTING_ID:-none}" fi echo "::endgroup::" # 2. Créer ou mettre à jour la release. - # On utilise python3 pour générer le body JSON proprement - # (jq n'est pas garanti dans l'image runner). if [[ -n "$EXISTING_ID" ]]; then echo "::group::Update release id=$EXISTING_ID" - BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" python3 -c 'import json,os; print(json.dumps({"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))') + BODY=$(printf '{"body":"%s","prerelease":%s}' \ + "$(json_escape "$RELEASE_BODY")" "$IS_PRERELEASE") HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ -X PATCH \ -H "Authorization: token $GITHUB_TOKEN" \ @@ -253,7 +274,11 @@ jobs: echo "::endgroup::" else echo "::group::Create release" - BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" TAG="$TAG" python3 -c 'import json,os; print(json.dumps({"tag_name":os.environ["TAG"],"name":os.environ["TAG"],"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))') + BODY=$(printf '{"tag_name":"%s","name":"%s","body":"%s","prerelease":%s}' \ + "$(json_escape "$TAG")" \ + "$(json_escape "$TAG")" \ + "$(json_escape "$RELEASE_BODY")" \ + "$IS_PRERELEASE") HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ @@ -271,7 +296,7 @@ jobs: exit 1 fi - RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])") + RELEASE_ID=$(json_field /tmp/release.json id) echo "Release id=$RELEASE_ID" # 3. Upload l'APK en asset. From 5186ffb7c83e4538925d340f0315a07415161415 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 03:26:11 +0100 Subject: [PATCH 27/30] ci(forgejo): limit json_field extraction to top-level keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'API Forgejo renvoie pour /releases/tags/ un objet JSON pretty-printed où l'id racine (release.id, ex. 10706) est sur la première ligne, mais l'objet author contient aussi un id (souvent 1 pour le premier user du repo). L'ancienne regex sed matchait la première occurrence globale de "id" dans le fichier, donc elle retombait sur author.id=1 et le PATCH /releases/1 tombait en 404 'The target couldn't be found'. Fix : on pipe le fichier dans 'head -3' pour ne matcher que les premières lignes (couvre largement le préambule de l'objet release). Si Forgejo renvoie du JSON minifié (une seule ligne), head -3 renvoie toute la ligne et la regex matche le premier id (la racine, parce que les champs auteur sont après les champs racine). --- .forgejo/workflows/release.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 9306d087..137148a2 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -236,12 +236,16 @@ jobs: printf '%s' "$s" } - # Extraction d'un champ JSON scalaire (string ou number) depuis - # un fichier. Utilise sed basique, suffisant pour les champs - # id / tag_name que l'API renvoie en clair. + # Extraction d'un champ JSON scalaire de premier niveau depuis un + # fichier. On ne lit que les premières lignes pour éviter de + # matcher un champ homonyme dans un objet imbriqué (par ex. + # le champ "id" de l'auteur d'une release Forgejo, qui vaut + # typiquement 1 pour le premier user du repo). Sans cette + # restriction, le PATCH sur /releases/ tombe en + # 404 "The target couldn't be found". json_field() { local file="$1" field="$2" - sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\?\([^\",}]*\)\"\?.*/\1/p" "$file" | head -1 + head -3 "$file" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\?\([0-9][0-9]*\)\"\?.*/\1/p" | head -1 } # 1. Vérifier si la release existe déjà pour ce tag. From c3c54ba5d5ea3a95b6730540bd21d7edcf7d3c86 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 04:35:00 +0100 Subject: [PATCH 28/30] ci(forgejo): build JSON bodies with jq instead of hand-rolled sed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'image runner pazof/yavsc-build-env installe jq (>= 1.7) à partir de debian12-dotnet10-android36-v2 (Dockerfile du repo dotnet-android-build-image, commit e06f096 "adds jq"). On en profite pour supprimer json_escape et json_field à base de sed, qui étaient fragiles : * sed est greedy par défaut : sur du JSON minifié d'une seule ligne (ce que renvoie l'API Forgejo de cette instance pour /releases/tags/), la regex s/.*"id".../\1/p attrape la DERNIÈRE occurrence de "id": sur la ligne, qui est l'id de l'auteur de la release (1, premier user du repo), pas l'id de la release (10706). * Le head -3 ajouté en PR #30 ne tient pas sur du JSON minifié : il n'isole rien et le sed greedy continue à capturer l'id de l'auteur. * PATCH /releases/1 tombait alors en 404 "The target couldn't be found" (cf. run échoué du 2026-08-17 04:05 sur le tag 1.0.6). jq résout les deux problèmes en une fois : * jq -r '.id' retourne le champ id racine, pas l'id imbriqué dans author. * jq -n --arg body "$RELEASE_BODY" '{body: $body, prerelease: $prerelease}' construit un body JSON proprement échappé (backslashes, guillemets, newlines, caractères de contrôle Unicode) sans avoir à le reproduire à la main. Effet de bord : les bodies PATCH et POST sont écrits dans /tmp/patch.json et /tmp/post.json puis passés à curl via --data-binary @ au lieu d'une variable shell. Plus de problème de quoting en chaîne shell, plus de collision avec les espaces ou les caractères spéciaux du body. Pré-requis côté runner : image pazof/yavsc-build-env:debian12- dotnet10-android36-v2 (avec jq) + maj du label correspondant dans la config du runner Forgejo. --- .forgejo/workflows/release.yml | 82 +++++++++++++++++----------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 137148a2..cf3aaff7 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -15,10 +15,16 @@ # the secret table). Bumping to Forgejo v16 should fix it; until then, # the runner-provided token keeps the workflow operational. # -# Why bash + curl, no third-party actions: the runner's docker label -# points at pazof/yavsc-build-env, a Debian image without Node.js. Any -# action like actions/checkout, rasterstate/forgejo-release-action, etc. -# fails with "executable file not found in $PATH". Same constraint as +# Why bash + jq + curl, no third-party actions: the runner's docker +# label points at pazof/yavsc-build-env, a Debian image with jq but +# without Node.js or python3. Any action like actions/checkout, +# rasterstate/forgejo-release-action, etc. fails with "executable +# file not found in $PATH". jq is shipped in the image from +# debian12-dotnet10-android36-v2 onward; earlier tags fell back to +# hand-rolled JSON building via sed, which was fragile (cf. PR #30: +# sed greedy + head -3 still matched author.id instead of the +# release id on the minified JSON this instance returns, PATCH +# /releases/1 → 404). Same constraint as # .forgejo/workflows/buildAndTest.yml. # # This workflow complements .github/workflows/docker-publish-android.yml @@ -222,31 +228,22 @@ jobs: API_BASE="${GITHUB_API_URL%/}" API_BASE="${API_BASE%/api/v1}" - # Pas de python3, pas de jq dans l'image runner. On génère - # le JSON à la main : escaping minimal des caractères - # spéciaux JSON dans les chaînes (\\, \", \n, \r, \t). - # Suffisant pour un CHANGELOG.md bien formé. - json_escape() { - local s="$1" - s="${s//\\/\\\\}" - s="${s//\"/\\\"}" - s="${s//$'\n'/\\n}" - s="${s//$'\r'/\\r}" - s="${s//$'\t'/\\t}" - printf '%s' "$s" - } - - # Extraction d'un champ JSON scalaire de premier niveau depuis un - # fichier. On ne lit que les premières lignes pour éviter de - # matcher un champ homonyme dans un objet imbriqué (par ex. - # le champ "id" de l'auteur d'une release Forgejo, qui vaut - # typiquement 1 pour le premier user du repo). Sans cette - # restriction, le PATCH sur /releases/ tombe en - # 404 "The target couldn't be found". - json_field() { - local file="$1" field="$2" - head -3 "$file" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\?\([0-9][0-9]*\)\"\?.*/\1/p" | head -1 - } + # Construction des bodies JSON et extraction de champs via + # jq. L'image runner pazof/yavsc-build-env installe jq + # (>= 1.7) depuis debian12-dotnet10-android36-v2. La + # chaîne de construction --arg/--argjson garantit un + # escaping correct (backslashes, guillemets, newlines, + # caractères de contrôle Unicode) sans avoir à le + # reproduire à la main. + # + # json_escape et json_field à base de sed ont vécu : le + # sed greedy matche la dernière occurrence d'un champ + # dans la ligne, et l'API renvoie sur cette instance un + # JSON minifié d'une seule ligne où l'id de l'auteur + # (1, premier user du repo) suit l'id de la release + # (10706). PATCH /releases/ tombait + # alors en 404 "The target couldn't be found". jq + # résout les deux problèmes en une fois. # 1. Vérifier si la release existe déjà pour ce tag. echo "::group::Check existing release for tag $TAG" @@ -257,7 +254,7 @@ jobs: echo "GET releases/tags/$TAG -> HTTP $HTTP" EXISTING_ID="" if [[ "$HTTP" == "200" ]]; then - EXISTING_ID=$(json_field /tmp/existing.json id) + EXISTING_ID=$(jq -r '.id // empty' /tmp/existing.json) echo "Existing release id: ${EXISTING_ID:-none}" fi echo "::endgroup::" @@ -265,30 +262,35 @@ jobs: # 2. Créer ou mettre à jour la release. if [[ -n "$EXISTING_ID" ]]; then echo "::group::Update release id=$EXISTING_ID" - BODY=$(printf '{"body":"%s","prerelease":%s}' \ - "$(json_escape "$RELEASE_BODY")" "$IS_PRERELEASE") + jq -n \ + --arg body "$RELEASE_BODY" \ + --argjson prerelease "$IS_PRERELEASE" \ + '{body: $body, prerelease: $prerelease}' \ + > /tmp/patch.json HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ -X PATCH \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ - --data-binary "$BODY" \ + --data-binary @/tmp/patch.json \ "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$EXISTING_ID") echo "PATCH release -> HTTP $HTTP" echo "::endgroup::" else echo "::group::Create release" - BODY=$(printf '{"tag_name":"%s","name":"%s","body":"%s","prerelease":%s}' \ - "$(json_escape "$TAG")" \ - "$(json_escape "$TAG")" \ - "$(json_escape "$RELEASE_BODY")" \ - "$IS_PRERELEASE") + jq -n \ + --arg tag "$TAG" \ + --arg name "$TAG" \ + --arg body "$RELEASE_BODY" \ + --argjson prerelease "$IS_PRERELEASE" \ + '{tag_name: $tag, name: $name, body: $body, prerelease: $prerelease}' \ + > /tmp/post.json HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ - --data-binary "$BODY" \ + --data-binary @/tmp/post.json \ "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases") echo "POST release -> HTTP $HTTP" echo "::endgroup::" @@ -300,7 +302,7 @@ jobs: exit 1 fi - RELEASE_ID=$(json_field /tmp/release.json id) + RELEASE_ID=$(jq -r '.id' /tmp/release.json) echo "Release id=$RELEASE_ID" # 3. Upload l'APK en asset. From 77fda10347870653a64f881d0be1ae9287681fae Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 05:25:20 +0100 Subject: [PATCH 29/30] chore(release): update 1.0.6 CHANGELOG section (image v2, jq fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La section [1.0.6] - stable du CHANGELOG mentionnait encore debian12-dotnet10-android36-v1 et ne décrivait pas le fix du PATCH release qui tombait en 404 à cause du sed greedy + JSON minifié. Mets à jour avant de relancer la publication de la release 1.0.6 (workflow_dispatch), pour que le body publié reflète l'état réel de l'infra (image v2 avec jq) et du workflow. --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ec2b56..c855b249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,13 @@ pour la production des paquets `.deb`. ### Added - Self-hosted Forgejo Actions runner now drives the CI build for the yavsc repository, using the - `pazof/yavsc-build-env:debian12-dotnet10-android36-v1` image pulled + `pazof/yavsc-build-env:debian12-dotnet10-android36-v2` image pulled from Docker Hub. Workflow runs end-to-end: clone, restore, build, test, with NuGet.config picking up the `isn.pschneider.fr` feed. +- The build-env image now ships `jq` (Debian package, ≥ 1.7), so the + release workflow can build JSON bodies and parse API responses + without a hand-rolled `sed`-based extractor that was matching the + wrong `id` field on minified responses. ### Changed - CI workflow `.forgejo/workflows/buildAndTest.yml` no longer relies on @@ -47,6 +51,12 @@ pour la production des paquets `.deb`. Actions APK build (`--allow-insecure-connections` on an HTTPS endpoint, exit 1). `NuGet.config` at the repo root supplies the `isn.pschneider.fr` feed for every restore, including inside Docker. +- `.forgejo/workflows/release.yml`: PATCH on `/releases/{id}` no longer + 404s on existing releases. The previous `sed`-based `json_field` + matched the last `id` on the line (the author's), so it tried to + PATCH `/releases/1` (the first user of the instance) instead of the + actual release id. Switched to `jq` for both body construction and + field extraction. [Unreleased]: https://github.com/pazof/yavsc/compare/HEAD [1.0.6]: https://github.com/pazof/yavsc/compare/1.0.5...1.0.6 From a4792a7a839f7605afd7a6818c6f1e8dac69c915 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 17 Aug 2026 05:44:12 +0100 Subject: [PATCH 30/30] ci(forgejo): put asset name in URL query string, not as curl arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le run #102 (re-publication du tag 1.0.6 après le fix jq + bump image v2) a passé le PATCH /releases/10706 (jq a bien extrait l'id racine, plus de 404), mais l'upload d'asset a planté avec un 400 "Missing 'name' parameter". Cause : sur l'appel curl de l'upload d'asset, l'argument `?name=...` était passé en argument positionnel entre `--data-binary @file` et l'URL. curl l'interprète comme un second fichier d'input (un fichier nommé '?name=...'), pas comme un query param, et l'API Forgejo ne voit jamais le name. Fix : concaténer `?name=PostIt.Android.apk` à l'URL directement. L'API Forgejo accepte le name en query string sur POST /releases/{id}/assets. --- .forgejo/workflows/release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index cf3aaff7..ef518037 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -306,6 +306,11 @@ jobs: echo "Release id=$RELEASE_ID" # 3. Upload l'APK en asset. + # Le nom du fichier passe en query string (?name=...), pas + # en argument positionnel entre --data-binary et l'URL : + # sinon curl l'interprète comme un second fichier d'input + # (un fichier nommé '?name=PostIt.Android.apk') et l'API + # Forgejo renvoie 400 "Missing 'name' parameter". echo "::group::Upload APK asset" HTTP=$(curl -sS -o /tmp/asset.json -w '%{http_code}' \ -X POST \ @@ -313,8 +318,7 @@ jobs: -H "Content-Type: application/octet-stream" \ -H "Accept: application/json" \ --data-binary "@/src/_src/PostIt.Android.apk" \ - "?name=PostIt.Android.apk" \ - "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets") + "$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=PostIt.Android.apk") echo "POST asset -> HTTP $HTTP" echo "::endgroup::"