# 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/ 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<> "$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"