Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v10.72
2026-08-08Binaries 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.
| Bundle | Binary | From | Version | Checked | SHA256 |
|---|---|---|---|---|---|
| arm64 | FerretDB | wekan/FerretDB | latest | verified | b4a627780b746988… |
| arm64 | FerretDB | wekan/FerretDB | latest | verified | b4a627780b746988… |
| arm64 | Node.js | nodejs.org | v24.19.0 | verified | 01443c1e1a29e531… |
| arm64 | Node.js | nodejs.org | v24.19.0 | verified | 01443c1e1a29e531… |
| armhf | FerretDB | wekan/FerretDB | v1.46.0 | verified | 82c4f226202e1038… |
| armhf | FerretDB | wekan/FerretDB | v1.46.0 | verified | 82c4f226202e1038… |
| armhf | Node.js | wekan/node-patches | v24.19.0 | verified | b55350f3071b765a… |
| armhf | Node.js | wekan/node-patches | v24.19.0 | verified | b55350f3071b765a… |
| armv7 | FerretDB | wekan/FerretDB | v1.46.0 | verified | 82c4f226202e1038… |
| armv7 | FerretDB | wekan/FerretDB | v1.46.0 | verified | 82c4f226202e1038… |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | verified | 8dbe0a9aa8550ad5… |
| armv7 | Node.js | wekan/node-patches | v24.19.0 | verified | 8dbe0a9aa8550ad5… |
| i386 | FerretDB | wekan/FerretDB | v1.46.0 | verified | 0d2f948e0337e5b1… |
| i386 | FerretDB | wekan/FerretDB | v1.46.0 | verified | 0d2f948e0337e5b1… |
| i386 | Node.js | wekan/node-patches | v24.19.0 | verified | 3b0b3bbfe27daf58… |
| i386 | Node.js | wekan/node-patches | v24.19.0 | verified | 3b0b3bbfe27daf58… |
| mac-arm64 | FerretDB | wekan/FerretDB | latest | verified | 063c66968a5d0d84… |
| mac-arm64 | FerretDB | wekan/FerretDB | latest | verified | 063c66968a5d0d84… |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | verified | 3f1cf157479c1480… |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | verified | 3f1cf157479c1480… |
| ppc64le | FerretDB | wekan/FerretDB | v1.46.0 | verified | 9ced5b800d82d184… |
| ppc64le | FerretDB | wekan/FerretDB | v1.46.0 | verified | 9ced5b800d82d184… |
| ppc64le | Node.js | nodejs.org | v24.19.0 | verified | c510c6ce12f07010… |
| ppc64le | Node.js | nodejs.org | v24.19.0 | verified | c510c6ce12f07010… |
| riscv64 | FerretDB | wekan/FerretDB | v1.46.0 | verified | d1bcfde0227c68d9… |
| riscv64 | FerretDB | wekan/FerretDB | v1.46.0 | verified | d1bcfde0227c68d9… |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | verified | cd1f14af28121480… |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | verified | cd1f14af28121480… |
| s390x | FerretDB | wekan/FerretDB | v1.46.0 | verified | 62bae6c40e1ad486… |
| s390x | FerretDB | wekan/FerretDB | v1.46.0 | verified | 62bae6c40e1ad486… |
| s390x | Node.js | nodejs.org | v24.19.0 | verified | a4792e65962ffa0a… |
| s390x | Node.js | nodejs.org | v24.19.0 | verified | a4792e65962ffa0a… |
| win64 | FerretDB | wekan/FerretDB | latest | verified | 508dbd2f26469fc1… |
| win64 | FerretDB | wekan/FerretDB | latest | verified | 508dbd2f26469fc1… |
| win64 | Node.js | nodejs.org | v24.19.0 | verified | 57f71ab3652e797d… |
| win64 | Node.js | nodejs.org | v24.19.0 | verified | 57f71ab3652e797d… |
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.
| Platform | Binary | From | Version | SHA256 |
|---|---|---|---|---|
| amd64 | Node.js | nodejs.org | v24.19.0 | 14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647 |
| amd64 | FerretDB | wekan/FerretDB | v1.45.0 | 94713f605167abb45a3717482d35de4824cb4a8f199c1400e826a8a2b04f3893 |
| arm64 | Node.js | nodejs.org | v24.19.0 | 01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc |
| arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 275ae50ac97e6a70eee72e6de37766c458775c5997c896352db5189c6cf1f04b |
| loong64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | c24f224726f2d785bd18a1fd09f5e6d1fecf0269928451a60c5da9eac8e92e68 |
| loong64 | FerretDB | wekan/FerretDB | v1.45.0 | 28bf67981168dfc4bd67698b41dd62628aafe347a77f2b1e6ffcadf009d575e0 |
| mac-arm64 | Node.js | nodejs.org | v24.19.0 | 3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94 |
| mac-arm64 | FerretDB | wekan/FerretDB | v1.45.0 | 639ed58b84820b3d588f4161c64d0ab940d0cc6e7d022088d60c2b0b97f99f8e |
| mac-x64 | Node.js | nodejs.org | v24.19.0 | d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4 |
| mac-x64 | FerretDB | wekan/FerretDB | v1.45.0 | fd519903f5630e881e38e7c5814f00c0e89ad26f6785f1ddcbab4058356fc9f3 |
| ppc64le | Node.js | nodejs.org | v24.19.0 | c510c6ce12f07010f771e6edb22a3fe23f4f2e6f40b1ffd4941aed0646a0d8b3 |
| ppc64le | FerretDB | wekan/FerretDB | v1.45.0 | de4518c7774d302533369c477759ddd866785d6741d98d399388eb8de3df175a |
| riscv64 | Node.js | unofficial-builds.nodejs.org | v24.19.0 | cd1f14af2812148002f58b58a5f9af512a50e3b8e8c148e0db44019dcb68edfd |
| riscv64 | FerretDB | wekan/FerretDB | v1.45.0 | 7dc2952f554e8800c4029577901999e06e10272da686f7e402177080067028f9 |
| s390x | Node.js | nodejs.org | v24.19.0 | a4792e65962ffa0af42627aacf1122a60c3c88dbf4e4184f06820d66f9da8ba4 |
| s390x | FerretDB | wekan/FerretDB | v1.45.0 | 0ae2e2f2cffdc5dd2ea4f125281a5e12eea216fbe49b5561d9c001700c3fc0c1 |
| win64 | Node.js | nodejs.org | v24.19.0 | 57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73 |
| win64 | FerretDB | wekan/FerretDB | v1.45.0 | f6337994368a52d011d438c82b914b0cedb3178fd030acac8db3dab8017cee85 |
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.0builds move to their finals with it:ecmascript0.19.0,rspack1.2.0,mongo2.5.0 andaccounts-password3.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-pluginand the browser shims it carried (crypto-browserify,browserify-sign,elliptic,node-stdlib-browserand their dependencies).body-parsermoves 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,$.nowand$.parseJSON- gone fromnode_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.jsonso 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,
dompurify3.4.12 → 3.4.13,markdown-it14.2.0 → 14.3.0,markdown-it-emoji3.0.0 → 3.1.0,temml0.13.3 → 0.13.4,@rsdoctor/rspack-plugin1.5.11 → 1.6.1,puppeteer25.3.0 → 25.5.0 and both halves oftypescript-eslint8.65.0 → 8.66.0. Only the lockfile changes: the version ranges inpackage.jsonstay 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
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
.toolsis 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 holdingclient/,server/andmodels/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 installor a transient build-farm reset - the job scans the downloaded build log to tell that apart from a real credential failure. The scan pattern wasunauthoriz|401|403, and a bare401/403matches any three digits anywhere in a large build log - a package size, a hash, anattempt 4/6line - so it fired on almost every Stopped build and told the maintainer to re-createLP_CREDENTIALSthat were working: the build had reachedBuilding:, 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 LaunchpadStoppedbuilds themselves are a Launchpad-side limit on slow emulated architectures, not a WeKan bug; the job already retries three times and iscontinue-on-error, so it never fails the release.
and updates the documentation and translations:
- CLAUDE.md drops the standing rule that forbade pushing. Thanks to xet7.
- Serbian translations. Thanks to translators and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.