Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (218)

v10.72

2026-08-08

Binaries in these bundles

Each bundle carries a Node.js, a FerretDB and the MongoDB Database Tools. Which source has a given CPU varies from release to release - nodejs.org builds some architectures, unofficial-builds others, and the wekan/node fork the ones neither of them does - and not every source publishes a checksum. This is what went into this release, and which downloads were checked against a published SHA256.

BundleBinaryFromVersionCheckedSHA256
arm64FerretDBwekan/FerretDBlatestverifiedb4a627780b746988…
arm64FerretDBwekan/FerretDBlatestverifiedb4a627780b746988…
arm64Node.jsnodejs.orgv24.19.0verified01443c1e1a29e531…
arm64Node.jsnodejs.orgv24.19.0verified01443c1e1a29e531…
armhfFerretDBwekan/FerretDBv1.46.0verified82c4f226202e1038…
armhfFerretDBwekan/FerretDBv1.46.0verified82c4f226202e1038…
armhfNode.jswekan/node-patchesv24.19.0verifiedb55350f3071b765a…
armhfNode.jswekan/node-patchesv24.19.0verifiedb55350f3071b765a…
armv7FerretDBwekan/FerretDBv1.46.0verified82c4f226202e1038…
armv7FerretDBwekan/FerretDBv1.46.0verified82c4f226202e1038…
armv7Node.jswekan/node-patchesv24.19.0verified8dbe0a9aa8550ad5…
armv7Node.jswekan/node-patchesv24.19.0verified8dbe0a9aa8550ad5…
i386FerretDBwekan/FerretDBv1.46.0verified0d2f948e0337e5b1…
i386FerretDBwekan/FerretDBv1.46.0verified0d2f948e0337e5b1…
i386Node.jswekan/node-patchesv24.19.0verified3b0b3bbfe27daf58…
i386Node.jswekan/node-patchesv24.19.0verified3b0b3bbfe27daf58…
mac-arm64FerretDBwekan/FerretDBlatestverified063c66968a5d0d84…
mac-arm64FerretDBwekan/FerretDBlatestverified063c66968a5d0d84…
mac-arm64Node.jsnodejs.orgv24.19.0verified3f1cf157479c1480…
mac-arm64Node.jsnodejs.orgv24.19.0verified3f1cf157479c1480…
ppc64leFerretDBwekan/FerretDBv1.46.0verified9ced5b800d82d184…
ppc64leFerretDBwekan/FerretDBv1.46.0verified9ced5b800d82d184…
ppc64leNode.jsnodejs.orgv24.19.0verifiedc510c6ce12f07010…
ppc64leNode.jsnodejs.orgv24.19.0verifiedc510c6ce12f07010…
riscv64FerretDBwekan/FerretDBv1.46.0verifiedd1bcfde0227c68d9…
riscv64FerretDBwekan/FerretDBv1.46.0verifiedd1bcfde0227c68d9…
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0verifiedcd1f14af28121480…
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0verifiedcd1f14af28121480…
s390xFerretDBwekan/FerretDBv1.46.0verified62bae6c40e1ad486…
s390xFerretDBwekan/FerretDBv1.46.0verified62bae6c40e1ad486…
s390xNode.jsnodejs.orgv24.19.0verifieda4792e65962ffa0a…
s390xNode.jsnodejs.orgv24.19.0verifieda4792e65962ffa0a…
win64FerretDBwekan/FerretDBlatestverified508dbd2f26469fc1…
win64FerretDBwekan/FerretDBlatestverified508dbd2f26469fc1…
win64Node.jsnodejs.orgv24.19.0verified57f71ab3652e797d…
win64Node.jsnodejs.orgv24.19.0verified57f71ab3652e797d…

A row saying no checksum published is not a failed check - it is a source that publishes nothing to check against. Those are the ones worth fixing at the source.

v10.72 2026-08-08 WeKan ® release

In short: the prereleases WeKan was running on become finals - Meteor 3.5.1 and @meteorjs/rspack 2.1.0 - and two dependencies take a major version: jQuery 4 and @babel/parser 8. jQuery 4 stopped the server from starting at all. It throws jQuery requires a window with a document the moment it is loaded outside a browser, and the CSV importer carried an unused jQuery import that the server bundle pulled in, so every start died before the first route existed. That import is gone, and a new guard walks the server’s import graph so no browser-only package can reach it again. The snap builds are the other half: v10.71 published no snap for armhf, s390x, ppc64el or riscv64 and no wekan-gantt-gpl amd64, for three unrelated reasons - a Caddy version lookup rate-limited by the GitHub API, two MongoDB library packages under names Ubuntu 24.04 does not publish on armhf, and a bin that is staged when it is not a directory - and the snap-launchpad job now keeps the whole build log and outlives its own retries, which is what made those three take two attempts to find. On a board, picking up a card no longer stretches every list to fill the window: a card drag switches the board’s panning off by removing a class, and in lists view that same class was the only thing holding the lists at their width. Moving a card to another board failed for everyone with a 403 from Meteor’s insecure-write rule, and the REST route offered as a workaround left the card pointing at a list on a board it was not on; both are fixed. Below that: an npm dependency refresh, Tests -> EVERYTHING in build.sh and build.bat growing the one check it never ran and one browser log per browser on Windows, companion repositories moving into .tools/ with the build scripts cloning them on demand, an LDAP group base for directories that keep users and groups apart, a REST answer for when a list last changed, guards pinning what a board export contains, and the usual documentation and translation work.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.45.094713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.45.0275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b
loong64Node.jsunofficial-builds.nodejs.orgv24.19.0c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68
loong64FerretDBwekan/FerretDBv1.45.028bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.45.0639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.45.0fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3
ppc64leNode.jsnodejs.orgv24.19.0c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3
ppc64leFerretDBwekan/FerretDBv1.45.0de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a
riscv64Node.jsunofficial-builds.nodejs.orgv24.19.0cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd
riscv64FerretDBwekan/FerretDBv1.45.07dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9
s390xNode.jsnodejs.orgv24.19.0a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4
s390xFerretDBwekan/FerretDBv1.45.00ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1
win64Node.jsnodejs.orgv24.19.057f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73
win64FerretDBwekan/FerretDBv1.45.0f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85

This release updates the following dependencies:

  • Meteor 3.5.1-beta.0 → 3.5.1 — the framework WeKan is built on, now on the final release instead of the prerelease it was tracking. The four packages that were on -beta351.0 builds move to their finals with it: ecmascript 0.19.0, rspack 1.2.0, mongo 2.5.0 and accounts-password 3.3.1. Update. Thanks to Meteor developers and xet7.
  • @meteorjs/rspack 2.1.0-beta.0 → 2.1.0 — the bundler Meteor builds the client with, the counterpart of the Meteor release above and off its prerelease for the same reason. Fifteen packages leave the lockfile with it: node-polyfill-webpack-plugin and the browser shims it carried (crypto-browserify, browserify-sign, elliptic, node-stdlib-browser and their dependencies). body-parser moves 1.20.5 → 1.20.6 in the same install. Update. Thanks to developers of dependencies and xet7.
  • jquery 3.7.1 → 4.0.0 — the DOM library the whole client is written against, a major version. It drops Internet Explorer and the long-deprecated helpers $.isArray, $.isFunction, $.isNumeric, $.type, $.trim, $.proxy, $.now and $.parseJSON - gone from node_modules/jquery/dist/jquery.js, and none of them called anywhere in WeKan’s own client code - and it no longer tolerates being loaded where there is no document, which is the boot crash fixed below. Update. Thanks to dependabot, jQuery developers and xet7.
  • @babel/parser 7.29.7 → 8.0.4 — the parser Babel reads JavaScript source with, also a major version. Nothing in WeKan’s source imports it; it is declared in package.json so the build resolves a single version of it. Update. Thanks to dependabot and xet7.
  • An npm dependency refresh — 926 → 903 packages in the lockfile, moving the AWS S3 client 3.1095.0 → 3.1105.0, dompurify 3.4.12 → 3.4.13, markdown-it 14.2.0 → 14.3.0, markdown-it-emoji 3.0.0 → 3.1.0, temml 0.13.3 → 0.13.4, @rsdoctor/rspack-plugin 1.5.11 → 1.6.1, puppeteer 25.3.0 → 25.5.0 and both halves of typescript-eslint 8.65.0 → 8.66.0. Only the lockfile changes: the version ranges in package.json stay as they are. Update. Thanks to developers of dependencies and xet7.

and fixes the following bugs:

Signing in with LDAP - where WeKan looks for the groups.

Groups may live in their own subtree, not only under the user base. Thanks to leandro-cyberproject and xet7.

#5539: WeKan could not authenticate LDAP users whose groups sit in a different ou than the users. Both group searches - getUserGroups, which feeds the login restriction, admin status sync, group-to-role sync and org/team sync, and isUserInGroup - searched BaseDN, which is the USER base. A directory that keeps ou=groups beside ou=people has no groups under it, so every group search came back empty; with LDAP_GROUP_FILTER_ENABLE on, isUserInGroup concluded “not a member” and refused the login. Nothing in the package could say where the groups were.

LDAP_GROUP_BASEDN says where, and falls back to BaseDN when unset or blank, so a directory with one subtree behaves exactly as before - a present-but-empty variable is one somebody meant to fill in, and searching "" would silently search the directory root. The three USER searches keep BaseDN: pointing those at a group subtree would break login for everyone, so the guard pins which searches moved and which did not. Documented in docs/Login/LDAP.md and docker-compose.yml, because a setting nobody can find is one that does not exist.

The REST API - what a list can be asked about.

A list says when it changed, and when its cards last did. Thanks to mimZD and xet7.

#5251 asked for a list’s last change date, to build an offline client that syncs only what moved. Neither half existed: the list endpoint returned {_id, title} and no dates, and while the single-list endpoint returns the whole document, its modifiedAt answers the wrong question - it moves when the LIST changes (title, sort, archived), and a card being added, edited or archived does not touch it.

GET /api/boards/:boardId/lists reports both now: modifiedAt for the list itself, and cardsModifiedAt for the newest change among its cards, null when there is none. One query for the board and a reduction in memory, not a query per list - which is what made this expensive enough to ask about - and archived cards count, since archiving is one of the changes named. The reduction is a pure helper: it reads the legacy dateLastActivity as well as modifiedAt and takes whichever is newer, skips a card with no usable date rather than counting it as now, and leaves a list with no dated cards ABSENT so the endpoint reports null instead of an invented time - a client polling on a wrong date either never syncs or syncs forever.

Controls and the things they belong to - five reports, five causes.

Two misplaced checkboxes, a crowded Save button, and the WIP counter back on the list title line. Thanks to Alishara and xet7.

The screenshots in #6465, each with the fault circled.

“Checkbox is misplaced” - the Admin Panel people table. The select-all box and the “all users” it labels were two loose inline nodes in a centred table heading: the box is replaced content with its own baseline, and at that column’s width it landed on the word. One <label for> now, an inline-flex row with a gap that cannot collapse - which also makes the word click the box.

“This checkbox is crazy” - Member settings. That row carried a b &nbsp; spacer and .left, which is float: inline-start. A float inside a flex container is ignored and the spacer became a stray flex item, so the box drifted up beside the “Card settings” heading instead of sitting with its own text. It is the same shape every other checkbox row in that file uses now.

“(2/5) move this up here” and “make this same height as the lanes left and right” - one cause, not two. The WIP counter is already inside the heading right after the title; the title is a +viewer, and .viewer is display: block with a 22px min-height, so it pushed everything after it onto a second line AND reserved a band under it, which is what made that list’s header taller than its neighbours’. Inline, the counter stays where the markup already puts it.

“Please move the button down. This is too close” - the backup schedule’s Save sat directly under the 1-28 day buttons, close enough to hit while aiming for a date. The gap is on the group that FOLLOWS the day grid, so it applies where that grid is and nowhere else. And “the alignment is out of place” - the attachment move button - is normalisation rather than a measured fix: the row aligns at the bottom, so a margin of the button’s own offsets it from the controls it acts on; that is zeroed and the alignment made explicit. Whether that is the whole of what was circled needs a browser, and the guard says so.

Outgoing webhooks - who a webhook says did something.

A webhook sends the username as well as the display name. Thanks to xet7.

#3113: an outgoing webhook said who did something by DISPLAY name, and had no field for the login name. params.user comes from getActivityUserName(), which prefers getName() because the same params feed the e-mail notification text - “Lauri Ojansivu commented on …” is what a person wants to read there. A webhook consumer needs the identifier instead: it received the full name where it needed xet7, and matching users by display name is wrong the moment two people share one.

Changing what user means would break every consumer already reading it, which is why this sat in TODO Later as needing a decision. It needs none: the username travels as its own field. user is untouched, username is beside it in the default WEBHOOKS_ATTRIBUTES list, and a deployment that pinned its own list still gets exactly the fields it asked for.

Two more left the backlog with it. #6542 - “Users imported: 60, but only 25 are listed” - is already true in this source: the People pane paginates and shows its total beside the rows, which is what the report was about. #6500 was closed upstream and was only sitting there. The rest of the backlog stays, each with its reason - an SMTP server, an LDAP directory, a container, a browser to drag in, or a decision on an intended contract, none of which a source reading settles.

Logging in with OIDC - what happens when the provider says no.

A provider that refuses the request says so, instead of "Cannot read property 'ocs' of null". Thanks to Esther125 and xet7.

#5174: an OIDC login against a provider that rejected the request failed with Error in OAuth Server: Cannot read property 'ocs' of null, which says nothing about the refusal that caused it - the reporter’s actual problem was a scope the provider did not allow.

getTokenContent() returns NULL for a token it cannot parse, and the ADFS/B2C branch assigns that straight into userinfo. The next line was the Nextcloud hack, if (userinfo.ocs), so the first thing to touch the failed response was a property read on null. Every claim read after it had the same problem; that line was first only by accident of ordering. So the fix is not one null check but an order: the provider’s answer is validated once, as a whole, before anything reads a field off it, and each failure names what failed.

Checked now: that the token response is an object; that it carries an access_token or an id_token, with a 200 carrying neither reported by listing the fields that DID come back - keys only, because the values are secrets; that userinfo is a non-null object, saying which path produced nothing and pointing at OAUTH2_REQUEST_PERMISSIONS; that the ocs and metadata hacks unwrap to something, since ocs without ocs.data used to set userinfo to undefined and fail one line later; that Azure AD B2C’s emails claim is an array before it is indexed; and that expires_in parses to a finite number, since parseInt(undefined) is NaN and NaN propagated into the account’s expiry silently. The guard pins the ORDER, because an edit that reads a claim earlier would restore the bug without touching a check.

The size of things on a board - what is bigger than what.

A list heading is bigger than the cards under it again, and the Add button is a control. Thanks to xet7.

Reported by email with a screenshot: the Add card link and the Add button are too big, and things should be sized in relation to each other the way an h1 is bigger than an h2.

The measurements agreed. A list heading was 14px, while a minicard title and the Add card link set no size at all and inherited the document’s 16px, and the composer’s Add button was a 50px-tall slab. The column was labelled in fine print, the affordance for making a card was as loud as the cards themselves, and the biggest thing on screen while typing was a button.

The scale is heading 16 > card title 14 >= add-card link 13 now, each stating its own size instead of inheriting, and the Add button is a 34px control. Mobile keeps its own larger sizes - the .mobile-view rules and the @media blocks - because a phone is read at arm’s length. The guard pins the ORDER rather than the numbers, since that is what “in relation to each other” means, and that each of the three states a size of its own: an inherited size is exactly how the link and the card title both landed on 16px, level with each other and above their heading.

Moving a card to another board - the card dialog, and the REST route.

A cross-board move no longer fails with "Untrusted code may only updateAsync documents by ID". Thanks to jullbo and xet7.

#6572: every cross-board move failed immediately with Not permitted. Untrusted code may only updateAsync documents by ID. [403] - whether or not the card had any dependencies, and before the move itself ever ran, so the card stayed where it was.

models/cards.js is isomorphic, so its helpers run in the client bundle, and client code calls card.move() directly. Meteor lets untrusted code update only BY ID - a bare id, or an { _id: ... } selector. The cross-board branch of move() cleaned up inbound Red Strings (#3392) with a compound selector and multi: true, which the client rejects every time, including when it would have matched nothing. addDependency, setDependencyProps and removeDependency in the same file each carry a comment saying updates must be by _id only - this was the one place that did not follow it.

The card’s own dependencies are still cleared by the move; that rides along in the by-id update. The inbound half - the links pointing AT the card from the board it left - is a Cards.after.update hook in server/models/cards.js now, where a selector is allowed, and being server-side it also covers the REST API and import paths, which never called the helper at all. It pulls both stored shapes: the { cardId, ... } objects, and the bare id strings older data still holds, which normalizeDependencies hides on read.

The report also says the REST workaround corrupts the card, and it does. A PUT of boardId/listId/swimlaneId naming the DESTINATION board is not a board move - that needs newBoardId, newSwimlaneId and newListId - so the board-move branch never ran, while the same-board swimlane and list branches did: the card kept its old boardId and got the other board’s listId and swimlaneId written onto it, pointing at a list and a swimlane on a board it was not on. It showed on neither board and took a hand-written database update to undo. Both branches now check that the target belongs to the board in the URL and otherwise refuse with a 400 naming the parameters to use, and they are skipped during a board move - they would rewrite listId before the board-move update, whose selector pins the card’s original listId, so that update would match nothing and silently do nothing: the same broken card by another route.

Dragging a card - what the rest of the board does while one is in the air.

Picking up a card no longer stretches every list to fill the window. Thanks to yulqen and xet7.

#6573: the moment a card was picked up, every list on the board expanded horizontally to fill the page, and dropping it snapped them all back - which makes landing a card in the intended list a guess. It happened when dragging inside one list too.

Two changes that are each correct alone met. A list’s custom width is an inline --list-width custom property, and what turns that property into an actual width is a rule in list.css whose selector needs an ancestor: .js-swimlane, .dragscroll, or a swimlane-<id> id. In lists view the container is .swimlane.list-group.js-lists.dragscroll - it has no js-swimlane class and no such id - so .dragscroll was the only one of the three that matched, and every list’s width on that view hung on that one class. Then #6558 taught a card drag to stop the board panning under the same pointer, and the way it does that is to REMOVE the dragscroll class from the board for the duration of the drag and put it back on drop.

So the drag deleted the class the width rule was matching on: flex: none and the three width declarations switched off, the lists fell back to the flex rules and re-flowed to fill the window, for exactly as long as the drag lasted. It explains the workaround in the report too - dragging a list’s edge first sets that list’s width through the resize path, which is why that one column stopped jumping while the others still did.

A layout rule may not hang on a class that an interaction removes. .js-lists is on the container in BOTH views and nothing takes it off, so the width rules, their mobile-mode counterpart and the resize rules now name it. tests/listWidthDuringDrag.test.cjs pins that every width rule still matches in lists view with dragscroll gone - it fails on the previous CSS, and it also pins the premise, that suspending the pan really does remove that class.

The server bundle - what a client-side import may drag into it.

An unused jQuery import in the CSV importer crashed the server at boot under jQuery 4. Thanks to xet7.

jQuery 4 changed what loading it outside a browser does. Its CommonJS entry point runs the factory as soon as the module is required - module.exports = factory( global, true ) - and the factory’s first statement throws when window.document is missing. Under jQuery 3 the same import did not stop the server: it had been sitting in models/csvCreator.js since the CSV importer was written and WeKan started with it in place every time. Line 3 was import { isEmptyObject } from 'jquery', and the file never called isEmptyObject anywhere - the import was unused from the day it was added.

server/imports.js loads /models/csvCreator, because the server is what imports a CSV or TSV board, so that unused import put jQuery in the server bundle - where there is no window and no document. The bump to jQuery 4 turned it into a boot crash: the bundle threw while it was still being evaluated, before a single route, publication or method existed, so there was no error page and no log entry, only a stack trace ending in Object../models/csvCreator.js and Module../server/imports.js, and Exited with code: 1.

The import is removed, and nothing else changes, because nothing used it. It was the only jQuery import anywhere under models/, server/, imports/ and config/, and the rebuilt _build/main-dev/server-rspack.cjs now contains the csvCreator module with no reference to node_modules/jquery left in it.

The new tests/serverBundleBrowserImports.test.cjs keeps it that way. It walks the import graph from server/main.js - 410 files - and fails if any file it reaches names a package that needs a DOM: jquery, jquery-ui, the touch-punch and dragscroll add-ons, blaze, bootstrap. Its negative tests pin that the bug as it actually was is reported, that a deep path such as jquery/dist/jquery.js counts as the same package, and that an ordinary server package is not flagged. An unused import is invisible in review and free on the client, so a guard is what catches the next one.

and has the following developer-tooling fixes:

What the snap is built from - the parts in snapcraft.yaml.

The Caddy part stops asking the GitHub API which version to download. Thanks to xet7.

The caddy part resolved the newest Caddy release through api.github.com/repos/caddyserver/caddy/releases/latest. That API rate-limits unauthenticated callers by IP address, and a CI runner shares its address with every other job on the same host, so it answers 403 whenever the neighbours have been busy. In v10.71 it did, and one line failed the whole wekan-gantt-gpl amd64 snap: curl: (22) The requested URL returned error: 403, then 'override-build' in part 'caddy' failed with code 22.

There was already a pinned fallback for exactly this, on the very next line, and it never ran. snapcraft executes a scriptlet under set -o pipefail as well as set -e, so the 403 failed the assignment and ended the part one line above its own safety net - which is why the job log shows CADDY_VERSION= being set to nothing and then nothing more. A fallback that the failure it covers skips over is not a fallback.

The version now comes from github.com/caddyserver/caddy/releases/latest, which is a redirect to the newest tag rather than an API call and is not rate-limited the same way; the tag is read out of the URL it lands on. The lookup is allowed to fail (|| true keeps the substitution’s status 0), an empty answer selects the pin, and a release that publishes no archive for this architecture falls back to the pin as well instead of failing the snap. Setting CADDY_VERSION in the environment still overrides everything, for a reproducible build. Verified by running the scriptlet: it resolves 2.11.4 from the redirect, falls back to the pin with the lookup pointed at an unreachable host, honours an explicit CADDY_VERSION - and the old line, under the same shell options, dies before its fallback exactly as it did in the release.

The mongodb part asks for the package names Ubuntu 24.04 really publishes. Thanks to xet7.

The armhf snap never got as far as building anything: Stage package not found in part 'mongodb': libssl3. and, on the next attempts, the same for libgoogle-perftools4. Ubuntu 24.04’s 64-bit time_t transition renamed both packages to libssl3t64 and libgoogle-perftools4t64. On the 64-bit architectures the renamed package also provides the old name, so the old spelling resolves there and the mistake stays invisible; on armhf the ABI genuinely changed, there is no compatibility provide, and the old name does not exist at all. One architecture failing on a name every other architecture accepts is what that looks like from the outside.

Both are now spelled the way the archive spells them. Checked against the noble archive rather than assumed: 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, because core24 has no i386 port.

The mongodb part makes bin a real directory whatever it was before. Thanks to xet7.

The s390x, ppc64el and riscv64 snaps died in the stage step, right after Staging mongodb: /build/.../stage/bin: Is a directory, IsADirectoryError. The part is staged last of the ones that carry a bin/, so stage/bin is already a real directory by then, and staging something that is not a directory on top of it fails the whole snap rather than that one part.

This was fixed once, in v10.70, by replacing a bin symlink with a real directory - the shape the first failure had. The v10.71 logs show that guard running, its [ -L ... ] test coming out false, and the build dying in the identical way immediately afterwards. So bin was something else that is not a directory, the symlink was only one shape of the problem, and a guard written to one shape passes while the build breaks.

The condition is now the invariant rather than the diagnosis: when this part carries no mongod - the FerretDB-only architectures, where MongoDB ships no server and the build exits early - bin becomes an empty real directory whatever it was, since rm -rf takes a symlink, a regular file or a directory where the old rm -f took neither of the last two, and removing a symlink leaves what it pointed at alone. An empty real directory merges into stage/bin and changes nothing. Where mongod really is there, amd64 and arm64, nothing is touched.

It also prints ls -ld of bin before and after, because the reason this needed two attempts is that no log ever recorded what the thing actually was. Verified by running the scriptlet against each shape - symlink, regular file, missing, empty directory, and a directory holding mongod - and checking what it leaves behind, including that the symlink case does not delete the directory it points at.

The test suite itself - guards that described the world before a change.

Five guards catch up with the companion-repo move and the cross-board card fix. Thanks to xet7.

A whole-run found five failures, all of them consequences of the two changes before it, and every one a guard doing its job.

Three broke on the companion-repo move. Two of them - the .meteorignore scan scope and the swc helper guard - listed the foreign checkouts BY NAME, one ignore entry each: FerretDB, node, mongo-tools, TSC, gitea, the two WeKan variants. A list of names is a list of history; it fails for the ones that moved and says nothing about the next repo somebody clones. They ask the property now

  • .tools is excluded, and nothing at the top of the checkout that is its own git repository is left for Meteor to walk - and the variant check became the stronger version of itself: no directory holding client/, server/ and models/ is reachable from the top, since a second WeKan is loaded EAGERLY rather than merely scanned. The third reads FerretDB’s Go source and still opened the old path; it reads .tools/FerretDB, and skips with a note when the clone is not there, because that is another repository and not every checkout has it.

The compose guard caught a real omission: the LDAP_GROUP_BASEDN block went into docker-compose.yml only, and every compose file’s wekan service must be identical - what a user reads while editing their settings must not depend on which backend they picked.

The mocha failure is the one worth reading twice. Its test pinned move() issuing the inbound dependency cleanup itself: a multi-document update with a compound selector - exactly what the cross-board move fix removed, because that helper runs in the client bundle where Meteor allows updates only by id. The test was pinning the bug. It asserts the contract that replaced it now: move() clears the card’s own dependencies and makes no update that is not by id, with { _id: x } still counting as by id - the rule is “by id”, not “not an object”.

A retry loop that outlived its own test, and an ignore entry that explains itself. Thanks to xet7.

Two left over, and the second is not a flake.

The swc guard also required the .gitignore comment naming each clone - - node/ : clone of the Node.js ... - which went with the entries it described. .gitignore says what .tools/ holds now, naming the entries it replaced so the change is legible, and the guard requires that: an ignore of a whole directory with no explanation is the kind nobody dares remove later.

One WebKit test of 243 failed, in the shared boardPage fixture rather than in an assertion: Test timeout of 60000ms exceeded while setting up “boardPage”, then Target page, context or browser has been closed. openBoard retries five times, waiting up to 20s for a list each time with a second between - about 105 seconds, against a 60 second test timeout. The loop could never reach its own error: Playwright killed the test first, so the report said the page had closed instead of saying the board never rendered. Retrying past the point where the result can still be used is not resilience, it is a worse error message.

It is bounded by a deadline now - 45s, leaving room for the rest of the fixture inside the 60s timeout. The first attempt keeps its full 20s look, later attempts get whatever is left, and it stops rather than starting a wait it cannot finish, so a slow board still gets one long look and a board that will not render fails with “did not render any lists within 45s”. Simulated across never-renders, renders-at-4s and renders-at-19s: all inside the test timeout, where the old shape overran it by 45 seconds. The run after it was green - 328 node suites, 518 mocha tests, 249/243/243 in the three browsers, 98 conformance cases with none differing, and FerretDB’s own suites.

The build scripts - what Tests -> 1 runs, on both platforms.

Companion repos live in .tools/, and the build scripts clone them there. Thanks to xet7.

wekan/FerretDB is a separate git repository that WeKan’s test runs need - the conformance run builds it from source, and “Run all FerretDB tests” runs its own suites. It was cloned as a subdirectory of the repo root, which is why .gitignore and .meteorignore carried an entry per companion repo: nine of them, each one a chance for a clone to reach a commit or a Meteor rebuild. They live in .tools/ now, one directory both files already exclude.

build.sh gains ensure_tool_repo: it returns the path to .tools/<name>, cloning wekan/<name> if it is not there - SSH first, since a maintainer pushes, HTTPS after, so everyone else still gets a working clone. Its messages go to stderr, because the path is what it prints, and the directory comes from the script’s own location rather than the caller’s cwd. EVERYTHING’s FerretDB stage calls it rather than assuming an earlier stage cloned already - a whole-run must not depend on the order of its own stages - and build.bat clones into .tools\FerretDB with the same fallback instead of printing instructions and stopping.

The other half is that a repo inside .tools still has to find its way back out. FerretDB’s own build.sh writes its logs where WeKan writes its own, and reached them with $ROOT/../../log - correct from wekan/FerretDB, one level short from wekan/.tools/FerretDB, where it means wekan/log and nothing else looks. It walks up until it recognises a WeKan checkout now, then applies WeKan’s own rule: ../log when that is writable, log/ inside the checkout otherwise. Verified against five layouts, including the old one.

EVERYTHING runs the floating-promises guard too, and Windows gets one log per browser. Thanks to xet7.

./build.sh -> Tests -> EVERYTHING (sequential) is what a maintainer runs before a release, so a check that is in the Tests menu but not in EVERYTHING is a check that runs only when somebody remembers it. The floating-promises guard was exactly that: it is menu entry 12 and was in no whole-run. It is now the first stage of four - it takes seconds, so an unawaited permission check is reported before an hour of browsers rather than after it - and its return code is part of the verdict like every other stage’s.

What it runs there is checks only. The menu entry may install ripgrep and the @typescript-eslint packages and write the rule into .eslintrc.json, which is a person setting the guard up; EVERYTHING runs unattended, must not call sudo, and must not modify the tree it is testing - a run that rewrites .eslintrc.json is no longer testing the commit it started from. So the two checks themselves - the rule is configured, and every Authentication.checkBoardAccess / checkBoardWriteAccess in server/models is awaited - are one function that both callers share, and it uses grep rather than ripgrep so it also works where nothing may be installed to make it work. An unawaited permission check returns a pending promise, and a promise is truthy, so the call site passes a check that never ran.

On the Windows side, build.bat ran the three browsers as a single Playwright invocation writing one wekan-alltests-browsers.log, where “which browser failed” and “what did WebKit print” could not be answered afterwards - and CLAUDE.md’s “check the newest test logs” names the per-browser files. It now starts chromium, firefox and webkit as three jobs with a log, a status and a summary row each, as build.sh has always done, with a per-browser --output because Playwright clears its output directory at startup and three jobs sharing one would delete each other’s traces. EVERYTHING itself is not reimplemented there: Windows hands the whole run to releases/run-everything.sh, which calls build.sh --run-everything, so there is one implementation and the new stage arrives on both platforms at once. The parity guard now pins all of it, including that the shared checks install nothing.

Tests -> EVERYTHING runs again instead of quitting the script. Thanks to xet7.

Renaming the menu entry above broke it, between one commit and the next. choose() shows the SHORT labels and sets $opt to the FULL DESCRIPTION of the one picked, and the dispatcher hundreds of lines below matches that description as a case arm - so the description is an identifier written twice, and the rename changed only one of the two. With no arm matching and no catch-all, the case fell through, the for _once loop ended and the script EXITED: choosing Tests -> 1 printed nothing and ran nothing at all.

The arm carries the same string as the menu entry again. Two guards so it cannot come back: the parity test now checks every one of the 25 choose() entries against the case arms and fails on a description that matches none - it fails on the previous commit - and the dispatcher has a *) arm that names the option with no handler and says nothing was run, so if one ever does get past the test, the person at the menu is told rather than dropped back to the shell.

build.bat was checked for the same fault and cannot have it: its menus dispatch on the NUMBER typed rather than on a sentence, and its EVERYTHING hands the run to releases/run-everything.sh instead of reimplementing it. Verified in both directions anyway - every printed menu number has an if-dispatch in all seven menus, and every goto / call target resolves to a label - and that is pinned now too.

The "Playwright ALL browsers" option on Windows writes logs again. Thanks to xet7.

The EVERYTHING run above found this on its first green pass: the WeKan stage failed on one node suite, dbConformanceWiring, with “build.bat: playwright-all must be logged”. Splitting the whole-suite browser job into three uncovered a real gap rather than causing one.

call :onelog playwright-all existed in exactly one place - inside the combined three-project browser line of the sequential all-tests flow, which is gone now that each browser is its own job with its own log. Where it did exist it never worked: the call sat inside the cmd /c string of a STARTED child process, which has no build.bat labels to call, so %ONELOG% was empty and Tee-Object was handed an empty path.

Meanwhile the option that name was supposed to cover - Tests -> 11, “Playwright ALL browsers” - wrote no log at all: one playwright test call with three --project flags straight to the terminal, nothing left to read afterwards. That is exactly what the guard is about, and build.sh’s same option has always written one log per browser. It now runs the three browsers one at a time - still sequential, because three at once against one dev server exhausts RAM on smaller machines - each through the same :onelog helper as every other Tests option, and each with its own --output so Playwright does not clear another browser’s traces at startup. The guard drops playwright-all, which named an implementation that is gone and was broken, and gains what it was reaching for: that the ALL-browsers option logs, and covers all three.

Board export - what a backup contains, pinned against the source.

Every section of a board export is pinned, so a backup cannot quietly lose one. Thanks to Tuphal, KhaoulaMaleh and xet7.

#6274 was “export includes only comments from current year”: the exporters selected comments and activities by boardId, and records written by older WeKan versions carry no boardId, so every comment from a previous year was dropped from the JSON and the Excel export alike. #6275 fixed it by selecting on the cards’ ids, which every comment has. Its reporter then came back with a second one: “the export is now missing the lists part”.

Both were checked against the current source rather than assumed. The fix from PR #6275 is intact and survived the streaming-export rewrite that came after it: models/exporter.js and models/server/ExporterExcel.js each select comments by cardId and activities by boardId OR cardId, in all three of the streaming JSON writer, the in-memory one and the spreadsheet. The missing lists is not reproducible: result.lists has been written by the JSON exporter continuously since the CSV/TSV export was added, both JSON writers emit the same fourteen sections, and the flat formats carry each list’s name on the card’s own row.

So nothing needed fixing, and nothing about the export changed - what was missing was the guard. tests/exportBoardSections.test.cjs pins the section list, that the two JSON writers agree on it (a key only one of them writes is a section that appears or not depending on which path served the download), that no exporter selects comments by board again, and the two smaller fixes from that PR: the class has no this.boardId, which was the undefined field that ran a query against no board, and a comment whose author has been deleted falls back instead of writing undefined into the cell.

One thing that PR also fixed is deliberately gone: the Excel exporter used to LOAD custom fields and render them nowhere, and the streaming rewrite dropped that dead load along with the activities, checklists, subtasks and rules the spreadsheet also never showed. Custom fields survive where an import reads them back - the JSON export - and the guard pins them there.

The mocha test stage - what a suite on the client side may import.

A test that reads the repository moves to the side that has a filesystem. Thanks to xet7.

client/lib/tests/boardTriggersClass.tests.js is the regression guard for #5188, the rule trigger that could not be activated because a class name in the jade template and the one in the event handler had drifted apart by a letter. It reads both files off disk with fs and path and compares them - which is a server-side thing to do, and it ran on the client, where it worked only because node-polyfill-webpack-plugin’s browser shims were in the bundle.

The @meteorjs/rspack 2.1.0 update above drops that plugin, so the shims went with it and the meteor test client build stopped on one line: Cannot find module 'path' for matched aliased key 'path'. That is not one suite failing - the client bundle does not build, so every mocha suite is skipped and the stage reports a crash where its results should be. One file’s import took out the whole stage.

The suite was never wrong, only on the wrong side; it now lives in server/lib/tests, registered in that index. A new guard, tests/clientTestsNoNodeBuiltins.test.cjs, walks the client suites for imports of Node builtins - the mirror of the existing guard that keeps browser-only packages out of the server bundle - and pins where this one went. It strips comments before looking, or the sentence explaining the fix would be reported as the fault.

Running the exotic builds on Launchpad - and reading them afterwards.

A Launchpad build keeps its whole log, and the job outlives its own retries. Thanks to xet7.

Two things about the job made the failures above harder to fix than they should have been.

The build log is downloaded by remote-build and printed as its last 150 lines. For v10.71’s staging failure those 150 lines were the IsADirectoryError and then lpbuildd’s own Python traceback, the proxy-token revocation and the process scan - everything except the mongodb part’s output from an hour earlier, which is the part that would have said what bin was. The Launchpad build log is deleted along with the temporary snap recipe, so once the job ended, no copy of it existed anywhere. The job now prints the lines around every failure marker as well as the tail, and uploads the complete Launchpad and snapcraft logs as a snap-launchpad-logs-<arch> artifact - on always(), not failure(), because a build that succeeds on attempt 2 otherwise hides why attempt 1 did not.

The other is the job timeout, which was 180 minutes for a step that retries three times. The riscv64 leg spent 2h24m on attempt 1 alone, almost all of it queueing for a riscv64 builder, failed it on the bin bug above, and was cut off 35 minutes into attempt 2 - the The operation was canceled. in that job is this timeout and not Launchpad at all. A retry loop the job does not outlive is not a retry loop; 350 minutes fits two slow attempts and stays under GitHub’s 360-minute per-job ceiling.

The snap-launchpad job stops blaming LP_CREDENTIALS for every Stopped Launchpad build. Thanks to xet7.

The exotic snap architectures (ppc64el, s390x, riscv64, armhf) have no native runner and cannot be cross-built, so they are built with snapcraft remote-build on Launchpad. When such a build ends as Stopped with no .snap

  • Launchpad cancelling it mid-run, usually an out-of-memory in the Meteor npm install or a transient build-farm reset - the job scans the downloaded build log to tell that apart from a real credential failure. The scan pattern was unauthoriz|401|403, and a bare 401/403 matches any three digits anywhere in a large build log - a package size, a hash, an attempt 4/6 line - so it fired on almost every Stopped build and told the maintainer to re-create LP_CREDENTIALS that were working: the build had reached Building:, which already needs valid credentials. The pattern now matches those codes only in an HTTP-error context (HTTP Error 401, 403 Forbidden) or an explicit phrase (invalid credentials, not logged in, bad credentials), so the credential hint appears only for a genuine authorization failure. The Launchpad Stopped builds themselves are a Launchpad-side limit on slow emulated architectures, not a WeKan bug; the job already retries three times and is continue-on-error, so it never fails the release.

and updates the documentation and translations:

Thanks to above GitHub users for their contributions and translators for their translations.