mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 11:25:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
149944726e | ||
|
|
bf0b757862 | ||
|
|
1c7de782f0 | ||
|
|
a820b056b2 | ||
|
|
6dce9dda79 | ||
|
|
47c81cd478 | ||
|
|
bc426ba7c2 | ||
|
|
a0f445c723 | ||
|
|
ce69db8c5f | ||
|
|
5b31695343 | ||
|
|
4a8fbd954e | ||
|
|
04fb6e3381 | ||
|
|
5957fa0ad8 | ||
|
|
97e57e531b | ||
|
|
fa296d0c80 |
No files matched your search
@@ -92,7 +92,12 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag='${{ inputs.tag }}'
|
||||
[ -n "$tag" ] || tag="$(gh release list --limit 1 --json tagName --jq '.[0].tagName')"
|
||||
# --repo on every gh call, whether or not the job happens to have
|
||||
# checked out: a call that relies on an inferred remote breaks the day
|
||||
# somebody removes the checkout, and the error it gives - "fatal: not
|
||||
# a git repository" - names neither gh nor the workflow.
|
||||
[ -n "$tag" ] || tag="$(gh release list --repo "${{ github.repository }}" \
|
||||
--limit 1 --json tagName --jq '.[0].tagName')"
|
||||
if [ -z "$tag" ]; then
|
||||
echo "::error::No release to wrap, and no tag was given."
|
||||
exit 1
|
||||
@@ -107,6 +112,9 @@ jobs:
|
||||
name: AppImage ${{ matrix.appimage_arch }}
|
||||
needs: release
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
# A 32-bit job that cannot run on the runner it was given must not fail the
|
||||
# release for the architectures that built fine.
|
||||
continue-on-error: ${{ matrix.tolerate_failure || false }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -118,6 +126,28 @@ jobs:
|
||||
- appimage_arch: aarch64
|
||||
bundle_arch: arm64
|
||||
runs_on: ubuntu-24.04-arm
|
||||
# The other two architectures an AppImage runtime EXISTS for. There
|
||||
# are four in total - x86_64, aarch64, i686, armhf - and that is the
|
||||
# whole limit: the runtime is the ELF the kernel executes, so an
|
||||
# architecture with no runtime cannot have an AppImage at all. That is
|
||||
# why ppc64le, s390x, riscv64 and loong64 are not here and never will
|
||||
# be; they are served by the bundle zip and the .deb.
|
||||
#
|
||||
# i686 builds on the x86_64 runner, which runs 32-bit x86 natively -
|
||||
# no emulation, no multiarch, and appimagetool-i686 runs as-is.
|
||||
- appimage_arch: i686
|
||||
bundle_arch: i386
|
||||
runs_on: ubuntu-24.04
|
||||
# armhf builds on the aarch64 runner, which runs 32-bit ARM only if
|
||||
# the kernel was built with CONFIG_COMPAT and the 32-bit loader is
|
||||
# installed. GitHub does not promise either, so this one is allowed to
|
||||
# fail without taking the release with it - the same treatment TSC
|
||||
# gives its emulated armhf job. If it turns out to work reliably,
|
||||
# drop the flag; if it never does, drop the entry.
|
||||
- appimage_arch: armhf
|
||||
bundle_arch: armhf
|
||||
runs_on: ubuntu-24.04-arm
|
||||
tolerate_failure: true
|
||||
# WHICH entries this run wants, decided here because it cannot be decided
|
||||
# where it belongs. A job-level `if:` cannot see `matrix` - GitHub exposes
|
||||
# that context to runs-on, env, name, steps and a few others, but not to a
|
||||
@@ -163,7 +193,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zip="wekan-${{ needs.release.outputs.version }}-${{ matrix.bundle_arch }}.zip"
|
||||
gh release download '${{ needs.release.outputs.tag }}' --pattern "$zip" --clobber
|
||||
gh release download '${{ needs.release.outputs.tag }}' \
|
||||
--repo "${{ github.repository }}" --pattern "$zip" --clobber
|
||||
unzip -q "$zip"
|
||||
# A zip that exists is not a bundle that runs; these are the two files
|
||||
# the AppImage is built around.
|
||||
@@ -281,6 +312,22 @@ jobs:
|
||||
# for WeKan to answer on its port, then stop it - so a broken bundle
|
||||
# is caught here rather than by whoever downloads it.
|
||||
export WRITABLE_PATH="$PWD/smoke-data" PORT=8080
|
||||
|
||||
# A CHECK THAT CANNOT RUN IS NOT A FAILED CHECK. The 32-bit builds are
|
||||
# packed on a 64-bit runner, which executes them only if the kernel and
|
||||
# loader for that word size are present - GitHub promises neither. If
|
||||
# the binary cannot be executed AT ALL, the AppImage is as good as
|
||||
# anything here knows and the release must not be withheld for it; say
|
||||
# so and move on. Anything else - it starts and does not answer - is
|
||||
# the bug this step exists for and still fails.
|
||||
if ! APPIMAGE_EXTRACT_AND_RUN=1 "$app" --appimage-help >/dev/null 2>&1 \
|
||||
&& ! APPIMAGE_EXTRACT_AND_RUN=1 "$app" --help >/dev/null 2>&1; then
|
||||
if ! file "$app" | grep -q "$(uname -m)"; then
|
||||
echo "::warning::this runner cannot execute a ${{ matrix.appimage_arch }} binary, so the AppImage was packed but not started. It is uploaded unchecked."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 "$app" > smoke.log 2>&1 &
|
||||
pid=$!
|
||||
ok=false
|
||||
@@ -324,7 +371,11 @@ jobs:
|
||||
set -euo pipefail
|
||||
ls -lh assets 2>/dev/null || true
|
||||
if ls assets/*.AppImage >/dev/null 2>&1; then
|
||||
for want in x86_64 aarch64; do
|
||||
# Every architecture the matrix builds. A missing one WARNS rather
|
||||
# than fails: the 32-bit pair is allowed not to build, and an
|
||||
# architecture whose base bundle was never published has nothing to
|
||||
# wrap. What must not happen silently is one going missing unnoticed.
|
||||
for want in x86_64 aarch64 i686 armhf; do
|
||||
ls assets/*"-$want.AppImage" >/dev/null 2>&1 || \
|
||||
echo "::warning::no AppImage for $want - see that build job's log"
|
||||
done
|
||||
@@ -334,7 +385,7 @@ jobs:
|
||||
# build jobs skip (not fail) when the base bundle is not published yet.
|
||||
published="$(gh release view '${{ needs.release.outputs.tag }}' --repo "${{ github.repository }}" --json assets --jq '.assets[].name' 2>/dev/null || true)"
|
||||
base_present=false
|
||||
for arch in amd64 arm64; do
|
||||
for arch in amd64 arm64 i386 armhf; do
|
||||
printf '%s\n' "$published" | grep -qx "wekan-${{ needs.release.outputs.version }}-${arch}.zip" && base_present=true
|
||||
done
|
||||
if [ "$base_present" = false ]; then
|
||||
@@ -354,5 +405,13 @@ jobs:
|
||||
echo "Nothing to upload (no base bundle was published to wrap)."
|
||||
exit 0
|
||||
fi
|
||||
gh release upload '${{ needs.release.outputs.tag }}' assets/* --clobber
|
||||
echo "Uploaded to $(gh release view '${{ needs.release.outputs.tag }}' --json url --jq .url)"
|
||||
# --repo, because this job does NOT check the repository out - it only
|
||||
# downloads artifacts - so `gh` has no git remote to infer the repo
|
||||
# from and fails with "fatal: not a git repository". Every other gh
|
||||
# call in this workflow already passes it; these two did not, so the
|
||||
# AppImages were built, smoke-tested and uploaded as artifacts, and
|
||||
# then nothing was attached to the release.
|
||||
gh release upload '${{ needs.release.outputs.tag }}' assets/* \
|
||||
--repo "${{ github.repository }}" --clobber
|
||||
echo "Uploaded to $(gh release view '${{ needs.release.outputs.tag }}' \
|
||||
--repo "${{ github.repository }}" --json url --jq .url)"
|
||||
@@ -24,6 +24,15 @@ name: Flatpak
|
||||
# and WeKan publishes Linux bundles for amd64 and arm64, so those are the two.
|
||||
# Each builds on a runner of its own CPU rather than under emulation.
|
||||
#
|
||||
# AND THAT IS THE WHOLE LIST - there is no third to add. A flatpak runs against
|
||||
# a RUNTIME, not against the host's libraries, so an architecture exists here
|
||||
# only if freedesktop.org publishes org.freedesktop.Platform for it. It does so
|
||||
# for x86_64 and aarch64; the i386 and arm runtimes were discontinued. That is
|
||||
# the difference from AppImage.yml, which was able to grow from two
|
||||
# architectures to four: an AppImage carries its own runtime binary, and
|
||||
# runtimes exist for i686 and armhf as well. WeKan's ppc64le, s390x, riscv64,
|
||||
# loong64 and 32-bit bundles are served by the zip and the .deb.
|
||||
#
|
||||
# THE SANDBOX. WeKan is a server you reach in a browser, so it needs the network
|
||||
# and a place to keep its data, and nothing else - no X11, no audio, no access
|
||||
# to your files. --share=network and --persist=.wekan are the whole permission
|
||||
@@ -96,7 +105,11 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag='${{ inputs.tag }}'
|
||||
[ -n "$tag" ] || tag="$(gh release list --limit 1 --json tagName --jq '.[0].tagName')"
|
||||
# --repo on every gh call, whether or not the job happens to have
|
||||
# checked out: one that relies on an inferred remote breaks the day
|
||||
# somebody removes the checkout.
|
||||
[ -n "$tag" ] || tag="$(gh release list --repo "${{ github.repository }}" \
|
||||
--limit 1 --json tagName --jq '.[0].tagName')"
|
||||
if [ -z "$tag" ]; then
|
||||
echo "::error::No release to wrap, and no tag was given."
|
||||
exit 1
|
||||
@@ -154,7 +167,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zip="wekan-${{ needs.release.outputs.version }}-${{ matrix.bundle_arch }}.zip"
|
||||
gh release download '${{ needs.release.outputs.tag }}' --pattern "$zip" --clobber
|
||||
gh release download '${{ needs.release.outputs.tag }}' \
|
||||
--repo "${{ github.repository }}" --pattern "$zip" --clobber
|
||||
unzip -q "$zip"
|
||||
for f in bundle/main.js bundle/node bundle/ferretdb bundle/start-wekan.sh; do
|
||||
if [ ! -e "$f" ]; then
|
||||
@@ -318,8 +332,21 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release upload '${{ needs.release.outputs.tag }}' assets/* --clobber
|
||||
echo "Uploaded to $(gh release view '${{ needs.release.outputs.tag }}' --json url --jq .url)"
|
||||
# --repo, because this job does NOT check the repository out - it
|
||||
# only downloads artifacts - so `gh` has no git remote to infer the
|
||||
# repo from and fails with "fatal: not a git repository". The same
|
||||
# line was wrong in AppImage.yml for the same reason.
|
||||
#
|
||||
# And the bundles BY NAME, not assets/*: the artifacts also carry the
|
||||
# ostree repository the flatpak was exported through - config,
|
||||
# objects/, refs/, summaries/, summary, summary.idx - which is build
|
||||
# scaffolding, not something to attach to a release. TSC hit this
|
||||
# exact thing ("upload only the bundle files, not the ostree repo").
|
||||
gh release upload '${{ needs.release.outputs.tag }}' \
|
||||
assets/*.flatpak assets/*.flatpak.md5sum assets/*.flatpak.sha256sum \
|
||||
--repo "${{ github.repository }}" --clobber
|
||||
echo "Uploaded to $(gh release view '${{ needs.release.outputs.tag }}' \
|
||||
--repo "${{ github.repository }}" --json url --jq .url)"
|
||||
|
||||
# ---- the repository -------------------------------------------------
|
||||
- uses: actions/download-artifact@v8
|
||||
|
||||
@@ -87,3 +87,7 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Full build provenance (SLSA), not the minimal default that BuildKit
|
||||
# attaches on its own. See release-all.yml for what it is and why the
|
||||
# manifest list gains an `unknown/unknown` entry per platform.
|
||||
provenance: mode=max
|
||||
@@ -622,13 +622,23 @@ jobs:
|
||||
sha256sum "wekan-${VERSION}-amd64.zip" > "wekan-${VERSION}-amd64.zip.sha256sum"
|
||||
cat "wekan-${VERSION}-amd64.zip.sha256sum"
|
||||
# Where this bundle's binaries came from, for the release notes.
|
||||
mkdir -p provenance
|
||||
PROVENANCE_FILE=provenance/amd64.tsv bash releases/record-provenance.sh \
|
||||
#
|
||||
# $GITHUB_WORKSPACE, not a relative path: this step ran `cd .build`
|
||||
# above, so `mkdir -p provenance` made .build/provenance while the
|
||||
# upload-artifact below looks at provenance/ from the workspace root.
|
||||
# It found nothing - "No files were found with the provided path:
|
||||
# provenance/" - and amd64, the platform most people download, was the
|
||||
# one bundle missing from the v10.77 release-notes table. Every other
|
||||
# build job records from the workspace root, which is why only this one
|
||||
# was affected.
|
||||
mkdir -p "$GITHUB_WORKSPACE/provenance"
|
||||
PROVENANCE_FILE="$GITHUB_WORKSPACE/provenance/amd64.tsv" bash releases/record-provenance.sh \
|
||||
'amd64' 'Node.js' "${NODE_FROM:-}" "${NODE_FULL:-$NODE_VERSION}" \
|
||||
"${NODE_URL:-}" \
|
||||
"${NODE_SHA256:-}" || true
|
||||
PROVENANCE_FILE=provenance/amd64.tsv bash releases/record-provenance.sh \
|
||||
'amd64' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
FERRET_TAG="$(bash releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE="$GITHUB_WORKSPACE/provenance/amd64.tsv" bash releases/record-provenance.sh \
|
||||
'amd64' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-amd64' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
# A zip that EXISTS is not a zip that WORKS, and the log should say which of the
|
||||
@@ -832,8 +842,9 @@ jobs:
|
||||
'arm64' 'Node.js' "${NODE_FROM:-}" "${NODE_FULL:-$NODE_VERSION}" \
|
||||
"${NODE_URL:-}" \
|
||||
"${NODE_SHA256:-}" || true
|
||||
FERRET_TAG="$(bash releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE=provenance/arm64.tsv bash releases/record-provenance.sh \
|
||||
'arm64' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
'arm64' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-arm64' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
# A zip that EXISTS is not a zip that WORKS, and the log should say which of the
|
||||
@@ -1219,8 +1230,9 @@ jobs:
|
||||
"${node_url:-}" \
|
||||
"${node_sha256:-}" || true
|
||||
if [ -s bundle/ferretdb.exe ]; then
|
||||
FERRET_TAG="$(bash src/releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE=provenance/win64.tsv bash src/releases/record-provenance.sh \
|
||||
'win64' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
'win64' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-win64.exe' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
fi
|
||||
@@ -1561,8 +1573,9 @@ jobs:
|
||||
"${node_url:-}" \
|
||||
"${node_sha256:-}" || true
|
||||
if [ -s bundle/ferretdb.exe ]; then
|
||||
FERRET_TAG="$(bash src/releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE=provenance/win32.tsv bash src/releases/record-provenance.sh \
|
||||
'win32' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
'win32' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-win32.exe' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
fi
|
||||
@@ -1805,8 +1818,9 @@ jobs:
|
||||
"${node_url:-}" \
|
||||
"${node_sha256:-}" || true
|
||||
if [ -s bundle/ferretdb ]; then
|
||||
FERRET_TAG="$(bash releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE=provenance/mac-arm64.tsv bash releases/record-provenance.sh \
|
||||
'mac-arm64' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
'mac-arm64' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-mac-arm64' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
fi
|
||||
@@ -2057,8 +2071,9 @@ jobs:
|
||||
"${node_url:-}" \
|
||||
"${node_sha256:-}" || true
|
||||
if [ -s bundle/ferretdb ]; then
|
||||
FERRET_TAG="$(bash releases/ferretdb-latest-tag.sh)"
|
||||
PROVENANCE_FILE=provenance/mac-x64.tsv bash releases/record-provenance.sh \
|
||||
'mac-x64' 'FerretDB' 'wekan/FerretDB' 'latest' \
|
||||
'mac-x64' 'FerretDB' 'wekan/FerretDB' "${FERRET_TAG:-latest}" \
|
||||
'https://github.com/wekan/FerretDB/releases/latest/download/ferretdb-mac-amd64' \
|
||||
"${FERRET_SHA:-}" || true
|
||||
fi
|
||||
@@ -2421,7 +2436,7 @@ jobs:
|
||||
else
|
||||
echo "::warning::wekan/FerretDB publishes no checksum for ferretdb-${{ matrix.ferretdb_arch }} on this release, so it is unverified."
|
||||
fi
|
||||
FERRET_TAG="$(curl -fsSL https://api.github.com/repos/wekan/FerretDB/releases/latest | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tag_name",""))' || true)"
|
||||
FERRET_TAG="$(bash releases/ferretdb-latest-tag.sh)"
|
||||
bash releases/record-provenance.sh '${{ matrix.arch }}' 'FerretDB' \
|
||||
'wekan/FerretDB' "${FERRET_TAG:-latest}" "$FERRET_URL" "$FERRET_SHA"
|
||||
# Overwrite the inherited amd64 MongoDB Database Tools with this arch's ones.
|
||||
@@ -2966,7 +2981,21 @@ jobs:
|
||||
# manifest yet, so it ships as a .zip bundle only. Every candidate arch
|
||||
# is in debian:trixie's manifest AND has a fork Node the Dockerfile can
|
||||
# install.
|
||||
# --provenance=mode=max: full build provenance (SLSA), not the
|
||||
# minimal default - what built this image, from which commit, with
|
||||
# which base and build arguments. It rides in the manifest list as an
|
||||
# `unknown/unknown` entry per platform, the OCI marker for "not a
|
||||
# runnable image" so `docker pull` never selects it, which is why Quay
|
||||
# lists one beside each real platform. mode=max makes those entries
|
||||
# BIGGER, not fewer; `--provenance=false` is what removes them, at the
|
||||
# cost of the evidence.
|
||||
#
|
||||
# The comment is here and not among the arguments: a `#` after a line
|
||||
# continuation comments out the rest of the joined line, leaving
|
||||
# `docker buildx build` with no platforms and no tags. This block is a
|
||||
# shell script like any other, and it was written the wrong way first.
|
||||
docker buildx build \
|
||||
--provenance=mode=max \
|
||||
--platform "${PLATFORMS}" \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
@@ -3521,6 +3550,19 @@ jobs:
|
||||
fi
|
||||
done
|
||||
echo "::error::Remote build ${{ matrix.arch }} failed after $attempts attempts (no wekan_${VERSION}_${{ matrix.arch }}.snap produced). The Launchpad build log printed above says why; the builds are also at https://launchpad.net/~xet7/+snaps" >&2
|
||||
# A DETERMINISTIC failure, named before the guesses below. Three
|
||||
# identical attempts on ONE architecture while the others build on
|
||||
# attempt 1 is not the build farm having a bad day - and the guidance
|
||||
# below ("often an OOM, or a transient reset; re-run") sent v10.77's
|
||||
# armhf failure down exactly that path. snapcraft ends a missing
|
||||
# stage-package as a "Stopped" build with no artifact, which looks
|
||||
# identical to a cancelled one from the outside; the difference is one
|
||||
# line in the log, so say it here rather than leave it to be found.
|
||||
if grep -qs 'Stage package not found' snapcraft-wekan-*.txt; then
|
||||
missingpkg="$(grep -hs -o "Stage package not found in part '[^']*': [^ ]*" snapcraft-wekan-*.txt | sort -u | tr '\n' ' ')"
|
||||
echo "::error::This is NOT a transient failure and re-running will not fix it: ${missingpkg}. A stage-package that resolves on the 64-bit architectures can be missing on ${{ matrix.arch }} - Ubuntu 24.04's 64-bit time_t transition renamed libraries to a t64 suffix, and the renamed package carries 'Provides: <old name>' on amd64/arm64 but NOT on armhf, where the ABI really changed. Name the real package in snapcraft.yaml (libssl3 -> libssl3t64, libcurl4 -> libcurl4t64, libgoogle-perftools4 -> libgoogle-perftools4t64). Check a name against the archive: curl -fsSL http://ports.ubuntu.com/ubuntu-ports/dists/noble/main/binary-armhf/Packages.gz | gunzip -c | grep '^Package: <name>$'" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A "Stopped" build with no artifact is Launchpad ENDING the build
|
||||
# itself, not a compile error in it. Two causes, in the order to check:
|
||||
# 1. The auto-generated Launchpad PROJECT (xet7-craft-remote-build)
|
||||
@@ -3563,11 +3605,29 @@ jobs:
|
||||
run: |
|
||||
set -uo pipefail
|
||||
mkdir -p snap-logs
|
||||
cp -f snapcraft-wekan-*.txt snap-logs/ 2>/dev/null || true
|
||||
cp -f "$HOME"/.local/state/snapcraft/log/*.log snap-logs/ 2>/dev/null || true
|
||||
# RENAMED, not just copied. snapcraft names a remote-build log after the
|
||||
# recipe and an ISO timestamp:
|
||||
# snapcraft-wekan-f82a93c2…_s390x_2026-08-09T11:39:05.txt
|
||||
# and upload-artifact REFUSES a colon - "Invalid characters include:
|
||||
# Double quote, Colon, Less than, ..." - because NTFS cannot hold one.
|
||||
# That failed the whole job for s390x, ppc64el and riscv64 in v10.77
|
||||
# AFTER all three snaps had built and downloaded on attempt 1, so three
|
||||
# good builds were reported as FAILED by the step that saves their logs.
|
||||
# The timestamp is worth keeping, so the colons become dashes rather
|
||||
# than the name being thrown away.
|
||||
for f in snapcraft-wekan-*.txt "$HOME"/.local/state/snapcraft/log/*.log; do
|
||||
[ -f "$f" ] || continue
|
||||
base="$(basename "$f")"
|
||||
cp -f "$f" "snap-logs/${base//:/-}" || true
|
||||
done
|
||||
ls -la snap-logs || true
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
# A DIAGNOSTIC upload can never fail the build it is diagnosing. The snap
|
||||
# is the deliverable; these logs are here so a failure can be read
|
||||
# afterwards, and the renaming above is not the last thing that could
|
||||
# ever make one of them unuploadable.
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
name: snap-launchpad-logs-${{ matrix.arch }}
|
||||
@@ -4002,6 +4062,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
cd exapp
|
||||
docker buildx build --push \
|
||||
--provenance=mode=max \
|
||||
-t ghcr.io/wekan/wekan-exapp:v${VERSION} \
|
||||
-t ghcr.io/wekan/wekan-exapp:latest \
|
||||
--build-arg WEKAN_VERSION=${VERSION} .
|
||||
|
||||
+326
@@ -264,6 +264,332 @@ browser build to verify).
|
||||
|
||||
</details>
|
||||
|
||||
# v10.78 2026-08-09 WeKan ® release
|
||||
|
||||
**In short:** the **AppImage** workflow. It built both AppImages, started them,
|
||||
waited for WeKan to answer on port 8080, uploaded them as artifacts - and then
|
||||
attached nothing to the release, because the job that does the attaching never
|
||||
checks the repository out and its two `gh` calls were the only ones in the file
|
||||
without `--repo`. Fixed, and the matrix grows from two architectures to
|
||||
**four**: `i686` and `armhf` are the other two an AppImage runtime exists for.
|
||||
The **Flatpak** workflow had the same one-line bug and a second beside it - it
|
||||
was attaching the ostree repository along with the bundles - and it stays at two
|
||||
architectures, because a flatpak needs a published runtime and only x86_64 and
|
||||
aarch64 have one. Beside that, the **Docker** images now carry full SLSA
|
||||
**provenance** at every one of the four sites that pushes one, and a test pins
|
||||
that every **bundled binary** - FerretDB, the MongoDB Database Tools, Node.js -
|
||||
is still fetched as the NEWEST one everywhere it is fetched, which is what makes
|
||||
those projects' security fixes arrive without a commit here - and the table
|
||||
below is that working: **FerretDB v1.48.0** replaces v10.77's v1.45.0 on every
|
||||
platform, with no change in this repository, because `latest` now resolves to
|
||||
it. That is the release carrying `go1.25.11` and `x/sys v0.46.0`, which answers
|
||||
the Go advisories a Quay scan reported against the v10.77 image. Node.js stays
|
||||
v24.19.0, the newest 24.x. The rest of this release is the same theme - what a
|
||||
build REPORTS versus what it did. The **armhf snap** failed three times on a
|
||||
package name that has not existed since Ubuntu 24.04 renamed it, while the
|
||||
message blamed a transient build-farm reset; three snaps that BUILT were
|
||||
reported FAILED by the step that saves their logs; and the provenance table
|
||||
printed every row twice, left **amd64** out entirely, and gave six platforms the
|
||||
version "latest".
|
||||
|
||||
| Platform | Binary | From | Version | SHA256 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| amd64 | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz) | v24.19.0 | `14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647` |
|
||||
| amd64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-amd64) | v1.48.0 | `2737687fd29a8a761cd960e45f300b68cf7b4a87d50c4cc5280bcbd42b6aa163` |
|
||||
| arm64 | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-arm64.tar.xz) | v24.19.0 | `01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc` |
|
||||
| arm64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-arm64) | v1.48.0 | `5ae705dd49515a4ecd4e295c3b9aa4f3b454fad78613ec60fb99316bd7c34e3f` |
|
||||
| loong64 | Node.js | [unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org/download/release/v24.19.0/node-v24.19.0-linux-loong64.tar.xz) | v24.19.0 | `c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68` |
|
||||
| loong64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-loong64) | v1.48.0 | `06ec86263455a7b598d22a87df0e044ea73ab5a3b72e96ad12ebed03c1374ac2` |
|
||||
| mac-arm64 | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-arm64.tar.xz) | v24.19.0 | `3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94` |
|
||||
| mac-arm64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-mac-arm64) | v1.48.0 | `9b15f4c10e473cd0a2c4feb4cb43e18042bd60c7035ec66cab3cfbe13edaabab` |
|
||||
| mac-x64 | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-x64.tar.xz) | v24.19.0 | `d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4` |
|
||||
| mac-x64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-mac-amd64) | v1.48.0 | `4e188246dfa33bccef4cdd86701bc498b037cb3e91f579ff0dccb93aa0ef03ad` |
|
||||
| ppc64le | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-ppc64le.tar.xz) | v24.19.0 | `c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3` |
|
||||
| ppc64le | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-ppc64le) | v1.48.0 | `0400cd6dfc3d10d987a0fe80d75baa86c03c19170770fa2e602c92d558c3cfa6` |
|
||||
| riscv64 | Node.js | [unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org/download/release/v24.19.0/node-v24.19.0-linux-riscv64.tar.xz) | v24.19.0 | `cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd` |
|
||||
| riscv64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-riscv64) | v1.48.0 | `d37c35af988670b9ed182b8c5966c06a06362f6c6ace6aebd93ccdfa32c9a26b` |
|
||||
| s390x | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-s390x.tar.xz) | v24.19.0 | `a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4` |
|
||||
| s390x | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-s390x) | v1.48.0 | `6c7d61fbb8c79b2e8733be8f71910f710e8c5cd25208c451bdc513c8313b0340` |
|
||||
| win64 | Node.js | [nodejs.org](https://nodejs.org/dist/v24.19.0/node-v24.19.0-win-x64.zip) | v24.19.0 | `57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73` |
|
||||
| win64 | FerretDB | [wekan/FerretDB](https://github.com/wekan/FerretDB/releases/download/v1.48.0/ferretdb-win64.exe) | v1.48.0 | `ea57e1bcd153b51d2065ab01515b21ec05d8f615444c15603ab8158b8a661dd2` |
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
**AppImage** - which CPUs get one.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/97e57e531">Build the other two architectures an AppImage runtime exists for</a>. Thanks to xet7.</summary>
|
||||
|
||||
x86_64 and aarch64 become four, with **i686** and **armhf**. That is the whole
|
||||
set: an AppImage BEGINS with a runtime binary the kernel executes, and runtimes
|
||||
exist for exactly those four. `ppc64le`, `s390x`, `riscv64` and `loong64` have
|
||||
none, so they cannot have an AppImage however the job is written - they are
|
||||
served by the bundle zip and the .deb, and the matrix now says so where somebody
|
||||
would otherwise try.
|
||||
|
||||
The two are not equally safe and are not treated as though they were. **i686**
|
||||
builds on the x86_64 runner, which runs 32-bit x86 natively: no emulation, no
|
||||
multiarch, `appimagetool-i686` runs as-is. **armhf** builds on the aarch64
|
||||
runner, which runs 32-bit ARM only if the kernel has `CONFIG_COMPAT` and the
|
||||
32-bit loader is installed - GitHub promises neither - so it is
|
||||
`continue-on-error`, the same treatment TSC gives its emulated armhf job. It
|
||||
cannot decide whether the other three reach the release.
|
||||
|
||||
The smoke test needed the distinction too. It starts the AppImage and waits for
|
||||
port 8080, which a 64-bit runner cannot do for a 32-bit build it cannot execute.
|
||||
A check that cannot RUN is not a failed check: when the binary will not execute
|
||||
at all AND it is not this machine's architecture, it warns, says the AppImage is
|
||||
uploaded unchecked, and moves on. A build that DOES start and then does not
|
||||
answer still fails - that is the bug the step exists for.
|
||||
|
||||
</details>
|
||||
|
||||
**Docker images** - what an image records about how it was built.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/5b3169534">Attach full build provenance at every site that pushes an image</a>. Thanks to xet7.</summary>
|
||||
|
||||
BuildKit attaches MINIMAL provenance on its own, which is where the
|
||||
`unknown on unknown` rows beside each real platform on quay.io come from - that
|
||||
is the OCI marker for "not a runnable image", so `docker pull` never selects
|
||||
one. Minimal is a build id and a timestamp, which answers nothing anybody asks
|
||||
of a supply chain. `mode=max` records what does: which commit, which base
|
||||
image, which build arguments, which frontend.
|
||||
|
||||
The hazard is not getting it wrong once, it is that WeKan pushes images from
|
||||
FOUR places - `docker-publish.yml`, two sites in `release-all.yml`, and
|
||||
`releases/docker-build.sh` - and a fifth added later would silently fall back to
|
||||
the default. So the test finds the call sites by searching the repository
|
||||
instead of from a list, and fails when a pushing one lacks the flag.
|
||||
|
||||
SBOM stays off, deliberately: it enumerates every OS package and npm
|
||||
dependency, so the attestation grows from tens of kilobytes to megabytes per
|
||||
platform. Turning it on is a decision rather than a default to drift into.
|
||||
|
||||
The `--load` build in `docker-build.sh` must NOT ask for it - the docker
|
||||
exporter cannot carry an attestation at all - and that is a test of its own, so
|
||||
the flag is not added there by symmetry one day.
|
||||
|
||||
One more thing the test pins, because it was written the wrong way twice while
|
||||
this was being done: a `#` comment sitting among a continued command's
|
||||
arguments. `docker buildx build \` followed by a comment line comments out the
|
||||
REST OF THE JOINED LINE, so the command becomes a bare `docker buildx build`
|
||||
with every platform, tag and flag swallowed. `bash -n` accepts it - it is valid
|
||||
syntax, just a different command - and a YAML `run:` block is a shell script,
|
||||
which is where it happened the first time.
|
||||
|
||||
</details>
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
**The snap builds** - what the Launchpad jobs build, and what they report.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/a820b056b">libcurl4 does not exist on armhf, so name the package noble ships</a>. Thanks to xet7.</summary>
|
||||
|
||||
The armhf snap failed all three attempts in v10.77 while s390x, ppc64el and
|
||||
riscv64 each built on attempt 1. The Launchpad build log says why, one line into
|
||||
the mongodb part:
|
||||
|
||||
```
|
||||
Stage package not found in part 'mongodb': libcurl4.
|
||||
Build failed
|
||||
```
|
||||
|
||||
Ubuntu 24.04's 64-bit `time_t` transition renamed the library, and `libcurl4` is
|
||||
not a real package on **any** architecture in noble - the binary is
|
||||
`libcurl4t64` everywhere. On the 64-bit architectures the renamed package keeps
|
||||
the old name alive, `Provides: libcurl4 (= 8.5.0-2ubuntu10)`, and the armhf
|
||||
`libcurl4t64` has no `Provides` line at all, because there the ABI really
|
||||
changed. That is what makes this class of bug reach exactly one architecture -
|
||||
which is also what makes it look like flakiness on that architecture.
|
||||
|
||||
It had already happened, to `libssl3` and `libgoogle-perftools4` in v10.71, and
|
||||
the comment above this list describes it exactly; `libcurl4` was the same
|
||||
transition and was left behind. All fourteen stage-packages were checked against
|
||||
the noble armhf main+universe indices this time, and it was the only one that
|
||||
did not resolve.
|
||||
|
||||
The workflow's own guidance made it worse. snapcraft ends a missing
|
||||
stage-package as a *Stopped* build with no artifact, which from the outside is
|
||||
indistinguishable from Launchpad cancelling a build, so the message offered "an
|
||||
OOM in the Meteor npm install, or a transient build-farm reset - re-run" for a
|
||||
failure that will never succeed on a re-run. The step now looks for that line
|
||||
first, names the missing package, says it is not transient, explains the
|
||||
`Provides` asymmetry, and gives the one-line archive query that checks a name.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/6dce9dda7">Three good snaps were reported as FAILED by the step that saves their logs</a>. Thanks to xet7.</summary>
|
||||
|
||||
s390x, ppc64el and riscv64 each printed *"Remote build <arch> succeeded on
|
||||
attempt 1"* and downloaded their `.snap` - and each job then ended FAILED,
|
||||
because the step that saves the build log could not upload it:
|
||||
|
||||
```
|
||||
The path for one of the files in artifact is not valid:
|
||||
/snapcraft-wekan-f82a93c2…_s390x_2026-08-09T11:39:05.txt.
|
||||
Contains the following character: Colon :
|
||||
```
|
||||
|
||||
snapcraft names a remote-build log after the recipe and an ISO timestamp, and
|
||||
`upload-artifact` refuses a colon because NTFS cannot hold one. The colons
|
||||
become dashes now, so the timestamp is kept rather than the name thrown away -
|
||||
and the upload is `continue-on-error`, because a **diagnostic** upload must
|
||||
never be able to fail the build it is diagnosing. The snap is the deliverable,
|
||||
and the renaming is not the last thing that could ever make a log unuploadable.
|
||||
|
||||
</details>
|
||||
|
||||
**The release notes** - what the provenance table says each bundle carries.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/6dce9dda7">Every row appeared twice, amd64 appeared not at all, and six platforms said "latest"</a>. Thanks to xet7.</summary>
|
||||
|
||||
Three things wrong at once in the v10.77 table, and every one of them silent -
|
||||
nothing errored, the table was simply not what it claimed to be.
|
||||
|
||||
**Every row appeared twice.** `provenance-table.sh` globbed
|
||||
`provenance/**/*.tsv provenance/*.tsv`, and with `globstar` a `**/` matches
|
||||
**zero** or more directories, so the first pattern already covered the second
|
||||
and every file was read twice. It is one pattern now, and rows are also
|
||||
deduplicated on the WHOLE line - a retried step records an identical line and
|
||||
nothing tells the copies apart. Deliberately not `sort -u -k1,1 -k2,2`: two rows
|
||||
sharing a bundle and a binary but differing elsewhere are not a duplicate, they
|
||||
are a real disagreement about which Node.js went into a bundle, and hiding one
|
||||
at random is worse than showing both.
|
||||
|
||||
**amd64 was missing** - the platform most people download. Its step runs
|
||||
`cd .build` before `mkdir -p provenance`, so the rows went to
|
||||
`.build/provenance` while the upload looked at `provenance/` from the workspace
|
||||
root and found nothing. Every other build job records from the workspace root,
|
||||
which is why only this one was affected.
|
||||
|
||||
**Six platforms said Version `latest`.** amd64, arm64, win64, win32, mac-arm64
|
||||
and mac-x64 passed the literal string; only the extra-architecture job asked
|
||||
what `latest` actually was, with its own inline `curl`. "Which FerretDB did
|
||||
v10.77 ship" is the one question that column exists to answer. All seven sites
|
||||
now call `releases/ferretdb-latest-tag.sh`, which asks once per job and caches,
|
||||
authenticates when there is a token so a shared 60/hour limit is not what makes
|
||||
it fail, checks the answer is shaped like a tag before printing it into a
|
||||
markdown cell, and prints nothing and exits 0 when it cannot find out - a
|
||||
release note must never fail a build that produced a good bundle.
|
||||
|
||||
The table's prose also linked `wekan/node`; the binaries come from
|
||||
[wekan/node-patches](https://github.com/wekan/node-patches), which is what the
|
||||
rows themselves already linked.
|
||||
|
||||
</details>
|
||||
|
||||
**The release upload** - what reaches the release page.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/fa296d0c8">Attach the built AppImages to the release again</a>. Thanks to xet7.</summary>
|
||||
|
||||
Both AppImages built. Both passed the smoke test that starts them and waits for
|
||||
WeKan to answer. Both uploaded as artifacts, and both were downloaded by the
|
||||
attaching job with matching SHA256 digests. Then:
|
||||
|
||||
```
|
||||
gh release upload 'v10.77' assets/* --clobber
|
||||
failed to run git: fatal: not a git repository
|
||||
```
|
||||
|
||||
That job does not check the repository out - it has no reason to, it only
|
||||
downloads artifacts - so `gh` had no git remote to infer the repository from.
|
||||
Every other `gh` call in the workflow already passed `--repo`; those two did
|
||||
not.
|
||||
|
||||
Every call passes it now, including the ones in jobs that DO check out: a call
|
||||
relying on an inferred remote breaks the day somebody removes the checkout, and
|
||||
the error it gives names neither `gh` nor the workflow.
|
||||
|
||||
Worth recording for whoever reads that failed run, because the log looks far
|
||||
worse than the bug: `::error::` lines appear in the build jobs for the
|
||||
bundle-file check and the port check, and in the attaching job for "no
|
||||
architecture produced an AppImage - every build job failed". None of them fired.
|
||||
They carry the escape codes of the `##[group]` header - they are the SCRIPT
|
||||
being echoed, not output - and a few lines below them `ls -lh assets` shows both
|
||||
AppImages sitting there at 225M and 227M. One line in the whole run was a real
|
||||
error.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/04fb6e338">Flatpak: attach the bundles to the release, and only the bundles</a>. Thanks to xet7.</summary>
|
||||
|
||||
The same one-line bug as the AppImage workflow above, from the same cause: the
|
||||
job that attaches the bundles does not check the repository out - it only
|
||||
downloads artifacts - so `gh` had no git remote to infer the repository from and
|
||||
`gh release upload` failed with *"fatal: not a git repository"*. Both flatpaks
|
||||
had built and uploaded as artifacts; nothing reached the release. Every `gh`
|
||||
call in that workflow passes `--repo` now too.
|
||||
|
||||
And a second one beside it: `assets/*` was not the bundles. The artifacts also
|
||||
carry the **ostree repository** the flatpak was exported through - `config`,
|
||||
`objects/`, `refs/`, `summaries/`, `summary`, `summary.idx` - which is build
|
||||
scaffolding, and attaching it would have put a few hundred directories on the
|
||||
release beside the two files anyone wants. The upload names the bundles and
|
||||
their checksums instead.
|
||||
|
||||
No architectures could be added here, and the workflow header now says why
|
||||
rather than leaving it to be rediscovered. A flatpak runs against a RUNTIME, not
|
||||
the host's libraries, so an architecture exists only if freedesktop.org
|
||||
publishes `org.freedesktop.Platform` for it: x86_64 and aarch64, the i386 and
|
||||
arm runtimes having been discontinued. That is the difference from the AppImage
|
||||
work above, which could grow from two architectures to four - an AppImage
|
||||
carries its own runtime binary, and those exist for i686 and armhf as well.
|
||||
|
||||
</details>
|
||||
|
||||
and has the following developer-facing change:
|
||||
|
||||
**Bundled binaries** - keeping "newest" true everywhere it is claimed.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/ce69db8c5">Pin that every bundled binary is fetched as the newest one</a>. Thanks to xet7.</summary>
|
||||
|
||||
WeKan ships binaries other projects publish - FerretDB, the MongoDB Database
|
||||
Tools, Node.js - and fetches each from a release URL. There are many such URLs,
|
||||
across workflows, release scripts and compose files, and they must all agree:
|
||||
one that names a fixed version stops receiving that project's security fixes
|
||||
silently, and nothing about the build fails when it does.
|
||||
|
||||
That is not hypothetical. A Quay scan of the v10.77 image reported Go
|
||||
advisories in the FerretDB binary baked into the bundle - `stdlib 1.25.9`
|
||||
wanting 1.25.11, `golang.org/x/sys v0.38.0` wanting 0.44.0. The source was
|
||||
already fixed and v1.48.0 was already published carrying `go1.25.11` and
|
||||
`x/sys v0.46.0`; the image had simply captured an older `latest` at build time.
|
||||
Had any of these URLs been pinned instead, the rebuild would not have fixed it
|
||||
either.
|
||||
|
||||
The URLs are found by searching the repository rather than from a list, so a
|
||||
build site added later is checked too, and three shapes are accepted:
|
||||
`latest/download/<asset>`, the bare `latest` API endpoint that `release-all.yml`
|
||||
asks which version `latest` resolved to so the provenance table can record it,
|
||||
and `${VAR}` whose default is separately asserted to be `latest` - that is
|
||||
`FERRETDB_RELEASE` in the compose files, which an operator may pin for their own
|
||||
reasons but which must not freeze everybody who does not.
|
||||
|
||||
Node.js is a different mechanism with the same effect, so it is pinned too:
|
||||
`NODE_VERSION` is the bare major `24`, and `releases/resolve-node-source.sh`
|
||||
answers with that CPU's newest 24.x from whichever of nodejs.org,
|
||||
unofficial-builds or [wekan/node-patches](https://github.com/wekan/node-patches)
|
||||
has one.
|
||||
|
||||
What makes `latest` safe rather than merely convenient is the provenance table
|
||||
above: rebuilding an old release would embed a different FerretDB than it
|
||||
shipped with, and the only reason that is a trade rather than a hole is that
|
||||
every release RECORDS the versions and SHA256s it actually shipped. So that is
|
||||
asserted here as well - `releases/record-provenance.sh` exists, and the
|
||||
CHANGELOG still carries the table it produces.
|
||||
|
||||
</details>
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their
|
||||
translations.
|
||||
|
||||
# v10.77 2026-08-09 WeKan ® release
|
||||
|
||||
**In short:** the **snap** side, which had three problems that looked like one.
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=10.77
|
||||
ARG VERSION=10.78
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v10.77.0"
|
||||
appVersion: "v10.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-10.77-win64.zip](https://github.com/wekan/wekan/releases/download/v10.77/wekan-10.77-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
1. [wekan-10.78-win64.zip](https://github.com/wekan/wekan/releases/download/v10.78/wekan-10.78-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
|
||||
|
||||
@@ -22,7 +22,7 @@ Right click and download files 1-4:
|
||||
|
||||
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
|
||||
7. Unzip `wekan-10.77-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-10.78-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v10.77.0",
|
||||
"version": "v10.78.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wekan",
|
||||
"version": "v10.77.0",
|
||||
"version": "v10.78.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v10.77.0",
|
||||
"version": "v10.78.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<title>WeKan REST API v10.77</title>
|
||||
<title>WeKan REST API v10.78</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
@@ -423,7 +423,7 @@ data-styled.g139[id="sc-iIvZUO"]{content:"kAmnlO,"}/*!sc*/
|
||||
55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864
|
||||
-231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688,
|
||||
-104.0616 -231.873,-231.248 z
|
||||
" fill="currentColor"></path></g></svg></div></div><div class="sc-jtyQFi ldXCvc api-content"><div class="sc-eCQgVK jleRXN"><div class="sc-iCECmn lhRBUS"><div class="sc-hKVpXn kktglN api-info"><h1 class="sc-fuztkK sc-ctiVdb eRMVDe cBgvIi">Wekan REST API<!-- --> <span>(<!-- -->v10.77<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-summary" html="" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-description" html="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
" fill="currentColor"></path></g></svg></div></div><div class="sc-jtyQFi ldXCvc api-content"><div class="sc-eCQgVK jleRXN"><div class="sc-iCECmn lhRBUS"><div class="sc-hKVpXn kktglN api-info"><h1 class="sc-fuztkK sc-ctiVdb eRMVDe cBgvIi">Wekan REST API<!-- --> <span>(<!-- -->v10.78<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-summary" html="" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV"></div><div data-role="redoc-description" html="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
<p>If you are an end-user and not a dev or a tester, <a href="https://github.com/wekan/wekan/issues/new">create an issue</a> to request new APIs.</p>
|
||||
<blockquote>
|
||||
<p>All API calls in the documentation are made using <code>curl</code>. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.</p>
|
||||
@@ -2005,7 +2005,7 @@ comment-only / worker members are rejected).</p>
|
||||
" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG"><p>Attachment not found</p>
|
||||
</div></button></div></div></div><div class="sc-jSppWd sc-gKkgUA fpMlmc bdQQyo"><div class="sc-fXwuWv fYxpnv"><button class="sc-jWMFtl jzaJhV"><span type="delete" class="sc-eEFuoE iINwyG http-verb delete">delete</span><span class="sc-FpjRO dsIRaZ">/api/attachment/delete/{attachmentId}</span><svg class="sc-dYjPD bQKUih" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-fmtEmb tUxhh"><div class="sc-ljIcGq bAnPPh"><div html="" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv"></div><div tabindex="0" role="button"><div class="sc-jlJOIR PEJyb"><span></span>/api/attachment/delete/{attachmentId}</div></div></div></div></div></div></div></div></div><div class="sc-ekVkVN bjzTxL"></div></div></div>
|
||||
<script>
|
||||
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v10.77","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `<field>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the Line truncated
|
||||
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v10.78","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `<field>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the Line truncated
|
||||
|
||||
var container = document.getElementById('redoc');
|
||||
Redoc.hydrate(__redoc_state, container);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Wekan REST API
|
||||
version: v10.77
|
||||
version: v10.78
|
||||
description: |
|
||||
The REST API allows you to control and extend Wekan with ease.
|
||||
|
||||
|
||||
@@ -89,7 +89,16 @@ echo " OK: No Go buildinfo binaries found"
|
||||
'
|
||||
|
||||
echo "=== Step 3: Checks passed - building and pushing multi-arch image to registries ==="
|
||||
# --provenance=mode=max: full build provenance (SLSA), matching release-all.yml.
|
||||
# The local --load build above gets none - the docker exporter cannot carry
|
||||
# attestations - so only this pushing build has it.
|
||||
#
|
||||
# The comment is HERE and not among the arguments below: a `#` after a line
|
||||
# continuation comments out the REST OF THE JOINED LINE, so the command becomes
|
||||
# `buildx build` with every argument swallowed. `bash -n` accepts it, because it
|
||||
# is valid syntax - it is just a different command.
|
||||
${DOCKER} buildx build \
|
||||
--provenance=mode=max \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
-t wekanteam/wekan:latest \
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ferretdb-latest-tag.sh - which version `latest` is right now.
|
||||
#
|
||||
# WeKan fetches FerretDB from .../releases/latest/download/ferretdb-<arch>, so a
|
||||
# security fix published in wekan/FerretDB reaches the next bundle without a
|
||||
# commit here. The cost of that is that the URL does not say WHICH version was
|
||||
# fetched, and the release notes have to say it: "which FerretDB did v10.77
|
||||
# ship" must be answerable a year later, when `latest` is something else.
|
||||
#
|
||||
# The extra-architecture job already asked the API and recorded the answer. The
|
||||
# amd64, arm64, win64, win32, mac-arm64 and mac-x64 jobs passed the literal
|
||||
# string `latest` instead, so six rows of the v10.77 table - including amd64,
|
||||
# the platform most people download - had "latest" in the Version column, which
|
||||
# is the one question the column exists to answer.
|
||||
#
|
||||
# Usage: ferretdb-latest-tag.sh
|
||||
#
|
||||
# Prints the tag (e.g. v1.48.0) and exits 0. Prints NOTHING and still exits 0
|
||||
# when it cannot find out: this is for a release note, and no caller should fail
|
||||
# a build over it. Callers write "${TAG:-latest}", which degrades to exactly
|
||||
# what they had before.
|
||||
#
|
||||
# The answer is cached for the life of the job, so six callers in one job make
|
||||
# one request rather than six.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
cache="${FERRETDB_TAG_CACHE:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/ferretdb-latest-tag}"
|
||||
|
||||
if [ -s "$cache" ]; then
|
||||
cat "$cache"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
api="https://api.github.com/repos/wekan/FerretDB/releases/latest"
|
||||
|
||||
# -f so an HTTP error is a failure rather than an error page parsed as JSON.
|
||||
# The token is used when there is one - an unauthenticated runner shares a
|
||||
# 60/hour limit with every other job on that IP, and this is the failure that
|
||||
# put "latest" in the table.
|
||||
auth=()
|
||||
if [ -n "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]; then
|
||||
auth=(-H "Authorization: Bearer ${GH_TOKEN:-$GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
body="$(curl -fsSL --retry 3 --retry-delay 2 "${auth[@]}" "$api" 2>/dev/null || true)"
|
||||
[ -n "$body" ] || exit 0
|
||||
|
||||
# Prefer python3 - it is on every runner that builds a bundle - and fall back to
|
||||
# sed, so a container without it still answers instead of silently saying
|
||||
# "latest".
|
||||
tag=""
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
tag="$(printf '%s' "$body" \
|
||||
| python3 -c 'import json,sys
|
||||
try:
|
||||
print(json.load(sys.stdin).get("tag_name", ""))
|
||||
except Exception:
|
||||
pass' 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -z "$tag" ]; then
|
||||
tag="$(printf '%s' "$body" \
|
||||
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
||||
| head -1)"
|
||||
fi
|
||||
|
||||
# A tag, not a sentence. Anything else means the shape of the answer changed,
|
||||
# and printing it into a markdown table cell would be worse than printing
|
||||
# nothing - the caller's ${TAG:-latest} is the honest fallback.
|
||||
case "$tag" in
|
||||
v[0-9]*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$(dirname "$cache")" 2>/dev/null || true
|
||||
printf '%s' "$tag" > "$cache" 2>/dev/null || true
|
||||
printf '%s' "$tag"
|
||||
@@ -14,7 +14,11 @@ set -euo pipefail
|
||||
files=("$@")
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
shopt -s nullglob globstar
|
||||
files=(provenance/**/*.tsv provenance/*.tsv)
|
||||
# ONE pattern. With globstar, `**/` matches ZERO or more directories, so
|
||||
# provenance/**/*.tsv ALREADY covers provenance/*.tsv - and listing both
|
||||
# matched every top-level file twice, which is why the v10.77 release notes
|
||||
# printed every row of the table twice.
|
||||
files=(provenance/**/*.tsv)
|
||||
fi
|
||||
|
||||
rows=""
|
||||
@@ -24,8 +28,18 @@ for f in "${files[@]}"; do
|
||||
"
|
||||
done
|
||||
|
||||
# Drop blank lines, then sort by bundle and binary.
|
||||
rows="$(printf '%s' "$rows" | awk 'NF' | sort -t"$(printf '\t')" -k1,1 -k2,2)"
|
||||
# Drop blank lines, drop rows that are duplicates of a whole other row, then
|
||||
# sort by bundle and binary.
|
||||
#
|
||||
# Deduplicated on the WHOLE line, not on (bundle, binary): a row is (bundle,
|
||||
# binary, source, version, checked, sha, url), so an identical line is the same
|
||||
# FACT recorded twice - by a doubled glob, a retried step, a job that ran again
|
||||
# - and nothing distinguishes the copies. Two rows that share a bundle and a
|
||||
# binary but differ anywhere else are NOT that; they are a real disagreement
|
||||
# about which Node.js went into a bundle, and `sort -u -k1,1 -k2,2` would hide
|
||||
# one of the two at random. Better shown twice and noticed.
|
||||
rows="$(printf '%s' "$rows" | awk 'NF && !seen[$0]++' \
|
||||
| sort -t"$(printf '\t')" -k1,1 -k2,2)"
|
||||
|
||||
if [ -z "$rows" ]; then
|
||||
# Not fatal. The bundles are what the release is; a missing provenance
|
||||
@@ -41,10 +55,10 @@ echo
|
||||
echo "Each bundle carries a Node.js, a FerretDB and the MongoDB Database Tools."
|
||||
echo "Which source has a given CPU varies from release to release - nodejs.org"
|
||||
echo "builds some architectures, unofficial-builds others, and the"
|
||||
echo "[wekan/node](https://github.com/wekan/node) fork the ones neither of them"
|
||||
echo "does - and not every source publishes a checksum. This is what went into"
|
||||
echo "this release, and which downloads were checked against a published"
|
||||
echo "SHA256."
|
||||
echo "[wekan/node-patches](https://github.com/wekan/node-patches) build the ones"
|
||||
echo "neither of them does - and not every source publishes a checksum. This is"
|
||||
echo "what went into this release, and which downloads were checked against a"
|
||||
echo "published SHA256."
|
||||
echo
|
||||
echo "| Bundle | Binary | From | Version | Checked | SHA256 |"
|
||||
echo "| --- | --- | --- | --- | --- | --- |"
|
||||
|
||||
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
||||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 1077,
|
||||
appVersion = 1078,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "10.77~2026-08-09"),
|
||||
appMarketingVersion = (defaultText = "10.78~2026-08-09"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
||||
+21
-5
@@ -19,7 +19,7 @@
|
||||
# base: core24 (not core26) · no build-base: devel · grade: stable
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: wekan
|
||||
version: '10.77'
|
||||
version: '10.78'
|
||||
base: core24
|
||||
confinement: strict
|
||||
grade: stable
|
||||
@@ -201,9 +201,25 @@ parts:
|
||||
# against the noble archive: libssl3t64 is published for amd64, arm64,
|
||||
# armhf, i386, ppc64el, riscv64 and s390x, and libgoogle-perftools4t64
|
||||
# for every one of those except i386 - which builds no snap.
|
||||
#
|
||||
# libcurl4 was the SAME transition, left behind when those two were
|
||||
# renamed, and it failed the v10.77 armhf snap the same way and for the
|
||||
# same reason:
|
||||
# Stage package not found in part 'mongodb': libcurl4.
|
||||
# while s390x, ppc64el and riscv64 all built on attempt 1. Checked
|
||||
# against the noble indices the same way: `libcurl4` is not a real
|
||||
# package on ANY architecture - the binary is `libcurl4t64` everywhere -
|
||||
# and on amd64/arm64 it carries `Provides: libcurl4 (= 8.5.0-2ubuntu10)`
|
||||
# so the old name resolves, while the armhf libcurl4t64 has no Provides
|
||||
# line at all. That is what makes this class of bug reach exactly one
|
||||
# architecture and look like flakiness on it.
|
||||
#
|
||||
# Every other stage-package below was checked against the noble armhf
|
||||
# main+universe indices at the same time; libcurl4 was the only one that
|
||||
# did not resolve.
|
||||
stage-packages:
|
||||
- libssl3t64
|
||||
- libcurl4
|
||||
- libcurl4t64
|
||||
- libstemmer0d
|
||||
- zlib1g
|
||||
- libsnappy1v5
|
||||
@@ -353,18 +369,18 @@ parts:
|
||||
armhf) WEKAN_ARCH="armhf" ;;
|
||||
*) echo "Unsupported architecture: ${CRAFT_ARCH_BUILD_FOR}"; exit 1 ;;
|
||||
esac
|
||||
WEKAN_ZIP="wekan-10.77-${WEKAN_ARCH}.zip"
|
||||
WEKAN_ZIP="wekan-10.78-${WEKAN_ARCH}.zip"
|
||||
echo "Cleaning environment first"
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
#wget https://github.com/wekan/wekan/releases/download/v10.77/wekan-10.77-amd64.zip
|
||||
#wget https://github.com/wekan/wekan/releases/download/v10.78/wekan-10.78-amd64.zip
|
||||
# Retry: a release asset can 404 for a short while right after it is
|
||||
# uploaded (CDN/propagation lag), and a plain wget treated that as
|
||||
# fatal. --retry-on-http-error covers the 404/5xx window; a genuinely
|
||||
# missing bundle still fails after the tries are spent.
|
||||
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 \
|
||||
"https://github.com/wekan/wekan/releases/download/v10.77/${WEKAN_ZIP}"
|
||||
"https://github.com/wekan/wekan/releases/download/v10.78/${WEKAN_ZIP}"
|
||||
unzip "${WEKAN_ZIP}"
|
||||
rm "${WEKAN_ZIP}"
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
|
||||
@@ -208,6 +208,7 @@ test('every script in releases/ is reachable from BOTH menus', () => {
|
||||
'require-binaries.sh': 'release-workflow: called per build job',
|
||||
'record-provenance.sh': 'release-workflow: called per build job',
|
||||
'provenance-table.sh': 'release-workflow: called by the release job',
|
||||
'ferretdb-latest-tag.sh': 'release-workflow: a lookup the build jobs call so\n the provenance table can name the version `latest` resolved to - it prints\n a tag and downloads nothing',
|
||||
'repack-bundle-for-arch.sh': 'release-workflow: needs ./bundle, docker and '
|
||||
+ 'QEMU already set up by the build job around it',
|
||||
'expected-assets.sh': 'release-workflow: prints what a complete release '
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
'use strict';
|
||||
|
||||
// Every image WeKan pushes carries full build provenance, and no build site
|
||||
// quietly stops doing it.
|
||||
//
|
||||
// BuildKit attaches MINIMAL provenance on its own, which is why quay.io shows an
|
||||
// `unknown on unknown` row beside each real platform - the OCI marker for "not a
|
||||
// runnable image", so `docker pull` never selects it. Minimal is a build id and
|
||||
// a timestamp. `mode=max` records what actually answers a supply-chain question:
|
||||
// which commit, which base image, which build arguments.
|
||||
//
|
||||
// The hazard is not getting it wrong once - it is that WeKan pushes images from
|
||||
// FOUR places, and a fifth added later would silently fall back to the default.
|
||||
// So this checks every buildx/build-push-action call site in the repository,
|
||||
// found by searching rather than from a list.
|
||||
//
|
||||
// Run: node tests/dockerProvenance.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// Where images are built. Searched for, not listed, so a new one is found.
|
||||
function buildSites() {
|
||||
const files = [];
|
||||
const walk = dir => {
|
||||
fs.readdirSync(path.join(repoRoot, dir), { withFileTypes: true }).forEach(e => {
|
||||
const rel = `${dir}/${e.name}`;
|
||||
if (e.isDirectory()) return;
|
||||
if (!/\.(ya?ml|sh)$/.test(e.name)) return;
|
||||
const src = read(rel);
|
||||
if (/buildx build|build-push-action/.test(src)) files.push(rel);
|
||||
});
|
||||
};
|
||||
walk('.github/workflows');
|
||||
walk('releases');
|
||||
return files;
|
||||
}
|
||||
|
||||
// One entry per `docker buildx build` invocation: the whole command, joined.
|
||||
function buildxCommands(src) {
|
||||
const out = [];
|
||||
const lines = src.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
// The COMMAND, not a comment that mentions it.
|
||||
if (!/buildx build/.test(lines[i])) continue;
|
||||
if (/^\s*#/.test(lines[i])) continue;
|
||||
let cmd = lines[i];
|
||||
let j = i;
|
||||
while (/\\\s*$/.test(lines[j]) && j + 1 < lines.length) {
|
||||
j += 1;
|
||||
cmd += ' ' + lines[j].trim();
|
||||
}
|
||||
out.push(cmd);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('every pushing buildx build asks for full provenance', () => {
|
||||
buildSites().forEach(file => {
|
||||
buildxCommands(read(file)).forEach(cmd => {
|
||||
// Only a build that PUSHES can carry an attestation. The local --load
|
||||
// build is a check, and the docker exporter cannot carry one at all.
|
||||
if (!/--push/.test(cmd)) return;
|
||||
assert.ok(/--provenance=mode=max/.test(cmd),
|
||||
`${file}: a pushing buildx build without --provenance=mode=max:\n ${cmd.slice(0, 120)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('negative: the local --load build does NOT ask for it', () => {
|
||||
// The docker exporter cannot carry attestations; asking would fail the build.
|
||||
const cmds = buildxCommands(read('releases/docker-build.sh'));
|
||||
const load = cmds.filter(c => /--load/.test(c));
|
||||
assert.ok(load.length >= 1, 'the check build is still there');
|
||||
load.forEach(c => assert.ok(!/--provenance/.test(c),
|
||||
'the --load build must not ask for provenance'));
|
||||
});
|
||||
|
||||
test('every build-push-action sets provenance: mode=max', () => {
|
||||
buildSites().filter(f => f.endsWith('.yml') || f.endsWith('.yaml')).forEach(file => {
|
||||
const src = read(file);
|
||||
if (!/build-push-action/.test(src)) return;
|
||||
assert.ok(/provenance:\s*mode=max/.test(src),
|
||||
`${file}: build-push-action without provenance: mode=max`);
|
||||
});
|
||||
});
|
||||
|
||||
test('THE TRAP: no comment sits among a continued command\'s arguments', () => {
|
||||
// A `#` after a line continuation comments out the REST OF THE JOINED LINE, so
|
||||
// docker buildx build \
|
||||
// # explain the next flag
|
||||
// --provenance=mode=max \
|
||||
// --platform ... \
|
||||
// becomes `docker buildx build` with EVERY argument swallowed. `bash -n`
|
||||
// accepts it - it is valid syntax, just a different command - and this was
|
||||
// written exactly that way once before the test existed.
|
||||
// Every build site, not only .sh: a YAML `run:` block is a shell script, and
|
||||
// that is exactly where this was written the wrong way the first time.
|
||||
buildSites().forEach(file => {
|
||||
const lines = read(file).split('\n');
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
if (!/\\\s*$/.test(lines[i])) continue;
|
||||
// A continuation INSIDE a comment is not a command: a comment block that
|
||||
// quotes a multi-line shell example ends its lines with `\` too, and every
|
||||
// line of it starts with `#`. Only a continued COMMAND can be broken this
|
||||
// way.
|
||||
if (/^\s*#/.test(lines[i])) continue;
|
||||
assert.ok(!/^\s*#/.test(lines[i + 1]),
|
||||
`${file}:${i + 2}: a comment after a line continuation swallows the rest of the command`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('the arguments really do survive the joined command', () => {
|
||||
// The property the trap above destroys, asserted directly: the pushing build
|
||||
// still carries its platforms and its tags.
|
||||
const cmd = buildxCommands(read('releases/docker-build.sh')).find(c => /--push/.test(c));
|
||||
assert.ok(cmd, 'the pushing build is found');
|
||||
assert.ok(/--platform linux\/amd64,linux\/arm64/.test(cmd), 'it still names its platforms');
|
||||
assert.ok(/-t wekanteam\/wekan:/.test(cmd), 'and its tags');
|
||||
assert.ok(/--provenance=mode=max/.test(cmd));
|
||||
});
|
||||
|
||||
test('SBOM stays off, deliberately', () => {
|
||||
// It is opt-in, and it is not free: an SBOM enumerates every OS package and
|
||||
// npm dependency, so the attestation grows from ~10 KiB to megabytes per
|
||||
// platform. Turning it on is a decision, not a default to drift into - this
|
||||
// fails if one appears without the comment being revisited.
|
||||
buildSites().forEach(file => {
|
||||
const src = read(file);
|
||||
assert.ok(!/--sbom=true|sbom:\s*true/.test(src),
|
||||
`${file}: SBOM was enabled - see tests/dockerProvenance.test.cjs about the size`);
|
||||
});
|
||||
});
|
||||
|
||||
test('all four pushing sites are found, so none is missed', () => {
|
||||
const sites = buildSites();
|
||||
const pushing = sites.reduce((n, f) => {
|
||||
const src = read(f);
|
||||
const buildx = buildxCommands(src).filter(c => /--push/.test(c)).length;
|
||||
const action = /build-push-action/.test(src) ? 1 : 0;
|
||||
return n + buildx + action;
|
||||
}, 0);
|
||||
assert.strictEqual(pushing, 4,
|
||||
`expected 4 pushing build sites, found ${pushing} - if that is a real change, ` +
|
||||
'give the new one --provenance=mode=max and update this count');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
// Every bundled binary is fetched as the NEWEST one, everywhere it is fetched.
|
||||
//
|
||||
// WeKan ships binaries other projects publish - FerretDB, the MongoDB Database
|
||||
// Tools, Node.js - and fetches each from a release URL. There are many such
|
||||
// URLs, across workflows, release scripts and compose files, and they must all
|
||||
// agree: one that names a fixed version stops receiving security fixes silently,
|
||||
// and nothing about the build fails when it does.
|
||||
//
|
||||
// That is not hypothetical. A Quay scan of the v10.77 image reported Go
|
||||
// advisories - `stdlib 1.25.9` wanting 1.25.11, `golang.org/x/sys v0.38.0`
|
||||
// wanting 0.44.0 - in the FerretDB binary baked into the bundle. The source was
|
||||
// already fixed and v1.48.0 was already published carrying `go1.25.11` and
|
||||
// `x/sys v0.46.0`; the image had simply captured an older `latest` at build
|
||||
// time. Had any of these URLs been pinned instead, the rebuild would not have
|
||||
// fixed it either.
|
||||
//
|
||||
// Run: node tests/newestBundledBinaries.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// The repositories WeKan takes binaries from. Node.js is deliberately absent:
|
||||
// its version is RESOLVED per architecture by releases/resolve-node-source.sh,
|
||||
// which is asked about the MAJOR and answers with that CPU's newest build from
|
||||
// whichever of the three sources has one - a different mechanism with the same
|
||||
// effect, and it has its own tests.
|
||||
const UPSTREAM = ['wekan/FerretDB', 'wekan/mongo-tools-patches'];
|
||||
|
||||
// Files that fetch things. Searched for rather than listed.
|
||||
function sourceFiles() {
|
||||
const out = [];
|
||||
const skip = new Set(['node_modules', '.git', '.tools', '.meteor', 'public']);
|
||||
const walk = dir => {
|
||||
for (const e of fs.readdirSync(path.join(repoRoot, dir), { withFileTypes: true })) {
|
||||
if (skip.has(e.name)) continue;
|
||||
const rel = dir === '.' ? e.name : `${dir}/${e.name}`;
|
||||
if (e.isDirectory()) walk(rel);
|
||||
else if (/\.(sh|ya?ml)$|^Dockerfile/.test(e.name)) out.push(rel);
|
||||
}
|
||||
};
|
||||
walk('.');
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every release URL for the upstreams above, with the file it came from.
|
||||
function releaseUrls() {
|
||||
const found = [];
|
||||
sourceFiles().forEach(file => {
|
||||
const src = fs.readFileSync(path.join(repoRoot, file), 'utf8');
|
||||
src.split('\n').forEach((line, i) => {
|
||||
UPSTREAM.forEach(repo => {
|
||||
const re = new RegExp(`${repo}/releases/([^"'\\s)]*)`, 'g');
|
||||
let m;
|
||||
while ((m = re.exec(line)) !== null) {
|
||||
found.push({ file, line: i + 1, repo, rest: m[1], text: line.trim() });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
test('there ARE such URLs, so this test is checking something', () => {
|
||||
const urls = releaseUrls();
|
||||
assert.ok(urls.length >= 5, `expected several release URLs, found ${urls.length}`);
|
||||
});
|
||||
|
||||
test('every FerretDB and mongo-tools URL asks for the newest build', () => {
|
||||
releaseUrls().forEach(u => {
|
||||
// Three acceptable shapes:
|
||||
// latest/download/<asset> - the newest release's asset
|
||||
// ${VAR}/... - an override whose DEFAULT is latest, checked
|
||||
// separately below
|
||||
// prose in a comment - a link to the releases page, not a fetch
|
||||
// `latest/download/<asset>` is a fetch; a bare `latest` is the API
|
||||
// endpoint that ASKS which version latest is - which release-all.yml does
|
||||
// to record it in the provenance table. Both are the newest, neither pins.
|
||||
const isLatest = u.rest === 'latest' || u.rest.startsWith('latest/');
|
||||
const isOverridable = /^\$\{?\$?\{?[A-Z_]/.test(u.rest) || /\$\{?\$?\{?[A-Z_]+\}?/.test(u.rest);
|
||||
const isProse = u.text.trim().startsWith('#') || /^\)?[,.]?$/.test(u.rest);
|
||||
|
||||
assert.ok(isLatest || isOverridable || isProse,
|
||||
`${u.file}:${u.line} fetches a FIXED version - it will stop getting ` +
|
||||
`security fixes and nothing will fail:\n ${u.text.slice(0, 110)}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('the overridable one defaults to latest', () => {
|
||||
// docker-compose lets an operator pin a FerretDB for their own reasons
|
||||
// (FERRETDB_RELEASE=download/v1.24.2). The DEFAULT must still be newest, or
|
||||
// every operator who does not set it is silently frozen.
|
||||
const composes = sourceFiles().filter(f => /^docker-compose.*\.ya?ml$/.test(f));
|
||||
assert.ok(composes.length >= 1, 'the compose files are found');
|
||||
|
||||
composes.forEach(file => {
|
||||
const src = fs.readFileSync(path.join(repoRoot, file), 'utf8');
|
||||
if (!/FERRETDB_RELEASE/.test(src)) return;
|
||||
assert.ok(/FERRETDB_RELEASE:\s*\$\{FERRETDB_RELEASE:-latest\/download\}/.test(src),
|
||||
`${file}: FERRETDB_RELEASE must default to latest/download`);
|
||||
});
|
||||
});
|
||||
|
||||
test('negative: no URL names a version number directly', () => {
|
||||
// The shape that would silently freeze a component: .../releases/download/v1.2.3/...
|
||||
releaseUrls().forEach(u => {
|
||||
if (u.text.trim().startsWith('#')) return; // a comment may show an example
|
||||
assert.ok(!/^download\/v[0-9]/.test(u.rest),
|
||||
`${u.file}:${u.line} pins a version:\n ${u.text.slice(0, 110)}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Node.js is resolved per architecture, not fetched from a fixed URL', () => {
|
||||
// A different mechanism with the same effect: resolve-node-source.sh is asked
|
||||
// about the MAJOR and answers with that CPU's newest build from whichever of
|
||||
// nodejs.org, unofficial-builds or node-patches has one.
|
||||
const resolver = 'releases/resolve-node-source.sh';
|
||||
assert.ok(fs.existsSync(path.join(repoRoot, resolver)), `${resolver} exists`);
|
||||
const src = fs.readFileSync(path.join(repoRoot, resolver), 'utf8');
|
||||
assert.ok(/nodejs\.org/.test(src) && /unofficial-builds/.test(src) && /node-patches/.test(src),
|
||||
'it still knows all three sources');
|
||||
});
|
||||
|
||||
test('the provenance table is what makes a `latest` build reproducible after the fact', () => {
|
||||
// `latest` means rebuilding an old release would embed a different FerretDB
|
||||
// than it shipped with. That is a deliberate trade - security fixes arrive
|
||||
// without a commit - and what makes it safe is that every release RECORDS the
|
||||
// versions and SHA256s it actually shipped.
|
||||
const rec = 'releases/record-provenance.sh';
|
||||
assert.ok(fs.existsSync(path.join(repoRoot, rec)),
|
||||
`${rec} must exist: it is what answers "which FerretDB did v10.77 ship?"`);
|
||||
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
||||
assert.ok(/\| Platform \| Binary \| From \| Version \| SHA256 \|/.test(changelog),
|
||||
'and the CHANGELOG carries the table it produces');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,336 @@
|
||||
'use strict';
|
||||
|
||||
// The release notes must say what actually went into each bundle - once, for
|
||||
// every platform, with a version rather than the word "latest".
|
||||
//
|
||||
// The v10.77 notes got all three wrong at the same time, and each failure was
|
||||
// silent: nothing errored, the table was simply not what it claimed to be.
|
||||
//
|
||||
// Every row appeared TWICE. provenance-table.sh globbed
|
||||
// `provenance/**/*.tsv provenance/*.tsv`, and with globstar `**/` matches ZERO
|
||||
// or more directories - so the first pattern already covered the second and
|
||||
// every file was read twice.
|
||||
//
|
||||
// amd64 was MISSING, the platform most people download. Its step runs
|
||||
// `cd .build` before `mkdir -p provenance`, so the rows went to
|
||||
// .build/provenance while upload-artifact looked at provenance/ - "No files
|
||||
// were found with the provided path: provenance/". Every other build job
|
||||
// records from the workspace root, which is why only amd64 was affected.
|
||||
//
|
||||
// Six platforms said Version "latest". amd64, arm64, win64, win32, mac-arm64
|
||||
// and mac-x64 passed the literal string; only the extra-architecture job asked
|
||||
// which version latest actually was. "Which FerretDB did v10.77 ship" is the
|
||||
// one question that column exists to answer.
|
||||
//
|
||||
// Run: node tests/provenanceTable.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(repoRoot, rel), 'utf8');
|
||||
|
||||
const TABLE = 'releases/provenance-table.sh';
|
||||
const RECORD = 'releases/record-provenance.sh';
|
||||
const RESOLVER = 'releases/ferretdb-latest-tag.sh';
|
||||
const WORKFLOW = '.github/workflows/release-all.yml';
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
const TAB = '\t';
|
||||
const row = (bundle, what, src, version, checked, sha, url) =>
|
||||
[bundle, what, src, version, checked, sha, url].join(TAB);
|
||||
|
||||
// Run provenance-table.sh over a directory of .tsv files and return its output.
|
||||
function tableFor(files) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-prov-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(dir, 'provenance'), { recursive: true });
|
||||
Object.entries(files).forEach(([name, lines]) => {
|
||||
const p = path.join(dir, 'provenance', name);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, lines.join('\n') + '\n');
|
||||
});
|
||||
return execFileSync('bash', [path.join(repoRoot, TABLE)], {
|
||||
cwd: dir, encoding: 'utf8',
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const bodyRows = out => out.split('\n')
|
||||
.filter(l => l.startsWith('| ') && !/^\| (Bundle|-)/.test(l));
|
||||
|
||||
// ───────────────────────────────────────────── every row appears exactly once
|
||||
|
||||
test('THE BUG: each recorded row is printed once, not twice', () => {
|
||||
const out = tableFor({
|
||||
'amd64.tsv': [
|
||||
row('amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'aa11', 'https://nodejs.org/x'),
|
||||
row('amd64', 'FerretDB', 'wekan/FerretDB', 'v1.48.0', 'verified', 'bb22', 'https://gh/f'),
|
||||
],
|
||||
's390x.tsv': [
|
||||
row('s390x', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'cc33', 'https://nodejs.org/s'),
|
||||
],
|
||||
});
|
||||
assert.strictEqual(bodyRows(out).length, 3,
|
||||
'three rows recorded, so three rows printed');
|
||||
});
|
||||
|
||||
test('...and the glob that read every file twice is still ONE pattern', () => {
|
||||
// The row-counting test above passes either way now, because the dedup added
|
||||
// with this fix would swallow a doubled glob - which is what defence in depth
|
||||
// is for, and also why the cause needs its own guard or it can come back
|
||||
// invisibly. With globstar, `provenance/**/*.tsv` already matches
|
||||
// `provenance/*.tsv`; listing both reads every top-level file twice.
|
||||
const src = read(TABLE);
|
||||
const globLine = src.split('\n').find(l => /files=\(provenance/.test(l));
|
||||
assert.ok(globLine, 'the default glob is still there');
|
||||
const patterns = globLine.replace(/.*files=\(|\).*/g, '').trim().split(/\s+/);
|
||||
assert.deepStrictEqual(patterns, ['provenance/**/*.tsv'],
|
||||
'one pattern: `**/` matches zero or more directories, so it covers both');
|
||||
assert.ok(/shopt -s nullglob globstar/.test(src),
|
||||
'and globstar is on, or `**` is just `*` and nested files are missed');
|
||||
});
|
||||
|
||||
test('a file in a SUBDIRECTORY is still found - the glob covers both', () => {
|
||||
// download-artifact can land each artifact in its own directory. Collapsing to
|
||||
// one glob must not lose those.
|
||||
const out = tableFor({
|
||||
'amd64.tsv': [row('amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'aa11', 'u')],
|
||||
'provenance-arm64/arm64.tsv': [
|
||||
row('arm64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'bb22', 'u'),
|
||||
],
|
||||
});
|
||||
const rows = bodyRows(out);
|
||||
assert.strictEqual(rows.length, 2);
|
||||
assert.ok(rows.some(r => r.startsWith('| amd64 ')), 'the top-level file');
|
||||
assert.ok(rows.some(r => r.startsWith('| arm64 ')), 'and the nested one');
|
||||
});
|
||||
|
||||
test('the same fact recorded twice collapses to one row', () => {
|
||||
// A retried step, or a job that ran again, records an identical line. It is
|
||||
// one fact and nothing tells the copies apart.
|
||||
const same = row('amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'aa11', 'u');
|
||||
const out = tableFor({ 'a.tsv': [same], 'b.tsv': [same] });
|
||||
assert.strictEqual(bodyRows(out).length, 1);
|
||||
});
|
||||
|
||||
test('NEGATIVE: a real disagreement is NOT collapsed', () => {
|
||||
// Two rows sharing bundle and binary but differing elsewhere are not a
|
||||
// duplicate - they are two answers to "which Node.js went into amd64", and
|
||||
// hiding one at random is worse than showing both. This is why the dedup is
|
||||
// on the whole line and not on the sort key.
|
||||
const out = tableFor({
|
||||
'a.tsv': [
|
||||
row('amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'aa11', 'u1'),
|
||||
row('amd64', 'Node.js', 'wekan/node-patches', 'v24.19.1', 'verified', 'bb22', 'u2'),
|
||||
],
|
||||
});
|
||||
assert.strictEqual(bodyRows(out).length, 2,
|
||||
'both must survive, so the disagreement is visible');
|
||||
});
|
||||
|
||||
test('rows are grouped by bundle, then by binary', () => {
|
||||
const out = tableFor({
|
||||
'x.tsv': [
|
||||
row('s390x', 'Node.js', 'nodejs.org', 'v1', 'verified', 'a', 'u'),
|
||||
row('amd64', 'Node.js', 'nodejs.org', 'v1', 'verified', 'b', 'u'),
|
||||
row('amd64', 'FerretDB', 'wekan/FerretDB', 'v1', 'verified', 'c', 'u'),
|
||||
],
|
||||
});
|
||||
const bundles = bodyRows(out).map(r => r.split('|')[1].trim());
|
||||
assert.deepStrictEqual(bundles, ['amd64', 'amd64', 's390x']);
|
||||
const binaries = bodyRows(out).slice(0, 2).map(r => r.split('|')[2].trim());
|
||||
assert.deepStrictEqual(binaries, ['FerretDB', 'Node.js']);
|
||||
});
|
||||
|
||||
test('a source with no checksum says so instead of showing a blank', () => {
|
||||
const out = tableFor({
|
||||
'x.tsv': [row('s390x', 'Node.js', 'nodejs.org', 'v1', 'no checksum published', '-', 'u')],
|
||||
});
|
||||
const r = bodyRows(out)[0];
|
||||
assert.ok(/no checksum published/.test(r));
|
||||
assert.ok(/\| — \|/.test(r), 'and an em dash, not an empty cell');
|
||||
assert.ok(!/`-`/.test(r), 'not a literal dash in backticks');
|
||||
});
|
||||
|
||||
test('no rows at all is a note, not a failed release', () => {
|
||||
const out = tableFor({});
|
||||
assert.ok(/no build job recorded/.test(out));
|
||||
assert.ok(!/\| Bundle \|/.test(out), 'and no empty table');
|
||||
});
|
||||
|
||||
test('the table names the right fork', () => {
|
||||
// It said [wekan/node](https://github.com/wekan/node); the binaries come from
|
||||
// wekan/node-patches, which is also what the rows themselves link to.
|
||||
const src = read(TABLE);
|
||||
assert.ok(/wekan\/node-patches/.test(src));
|
||||
assert.ok(!/github\.com\/wekan\/node\)/.test(src),
|
||||
'the old wrong link is gone');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────── amd64 records where the upload looks
|
||||
|
||||
test('THE BUG: amd64 records into the workspace, not into .build', () => {
|
||||
const src = read(WORKFLOW);
|
||||
// The amd64 zip step, from its `cd .build` to the end of the run block.
|
||||
const step = src.slice(src.indexOf('- name: Create amd64 zip'));
|
||||
const upTo = step.slice(0, step.indexOf('- uses: actions/upload-artifact'));
|
||||
assert.ok(/cd \.build/.test(upTo), 'the step still cds, which is what made this bite');
|
||||
|
||||
const calls = upTo.split('\n').filter(l => /PROVENANCE_FILE=/.test(l));
|
||||
assert.strictEqual(calls.length, 2, 'Node.js and FerretDB are both recorded');
|
||||
calls.forEach(l => assert.ok(/GITHUB_WORKSPACE/.test(l),
|
||||
`a relative provenance path inside "cd .build" writes where nothing reads:\n ${l.trim()}`));
|
||||
assert.ok(/mkdir -p "\$GITHUB_WORKSPACE\/provenance"/.test(upTo),
|
||||
'and the directory is made there too');
|
||||
});
|
||||
|
||||
test('every build job that records also uploads what it recorded', () => {
|
||||
const src = read(WORKFLOW);
|
||||
const recorded = [...src.matchAll(/PROVENANCE_FILE=\S*?provenance\/([a-z0-9-]+)\.tsv/g)]
|
||||
.map(m => m[1]);
|
||||
assert.ok(recorded.length >= 6, `expected several recording sites, found ${recorded.length}`);
|
||||
new Set(recorded).forEach(arch => {
|
||||
assert.ok(src.includes(`name: provenance-${arch}`),
|
||||
`${arch} records provenance but no job uploads it`);
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────── the version is a version
|
||||
|
||||
test('THE BUG: no site records the literal string "latest" as the version', () => {
|
||||
const src = read(WORKFLOW);
|
||||
assert.ok(!/'wekan\/FerretDB' 'latest'/.test(src),
|
||||
'a Version column reading "latest" answers nothing a year later');
|
||||
});
|
||||
|
||||
test('every FerretDB provenance call resolves the tag first', () => {
|
||||
const src = read(WORKFLOW);
|
||||
const lines = src.split('\n');
|
||||
const calls = lines
|
||||
.map((l, i) => ({ l, i }))
|
||||
.filter(x => /'FerretDB'/.test(x.l) && /wekan\/FerretDB/.test(x.l));
|
||||
assert.ok(calls.length >= 6, `expected the FerretDB rows, found ${calls.length}`);
|
||||
calls.forEach(c => {
|
||||
assert.ok(/\$\{FERRET_TAG:-latest\}/.test(c.l),
|
||||
`${WORKFLOW}:${c.i + 1} does not use the resolved tag:\n ${c.l.trim()}`);
|
||||
// ...and FERRET_TAG has to have been set just above it.
|
||||
const above = lines.slice(Math.max(0, c.i - 4), c.i).join('\n');
|
||||
assert.ok(/FERRET_TAG="\$\(bash \S*ferretdb-latest-tag\.sh\)"/.test(above),
|
||||
`${WORKFLOW}:${c.i + 1} uses FERRET_TAG without resolving it above`);
|
||||
});
|
||||
});
|
||||
|
||||
test('there is ONE implementation of "which version is latest"', () => {
|
||||
const src = read(WORKFLOW);
|
||||
assert.ok(!/api\.github\.com\/repos\/wekan\/FerretDB/.test(src),
|
||||
'the API call is inlined again; it belongs in the resolver only');
|
||||
assert.ok(fs.existsSync(path.join(repoRoot, RESOLVER)), `${RESOLVER} exists`);
|
||||
});
|
||||
|
||||
test('the resolver never fails its caller', () => {
|
||||
// It feeds a release note. A rate-limited or offline runner must degrade to
|
||||
// the old behaviour, not fail a build that produced a perfectly good bundle.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-ftag-'));
|
||||
try {
|
||||
const out = execFileSync('bash', [path.join(repoRoot, RESOLVER)], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
FERRETDB_TAG_CACHE: path.join(dir, 'cache'),
|
||||
http_proxy: 'http://127.0.0.1:9',
|
||||
https_proxy: 'http://127.0.0.1:9',
|
||||
HTTPS_PROXY: 'http://127.0.0.1:9',
|
||||
},
|
||||
});
|
||||
assert.strictEqual(out.trim(), '', 'it prints nothing when it cannot find out');
|
||||
} catch (e) {
|
||||
assert.fail(`the resolver exited non-zero with no network: ${e.status}`);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('the resolver refuses to print anything that is not a tag', () => {
|
||||
// Whatever it prints goes into a markdown cell. An error page, an HTML body
|
||||
// or a rate-limit message must fall through to the caller's :-latest.
|
||||
const src = read(RESOLVER);
|
||||
assert.ok(/v\[0-9\]\*\)/.test(src), 'the shape of the answer is checked');
|
||||
assert.ok(/GITHUB_TOKEN|GH_TOKEN/.test(src),
|
||||
'and it authenticates, so a shared 60/hour limit is not what makes it fail');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────── the record script itself
|
||||
|
||||
test('a recorded row has the seven fields the table reads', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-rec-'));
|
||||
try {
|
||||
const file = path.join(dir, 'p.tsv');
|
||||
execFileSync('bash', [path.join(repoRoot, RECORD),
|
||||
'amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'https://x', 'abc123'],
|
||||
{ env: { ...process.env, PROVENANCE_FILE: file }, encoding: 'utf8' });
|
||||
const fields = fs.readFileSync(file, 'utf8').trim().split(TAB);
|
||||
assert.strictEqual(fields.length, 7);
|
||||
assert.deepStrictEqual(fields.slice(0, 6),
|
||||
['amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'verified', 'abc123']);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('an omitted checksum is recorded as unpublished, not as verified', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-rec-'));
|
||||
try {
|
||||
const file = path.join(dir, 'p.tsv');
|
||||
execFileSync('bash', [path.join(repoRoot, RECORD),
|
||||
'amd64', 'Node.js', 'nodejs.org', 'v24.19.0', 'https://x'],
|
||||
{ env: { ...process.env, PROVENANCE_FILE: file }, encoding: 'utf8' });
|
||||
const fields = fs.readFileSync(file, 'utf8').trim().split(TAB);
|
||||
assert.strictEqual(fields[4], 'no checksum published');
|
||||
assert.strictEqual(fields[5], '-');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ────────────────────────── a diagnostic upload cannot fail a good build
|
||||
|
||||
test('THE BUG: snapcraft build logs are renamed before they are uploaded', () => {
|
||||
// snapcraft names a remote-build log after the recipe and an ISO timestamp:
|
||||
// snapcraft-wekan-f82a93c2…_s390x_2026-08-09T11:39:05.txt
|
||||
// and upload-artifact REFUSES a colon, because NTFS cannot hold one. That
|
||||
// failed the whole job for s390x, ppc64el and riscv64 in v10.77 AFTER all
|
||||
// three snaps had built and downloaded on attempt 1 - "Remote build s390x
|
||||
// succeeded on attempt 1" is in the same log as "FAILED snap-launchpad
|
||||
// s390x".
|
||||
const src = read(WORKFLOW);
|
||||
const step = src.slice(src.indexOf('mkdir -p snap-logs'));
|
||||
const upTo = step.slice(0, step.indexOf('- name: Push'));
|
||||
assert.ok(/\$\{base\/\/:\/-\}/.test(upTo),
|
||||
'the colons in the log name must be replaced, or the upload refuses the file');
|
||||
assert.ok(!/cp -f snapcraft-wekan-\*\.txt snap-logs\/\s/.test(upTo),
|
||||
'the plain copy that kept the colons is gone');
|
||||
});
|
||||
|
||||
test('and that upload can never fail the job it is diagnosing', () => {
|
||||
const src = read(WORKFLOW);
|
||||
const i = src.indexOf('name: snap-launchpad-logs-');
|
||||
assert.ok(i > 0, 'the log upload step is still there');
|
||||
// Look back at the step this `with:` belongs to.
|
||||
const step = src.slice(src.lastIndexOf('- uses: actions/upload-artifact', i), i);
|
||||
assert.ok(/continue-on-error:\s*true/.test(step),
|
||||
'the snap is the deliverable; saving its log must not be able to fail it');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
// A snap stage-package must be a package that REALLY EXISTS on every
|
||||
// architecture the snap is built for - not one that merely resolves on most of
|
||||
// them.
|
||||
//
|
||||
// Ubuntu 24.04's 64-bit time_t transition renamed a set of libraries to a `t64`
|
||||
// suffix. On the 64-bit architectures the renamed package keeps the old name
|
||||
// alive:
|
||||
//
|
||||
// Package: libcurl4t64
|
||||
// Architecture: amd64
|
||||
// Provides: libcurl4 (= 8.5.0-2ubuntu10)
|
||||
//
|
||||
// but on **armhf**, where the ABI genuinely changed, there is no Provides line
|
||||
// at all. So the old name resolves everywhere except armhf, and the snap fails
|
||||
// on exactly one architecture, before it compiles anything:
|
||||
//
|
||||
// Stage package not found in part 'mongodb': libcurl4.
|
||||
// Build failed
|
||||
//
|
||||
// That is a Launchpad "Stopped" build with no artifact, which looks like the
|
||||
// build farm cancelling a job - so it reads as flakiness and gets retried. It
|
||||
// was retried three times in v10.77 while s390x, ppc64el and riscv64 all built
|
||||
// on attempt 1.
|
||||
//
|
||||
// It had already happened once, to libssl3 and libgoogle-perftools4 in v10.71.
|
||||
// They were fixed; libcurl4 was the same transition and was left behind. This
|
||||
// test is here so the third one is caught by a test run instead of by a release.
|
||||
//
|
||||
// Re-check the list against the archive with:
|
||||
// curl -fsSL http://ports.ubuntu.com/ubuntu-ports/dists/noble/main/binary-armhf/Packages.gz \
|
||||
// | gunzip -c | grep -E '^Package: ' | sed 's/^Package: //' | sort -u
|
||||
// (and the same for universe, and archive.ubuntu.com for amd64)
|
||||
//
|
||||
// Run: node tests/snapStagePackages.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const yaml = fs.readFileSync(path.join(repoRoot, 'snapcraft.yaml'), 'utf8');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// Every package named under a `stage-packages:` block.
|
||||
function stagePackages() {
|
||||
const out = [];
|
||||
let inBlock = false;
|
||||
yaml.split('\n').forEach(line => {
|
||||
if (/^\s*stage-packages:\s*$/.test(line)) { inBlock = true; return; }
|
||||
if (!inBlock) return;
|
||||
const m = line.match(/^\s*-\s+([a-z0-9][a-z0-9.+-]*)\s*$/);
|
||||
if (m) { out.push(m[1]); return; }
|
||||
if (line.trim() && !line.trim().startsWith('#')) inBlock = false;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// The noble t64 renames that bite here: OLD NAME -> what noble actually ships.
|
||||
// Each of these exists ONLY as the t64 package; the old name is a Provides on
|
||||
// the 64-bit architectures and nothing at all on armhf. Verified against the
|
||||
// noble main+universe indices for armhf and arm64.
|
||||
const T64_RENAMED = {
|
||||
'libssl3': 'libssl3t64',
|
||||
'libcurl4': 'libcurl4t64',
|
||||
'libcurl3-gnutls': 'libcurl3t64-gnutls',
|
||||
'libgoogle-perftools4': 'libgoogle-perftools4t64',
|
||||
'libboost-filesystem1.83.0': null, // NOT renamed - real on armhf
|
||||
};
|
||||
|
||||
test('snapcraft.yaml declares stage-packages at all', () => {
|
||||
const pkgs = stagePackages();
|
||||
assert.ok(pkgs.length >= 10,
|
||||
`expected the mongodb part's package list, found ${pkgs.length}`);
|
||||
});
|
||||
|
||||
test('THE BUG: no stage-package is a pre-t64 name armhf cannot resolve', () => {
|
||||
const pkgs = stagePackages();
|
||||
const broken = pkgs.filter(p => T64_RENAMED[p]);
|
||||
assert.deepStrictEqual(broken, [],
|
||||
'these resolve on the 64-bit arches through a Provides and fail on armhf; ' +
|
||||
'use ' + broken.map(p => `${p} -> ${T64_RENAMED[p]}`).join(', '));
|
||||
});
|
||||
|
||||
test('libcurl is asked for by the name noble actually publishes', () => {
|
||||
const pkgs = stagePackages();
|
||||
assert.ok(pkgs.includes('libcurl4t64'),
|
||||
'the mongodb part still needs libcurl');
|
||||
assert.ok(!pkgs.includes('libcurl4'),
|
||||
'libcurl4 is not a real package on any noble architecture');
|
||||
});
|
||||
|
||||
test('the two that were fixed in v10.71 have not regressed', () => {
|
||||
const pkgs = stagePackages();
|
||||
assert.ok(pkgs.includes('libssl3t64'));
|
||||
assert.ok(pkgs.includes('libgoogle-perftools4t64'));
|
||||
assert.ok(!pkgs.includes('libssl3'));
|
||||
assert.ok(!pkgs.includes('libgoogle-perftools4'));
|
||||
});
|
||||
|
||||
test('NEGATIVE: a name that was never renamed is left alone', () => {
|
||||
// The rule is not "add t64 to everything". libboost-filesystem1.83.0,
|
||||
// zlib1g, libstemmer0d and the rest are real packages on armhf under their
|
||||
// plain names, and a t64 suffix on them would not exist.
|
||||
const pkgs = stagePackages();
|
||||
['zlib1g', 'libstemmer0d', 'libsnappy1v5', 'libpcre2-8-0']
|
||||
.forEach(p => assert.ok(pkgs.includes(p), `${p} must keep its plain name`));
|
||||
pkgs.forEach(p => {
|
||||
if (p.endsWith('t64')) {
|
||||
assert.ok(Object.values(T64_RENAMED).includes(p),
|
||||
`${p} carries a t64 suffix that is not one of the known renames - ` +
|
||||
'check it exists before trusting it');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('the reason is written down where the next person will read it', () => {
|
||||
// A bare rename is a mystery in six months; this one has now cost two
|
||||
// releases, and the comment is what stops it costing a third.
|
||||
const block = yaml.slice(0, yaml.indexOf('- libcurl4t64'));
|
||||
const near = block.slice(-2500);
|
||||
assert.ok(/Provides/.test(near), 'the Provides mechanism is explained');
|
||||
assert.ok(/armhf/.test(near), 'and which architecture it breaks');
|
||||
assert.ok(/Stage package not found/.test(near),
|
||||
'and the error it produces, so a search for it lands here');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
Reference in new issue
Block a user