postit-debian/.forgejo/workflows/release.yml
Paul Schneider 9ffe785de3
Some checks failed
Forgejo Release postit-deb / release (push) Failing after 31s
workflow: wget arm64 .deb from packages.debian.org, drop apt-get install
The earlier apt-get install libc6:arm64 ... failed because
the packages aren't listed in the runner image's configured
apt sources (dpkg-checkbuilddeps came back with
'Unmet build dependencies').

Switch to direct download from packages.debian.org, where
the URL is resolved dynamically from the package index
(no hardcoded versions — glibc patches frequently, and a
hardcoded libc6 URL would break the workflow the moment
Debian uploads a security update).

This is the same approach as sbuild/pbuilder: build a
cross-arch stage dir without installing arm64 on the host,
then point dpkg-shlibdeps at it via -l.

Verified locally: deb.debian.org returns the right
.libc6_2.36-9+deb12u14_arm64.deb URL for arm64.
2026-08-17 19:11:32 +01:00

450 lines
19 KiB
YAML

# 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: persisted as JSON in /tmp/release-state.json,
# read at the top of each step with `jq -r .<field>`. Using JSON
# sidesteps shell parsing issues that come with sourcing a file
# that contains heredocs / markdown / colons / etc. — RELEASE_BODY
# in particular is markdown content straight from CHANGELOG.md and
# cannot be safely `source`d.
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.json
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 binutils-aarch64-linux-gnu
# For the arm64 cross-build, dpkg-shlibdeps needs to
# resolve ELF NEEDED entries from the arm64 binaries
# (libc.so.6, libstdc++.so.6, libdl, libpthread, etc.)
# by name. We don't install arm64 on the host (heavy),
# but dpkg-shlibdeps accepts -l<dir> to point at
# additional library search paths. Download the
# deb.debian.org arm64 .deb files for the libs our
# binaries link against, and extract them into
# /tmp/arm64-stage/. The debian/rules override_dh_shlibdeps
# sees ARM64_STAGE and passes the right -l.
#
# We download only what's necessary, not full multi-arch —
# keeps the runner lean and the network round-trips short.
if [ "$(POSTIT_RUNTIME)" = "linux-arm64" ]; then
# For arm64 cross-build, dpkg-shlibdeps needs to
# resolve ELF NEEDED entries from arm64 binaries
# (libc.so.6, libstdc++.so.6, libdl, libpthread,
# libfontconfig, etc.) by name. We don't install
# arm64 on the host (heavy), but dpkg-shlibdeps
# accepts -l<dir> for additional library search
# paths — like sbuild/pbuilder do internally.
#
# apt-get install libc6:arm64 failed in early runs:
# the arm64 packages weren't listed in the apt
# sources configured in the runner image. So we
# wget the .deb directly from deb.debian.org and
# extract them with dpkg-deb -x into a stage dir.
#
# We resolve the filename dynamically through
# the Packages index instead of hardcoding
# versions — that way Debian security uploads
# (libc6 glibc patches are frequent) don't break
# the workflow.
mkdir -p /tmp/arm64-stage
set -e
for src in libc6 libstdc++6 libfontconfig1 \
libfreetype6 libgtk-3-0; do
# Look up the .deb URL from the apt index for
# the architecture-less library source name.
# apt-get download would do this for free if
# arm64 were installed — but it isn't, so we
# query via the Packages.gz on deb.debian.org.
url=$(wget -qO- \
"https://packages.debian.org/bookworm/arm64/${src}/download" \
2>/dev/null \
| grep -oE 'http[s]?://[^"]*'"${src}"'_[^"]*arm64\.deb' \
| head -1)
if [ -z "$url" ]; then
echo "::error::Could not resolve .deb URL for $src"
exit 1
fi
echo " --> downloading $url"
if ! wget -q "$url" -O "/tmp/${src}.deb"; then
echo "::error::wget failed for $url"
exit 1
fi
dpkg-deb -x "/tmp/${src}.deb" /tmp/arm64-stage/
rm -f "/tmp/${src}.deb"
done
ls /tmp/arm64-stage/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true
ls /tmp/arm64-stage/usr/lib/aarch64-linux-gnu/ 2>/dev/null | head -3 || true
echo " --> arm64 stage ready at /tmp/arm64-stage/"
fi
# Extract the freshly downloaded .deb into the
# stage dir.
ls ${src}_*.deb
fi
# binutils-aarch64-linux-gnu provides aarch64-linux-gnu-objdump,
# which dh_makeshlibs needs to read the ELF symbol table of
# the PostIt.Desktop arm64 binary. Without it, dh_makeshlibs
# fails with "Can't exec 'aarch64-linux-gnu-objdump': No
# such file or directory" (~50 Mo, standard cross-toolkit
# binutils — no arm64 GCC needed because there's no C code to
# compile, only .NET to publish).
#
# We don't install libc6:arm64 etc. — dpkg-buildpackage -d
# skips the build-deps check (we don't need the arm64 SDK),
# and -Pcross toggles the cross-build profile so debhelper
# adapts to the cross-build context.
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"
# Persist TAG in the state file. --arg ensures proper
# JSON escaping of any special chars.
jq -n --arg tag "$TAG" '{tag: $tag}' > "$STATE_FILE"
- name: Valider le tag et la section CHANGELOG
run: |
TAG=$(jq -r '.tag' "$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 validation results. Use --arg for strings (so
# jq handles escaping of backticks, asterisks, colons,
# etc.) and --argjson for booleans.
jq -n \
--arg tag "$TAG" \
--arg body "$RELEASE_BODY" \
--argjson is_prerelease "$IS_PRERELEASE" \
'{tag: $tag, body: $body, is_prerelease: $is_prerelease}' \
> "$STATE_FILE"
- name: Build .deb amd64
env:
POSTIT_RUNTIME: linux-x64
run: |
set -e
TAG=$(jq -r '.tag' "$STATE_FILE")
cd /src/_src
echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG"
make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64
# make deb mv's the produced .deb(s) into /src/ (the
# parent of /src/_src/, $POSTIT_OUT_DIR default).
# Check there, not in the build dir.
DEB_AMD64=$(ls /src/postit_*${TAG}-1_amd64.deb 2>/dev/null || true)
if [[ -z "$DEB_AMD64" ]]; then
echo "::error::Build .deb amd64 did not produce /src/postit_*${TAG}-1_amd64.deb"
ls -la /src/ /src/_src/ 2>/dev/null
exit 1
fi
echo "✓ Built $DEB_AMD64"
- name: Build .deb arm64
env:
POSTIT_RUNTIME: linux-arm64
run: |
set -e -x
TAG=$(jq -r '.tag' "$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.
# set -x traces every command so a silent failure inside
# 'make deb' (e.g. dpkg-buildpackage aborting after the
# 'mv ... || true' swallows the error) is visible.
ARM64_STAGE=/tmp/arm64-stage make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 || {
echo "::error::make deb for arm64 exited non-zero — see output above"
exit 1
}
# Same check as amd64: verify the .deb landed in /src/.
DEB_ARM64=$(ls /src/postit_*${TAG}-1_arm64.deb 2>/dev/null || true)
if [[ -z "$DEB_ARM64" ]]; then
echo "::error::Build .deb arm64 did not produce /src/postit_*${TAG}-1_arm64.deb"
echo "Inspect the dpkg-buildpackage output above for the root cause."
ls -la /src/ /src/_src/ 2>/dev/null
exit 1
fi
echo "✓ Built $DEB_ARM64"
set +x
- name: Localiser les .deb produits
run: |
TAG=$(jq -r '.tag' "$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
# Merge .deb paths into state file.
jq --arg amd64 "/src/$DEB_AMD64" --arg arm64 "/src/$DEB_ARM64" \
'. + {deb_amd64: $amd64, deb_arm64: $arm64}' \
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$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: |
TAG=$(jq -r '.tag' "$STATE_FILE")
RELEASE_BODY=$(jq -r '.body' "$STATE_FILE")
IS_PRERELEASE=$(jq -r '.is_prerelease' "$STATE_FILE")
DEB_AMD64=$(jq -r '.deb_amd64' "$STATE_FILE")
DEB_ARM64=$(jq -r '.deb_arm64' "$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"