mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
149944726e | ||
|
|
bf0b757862 | ||
|
|
1c7de782f0 | ||
|
|
a820b056b2 | ||
|
|
6dce9dda79 | ||
|
|
47c81cd478 | ||
|
|
bc426ba7c2 | ||
|
|
a0f445c723 | ||
|
|
ce69db8c5f | ||
|
|
5b31695343 | ||
|
|
4a8fbd954e | ||
|
|
04fb6e3381 | ||
|
|
5957fa0ad8 | ||
|
|
97e57e531b | ||
|
|
fa296d0c80 | ||
|
|
040c18bc23 | ||
|
|
7241570491 | ||
|
|
979ad3a5cd | ||
|
|
3c9291338c | ||
|
|
a5d8a1fd97 |
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} .
|
||||
|
||||
+439
@@ -264,6 +264,445 @@ 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.
|
||||
The helper that put a build on channels released ONE snap, ONE revision, to
|
||||
THREE channels - and a revision number is per architecture, so it could only
|
||||
ever
|
||||
be right for one of them. The page documenting the CPU platforms listed five
|
||||
architectures and omitted **armhf**, which has been built all along. And the two
|
||||
architectures that are release bundles but NOT snaps - **i386** and **armv7** -
|
||||
were nowhere, so "missing" and "cannot be there" looked identical. The binaries
|
||||
below are v10.76's: nothing here rebuilds them.
|
||||
|
||||
| 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.45.0/ferretdb-amd64) | v1.45.0 | `94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893` |
|
||||
| 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.45.0/ferretdb-arm64) | v1.45.0 | `275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b` |
|
||||
| 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.45.0/ferretdb-loong64) | v1.45.0 | `28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0` |
|
||||
| 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.45.0/ferretdb-mac-arm64) | v1.45.0 | `639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e` |
|
||||
| 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.45.0/ferretdb-mac-amd64) | v1.45.0 | `fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3` |
|
||||
| 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.45.0/ferretdb-ppc64le) | v1.45.0 | `de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a` |
|
||||
| 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.45.0/ferretdb-riscv64) | v1.45.0 | `7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9` |
|
||||
| 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.45.0/ferretdb-s390x) | v1.45.0 | `0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1` |
|
||||
| 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.45.0/ferretdb-win64.exe) | v1.45.0 | `f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85` |
|
||||
|
||||
This release adds the following new feature:
|
||||
|
||||
**Snap publishing** - getting every snap onto every channel without typing a
|
||||
revision number.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/a5d8a1fd9">Release every snap, every architecture, to all four channels</a>. Thanks to xet7.</summary>
|
||||
|
||||
The helper this replaces was `snapcraft release wekan $1 edge,beta,candidate`,
|
||||
which is wrong three ways at once. It names only `wekan`, leaving `wekan-ondra`
|
||||
and `wekan-gantt-gpl` to be done by hand. It takes ONE revision number, and
|
||||
revisions are PER ARCHITECTURE - the store shows `wekan` at 3601 on amd64 and
|
||||
3600 on arm64 for the same 10.76 - so one number can only ever be right for one
|
||||
of them. And it leaves out `stable`, so a build reached three channels of four
|
||||
and somebody had to remember the fourth.
|
||||
|
||||
`releases/snap-release-all-channels.sh` resolves the revision per (snap,
|
||||
architecture) from the store itself, so no revision number is ever typed, and
|
||||
releases it to all four channels in ONE call - a revision reaches all of them or
|
||||
none. A pair with no revision is reported and skipped rather than failing the
|
||||
run: the three snaps genuinely have different architecture sets today.
|
||||
|
||||
The mapping is the hazard, and it lives in one place now
|
||||
(`models/lib/snapArchitectures.js`) with both directions tested. `ppc64le` and
|
||||
`ppc64el` ARE the same hardware - the bundles use the kernel's name, the store
|
||||
uses Debian's - and it is the only rename. Nothing warns when the wrong one is
|
||||
used: an unrecognised architecture is simply one the store has never heard of,
|
||||
so it looks like it worked.
|
||||
|
||||
Pass the version to pin it. Without one the newest revision of each architecture
|
||||
is promoted, and edge is often ahead of stable, so a bare run publishes edge
|
||||
builds to stable users; `--dry-run` prints the plan first.
|
||||
|
||||
</details>
|
||||
|
||||
and improves the following documentation:
|
||||
|
||||
**Snap CPU platforms** - which six, why not the other two, and how the names
|
||||
differ.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/3c9291338">Say which six architectures are snaps, and why i386 and armv7 are not</a>. Thanks to xet7.</summary>
|
||||
|
||||
`docs/Platforms/FOSS/Container/Snap/CPU-platforms.md` listed five architectures
|
||||
and omitted **armhf**, which `snapcraft.yaml` has built all along. It said the
|
||||
release publishes candidate, beta and edge and that stable "is published
|
||||
manually
|
||||
later" - no longer true, and the reason a build reached three channels of four.
|
||||
|
||||
The matrix is now the six `build-for:` entries with the bundle name beside each,
|
||||
and a new section explains the three ways the two naming systems differ.
|
||||
|
||||
**armhf and armv7 are not a rename**, and getting it wrong ships a snap that
|
||||
crashes. [node-patches](https://github.com/wekan/node-patches) builds `armhf` to
|
||||
the Debian baseline - hard-float, VFPv3-D16, assuming no NEON - so it runs on
|
||||
any
|
||||
ARMv7-A, and `armv7` with NEON for boards that have it. The Snap Store has ONE
|
||||
32-bit ARM architecture serving every such device, so it must carry the BASELINE
|
||||
build: the NEON one would be an illegal instruction on a board without NEON. So
|
||||
armv7 ships as a bundle only, and a test cross-checks that explanation against
|
||||
node-patches' own workflow so it cannot drift from the binaries.
|
||||
|
||||
**i386 cannot have a new snap at all**, and it is categorically different from a
|
||||
missing Node.js build. node-patches patches SOURCE so a binary can be built;
|
||||
here
|
||||
the BASE SNAP does not exist, because Ubuntu 24.04 has no i386 port - no patch
|
||||
set produces a base Canonical does not publish. The last base with one was
|
||||
`core18`, end-of-life. The store still shows an i386 column for `wekan-ondra`
|
||||
because it keeps whatever was ever uploaded; that revision is `0.X-ci` and
|
||||
nothing can replace it.
|
||||
|
||||
The page also records what each snap has in the store today and what is still to
|
||||
upload, including the two fossils channel promotion cannot fix - `wekan-ondra`'s
|
||||
armhf at 0.22 and its i386 at 0.X-ci, which have no newer revision to promote.
|
||||
|
||||
</details>
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their
|
||||
translations.
|
||||
|
||||
# v10.76 2026-08-09 WeKan ® release
|
||||
|
||||
**In short:** three round-trip counts that were linear in the input, and are not
|
||||
|
||||
+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.76
|
||||
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.76.0"
|
||||
appVersion: "v10.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -303,6 +303,7 @@ if "%choice%"=="3" call :rel_run "releases/snap-push-to-store.sh" "Path to the .
|
||||
if "%choice%"=="4" call :rel_run "releases/release-snap.sh" "WeKan version, e.g. 10.50"
|
||||
if "%choice%"=="5" call :rel_run "releases/snap-store-revisions.sh" ""
|
||||
if "%choice%"=="6" call :rel_run "releases/snap-store-release-revision-to-channels.sh" "Snap Store revision number"
|
||||
if "%choice%"=="7" call :rel_run "releases/snap-release-all-channels.sh" "Version, or empty for the newest"
|
||||
if "%choice%"=="7" call :rel_run "releases/snap-edge.sh" ""
|
||||
if "%choice%"=="8" call :rel_run "releases/snap-stable.sh" ""
|
||||
if "%choice%"=="9" call :rel_run "releases/snapcraft-help.sh" ""
|
||||
@@ -560,6 +561,7 @@ echo snap-push-to-store Push one .snap to the Snap Store ^<P
|
||||
echo release-snap Release the snap for one version ^<WeKan version, e.g. 10.50^>
|
||||
echo snap-store-revisions List the newest Snap Store revisions
|
||||
echo snap-store-release-revision-to-channels Release one store revision to edge, beta and candidate ^<Snap Store revision number^>
|
||||
echo snap-release-all-channels Release every snap, every architecture, to all four channels ^<Version, or empty for the newest^>
|
||||
echo snap-edge Switch the installed snap to the edge channel
|
||||
echo snap-stable Switch the installed snap to the stable channel
|
||||
echo snapcraft-help snapcraft help topics
|
||||
@@ -642,6 +644,7 @@ if /I "%K%"=="snap-push-to-store" (set "CMD=bash releases/snap-push-to-store.sh"
|
||||
if /I "%K%"=="release-snap" (set "CMD=bash releases/release-snap.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snap-store-revisions" (set "CMD=bash releases/snap-store-revisions.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snap-store-release-revision-to-channels" (set "CMD=bash releases/snap-store-release-revision-to-channels.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snap-release-all-channels" (set "CMD=bash releases/snap-release-all-channels.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snap-edge" (set "CMD=bash releases/snap-edge.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snap-stable" (set "CMD=bash releases/snap-stable.sh" ^& goto cli_go)
|
||||
if /I "%K%"=="snapcraft-help" (set "CMD=bash releases/snapcraft-help.sh" ^& goto cli_go)
|
||||
|
||||
@@ -1873,6 +1873,7 @@ RELEASE_SCRIPTS=( "Release|Release ALL platforms: push CHANGELOG, trigger releas
|
||||
"Snap|Release the snap for one version|releases/release-snap.sh|WeKan version, e.g. 10.50||"
|
||||
"Snap|List the newest Snap Store revisions|releases/snap-store-revisions.sh|||"
|
||||
"Snap|Release one store revision to edge, beta and candidate|releases/snap-store-release-revision-to-channels.sh|Snap Store revision number||"
|
||||
"Snap|Release every snap, every architecture, to all four channels|releases/snap-release-all-channels.sh|Version, or empty for the newest||snap-release-all-channels"
|
||||
"Snap|Switch the installed snap to the edge channel|releases/snap-edge.sh|||"
|
||||
"Snap|Switch the installed snap to the stable channel|releases/snap-stable.sh|||"
|
||||
"Snap|snapcraft help topics|releases/snapcraft-help.sh|||"
|
||||
|
||||
@@ -9,9 +9,13 @@ and how the one-time MongoDB 3 → FerretDB v1 migration works.
|
||||
- **New installs use FerretDB v1 (SQLite) by default on every CPU architecture.**
|
||||
FerretDB v1 is the wekan/FerretDB fork with an embedded pure-Go SQLite backend
|
||||
that speaks the MongoDB wire protocol, so WeKan talks to it exactly like MongoDB.
|
||||
- **Six architectures are built as snaps**: amd64, arm64, armhf, ppc64el, riscv64
|
||||
and s390x — exactly `snapcraft.yaml`'s `build-for:` list. i386 and armv7 are
|
||||
release bundles but **not** snaps, for reasons that are not oversights; see
|
||||
[CPU architecture names](#cpu-architecture-names-and-what-they-mean).
|
||||
- **MongoDB 7 *server* binaries (mongod) are included for amd64 and arm64 only.**
|
||||
The other architectures (s390x, riscv64, ppc64le) have no MongoDB server — they
|
||||
run FerretDB v1 only.
|
||||
The other architectures (armhf, ppc64el, riscv64, s390x) have no MongoDB server
|
||||
— they run FerretDB v1 only.
|
||||
- **Every Snap platform includes the FerretDB v1 server and the MongoDB Database
|
||||
Tools** (`mongodump`, `mongorestore`, `mongofiles`, `mongoexport`, …), on every
|
||||
architecture.
|
||||
@@ -30,13 +34,20 @@ and how the one-time MongoDB 3 → FerretDB v1 migration works.
|
||||
|
||||
## Per-architecture matrix
|
||||
|
||||
| CPU arch | MongoDB 7 server (mongod) | MongoDB Database Tools | FerretDB v1 server | MongoDB 3 → FerretDB migration | Default DB (new install) |
|
||||
|----------|:------------------------:|:----------------------:|:------------------:|:------------------------------:|:------------------------:|
|
||||
| amd64 | ✅ | ✅ | ✅ | ✅ (if MongoDB 3 data present) | FerretDB v1 |
|
||||
| arm64 | ✅ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| s390x | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| ppc64le | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| riscv64 | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| Snap arch | Bundle | MongoDB 7 server (mongod) | MongoDB Database Tools | FerretDB v1 server | MongoDB 3 → FerretDB migration | Default DB (new install) |
|
||||
|-----------|--------|:------------------------:|:----------------------:|:------------------:|:------------------------------:|:------------------------:|
|
||||
| amd64 | `amd64` | ✅ | ✅ | ✅ | ✅ (if MongoDB 3 data present) | FerretDB v1 |
|
||||
| arm64 | `arm64` | ✅ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| armhf | `armhf` | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| ppc64el | `ppc64le` | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| riscv64 | `riscv64` | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
| s390x | `s390x` | ❌ | ✅ | ✅ | ❌ | FerretDB v1 |
|
||||
|
||||
These six are exactly the `build-for:` entries in `snapcraft.yaml`. The **Bundle**
|
||||
column is the release-asset name at
|
||||
[github.com/wekan/wekan/releases](https://github.com/wekan/wekan/releases), which
|
||||
is not always the Snap Store's name for the same hardware — see
|
||||
[CPU architecture names](#cpu-architecture-names-and-what-they-mean).
|
||||
|
||||
- **MongoDB 7 server (mongod)**: amd64/arm64 only (MongoDB ships no server for the others).
|
||||
- **MongoDB Database Tools**: every arch — WeKan builds them for all platforms from
|
||||
@@ -47,6 +58,60 @@ and how the one-time MongoDB 3 → FerretDB v1 migration works.
|
||||
All architectures default new installs to FerretDB v1 with `DDP_TRANSPORT=sockjs`
|
||||
and polling reactivity.
|
||||
|
||||
## CPU architecture names and what they mean
|
||||
|
||||
An architecture can go by one name in the release bundles and another in the Snap
|
||||
Store, and two names that look like variants of each other can be different
|
||||
builds. Nothing warns about either: an unrecognised `--arch` is simply an
|
||||
architecture the store has never heard of, so a wrong name looks like it worked.
|
||||
The mapping is kept in one place, `models/lib/snapArchitectures.js`, and is unit
|
||||
tested (`tests/snapArchitectures.test.cjs`).
|
||||
|
||||
### ppc64le and ppc64el are the same hardware
|
||||
|
||||
64-bit little-endian POWER. The bundles name it the way Node.js and the kernel do
|
||||
(`ppc64le`); the Snap Store names it the way Debian does (`ppc64el`). It is the
|
||||
**only** architecture whose two names differ, and a test asserts that stays true.
|
||||
|
||||
### armhf and armv7 are the same CPU family, built to different baselines
|
||||
|
||||
This one is not a naming difference, and getting it wrong ships a snap that
|
||||
crashes. [wekan/node-patches](https://github.com/wekan/node-patches) builds two
|
||||
32-bit ARM Node.js binaries:
|
||||
|
||||
| Bundle | Built for | Runs on |
|
||||
|--------|-----------|---------|
|
||||
| `armhf` | hard-float, **VFPv3-D16**, no NEON assumed — the Debian armhf baseline | **any** ARMv7-A board |
|
||||
| `armv7` | hard-float, **NEON**, ARMv7-A tuned | only boards that have NEON (an ODroid-U3, say) |
|
||||
|
||||
The Snap Store has **one** 32-bit ARM architecture, `armhf`, and it serves every
|
||||
such device. It must therefore carry the **baseline** build: the NEON one would
|
||||
be an illegal instruction on any armhf board without NEON. So the armhf snap uses
|
||||
the `armhf` bundle, and **`armv7` has no snap** — it ships as a bundle only. A
|
||||
separate snap name or track would be the only way to offer the NEON build, which
|
||||
is not worth a second listing for one board family.
|
||||
|
||||
### i386 has no snap, and cannot have a new one
|
||||
|
||||
core24 is Ubuntu 24.04, which has **no i386 port**, so there is no i386 core24
|
||||
base snap. `build-on: i386` is not merely unsupported — it is a **parse error**,
|
||||
and because snapcraft parses the whole file first it failed *every* architecture's
|
||||
build, not only i386's. This is not something a patch set can fix the way
|
||||
[node-patches](https://github.com/wekan/node-patches) fixes a missing Node.js
|
||||
build: node-patches patches source so a binary *can* be built, whereas here the
|
||||
**base snap itself does not exist** for the architecture. The last base with a
|
||||
real i386 port was `core18`, which is end-of-life.
|
||||
|
||||
The Snap Store still shows an i386 column for **wekan-ondra** (revision 70,
|
||||
version `0.X-ci`) because the store keeps whatever was ever uploaded. It is years
|
||||
stale, nothing new can be built for it, and re-releasing it would only re-publish
|
||||
`0.X-ci`. **i386 users are served by the .deb and the AppImage.**
|
||||
|
||||
### loong64, win64, win32, mac-x64, mac-arm64
|
||||
|
||||
Bundle platforms with no Snap Store architecture at all. They are not snaps and
|
||||
are not expected to be.
|
||||
|
||||
## New installs
|
||||
|
||||
On a fresh install, every architecture starts on **FerretDB v1 (SQLite)** with
|
||||
@@ -151,18 +216,82 @@ bundle the snap's `wekan` part downloads, and staged to `$SNAP/ferretdb`
|
||||
`<tool>-<arch>` from [wekan/mongo-tools-patches](https://github.com/wekan/mongo-tools-patches/releases),
|
||||
and `mongosh-<arch>` from [wekan/mongosh](https://github.com/wekan/mongosh/releases).
|
||||
|
||||
## Snap channels
|
||||
## Snaps and channels
|
||||
|
||||
The default `snapcraft.yaml` is built on `base: core24`, which allows the
|
||||
**candidate** channel (core26 does not). The automated release workflow publishes
|
||||
it to the **candidate**, **beta**, and **edge** channels; the **stable** channel is
|
||||
published manually later, once it is proven.
|
||||
`snapcraft-core26.yaml` is the same WeKan on the newer `base: core26`
|
||||
(`build-base: devel`, `grade: devel`), kept for testing the next base; it can only
|
||||
go to Beta/Edge until core26 is a released stable base.
|
||||
There are **three snaps** of the same application, and each should reach **all
|
||||
four channels on every architecture it is built for**:
|
||||
|
||||
| Snap | What it is |
|
||||
|------|------------|
|
||||
| [`wekan`](https://snapcraft.io/wekan) | the default |
|
||||
| [`wekan-ondra`](https://snapcraft.io/wekan-ondra) | same WeKan, separate listing |
|
||||
| [`wekan-gantt-gpl`](https://snapcraft.io/wekan-gantt-gpl) | same WeKan, separate listing |
|
||||
|
||||
**All four channels — Stable, Candidate, Beta and Edge.** The default
|
||||
`snapcraft.yaml` is built on `base: core24`, a *released* base, so the snap carries
|
||||
`grade: stable` and may be published to **stable** as well as the other three.
|
||||
That is what makes the full set possible: a `grade: devel` snap is refused by
|
||||
stable and candidate outright.
|
||||
|
||||
`snapcraft-core26.yaml` is the same WeKan on `base: core26`, kept for testing the
|
||||
next base and **not** built by the release: core26 still requires
|
||||
`build-base: devel` and therefore `grade: devel`, so it can only reach beta and
|
||||
edge. That is the whole reason the release builds core24.
|
||||
|
||||
### Publishing to every channel
|
||||
|
||||
A revision is **per architecture** — the store's own listing shows `wekan` at
|
||||
revision 3601 on amd64 and 3600 on arm64 for the same version 10.76 — so one
|
||||
revision number can never be released to every architecture. Use:
|
||||
|
||||
```bash
|
||||
releases/snap-release-all-channels.sh --dry-run # see the plan first
|
||||
releases/snap-release-all-channels.sh 10.76 # pin the version
|
||||
releases/snap-release-all-channels.sh # or the newest of each
|
||||
```
|
||||
|
||||
It resolves the revision per (snap, architecture) from the store and releases it
|
||||
to `stable,candidate,beta,edge` in **one** call, so a revision reaches all four
|
||||
or none. A (snap, architecture) with no revision is reported and skipped — the
|
||||
three snaps genuinely have different architecture sets today. It reads the
|
||||
architecture table from `models/lib/snapArchitectures.js`, so there is no second
|
||||
copy of it in shell.
|
||||
|
||||
**Pinning the version matters.** Without one, the newest revision of each
|
||||
architecture is promoted — and edge is often ahead of stable, so a bare run
|
||||
publishes edge builds to stable users. Pass the version, or `--dry-run` first.
|
||||
|
||||
### What each snap currently has, and what is still to add
|
||||
|
||||
As of WeKan 10.76 the store holds:
|
||||
|
||||
| Snap | amd64 | arm64 | armhf | ppc64el | riscv64 | s390x |
|
||||
|------|:-----:|:-----:|:-----:|:-------:|:-------:|:-----:|
|
||||
| `wekan` | ✅ | ✅ | — | ✅ | — | ✅ |
|
||||
| `wekan-ondra` | ✅ | ✅ | ⚠️ | — | — | — |
|
||||
| `wekan-gantt-gpl` | ✅ | ✅ | — | — | — | — |
|
||||
|
||||
- **—** the architecture is built by `snapcraft.yaml` but that snap has no
|
||||
revision for it yet. Uploading one is all that is needed; the release script
|
||||
then puts it on all four channels.
|
||||
- **⚠️** `wekan-ondra`'s armhf is stuck at version **0.22**, and its i386 column at
|
||||
**0.X-ci**, on all four channels — fossils the store kept from years ago.
|
||||
Promoting channels cannot fix these: there is no newer revision to promote.
|
||||
armhf needs a build uploaded for that snap; i386 cannot be built at all (see
|
||||
[i386](#i386-has-no-snap-and-cannot-have-a-new-one)).
|
||||
|
||||
Publishing the **stable** channel is safe for these because the snap performs its
|
||||
**automatic migrations** on start — the schema upgrades and the one-time
|
||||
MongoDB 3 → FerretDB v1 migration on amd64 — so an existing installation moving
|
||||
from an older revision to this one is upgraded in place rather than needing a
|
||||
manual step.
|
||||
|
||||
## Related
|
||||
|
||||
- FerretDB v1 fork: https://github.com/wekan/FerretDB
|
||||
- Snap install: [Install.md](Install.md)
|
||||
- Snap settings keys: [Supported-settings-keys.md](Supported-settings-keys.md)
|
||||
- The architecture table in code: `models/lib/snapArchitectures.js`
|
||||
(tested by `tests/snapArchitectures.test.cjs`)
|
||||
- Publish every snap to every channel: `releases/snap-release-all-channels.sh`
|
||||
- Node.js builds per platform: [wekan/node-patches](https://github.com/wekan/node-patches)
|
||||
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-10.76-win64.zip](https://github.com/wekan/wekan/releases/download/v10.76/wekan-10.76-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.76-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)
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Snap names, channels, and the two names every architecture has
|
||||
// ----------------------------------------------------------------------------
|
||||
// WeKan publishes three snaps of the same application - `wekan`, and the
|
||||
// `wekan-ondra` and `wekan-gantt-gpl` variants - and each must reach every
|
||||
// architecture on every channel.
|
||||
//
|
||||
// Two things make this less obvious than "three names times eight arches".
|
||||
//
|
||||
// 1. ppc64le and ppc64el are the SAME hardware under two names. The release
|
||||
// bundles at https://github.com/wekan/wekan are named the way Node.js and the
|
||||
// kernel name things (`ppc64le`); the Snap Store names it the way Debian does
|
||||
// (`ppc64el`). Nothing warns when the wrong one is used - an unrecognised
|
||||
// --arch is simply an architecture the store has never heard of - so the
|
||||
// rename is written down once, here.
|
||||
//
|
||||
// 2. armhf and armv7 are NOT two names for one thing, and must never be mapped
|
||||
// onto each other. They are the same CPU family built to different
|
||||
// baselines: node-patches builds `armhf` to the Debian armhf baseline
|
||||
// (hard-float, VFPv3-D16, assuming NO NEON) so it runs on any ARMv7-A, and
|
||||
// `armv7` with NEON for boards that have it. The Snap Store has ONE 32-bit
|
||||
// ARM architecture serving every such device, so it must carry the BASELINE
|
||||
// build; the NEON one would be an illegal instruction on a board without
|
||||
// NEON. armv7 therefore has no snap - see NOT_SNAP_ARCHITECTURES below.
|
||||
//
|
||||
// The list is therefore the six architectures snapcraft.yaml actually declares a
|
||||
// `build-for` for, not the eight the release bundles cover. Both directions are
|
||||
// unit tested (tests/snapArchitectures.test.cjs).
|
||||
//
|
||||
// Pure and dependency-free: the shell scripts read it through node, so there is
|
||||
// no second copy of this table in bash.
|
||||
// ============================================================================
|
||||
|
||||
// The three snaps. Same application, three store listings.
|
||||
const SNAP_NAMES = ['wekan', 'wekan-ondra', 'wekan-gantt-gpl'];
|
||||
|
||||
// Every channel a revision is released to. `stable` is included deliberately:
|
||||
// the old helper released to `edge,beta,candidate` only, so a build reached
|
||||
// three of the four and somebody had to remember the fourth by hand.
|
||||
const SNAP_CHANNELS = ['stable', 'candidate', 'beta', 'edge'];
|
||||
|
||||
// One row per architecture the Snap Store gets, matching snapcraft.yaml's
|
||||
// `build-for` list exactly. `bundle` is null where the release bundle goes by
|
||||
// the same name - all but one - so the single rename stands out.
|
||||
const ARCHITECTURES = [
|
||||
{ snap: 'amd64', bundle: null },
|
||||
{ snap: 'arm64', bundle: null },
|
||||
{ snap: 'armhf', bundle: null }, // 32-bit Raspberry Pi OS
|
||||
{ snap: 'ppc64el', bundle: 'ppc64le' }, // the store spells POWER the Debian way
|
||||
{ snap: 'riscv64', bundle: null },
|
||||
{ snap: 's390x', bundle: null },
|
||||
];
|
||||
|
||||
// Architectures WeKan SHIPS but does not BUILD a snap for, and why.
|
||||
//
|
||||
// "Does not build" is not the same as "the store has no column": the store keeps
|
||||
// whatever was ever uploaded, so an architecture can appear there with an
|
||||
// ancient revision on every channel long after it stopped being buildable.
|
||||
// That is what makes these worth writing down rather than inferring from the
|
||||
// store's own listing.
|
||||
// Written down because "it is missing" and "it cannot be there" look identical
|
||||
// from the outside, and somebody re-adding one of these would be repeating a
|
||||
// mistake that has already been made.
|
||||
const NOT_SNAP_ARCHITECTURES = {
|
||||
i386:
|
||||
'core24 (Ubuntu 24.04) has no i386 port, so snapcraft rejects `build-on: i386` - ' +
|
||||
'and because that is a PARSE error it failed EVERY architecture\'s snap build, not ' +
|
||||
'only i386\'s. The STORE still holds old i386 revisions (wekan-ondra has one, at ' +
|
||||
'version 0.X-ci, sitting on all four channels), so the column exists there and is ' +
|
||||
'years stale; nothing new can be built for it, and re-releasing what is there would ' +
|
||||
'only re-publish 0.X-ci. i386 users are served by the .deb and the AppImage.',
|
||||
armv7:
|
||||
'the same CPU family as armhf, tuned differently: node-patches builds armhf to the ' +
|
||||
'Debian armhf baseline (hard-float, VFPv3-D16, NO NEON assumed) so it runs on any ' +
|
||||
'ARMv7-A, and armv7 with NEON for boards that have it (an ODroid-U3, say). The Snap ' +
|
||||
'Store has ONE 32-bit ARM architecture and it serves every such device, so it must ' +
|
||||
'carry the BASELINE build - shipping the NEON one would be an illegal instruction on ' +
|
||||
'any armhf board without NEON. armv7 ships as a bundle only.',
|
||||
};
|
||||
|
||||
/** Every Snap Store architecture, in a stable order. */
|
||||
function snapArchitectures() {
|
||||
return ARCHITECTURES.map(a => a.snap);
|
||||
}
|
||||
|
||||
/** Every release-bundle architecture name, in the same order. */
|
||||
function bundleArchitectures() {
|
||||
return ARCHITECTURES.map(a => a.bundle || a.snap);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Snap Store's name for a bundle architecture.
|
||||
*
|
||||
* Accepts either spelling, so a caller that already has the store's name is not
|
||||
* punished for it - `snapArchOf('ppc64el')` is `ppc64el`. Unknown input returns
|
||||
* '' rather than being passed through: an architecture nobody recognises must
|
||||
* not reach `snapcraft release`, where it would be a silent no-op.
|
||||
*
|
||||
* @param {string} name
|
||||
* @return {string} the Snap Store architecture, or '' when it is not one
|
||||
*/
|
||||
function snapArchOf(name) {
|
||||
if (typeof name !== 'string' || !name) return '';
|
||||
const key = name.trim().toLowerCase();
|
||||
|
||||
const row = ARCHITECTURES.find(a => a.snap === key || a.bundle === key);
|
||||
if (row) return row.snap;
|
||||
|
||||
// Names the same hardware also goes by upstream. armv7/armv7l is NOT here:
|
||||
// it is a different CPU from armhf, not another spelling of it.
|
||||
const ALIASES = { x64: 'amd64', 'x86_64': 'amd64', aarch64: 'arm64' };
|
||||
return ALIASES[key] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The release bundle's name for an architecture. Same tolerance in reverse.
|
||||
* @param {string} name
|
||||
* @return {string} the bundle architecture, or '' when it is not one
|
||||
*/
|
||||
function bundleArchOf(name) {
|
||||
const snap = snapArchOf(name);
|
||||
if (!snap) return '';
|
||||
const row = ARCHITECTURES.find(a => a.snap === snap);
|
||||
return row.bundle || row.snap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every (snap, architecture) pair that has to be released, in a stable order.
|
||||
* The channels are not part of the pair: one `snapcraft release` call takes them
|
||||
* all at once, so a revision reaches all four or none.
|
||||
* @return {Array<{snap: string, arch: string, bundleArch: string}>}
|
||||
*/
|
||||
function releaseTargets() {
|
||||
const targets = [];
|
||||
SNAP_NAMES.forEach(snap => {
|
||||
ARCHITECTURES.forEach(a => {
|
||||
targets.push({ snap, arch: a.snap, bundleArch: a.bundle || a.snap });
|
||||
});
|
||||
});
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the revision to release for one architecture out of `snapcraft
|
||||
* list-revisions` rows.
|
||||
*
|
||||
* Revisions are PER ARCHITECTURE - amd64 revision 42 and arm64 revision 42 are
|
||||
* different uploads - which is why one revision number can never be released to
|
||||
* every architecture at once, and why this exists at all.
|
||||
*
|
||||
* The highest revision wins, not the first row: the store's ordering is not
|
||||
* something to depend on. When `version` is given, only that version's rows are
|
||||
* considered, so a release publishes what it built rather than whatever was
|
||||
* uploaded last.
|
||||
*
|
||||
* @param {Array<{revision: number, arch: string, version: string}>} rows
|
||||
* @param {string} arch a Snap Store architecture
|
||||
* @param {string} [version]
|
||||
* @return {number|null}
|
||||
*/
|
||||
function pickRevision(rows, arch, version) {
|
||||
if (!Array.isArray(rows)) return null;
|
||||
const want = snapArchOf(arch);
|
||||
if (!want) return null;
|
||||
|
||||
const candidates = rows.filter(r => {
|
||||
if (!r || snapArchOf(r.arch) !== want) return false;
|
||||
if (!Number.isFinite(Number(r.revision))) return false;
|
||||
if (version && String(r.version) !== String(version)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!candidates.length) return null;
|
||||
return candidates.reduce((best, r) => Math.max(best, Number(r.revision)), -Infinity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `snapcraft list-revisions` output.
|
||||
*
|
||||
* Its columns are `Rev. Uploaded Arch Version Channels`, the header is skipped,
|
||||
* and a revision with no channels has an empty last column - so the parse keys
|
||||
* on the leading revision number and takes the next three fields, rather than
|
||||
* counting columns from the right.
|
||||
*
|
||||
* @param {string} text
|
||||
* @return {Array<{revision: number, uploaded: string, arch: string, version: string, channels: string}>}
|
||||
*/
|
||||
function parseRevisions(text) {
|
||||
if (typeof text !== 'string') return [];
|
||||
|
||||
return text
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => /^\d+\s/.test(line))
|
||||
.map(line => {
|
||||
const [revision, uploaded, arch, version, ...rest] = line.split(/\s+/);
|
||||
return {
|
||||
revision: Number(revision),
|
||||
uploaded,
|
||||
arch,
|
||||
version,
|
||||
channels: rest.join(' '),
|
||||
};
|
||||
})
|
||||
.filter(r => r.arch && r.version);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SNAP_NAMES,
|
||||
SNAP_CHANNELS,
|
||||
ARCHITECTURES,
|
||||
NOT_SNAP_ARCHITECTURES,
|
||||
snapArchitectures,
|
||||
bundleArchitectures,
|
||||
snapArchOf,
|
||||
bundleArchOf,
|
||||
releaseTargets,
|
||||
pickRevision,
|
||||
parseRevisions,
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v10.76.0",
|
||||
"version": "v10.78.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wekan",
|
||||
"version": "v10.76.0",
|
||||
"version": "v10.78.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v10.76.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.76</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.76<!-- -->)</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.76","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.76
|
||||
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 "| --- | --- | --- | --- | --- | --- |"
|
||||
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# snap-release-all-channels.sh - put every snap, on every architecture, on every
|
||||
# channel.
|
||||
#
|
||||
# WeKan publishes THREE snaps of the same application - wekan, wekan-ondra and
|
||||
# wekan-gantt-gpl - across SIX Snap Store architectures, and each has to reach
|
||||
# FOUR channels. That is 72 (snap, architecture, channel) combinations, and the
|
||||
# helper this replaces released one snap, one revision, to three channels:
|
||||
#
|
||||
# snapcraft release wekan $1 edge,beta,candidate
|
||||
#
|
||||
# which is wrong three ways at once. It names only `wekan`. It takes ONE revision
|
||||
# number, and revisions are PER ARCHITECTURE - amd64 revision 42 and arm64
|
||||
# revision 42 are different uploads - so a single number can only ever be right
|
||||
# for one of them. And it leaves out `stable`, so somebody had to remember the
|
||||
# fourth channel by hand, every time.
|
||||
#
|
||||
# This resolves the revision per (snap, architecture) from the store itself, so
|
||||
# the caller never types a revision number, and releases it to all four channels
|
||||
# in one call - a revision reaches all of them or none.
|
||||
#
|
||||
# SIX ARCHITECTURES, NOT EIGHT. The release bundles cover eight; snapcraft.yaml
|
||||
# declares a `build-for` for six, and the two missing ones are missing for
|
||||
# reasons, not by oversight:
|
||||
#
|
||||
# i386 core24 has no i386 port. `build-on: i386` is a PARSE error, which
|
||||
# failed EVERY architecture's snap build, not only i386's. Served by
|
||||
# the .deb and the AppImage instead.
|
||||
# armv7 the Snap Store has ONE 32-bit ARM architecture, `armhf`, and that is
|
||||
# the 32-bit Raspberry Pi OS build. armv7 is the ODroid-U3 - a DIFFERENT
|
||||
# CPU with its own bundle. Publishing it as armhf would hand Raspberry
|
||||
# Pi users the ODroid build, so it ships as a bundle only.
|
||||
#
|
||||
# ppc64le AND ppc64el ARE THE SAME HARDWARE. The bundles at
|
||||
# https://github.com/wekan/wekan name it the way Node.js and the kernel do
|
||||
# (ppc64le); the Snap Store names it the way Debian does (ppc64el). Nothing warns
|
||||
# when the wrong one is used - an unrecognised --arch is simply an architecture
|
||||
# the store has never heard of - so the mapping lives in ONE place,
|
||||
# models/lib/snapArchitectures.js, and this script reads it from there rather
|
||||
# than keeping a second copy in bash.
|
||||
#
|
||||
# Usage:
|
||||
# releases/snap-release-all-channels.sh # newest revision of each
|
||||
# releases/snap-release-all-channels.sh 10.75 # only that version's
|
||||
# releases/snap-release-all-channels.sh --dry-run # print, do not release
|
||||
# releases/snap-release-all-channels.sh --snap wekan # one snap only
|
||||
# releases/snap-release-all-channels.sh --arch ppc64le # one architecture (either spelling)
|
||||
#
|
||||
# Needs `snapcraft` on PATH and a logged-in store session (`snapcraft login`).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
version=""
|
||||
dry_run=0
|
||||
only_snap=""
|
||||
only_arch=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
--snap) only_snap="${2:-}"; shift 2 ;;
|
||||
--arch) only_arch="${2:-}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
-*) echo "unknown option: $1" >&2; exit 2 ;;
|
||||
*) version="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
node_bin="$(command -v node || true)"
|
||||
if [ -z "$node_bin" ]; then
|
||||
echo "snap-release-all-channels.sh: node is required (it reads the architecture table" >&2
|
||||
echo " from models/lib/snapArchitectures.js so there is only one copy of it)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$dry_run" -eq 0 ] && ! command -v snapcraft >/dev/null 2>&1; then
|
||||
echo "snap-release-all-channels.sh: snapcraft is not on PATH." >&2
|
||||
echo " Install it and run 'snapcraft login', or pass --dry-run to see the plan." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The single source of truth, read once.
|
||||
snaps="$("$node_bin" -e 'process.stdout.write(require("'"$repo_root"'/models/lib/snapArchitectures").SNAP_NAMES.join(" "))')"
|
||||
arches="$("$node_bin" -e 'process.stdout.write(require("'"$repo_root"'/models/lib/snapArchitectures").snapArchitectures().join(" "))')"
|
||||
channels="$("$node_bin" -e 'process.stdout.write(require("'"$repo_root"'/models/lib/snapArchitectures").SNAP_CHANNELS.join(","))')"
|
||||
|
||||
# Accept either spelling of --arch, and refuse one that is neither.
|
||||
if [ -n "$only_arch" ]; then
|
||||
resolved="$("$node_bin" -e 'process.stdout.write(require("'"$repo_root"'/models/lib/snapArchitectures").snapArchOf(process.argv[1]))' "$only_arch")"
|
||||
if [ -z "$resolved" ]; then
|
||||
echo "snap-release-all-channels.sh: '$only_arch' is not an architecture WeKan builds." >&2
|
||||
echo " Known: $arches (and their bundle names ppc64le, armv7)." >&2
|
||||
exit 2
|
||||
fi
|
||||
only_arch="$resolved"
|
||||
fi
|
||||
|
||||
echo "Channels: $channels"
|
||||
echo "Snaps: ${only_snap:-$snaps}"
|
||||
echo "Architectures: ${only_arch:-$arches}"
|
||||
echo "Version: ${version:-<newest uploaded>}"
|
||||
# Say what is NOT being published, every run. "It is missing" and "it cannot
|
||||
# be there" look identical from the outside, and this is the difference.
|
||||
"$node_bin" -e 'const n = require(process.argv[1] + "/models/lib/snapArchitectures").NOT_SNAP_ARCHITECTURES;'\
|
||||
'Object.entries(n).forEach(([a, why]) => console.log("Not a snap: " + a + " - " + why));' "$repo_root"
|
||||
[ "$dry_run" -eq 1 ] && echo "DRY RUN - nothing will be released."
|
||||
echo
|
||||
|
||||
released=0
|
||||
skipped=0
|
||||
failed=0
|
||||
|
||||
for snap in $snaps; do
|
||||
[ -n "$only_snap" ] && [ "$snap" != "$only_snap" ] && continue
|
||||
|
||||
# One store round-trip per snap, not one per architecture.
|
||||
revisions="$(snapcraft list-revisions "$snap" 2>/dev/null)"
|
||||
if [ -z "$revisions" ]; then
|
||||
echo " $snap: could not list revisions (not logged in, or no such snap) - skipping"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
for arch in $arches; do
|
||||
[ -n "$only_arch" ] && [ "$arch" != "$only_arch" ] && continue
|
||||
|
||||
revision="$(printf '%s' "$revisions" | "$node_bin" -e '
|
||||
const { parseRevisions, pickRevision } = require("'"$repo_root"'/models/lib/snapArchitectures");
|
||||
let text = "";
|
||||
process.stdin.on("data", d => { text += d; });
|
||||
process.stdin.on("end", () => {
|
||||
const rev = pickRevision(parseRevisions(text), process.argv[1], process.argv[2] || undefined);
|
||||
process.stdout.write(rev === null ? "" : String(rev));
|
||||
});
|
||||
' "$arch" "$version")"
|
||||
|
||||
if [ -z "$revision" ]; then
|
||||
# Not every architecture is built for every release, and a snap that
|
||||
# has none of an architecture is not an error - it is a fact about
|
||||
# what was uploaded. Say so and carry on.
|
||||
echo " $snap $arch: no revision${version:+ for $version} - skipping"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " $snap $arch: revision $revision -> $channels"
|
||||
if [ "$dry_run" -eq 1 ]; then
|
||||
released=$((released + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if snapcraft release "$snap" "$revision" "$channels"; then
|
||||
released=$((released + 1))
|
||||
else
|
||||
# One architecture failing must not stop the other seventeen.
|
||||
echo " $snap $arch: RELEASE FAILED" >&2
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== released $released, skipped $skipped, failed $failed ====="
|
||||
[ "$failed" -eq 0 ]
|
||||
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
||||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 1076,
|
||||
appVersion = 1078,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "10.76~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.76'
|
||||
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.76-${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.76/wekan-10.76-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.76/${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,265 @@
|
||||
'use strict';
|
||||
|
||||
// Which snaps, on which architectures, to which channels - and the two
|
||||
// architectures that are NOT the two names of one thing.
|
||||
//
|
||||
// WeKan publishes three snaps of the same application across six Snap Store
|
||||
// architectures, and each has to reach four channels. The helper this replaces
|
||||
// released ONE snap, ONE revision, to THREE channels - and a revision number is
|
||||
// per architecture, so a single number could only ever be right for one of them.
|
||||
//
|
||||
// The mapping is where the real hazard is, and most of this suite is about it:
|
||||
//
|
||||
// ppc64le and ppc64el ARE the same hardware under two names.
|
||||
// armhf and armv7 are NOT. armhf is 32-bit Raspberry Pi OS; armv7 is
|
||||
// the ODroid-U3. Mapping one onto the other would hand
|
||||
// Raspberry Pi users the ODroid build, which is why the
|
||||
// negative test below exists at all.
|
||||
//
|
||||
// Run: node tests/snapArchitectures.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');
|
||||
|
||||
const {
|
||||
SNAP_NAMES,
|
||||
SNAP_CHANNELS,
|
||||
NOT_SNAP_ARCHITECTURES,
|
||||
snapArchitectures,
|
||||
bundleArchitectures,
|
||||
snapArchOf,
|
||||
bundleArchOf,
|
||||
releaseTargets,
|
||||
pickRevision,
|
||||
parseRevisions,
|
||||
} = require('../models/lib/snapArchitectures');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────── the three snaps, four channels
|
||||
|
||||
test('all three snaps are published, not just wekan', () => {
|
||||
assert.deepStrictEqual(SNAP_NAMES, ['wekan', 'wekan-ondra', 'wekan-gantt-gpl']);
|
||||
});
|
||||
|
||||
test('all FOUR channels, stable included', () => {
|
||||
// The old helper released to edge,beta,candidate - so a build reached three of
|
||||
// the four and somebody had to remember stable by hand, every time.
|
||||
assert.deepStrictEqual([...SNAP_CHANNELS].sort(),
|
||||
['beta', 'candidate', 'edge', 'stable']);
|
||||
});
|
||||
|
||||
test('every snap gets every architecture', () => {
|
||||
const targets = releaseTargets();
|
||||
assert.strictEqual(targets.length, SNAP_NAMES.length * snapArchitectures().length);
|
||||
SNAP_NAMES.forEach(snap => {
|
||||
const forSnap = targets.filter(t => t.snap === snap).map(t => t.arch);
|
||||
assert.deepStrictEqual(forSnap, snapArchitectures(), `${snap} is missing architectures`);
|
||||
});
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────── ppc64le and ppc64el: one thing
|
||||
|
||||
test('ppc64le (bundle) and ppc64el (store) are the same architecture', () => {
|
||||
assert.strictEqual(snapArchOf('ppc64le'), 'ppc64el');
|
||||
assert.strictEqual(snapArchOf('ppc64el'), 'ppc64el', 'the store name maps to itself');
|
||||
assert.strictEqual(bundleArchOf('ppc64el'), 'ppc64le');
|
||||
assert.strictEqual(bundleArchOf('ppc64le'), 'ppc64le');
|
||||
});
|
||||
|
||||
test('it is the ONLY architecture whose two names differ', () => {
|
||||
const renamed = snapArchitectures().filter(a => bundleArchOf(a) !== a);
|
||||
assert.deepStrictEqual(renamed, ['ppc64el'],
|
||||
'a second rename would need its own reason written down');
|
||||
});
|
||||
|
||||
// ─────────────────────────────── armhf and armv7: two things, and must stay two
|
||||
|
||||
test('NEGATIVE, THE IMPORTANT ONE: armv7 is not another name for armhf', () => {
|
||||
// armhf is 32-bit Raspberry Pi OS. armv7 is the ODroid-U3. They are different
|
||||
// CPUs with different bundles, and mapping one onto the other would publish
|
||||
// the ODroid build to Raspberry Pi users.
|
||||
assert.strictEqual(snapArchOf('armhf'), 'armhf');
|
||||
assert.strictEqual(snapArchOf('armv7'), '',
|
||||
'armv7 must NOT resolve to a Snap Store architecture');
|
||||
assert.strictEqual(snapArchOf('armv7l'), '',
|
||||
'nor its Node.js spelling');
|
||||
assert.strictEqual(bundleArchOf('armhf'), 'armhf',
|
||||
"armhf's bundle is armhf, not armv7");
|
||||
});
|
||||
|
||||
test('armv7 is recorded as not-a-snap, with the reason', () => {
|
||||
assert.ok(NOT_SNAP_ARCHITECTURES.armv7, 'it is written down, not merely absent');
|
||||
// The reason is NEON, not the CPU generation: node-patches builds armhf to the
|
||||
// Debian baseline (VFPv3-D16, no NEON) and armv7 with NEON. The store's single
|
||||
// 32-bit ARM architecture must carry the baseline, or the snap is an illegal
|
||||
// instruction on any board without NEON.
|
||||
assert.ok(/NEON/.test(NOT_SNAP_ARCHITECTURES.armv7));
|
||||
assert.ok(/baseline/i.test(NOT_SNAP_ARCHITECTURES.armv7),
|
||||
'and says which build the store must carry');
|
||||
assert.ok(/illegal instruction/i.test(NOT_SNAP_ARCHITECTURES.armv7),
|
||||
'and what happens if it carries the other one');
|
||||
});
|
||||
|
||||
test('the reason matches what node-patches actually builds', () => {
|
||||
// The single thing that would make the note above a story rather than a fact.
|
||||
const wf = path.join(repoRoot, '.tools/node-patches/.github/workflows/release-all.yml');
|
||||
if (!fs.existsSync(wf)) {
|
||||
console.log(' -- .tools/node-patches not cloned; skipping the cross-check');
|
||||
return;
|
||||
}
|
||||
const src = fs.readFileSync(wf, 'utf8');
|
||||
assert.ok(/node-armhf[\s\S]{0,200}?VFPv3-D16/.test(src),
|
||||
'armhf is the VFPv3-D16 Debian baseline');
|
||||
assert.ok(/node-armv7[\s\S]{0,200}?NEON/.test(src),
|
||||
'armv7 is the NEON-tuned build');
|
||||
});
|
||||
|
||||
test('i386 is recorded as not-a-snap, with the reason', () => {
|
||||
assert.ok(NOT_SNAP_ARCHITECTURES.i386);
|
||||
assert.ok(/core24/i.test(NOT_SNAP_ARCHITECTURES.i386));
|
||||
assert.strictEqual(snapArchOf('i386'), '', 'and it does not resolve');
|
||||
});
|
||||
|
||||
test('the architecture list matches what snapcraft.yaml actually builds', () => {
|
||||
// The single thing that would make this whole table a lie.
|
||||
const yaml = read('snapcraft.yaml');
|
||||
const buildFor = [...yaml.matchAll(/^\s*build-for:\s*(\S+)\s*$/gm)].map(m => m[1]).sort();
|
||||
assert.deepStrictEqual([...new Set(buildFor)], [...snapArchitectures()].sort(),
|
||||
'snapcraft.yaml and the table disagree about which architectures are built');
|
||||
});
|
||||
|
||||
test('negative: junk is refused rather than passed through to snapcraft', () => {
|
||||
// An unrecognised --arch reaching `snapcraft release` is a silent no-op, which
|
||||
// is the worst outcome: it looks like it worked.
|
||||
['', null, undefined, 'x86', 'sparc', 'ppc64', 'arm', 42, {}]
|
||||
.forEach(bad => assert.strictEqual(snapArchOf(bad), '', String(bad)));
|
||||
assert.strictEqual(bundleArchOf('sparc'), '');
|
||||
});
|
||||
|
||||
test('either spelling and any case is accepted, whitespace trimmed', () => {
|
||||
assert.strictEqual(snapArchOf(' PPC64LE '), 'ppc64el');
|
||||
assert.strictEqual(snapArchOf('x64'), 'amd64');
|
||||
assert.strictEqual(snapArchOf('aarch64'), 'arm64');
|
||||
});
|
||||
|
||||
// ───────────────────────────────────── revisions are per architecture
|
||||
|
||||
test('the newest revision for THAT architecture wins', () => {
|
||||
const rows = [
|
||||
{ revision: 10, arch: 'amd64', version: '10.75' },
|
||||
{ revision: 11, arch: 'arm64', version: '10.75' },
|
||||
{ revision: 12, arch: 'amd64', version: '10.75' },
|
||||
];
|
||||
assert.strictEqual(pickRevision(rows, 'amd64', '10.75'), 12);
|
||||
assert.strictEqual(pickRevision(rows, 'arm64', '10.75'), 11,
|
||||
"arm64 must not be given amd64's higher revision");
|
||||
});
|
||||
|
||||
test('the highest wins, not whichever row came first', () => {
|
||||
const rows = [
|
||||
{ revision: 30, arch: 'amd64', version: '10.75' },
|
||||
{ revision: 9, arch: 'amd64', version: '10.75' },
|
||||
];
|
||||
assert.strictEqual(pickRevision(rows, 'amd64'), 30);
|
||||
});
|
||||
|
||||
test('a version filter publishes what was built, not whatever was uploaded last', () => {
|
||||
const rows = [
|
||||
{ revision: 40, arch: 'amd64', version: '10.76' },
|
||||
{ revision: 39, arch: 'amd64', version: '10.75' },
|
||||
];
|
||||
assert.strictEqual(pickRevision(rows, 'amd64', '10.75'), 39);
|
||||
assert.strictEqual(pickRevision(rows, 'amd64'), 40, 'and without one, the newest');
|
||||
});
|
||||
|
||||
test('the bundle spelling works here too', () => {
|
||||
const rows = [{ revision: 7, arch: 'ppc64el', version: '10.75' }];
|
||||
assert.strictEqual(pickRevision(rows, 'ppc64le'), 7,
|
||||
'asking with the bundle name finds the store row');
|
||||
});
|
||||
|
||||
test('negative: no revision for an architecture is null, not a wrong one', () => {
|
||||
const rows = [{ revision: 5, arch: 'amd64', version: '10.75' }];
|
||||
assert.strictEqual(pickRevision(rows, 'riscv64'), null);
|
||||
assert.strictEqual(pickRevision(rows, 'armv7'), null, 'and an unknown arch is null');
|
||||
assert.strictEqual(pickRevision(rows, 'amd64', '9.99'), null, 'wrong version, no guess');
|
||||
assert.strictEqual(pickRevision(null, 'amd64'), null);
|
||||
assert.strictEqual(pickRevision([], 'amd64'), null);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────── parsing what the store prints
|
||||
|
||||
test('snapcraft list-revisions output is parsed', () => {
|
||||
const out = [
|
||||
'Rev. Uploaded Arch Version Channels',
|
||||
'42 2026-08-09T04:00:00Z amd64 10.75 stable*',
|
||||
'41 2026-08-09T03:00:00Z ppc64el 10.75 -',
|
||||
'40 2026-08-08T03:00:00Z armhf 10.74 ',
|
||||
].join('\n');
|
||||
const rows = parseRevisions(out);
|
||||
assert.strictEqual(rows.length, 3, 'the header is skipped');
|
||||
assert.deepStrictEqual(rows[0], {
|
||||
revision: 42, uploaded: '2026-08-09T04:00:00Z', arch: 'amd64',
|
||||
version: '10.75', channels: 'stable*',
|
||||
});
|
||||
// A revision with no channels still parses: the columns are taken from the
|
||||
// left, not counted from the right.
|
||||
assert.strictEqual(rows[2].arch, 'armhf');
|
||||
assert.strictEqual(rows[2].version, '10.74');
|
||||
assert.strictEqual(pickRevision(rows, 'ppc64le'), 41);
|
||||
});
|
||||
|
||||
test('negative: junk output yields no rows rather than nonsense', () => {
|
||||
assert.deepStrictEqual(parseRevisions(''), []);
|
||||
assert.deepStrictEqual(parseRevisions(null), []);
|
||||
assert.deepStrictEqual(parseRevisions('error: not logged in'), []);
|
||||
assert.deepStrictEqual(parseRevisions('Rev. Uploaded Arch Version Channels'), []);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────── the script
|
||||
|
||||
test('the release script reads the table instead of copying it', () => {
|
||||
const sh = read('releases/snap-release-all-channels.sh');
|
||||
assert.ok(/models\/lib\/snapArchitectures/.test(sh));
|
||||
// A second copy of the arch list in bash is how the two would drift.
|
||||
assert.ok(!/ppc64el\s+riscv64\s+s390x/.test(sh.replace(/#.*$/gm, '')),
|
||||
'no hardcoded architecture list outside the comments');
|
||||
});
|
||||
|
||||
test('it releases to every channel in ONE call', () => {
|
||||
const sh = read('releases/snap-release-all-channels.sh');
|
||||
assert.ok(/snapcraft release "\$snap" "\$revision" "\$channels"/.test(sh),
|
||||
'a revision reaches all four channels or none');
|
||||
});
|
||||
|
||||
test('one architecture failing does not stop the rest', () => {
|
||||
const sh = read('releases/snap-release-all-channels.sh');
|
||||
assert.ok(/failed=\$\(\(failed \+ 1\)\)/.test(sh));
|
||||
assert.ok(/RELEASE FAILED/.test(sh));
|
||||
});
|
||||
|
||||
test('it says what it is NOT publishing, every run', () => {
|
||||
const sh = read('releases/snap-release-all-channels.sh');
|
||||
assert.ok(/NOT_SNAP_ARCHITECTURES/.test(sh),
|
||||
'"it is missing" and "it cannot be there" look identical without this');
|
||||
});
|
||||
|
||||
test('the superseded three-channel helper is not what a release calls', () => {
|
||||
const old = read('releases/snap-store-release-revision-to-channels.sh');
|
||||
assert.ok(/edge,beta,candidate/.test(old), 'it is still there for a single manual release');
|
||||
// ...but the new one is what covers everything.
|
||||
const sh = read('releases/snap-release-all-channels.sh');
|
||||
assert.ok(/stable/.test(sh));
|
||||
});
|
||||
|
||||
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