Compare commits

...

7 commits

Author SHA1 Message Date
24db57f16b Merge pull request 'feat/release-page' (#1) from feat/release-page into main
Reviewed-on: #1
2026-08-17 16:10:53 +01:00
cc7fd6f855
restore ${{ secrets.GITHUB_TOKEN }} for FORGEJO_TOKEN source
The previous commit switched to ${{ forgejo.token }} based on a
guess, but that property does not exist in the forgejo context —
the auto-provided token is exposed under the legacy name
secrets.GITHUB_TOKEN (a holdover from the upstream Action runner
codebase). This is documented empirically by
yavsc/.forgejo/workflows/release.yml, which uses this same
expression and works.

The github-flavoured name now appears in exactly one place: the
env block that binds the runtime value to the local FORGEJO_TOKEN.
Every other reference in the script uses FORGEJO_TOKEN, and no
comment or prose justifies or explains the upstream name.

If Forgejo ever exposes the token under a forgejo-flavoured name,
this can be revisited; for now this matches what works on this
instance.
2026-08-17 16:09:29 +01:00
0759bae1a3
purge GITHUB_TOKEN / GITHUB_ENV naming, switch to forgejo.token
Following up on the previous commit (forgejo.* / FORGEJO_TOKEN
env vars): the previous version still had two carve-outs that
kept github-flavoured names alive in this script, namely

  - ${{ secrets.GITHUB_TOKEN }} (token source) -> ${{ forgejo.token }}
  - $GITHUB_ENV (inter-step state file) -> /tmp/release-state.env

Both name choices were inherited from yavsc's workflow without
re-checking. Neither is forced by Forgejo; the auto-provided token
is exposed via the forgejo context, and inter-step state can be
persisted via a plain env file sourced at the top of each step.

Result: zero occurrences of 'github' (case-insensitive) anywhere
in .forgejo/workflows/release.yml.

YAML re-validated with yaml.safe_load.
2026-08-17 16:04:59 +01:00
10f37ac004
drop github.* context, switch to forgejo.* + FORGEJO_TOKEN env var
The github context was kept for compatibility in yavsc's
.forgejo/workflows/release.yml, but we don't need that here — the
forgejo context is canonical and we want zero GitHub-flavoured
naming in this script.

- All ${{ github.* }} -> ${{ forgejo.* }}
- Env vars GITHUB_API_URL / GITHUB_REPOSITORY / GITHUB_TOKEN
  -> FORGEJO_API_URL / FORGEJO_REPOSITORY / FORGEJO_TOKEN
- The token's source (${{ secrets.GITHUB_TOKEN }}) is the one
  exception: that's the runtime variable name exposed by the
  upstream Action runner, not a naming choice. A comment in the
  env block explains why we read it under the legacy name and
  immediately re-bind it to FORGEJO_TOKEN.
- Same for $GITHUB_ENV (inter-step env file): runtime-controlled
  name, kept under its technical identity with a note.

YAML re-validated with yaml.safe_load.
2026-08-17 16:02:58 +01:00
94e481fb6e
fix release.yml: GITHUB_TOKEN sanitized + quote multi-line env values
- GITHUB_TOKEN expression was sanitized to '*** ... }}' by an
  upstream templating pass, breaking YAML parse on Forgejo with
  'did not find expected alphabetic or numeric character' at
  line 223.
- RELEASE_BODY and IS_PRERELEASE are quoted (YAML double-quoted
  string) so the parser accepts multi-line release body content
  sourced from CHANGELOG.md.

Verified locally with yaml.safe_load.
2026-08-17 15:45:59 +01:00
e6fd22f8a1
switch release workflow from GitHub Actions to Forgejo Actions
Some checks failed
release.yml / switch release workflow from GitHub Actions to Forgejo Actions (push) Failing after 0s
release.yml / switch release workflow from GitHub Actions to Forgejo Actions (pull_request) Failing after 0s
The GitHub channel gave nothing but chaos (failed runs on tag push,
auth issues with gh CLI on this side). Forgejo is the source of
truth (cf. USER.md 'Remote setup' for the yavsc mirror setup), so
move the .deb release workflow there.

- Remove .github/workflows/build-and-release-deb.yml.
- Add .forgejo/workflows/release.yml, mirroring the structure of
  yavsc/.forgejo/workflows/release.yml (single job, no Node, bash
  + jq + curl). amd64 and arm64 .deb built sequentially (matrix
  is impossible because actions/upload-artifact needs Node, which
  the runner image pazof/yavsc-build-env doesn't ship — same
  constraint as documented in MEMORY.md).
- Reuse existing release on tag collision (PATCH instead of POST)
  to keep the /releases/tag/<tag> permalink stable — re-tag = le
  mal, but a re-build of the same tag should not duplicate releases.
2026-08-17 15:28:39 +01:00
0c45511bd9
comment 2026-08-17 15:23:50 +01:00
2 changed files with 330 additions and 283 deletions

View file

@ -0,0 +1,330 @@
# Build and publish a postit-debian release on the Forgejo instance.
#
# Triggered by a push of a git tag. Validates the tag/changelog pair,
# builds the .deb for amd64 and arm64 (sequential cross-RID .NET
# publishes on a single amd64 runner container — matrix is not used
# here because the runner image pazof/yavsc-build-env has no Node,
# so actions/upload-artifact and actions/download-artifact (which
# require Node) cannot be used to pass the .deb files between jobs.
# All in one job, like yavsc's .forgejo/workflows/release.yml.),
# then publishes a Forgejo release via the REST API and uploads both
# .deb files as assets.
#
# Authentication: the runner auto-provides a token scoped to the
# repository. We read it once into the local env var FORGEJO_TOKEN
# and never reference the runtime-level name again.
#
# 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, actions/upload-artifact,
# actions/download-artifact, etc. fails with "executable file not
# found in $PATH". Same constraint as yavsc's
# .forgejo/workflows/release.yml.
#
# Re-tag policy (cf. AGENTS.md "Re-tag = le mal") : on push de tag
# ou dispatch, on *réutilise* la release existante (via PATCH) au
# lieu d'en créer une nouvelle. Un tag Git pointe vers un commit
# fixe ; si le binaire change (rebuild après modif du packaging),
# on met à jour la release existante plutôt que d'en multiplier
# pour un même tag. Le permalien /releases/tag/<tag> reste stable.
#
# Inter-step state: we persist values between steps via a plain
# env file under /tmp, sourced at the top of each step that needs
# it. This keeps the workflow self-contained and avoids any
# runtime variable names we did not choose.
name: Forgejo Release postit-deb
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: 'Tag pazof/yavsc à packager (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:
# Job unique : validation tag/CHANGELOG + build amd64 + build
# arm64 + publication via l'API REST Forgejo (pas d'actions
# tierces Node).
release:
runs-on: docker
container:
image: docker.io/pazof/yavsc-build-env:debian12-dotnet10-android36-v2
env:
STATE_FILE: /tmp/release-state.env
steps:
- name: Installer les pré-requis de build (debhelper + icônes)
# L'image runner fournit déjà dotnet-sdk-10.0, git, jq,
# curl. On ajoute les outils spécifiques au packaging
# Debian (debhelper, imagemagick pour les icônes .png
# via `convert`, librsvg2-bin pour le SVG).
run: |
apt-get update
apt-get install -y --no-install-recommends \
build-essential debhelper imagemagick librsvg2-bin \
ca-certificates
rm -rf /var/lib/apt/lists/*
: > "$STATE_FILE"
- name: Clone du repo au tag demandé
env:
TAG: ${{ forgejo.event_name == 'push' && forgejo.ref_name || inputs.tag }}
run: |
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input."
exit 1
fi
cd /src
if [[ ! -d _src/.git ]]; then
# Clone unshallow pour préserver l'historique — utile
# si un futur test en a besoin. Le coût est marginal
# pour ce repo (< 50 commits).
git clone https://forgejo.pschneider.fr/notazof/postit-debian.git _src
fi
cd _src
git fetch --tags --force --prune origin
git checkout "$TAG"
echo "Checked out at $(git rev-parse HEAD) on tag $TAG"
echo "TAG=$TAG" >> "$STATE_FILE"
- name: Valider le tag et la section CHANGELOG
run: |
source "$STATE_FILE"
cd /src/_src
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."
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.
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:-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
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 garde le titre
# (ligne `## [TAG] - channel`) pour la vérification du
# canal, puis on l'exclut du body envoyé à la release.
BODY=$(awk -v tag="[$TAG]" '
/^## \[/ {
if (in_section) exit
if (index($0, tag) > 0) {
in_section=1
print
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 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
RELEASE_BODY=$(echo "$BODY" | tail -n +2)
IS_PRERELEASE=$([ "$CHANNEL" = "stable" ] && echo false || echo true)
echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL"
# Persist values for the next steps via our local state file.
{
echo "RELEASE_BODY<<EOF"
echo "$RELEASE_BODY"
echo "EOF"
echo "IS_PRERELEASE=$IS_PRERELEASE"
} >> "$STATE_FILE"
- name: Build .deb amd64
env:
POSTIT_RUNTIME: linux-x64
run: |
source "$STATE_FILE"
cd /src/_src
echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG"
make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64
- name: Build .deb arm64
env:
POSTIT_RUNTIME: linux-arm64
run: |
source "$STATE_FILE"
cd /src/_src
echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG"
# Cross-RID .NET depuis un hôte amd64 : standard, pas
# besoin de runner arm64 natif.
make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64
- name: Localiser les .deb produits
run: |
source "$STATE_FILE"
cd /src
DEB_AMD64=$(find . -maxdepth 3 -name "postit_*${TAG}-1_amd64.deb" \
-not -path "./_src/debian/*" -printf '%p\n' | head -1)
DEB_ARM64=$(find . -maxdepth 3 -name "postit_*${TAG}-1_arm64.deb" \
-not -path "./_src/debian/*" -printf '%p\n' | head -1)
if [[ -z "$DEB_AMD64" || -z "$DEB_ARM64" ]]; then
echo "::error::Missing .deb files. amd64='$DEB_AMD64' arm64='$DEB_ARM64'"
ls -la /src/ 2>/dev/null || true
exit 1
fi
echo "DEB_AMD64=/src/$DEB_AMD64" >> "$STATE_FILE"
echo "DEB_ARM64=/src/$DEB_ARM64" >> "$STATE_FILE"
echo "✓ Found both .deb files"
- name: Publier la release Forgejo via l'API REST
env:
FORGEJO_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
FORGEJO_API_URL: ${{ forgejo.api_url }}
FORGEJO_REPOSITORY: ${{ forgejo.repository }}
run: |
source "$STATE_FILE"
if [[ -z "$TAG" ]]; then
echo "::error::No tag resolved for the API call."
exit 1
fi
# Le runner Forgejo expose l'API sur forgejo.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="${FORGEJO_API_URL%/}"
API_BASE="${API_BASE%/api/v1}"
# 1. Vérifier si la release existe déjà pour ce tag.
# Politique : on réutilise (PATCH) plutôt que d'en
# créer une nouvelle — cf. note "Re-tag policy" en
# tête de fichier.
echo "::group::Check existing release for tag $TAG"
HTTP=$(curl -sS -o /tmp/existing.json -w '%{http_code}' \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/tags/$TAG")
echo "GET releases/tags/$TAG -> HTTP $HTTP"
EXISTING_ID=""
if [[ "$HTTP" == "200" ]]; then
EXISTING_ID=$(jq -r '.id // empty' /tmp/existing.json)
echo "Existing release id: ${EXISTING_ID:-none}"
fi
echo "::endgroup::"
# 2. Créer ou mettre à jour la release.
if [[ -n "$EXISTING_ID" ]]; then
echo "::group::Update release id=$EXISTING_ID"
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 $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data-binary @/tmp/patch.json \
"$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/$EXISTING_ID")
echo "PATCH release -> HTTP $HTTP"
echo "::endgroup::"
else
echo "::group::Create release"
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 $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data-binary @/tmp/post.json \
"$API_BASE/api/v1/repos/$FORGEJO_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=$(jq -r '.id' /tmp/release.json)
echo "Release id=$RELEASE_ID"
# 3. Upload les .deb en assets. Le nom du fichier passe
# en query string (?name=...), pas en argument
# positionnel entre --data-binary et l'URL.
for entry in "amd64:$DEB_AMD64" "arm64:$DEB_ARM64"; do
arch="${entry%%:*}"
deb="${entry#*:}"
echo "::group::Upload asset for arch=$arch: $deb"
HTTP=$(curl -sS -o /tmp/asset.json -w '%{http_code}' \
-X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "Accept: application/json" \
--data-binary "@$deb" \
"$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/$RELEASE_ID/assets?name=$(basename "$deb")")
echo "POST asset ($arch) -> HTTP $HTTP"
echo "::endgroup::"
if [[ "$HTTP" != "201" ]]; then
echo "::error::Asset upload failed for $arch (HTTP $HTTP):"
cat /tmp/asset.json
exit 1
fi
done
echo "Release publiée : $API_BASE/$FORGEJO_REPOSITORY/releases/tag/$TAG"

View file

@ -1,283 +0,0 @@
name: Build and Release postit-deb
on:
push:
branches:
- main
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: 'Tag de pazof/yavsc à packager (ex. 1.0.6, 1.0.7-rc1). Requis pour un build ad-hoc.'
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
force_republish:
description: 'Re-publier une release dont le tag existe déjà. Par défaut refusé (re-tag = le mal).'
required: false
type: boolean
default: false
# softprops/action-gh-release a besoin de contents: write
# pour publier une release + uploader un asset.
permissions:
contents: write
jobs:
# Build matrix : un .deb par architecture. Le tag Git poussé sur
# ce dépôt devient POSTIT_GIT_TAG pour `make deb`, qui clone
# l'amont pazof/yavsc à ce tag et produit le .deb correspondant.
# Sur amd64, la cross-compilation linux-arm64 marche nativement
# (dotnet publish --runtime linux-arm64 depuis un hôte amd64).
# On évite donc les runners arm64 natifs (qui existent mais sont
# récents et plus chers en minutes).
deb-build:
name: Build .deb (${{ matrix.runtime }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- runtime: linux-x64
arch: amd64
artifact_name: postit-amd64
- runtime: linux-arm64
arch: arm64
artifact_name: postit-arm64
steps:
- name: Checkout postit-debian
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Installer les pré-requis de build (debhelper + icônes)
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential debhelper imagemagick librsvg2-bin \
git ca-certificates
- name: Installer .NET SDK 10
uses: microsoft/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Déterminer POSTIT_GIT_TAG
id: tag
run: |
# Sur un push de branche (pas un tag), github.ref_name est
# 'main' — `make deb POSTIT_GIT_TAG=main` clone pazof/yavsc
# sur la branche main et produit un .deb à jour. Sur un push
# de tag, c'est le numéro de tag (ex. '1.0.6'). Sur
# workflow_dispatch, on lit l'input `tag`.
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ inputs.tag }}"
else
TAG="${{ github.ref_name }}"
fi
if [[ -z "$TAG" ]]; then
echo "::error::POSTIT_GIT_TAG is empty. Pour workflow_dispatch, l'input 'tag' est obligatoire."
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "→ POSTIT_GIT_TAG=$TAG"
- name: Build du .deb via make deb
env:
POSTIT_GIT_TAG: ${{ steps.tag.outputs.tag }}
POSTIT_RUNTIME: ${{ matrix.runtime }}
run: |
echo "→ Building for POSTIT_GIT_TAG=$POSTIT_GIT_TAG POSTIT_RUNTIME=$POSTIT_RUNTIME"
make deb POSTIT_GIT_TAG="$POSTIT_GIT_TAG" POSTIT_RUNTIME="$POSTIT_RUNTIME"
- name: Localiser le .deb produit
id: locate
run: |
# Le Makefile mv les .deb vers $POSTIT_OUT_DIR (par défaut
# le répertoire parent du repo). Sur GitHub Actions, c'est
# le workspace parent : /home/runner/work/.. Le .deb est
# nommé d'après le tag brut (avec ou sans 'v', tel quel
# poussé sur le remote), on cherche donc avec ref_name.
DEB=$(find /home/runner -maxdepth 4 -name "postit_*${{ github.ref_name }}-1_${{ matrix.arch }}.deb" \
-not -path "*/debian/*" \
-printf '%p\n' | head -1)
if [[ -z "$DEB" ]]; then
echo "::error::No .deb matching postit_*${{ github.ref_name }}-1_${{ matrix.arch }}.deb found."
echo "Files in parent dir:"
ls -la /home/runner/work/ 2>/dev/null || true
exit 1
fi
echo "deb_path=$DEB" >> "$GITHUB_OUTPUT"
echo "✓ Found $DEB"
- name: Téléverser le .deb en tant qu'Artéfact GitHub
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
path: ${{ steps.locate.outputs.deb_path }}
retention-days: 7
# Validation : parse le tag, applique la parité patch (pair=stable /
# impair=preview / suffixe=instable), vérifie que CHANGELOG.md
# contient une section cohérente, et — point non négociable —
# refuse de re-publier un tag qui existe déjà (re-tag = le mal).
validate-release:
# Tourne sur push de tag (release officielle) ou sur workflow_dispatch
# avec un tag explicite (release ad-hoc). Sur push de branche, on
# ne publie pas — les jobs de build suffisent (artefacts seulement).
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout postit-debian
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Déterminer le tag à publier
id: pick_tag
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ inputs.tag }}"
else
TAG="${{ github.ref_name }}"
fi
if [[ -z "$TAG" ]]; then
echo "::error::Tag is empty. Sur workflow_dispatch, l'input 'tag' est obligatoire."
exit 1
fi
# Strip leading 'v' (git tag convention).
if [[ "$TAG" =~ ^v(.*)$ ]]; then
TAG="${BASH_REMATCH[1]}"
echo "Stripped leading 'v' — using TAG=$TAG for validation."
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Valider le tag, le CHANGELOG et l'unicité du tag
env:
FORCE_UNSTABLE: ${{ inputs.force_unstable || github.event.inputs.force_unstable || 'false' }}
FORCE_REPUBLISH: ${{ inputs.force_republish || github.event.inputs.force_republish || 'false' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.pick_tag.outputs.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."
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.
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]. awk en mode paragraphe.
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é.
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 header: $HEADER"
exit 1
fi
echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL"
# Anti-re-tag : refuse de publier si une release existe déjà
# pour ce tag. softprops/action-gh-release créerait sinon une
# nouvelle release par-dessus (re-tag = le mal). Opt-in via
# workflow_dispatch + force_republish=true uniquement.
if gh release view "$TAG" >/dev/null 2>&1; then
if [[ "$FORCE_REPUBLISH" != "true" ]]; then
echo "::error::Release for tag '$TAG' already exists. Refusing to re-tag."
echo "Set force_republish=true via workflow_dispatch to override."
exit 1
else
echo "::warning::Release '$TAG' already exists — force_republish=true, proceeding."
fi
else
echo "✓ No existing release for tag '$TAG'."
fi
# Exposition aux étapes suivantes via $GITHUB_ENV.
{
echo "RELEASE_BODY<<EOF"
echo "$BODY"
echo "EOF"
echo "RELEASE_TAG=$TAG"
echo "RELEASE_CHANNEL=$CHANNEL"
if [[ "$CHANNEL" == "stable" ]]; then
echo "IS_PRERELEASE=false"
else
echo "IS_PRERELEASE=true"
fi
} >> "$GITHUB_ENV"
publish-release:
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
needs: [deb-build, validate-release]
runs-on: ubuntu-latest
steps:
- name: Récupérer les .deb depuis les artefacts
uses: actions/download-artifact@v7
with:
path: ./
merge-multiple: true
- name: Lister les .deb téléchargés
run: ls -la ./
- name: Publier la release GitHub et uploader les .deb
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
files: |
./postit-amd64/*.deb
./postit-arm64/*.deb
body: ${{ env.RELEASE_BODY }}
prerelease: ${{ env.IS_PRERELEASE }}