The armhf snap failed all three attempts in v10.77 while s390x, ppc64el
and riscv64 each built on attempt 1. The Launchpad build log says why,
one line into the mongodb part:
Stage package not found in part 'mongodb': libcurl4.
Build failed
Ubuntu 24.04's 64-bit time_t transition renamed the library, and
`libcurl4` is not a real package on ANY architecture in noble - the
binary is `libcurl4t64` everywhere. On the 64-bit architectures the
renamed package keeps the old name alive:
Package: libcurl4t64
Architecture: amd64
Provides: libcurl4 (= 8.5.0-2ubuntu10)
and the armhf libcurl4t64 has no Provides line at all, because there the
ABI really changed. That is what makes this class of bug reach exactly
one architecture, which is also what makes it look like flakiness on
that architecture.
It had already happened, to libssl3 and libgoogle-perftools4 in v10.71;
the comment above this list describes it exactly. libcurl4 was the same
transition and was left behind. Every other stage-package was checked
against the noble armhf main+universe indices at the same time - all 14
of them - and libcurl4 was the only one that did not resolve.
The workflow's own guidance made it worse. snapcraft ends a missing
stage-package as a "Stopped" build with no artifact, which from the
outside is indistinguishable from Launchpad cancelling a build, so the
message said "often an OOM in the Meteor npm install, or a transient
build-farm reset ... re-run, and if it persists on this arch open a
Launchpad question" - the wrong advice for a failure that will never
succeed on a re-run. The step now looks for the line first, names the
package that is missing, says it is NOT transient, explains the t64
Provides asymmetry, and gives the one-line archive query that checks a
name.
New test tests/snapStagePackages.test.cjs, 6 checks: no stage-package is
a known pre-t64 name, libcurl is asked for by the name noble publishes,
the two fixed in v10.71 have not regressed, and - negatively - a name
that was never renamed keeps its plain name, because the rule is not
"add t64 to everything". It also pins that the reason stays written down
beside the list. Verified it fails when libcurl4 is put back.
Thanks to xet7 !
Four things the v10.77 run got wrong, all of them silent - nothing
errored, the reports were simply not what they claimed to be.
THREE GOOD SNAPS WERE REPORTED AS FAILED. s390x, ppc64el and riscv64
each printed "Remote build <arch> succeeded on attempt 1" and downloaded
their .snap, and each job then ended FAILED - because the step that
SAVES the build log could not upload it:
The path for one of the files in artifact is not valid:
/snapcraft-wekan-f82a93c2…_s390x_2026-08-09T11:39:05.txt.
Contains the following character: Colon :
snapcraft names a remote-build log after the recipe and an ISO
timestamp, and upload-artifact refuses a colon because NTFS cannot hold
one. The colons become dashes now, so the timestamp is kept rather than
the name thrown away - and the upload is continue-on-error, because a
DIAGNOSTIC upload must never be able to fail the build it is diagnosing.
The snap is the deliverable; the renaming is not the last thing that
could ever make a log file unuploadable.
EVERY ROW OF THE PROVENANCE TABLE APPEARED TWICE. provenance-table.sh
globbed `provenance/**/*.tsv provenance/*.tsv`, and with globstar `**/`
matches ZERO or more directories, so the first pattern already covered
the second and every file was read twice. It is one pattern now, and
rows are also deduplicated on the WHOLE line - a retried step or a job
that ran again records an identical line, and nothing tells the copies
apart. Deliberately not `sort -u -k1,1 -k2,2`: two rows sharing a bundle
and a binary but differing elsewhere are not a duplicate, they are a
real disagreement about which Node.js went into a bundle, and hiding one
at random is worse than showing both.
AMD64 WAS MISSING FROM THAT TABLE - the platform most people download.
Its step runs `cd .build` before `mkdir -p provenance`, so the rows went
to .build/provenance while upload-artifact looked at provenance/ from
the workspace root and found nothing: "No files were found with the
provided path: provenance/". Every other build job records from the
workspace root, which is why only this one was affected. It uses
$GITHUB_WORKSPACE now.
SIX PLATFORMS SAID VERSION "LATEST". amd64, arm64, win64, win32,
mac-arm64 and mac-x64 passed the literal string; only the
extra-architecture job asked which version `latest` actually was, and it
did so with its own inline curl+python. "Which FerretDB did v10.77 ship"
is the one question that column exists to answer. All seven sites now
call the new releases/ferretdb-latest-tag.sh, which asks once per job
and caches, authenticates when there is a token so a shared 60/hour
limit is not what makes it fail, checks that the answer is shaped like a
tag before printing it into a markdown cell, and prints NOTHING and
exits 0 when it cannot find out - the caller's ${FERRET_TAG:-latest} is
then exactly what it had before. A release note must never fail a build
that produced a good bundle.
The table's own prose linked [wekan/node](https://github.com/wekan/node);
the binaries come from wekan/node-patches, which is what the rows
themselves already linked.
New test tests/provenanceTable.test.cjs, 20 checks, most of them running
the scripts rather than reading them. Verified in both directions: with
the doubled glob restored the glob guard fails, with amd64's relative
path restored its guard fails, and with the literal 'latest' restored
its guard fails.
Thanks to xet7 !
WeKan ships binaries other projects publish - FerretDB, the MongoDB
Database Tools, Node.js - and fetches each from a release URL. There are
many such URLs, across workflows, release scripts and compose files, and
they must all agree: one that names a fixed version stops receiving
security fixes silently, and nothing about the build fails when it does.
That is not hypothetical. A Quay scan of the v10.77 image reported Go
advisories in the FerretDB binary baked into the bundle - stdlib 1.25.9
wanting 1.25.11, golang.org/x/sys v0.38.0 wanting 0.44.0. The source was
already fixed and v1.48.0 was already published carrying go1.25.11 and
x/sys v0.46.0; the image had simply captured an older latest at build
time. Had any of these URLs been pinned instead, the rebuild would not
have fixed it either.
The test finds the URLs by searching the repository rather than from a
list, so a build site added later is checked too, and it accepts three
shapes: latest/download/<asset>, the bare latest API endpoint that
release-all.yml asks which version latest resolved to for the provenance
table, and ${VAR} whose default is checked separately to be latest.
Node.js is a different mechanism with the same effect - NODE_VERSION is
the bare major 24 and resolve-node-source.sh answers with that CPU's
newest 24.x from whichever of nodejs.org, unofficial-builds or
wekan/node-patches has one - so it is asserted to stay that way.
Thanks to xet7 !
BuildKit attaches MINIMAL provenance by default - a build id and a timestamp -
which is what those `unknown on unknown` rows on quay.io are: the OCI marker for
"not a runnable image", one per platform, so `docker pull` never selects them.
mode=max records what actually answers a supply-chain question instead: which
commit, which base image, which build arguments.
WeKan pushes images from FOUR places - docker-publish.yml's build-push-action,
two `docker buildx build --push` in release-all.yml, and one in
releases/docker-build.sh - and none of them said anything about provenance, so
all four took the default. All four ask for mode=max now. The local `--load`
check build in docker-build.sh deliberately does not: the docker exporter cannot
carry an attestation at all.
SBOM stays off, and the test says why so it is a decision rather than a drift:
it enumerates every OS package and npm dependency, taking the attestation from
~10 KiB per platform to megabytes.
tests/dockerProvenance.test.cjs finds the build sites by SEARCHING for them
rather than from a list, so a fifth one cannot quietly fall back to the default.
It also pins a trap I fell into twice while writing this, once in the shell
script and once in a YAML `run:` block:
docker buildx build \
# explain the next flag
--provenance=mode=max \
--platform ... \
A `#` after a line continuation comments out the REST OF THE JOINED LINE, so
that is `docker buildx build` with every argument swallowed - no platforms, no
tags, no push. `bash -n` accepts it, because it is valid syntax; it is simply a
different command. Both comments live above their command now, and the test
fails on any comment sitting among a continued command's arguments - while
ignoring a comment block that merely quotes a multi-line example, which is what
it flagged first.
Thanks to xet7 !
Same group, because it is the same area and the same one-line cause - a gh call
without --repo in a job that does not check out. The subsection is 'bugs' now
rather than 'bug'.
Thanks to xet7 !
The same one-line bug AppImage.yml had, and a second one beside it.
gh release upload 'v10.77' assets/* --clobber
failed to run git: fatal: not a git repository
Both flatpaks built and were uploaded as artifacts; the job that attaches them
does not check the repository out - it only downloads artifacts - so `gh` had no
git remote to infer the repository from. Every gh call in this workflow passes
--repo now, including the ones in jobs that DO check out.
The second: `assets/*` is not the bundles. The artifacts also carry the ostree
repository the flatpak was exported through - config, objects/, refs/,
summaries/, summary, summary.idx - which is build scaffolding, and attaching it
to a release would have put a few hundred directories there beside the two
files anyone wants. The upload names the bundles and their checksums instead.
TSC hit this exact thing and fixed it the same way.
No architectures to add, and the header now says why rather than leaving it to
be rediscovered. A flatpak runs against a RUNTIME, so an architecture exists
here only if freedesktop.org publishes org.freedesktop.Platform for it: x86_64
and aarch64, the i386 and arm runtimes having been discontinued. That is the
difference from AppImage.yml, which could grow to four - an AppImage carries its
own runtime binary, and those exist for i686 and armhf too.
Thanks to xet7 !
v10.77 was cut while they were being written, so they get their own Upcoming
above the release, and the links point at the hashes the rebase onto the version
bump produced rather than the ones that will be collected.
Thanks to xet7 !
x86_64 and aarch64 become four, with i686 and armhf. That is the whole set: an
AppImage BEGINS with a runtime binary the kernel executes, and runtimes exist
for exactly x86_64, aarch64, i686 and armhf. ppc64le, s390x, riscv64 and loong64
have none, so they cannot have an AppImage at all however the job is written -
they are served by the bundle zip and the .deb, and the matrix now says so where
somebody would otherwise try.
The two are not equally safe, and they are not treated as though they were.
i686 builds on the x86_64 runner, which runs 32-bit x86 natively: no emulation,
no multiarch, and appimagetool-i686 runs as-is. It is an ordinary job.
armhf builds on the aarch64 runner, which runs 32-bit ARM only if the kernel has
CONFIG_COMPAT and the 32-bit loader is installed. GitHub promises neither, so it
is continue-on-error - the same treatment TSC gives its emulated armhf job. If
it proves reliable, drop the flag; if it never works, drop the entry. What it
must not do is decide whether the other three reach the release.
The smoke test needed the distinction too. It starts the AppImage and waits for
WeKan to answer on port 8080, which a 64-bit runner cannot do for a 32-bit build
it cannot execute. A CHECK THAT CANNOT RUN IS NOT A FAILED CHECK: when the
binary will not execute at all AND it is not this machine's architecture, the
AppImage is as good as anything here knows, so it warns, says it is uploaded
unchecked, and moves on. A build that DOES start and then does not answer still
fails - that is the bug the step exists for.
The publish job's two hardcoded architecture lists learned all four, so a
missing one warns instead of going unnoticed, and the base-bundle check looks
for wekan-<v>-{i386,armhf}.zip as well.
Untested here: this is a workflow, so the first real run is the test. The two
new jobs are the only ones that can fail, and armhf cannot take the release with
it.
Thanks to xet7 !
Both AppImages built, both passed the smoke test that starts them and waits for
WeKan to answer on port 8080, both uploaded as artifacts, both downloaded by the
attach job with matching digests - and then nothing reached the release:
gh release upload 'v10.77' assets/* --clobber
failed to run git: fatal: not a git repository
The attach job does not check the repository out. It has no reason to - it only
downloads artifacts - so `gh` has no git remote to infer the repository from.
Every other gh call in this workflow already passes --repo; these two did not.
Every gh call passes it now, including the two in jobs that DO check out. A call
that relies on an inferred remote breaks the day somebody removes the checkout,
and the error it gives names neither gh nor the workflow.
Worth recording for whoever reads the failed run: the log looks far worse than
the bug. `::error::` lines appear in the build jobs' output for the bundle-file
check and the port-8080 check, and in the attach job for "no architecture
produced an AppImage" - none of which fired. They are the SCRIPT being echoed by
the ##[group] header, not output. The real failure is one line, in the last step
of the last job.
Thanks to xet7 !
The page listed five architectures and omitted armhf, which snapcraft.yaml has
built all along. It said the release publishes candidate, beta and edge and that
"the stable channel is published manually later" - which is no longer true and
was the reason a build reached three channels of four. And it said nothing about
the two architectures that are bundles but not snaps, so "missing" and "cannot
be there" looked identical.
The matrix is the six `build-for:` entries with the bundle name beside each,
because the two names are not always the same, and a new section explains the
three ways they differ:
ppc64le / ppc64el the same 64-bit POWER under two names - the bundles use
the kernel's, the store uses Debian's. The only rename.
armhf / armv7 NOT a rename, and getting this wrong ships a snap that
crashes. node-patches builds armhf to the Debian baseline
(hard-float, VFPv3-D16, no NEON assumed) so it runs on any
ARMv7-A, and armv7 with NEON for boards that have it. The
store has ONE 32-bit ARM architecture serving every such
device, so it must carry the BASELINE build; the NEON one
would be an illegal instruction on a board without NEON.
armv7 ships as a bundle only.
i386 cannot have a new snap at all, and this is categorically
different from a missing Node.js build. node-patches
patches SOURCE so a binary can be built; here the BASE SNAP
does not exist, because Ubuntu 24.04 has no i386 port. No
patch set can produce a base Canonical does not publish.
The last base with one was core18, end-of-life.
Then what to do about it: the three snaps, all four channels, and why stable is
possible - core24 is a released base, so the snap carries grade: stable, which a
grade: devel core26 snap cannot. A table of what each snap has in the store
today and what is still to upload, including the two fossils that channel
promotion cannot fix: wekan-ondra's armhf at 0.22 and its i386 at 0.X-ci, which
have no newer revision to promote.
And the caveat that matters when running it: without a pinned version the newest
revision of each architecture is promoted, and edge is often ahead of stable, so
a bare run publishes edge builds to stable users.
The armv7 note in models/lib/snapArchitectures.js said "different CPUs", which
was the right conclusion from the wrong reason. It now says what node-patches
actually builds, and a test cross-checks it against that repo's own workflow so
the explanation cannot drift from the binaries.
Thanks to xet7 !
The helper that did this released ONE snap, ONE revision, to THREE channels:
snapcraft release wekan $1 edge,beta,candidate
wrong three ways at once. It names only `wekan`, leaving wekan-ondra and
wekan-gantt-gpl to be done by hand. It takes ONE revision number, and revisions
are PER ARCHITECTURE - the store's own listing shows wekan at 3601 on amd64 and
3600 on arm64 for the same 10.76 - so one number can only ever be right for one
of them. And it leaves out `stable`, so a build reached three channels of four
and somebody had to remember the fourth.
releases/snap-release-all-channels.sh resolves the revision per (snap,
architecture) from the store itself, so no revision number is ever typed, and
releases it to all four channels in ONE call - a revision reaches all of them or
none. A (snap, architecture) with no revision is reported and skipped rather
than failing the run: the three snaps genuinely have different architecture
sets, and one architecture failing must not stop the rest.
THE MAPPING IS THE HAZARD, and it is now in one place with both directions
tested (models/lib/snapArchitectures.js):
ppc64le and ppc64el ARE the same hardware. The bundles at
https://github.com/wekan/wekan name it the way Node.js and the kernel do; the
Snap Store names it the way Debian does. Nothing warns when the wrong one is
used - an unrecognised --arch is simply an architecture the store has never
heard of, so it looks like it worked.
armhf and armv7 are NOT. armhf is 32-bit Raspberry Pi OS; armv7 is the
ODroid-U3. Different CPUs, different bundles. The store has ONE 32-bit ARM
architecture, armhf, so that is what is published; mapping armv7 onto it would
hand Raspberry Pi users the ODroid build. The negative test for that is the
most important one in the suite.
The six architectures are exactly snapcraft.yaml's `build-for` list, and a test
compares the two so the table cannot drift from what is actually built. i386 and
armv7 are recorded as not-built, with their reasons, and the script prints them
on every run - "it is missing" and "it cannot be there" look identical from the
outside.
tests/snapArchitectures.test.cjs: 23 checks including that the newest revision
for THAT architecture wins, that a version filter publishes what was built
rather than whatever was uploaded last, that `snapcraft list-revisions` parses
when a revision has no channels, and that junk never reaches snapcraft.
Thanks to xet7 !
A rebase rewrote the commits these entries link to, so the links named commits
that no longer exist and would have 404ed on GitHub. Repointed by
releases/fix-changelog-hashes.sh, which only touches a link no ref in this clone
can reach.
Thanks to xet7 !
v10.75 was cut while they were being written, so they get their own Upcoming
above the release. The FerretDB one is summarised here and told in full in that
repo's CHANGELOG, since it is the database that changed.
Thanks to xet7 !
Neither of these gave a wrong answer. They asked the right question one document
at a time, and each await is a full round-trip that starts only once the last
one finished - invisible on a board with four cards, and the whole cost of the
request on a real one.
The bulk label endpoint (PUT .../cards/labels) awaited a getCard per id and then
an update per id. At BULK_CARDS_MAX = 500 that is ONE THOUSAND sequential
round-trips for a single request. The reads are all the same question, so they
are one `$in` query indexed into a Map; the writes genuinely differ - each card
merges its own labelIds - so they stay individual updates, but they are issued
together and awaited once instead of each waiting for the last.
Two details that had to survive: the loop still iterates cardIds rather than the
query result, because a batch read comes back in the database's order and
without the ids that matched nothing - iterating it would silently reorder
`updated` and lose `notFound`. And every write is awaited before the 200 is
sent, so the response still means what it says.
The bulk DELETE beside it is deliberately left alone: its per-card work is real
(cardRemover runs the sub-item hooks, each card gets its own activity), so
batching the reads would save one query inside a loop that does far more than
query.
Global search resolved each `user:`/`member:`/`assignee:`/`creator:` name with
its own awaited findOne, so `member:ann member:bob member:carol` was three
serial round-trips before the search itself could start. They are all the same
question - which of these names is an account - so it is asked once, with `$in`,
and answered from a map. A name typed under two operators is one lookup now, not
two. Unknown names are still reported against the operator they were typed
under, because "ann is not a user" is not useful without saying where ann was
typed.
tests/queryBatching.test.cjs pins both, including the negatives: no per-card
read remains in the label handler, no per-username query remains in search, the
response order and notFound list are preserved, and the delete handler is still
per-card on purpose.
Thanks to xet7 !
The release script renamed the old Upcoming to v10.74, and two commits landed
after it: the linked-card cache that could not see a card added later, and the
build-script parity fix. They get a fresh Upcoming section above the release,
which is where development entries go.
Both were rebased onto the version bump, so their hashes changed; the links
point at the ones that will be pushed, not the ones that will be collected.
Thanks to xet7 !
The helper the five linked-card cursors share memoized its result per board, to
spare them the duplicate queries the five copies of that preamble used to make.
That is wrong: publishComposite re-runs a child's find() when the parent
document changes, and a cache living for the whole subscription then serves the
ids computed the FIRST time forever - a linked card added later would never be
published, and one removed would go on being published.
The five cursors each ran these same queries before the helper existed, so
computing per call is exactly the cost they always had, and it is correct. The
guard that pinned the memoization now pins its absence, with the reason.
Thanks to xet7 !
The one red suite in the test run, and it was right twice over.
`Setup: "git pull" and "git push" that finish the job` added two entries to
build.sh's menu and left build.bat behind, so `build.bat git-pull` answered
nothing. It answers to both names now, with its own :gitpull/:gitpush labels -
the ones its interactive menu already used.
The guard also had to learn what a `!` entry is. It marks something build.sh
runs ITSELF rather than by executing a file in releases/. Most are raw commands
and the .bat runs those the same way, so those still compare as before; the
exception is an entry naming a build.sh FUNCTION, which is shell the .bat has no
way to call. Comparing those as scripts is what made this suite fail the moment
"Update git" was replaced. A function entry is now exempt from the script
comparison and checked separately: build.bat must implement a label of the same
name, so one dropping out of a menu still fails.
The node suites are 339 run, 0 failed for the first time in this branch.
Thanks to xet7 !
The five linked-card cursors are its own commit, and its story is told inside
the ParentBleed entry - so a reader could read about the fix and had no way to
reach it.
Thanks to xet7 !
Six dependabot bumps and two developer-facing commits since v10.73 had no entry
at all. Adding them showed the subsections were also out of order - fixes came
before features - so they are now CRITICAL SECURITY, features, dependencies,
bug fixes, developer-facing, which is what CLAUDE.md specifies.
Thanks to dependabot and xet7 !
The page was doing its job and nobody had acted on it. Two real WeKan bugs were
sitting there as rows marked "unknown / unclassified", whose advice was to add a
rule to models/lib/databaseErrors.js. Both are fixed, and both now classify.
1. moveSwimlane, four times over a week:
Error: update is not available on the server. Please use updateAsync()
Board.ensureDefaultSwimlaneId() called the SYNCHRONOUS Swimlanes.upsert().
Meteor 3 removed it on the server, so every move that reached the default
swimlane self-heal threw. The async twin already existed one function below.
This getter is synchronous and cannot await, so the upsert is now STARTED and
not waited for: the self-heal still happens, and the call returns what is in
the cache - which every caller already had to handle, because it was
undefined before the self-heal existed at all. A caller that needs the
swimlane back in the same tick uses ensureDefaultSwimlaneIdAsync().
2. moveList:
ValidationError: Failed validation
Cannot read properties of undefined (reading 'title')
List.move() inserted a new list with `title: this.title`. When a list has no
title, `title` is required by the schema, so the insert fails validation - and
collection2's own error formatter then reads a property of the undefined field
and throws, which is why the admin saw a crash naming neither the list nor the
real problem. It now says what is wrong before the insert: "This list has no
title, so it cannot be moved to another board. Give it a title first." The two
console.log lines that used to sit there printed the title to a log nobody
reads and did not stop the crash; they are gone.
And two classifier rules so neither reads as "unknown" again. Both say plainly
that this is WeKan's bug rather than the database's or the admin's - there is
nothing to configure - and send the reporter to the right place: upgrade, then
open an issue with the method name. Severity `warning`, the vocabulary this
stream uses.
Thanks to xet7 !
Attachments and avatars are files under WRITABLE_PATH, and the database holds one
document per file. NOTHING checked that the two still agree. A file can be
replaced, truncated, back-dated or deleted by anything that reaches the
filesystem - a bad restore, a sync tool, a container rebuild, a shell on the
volume - and WeKan would keep serving whatever is there now, silently.
A baseline is recorded per file in the existing WeKan database (no new files
under WRITABLE_PATH): path, size, modification time, and md5, sha256 AND sha512.
Three digests, because they answer different questions. md5 is what other tools
print, so an admin can compare against a backup with what they already have - it
is never alone, its collision resistance is gone. sha256 is the working digest.
sha512 earns its place on the day the two DISAGREE: two digests over the same
bytes cannot, so when they do, the bytes were not read the same way twice - a
failing disk, a partial write, a truncated copy - which is a different problem
from a substitution and is its own critical finding.
ed25519 is the fourth check and it is NOT a hash - it is a signature, and it
answers the one thing three digests cannot: who says these are the right hashes?
Anybody who can rewrite a file can rewrite a row of hashes. Each entry is signed
over a canonical line (fixed field order, no JSON, whose escaping is not
guaranteed stable) and verified on every scan. A record that does not match its
signature is critical and is NEVER explained away by an ordinary edit. The key
comes from WEKAN_INTEGRITY_PRIVATE_KEY, which WeKan never stores - that is the
configuration that means something against an attacker who reaches the database
too - or is generated and kept in the database, which detects filesystem-only
tampering and is honest about being no more than that. A MALFORMED supplied key
is reported rather than silently replaced by a generated one, which would look
like it was working while checking nothing the operator meant.
The scan is paced by policy, not by hope: once a day, checked hourly; nothing
starts or continues at or above 60% CPU; 50 ms between files plus 20 ms per
megabyte; a fifteen-minute budget, after which it stops and continues tomorrow;
and ONE read per file for all three digests. A run that stopped early does not
report the files it never reached as missing.
The finding is a change WITH NO RECORD SAYING WHY. Files change when people use
WeKan, and those changes are accounted for, reported once at info and
re-baselined so they do not repeat daily. A change with nothing to explain it is
at least medium and is deliberately NOT re-baselined, so it keeps showing until
somebody looks. A modification time that moved BACKWARDS is called out
separately: nothing does that by accident.
Crashes and downtime, in the same stream, because a crash leaves nothing behind:
the process is gone, so it cannot write a message about being gone. A heartbeat
every minute is the only thing that works, and the NEXT start reads it. A first
run and a clean stop record nothing - reporting every deliberate restart is how
a Problems page becomes one nobody opens. A short gap with no clean-shutdown
mark is a note; a long one is a high-severity "it stopped without shutting down
cleanly, and here is how long it was down". Uncaught exceptions and unhandled
rejections land here too, with their cause, before the process goes.
Admin Panel / Problems / Filesystem integrity is the new stream, with the
Username / IP address / Attempts columns. The baseline itself is never published
and never client-writable: it is a map of every file on the server, and the key
document is a private key.
tests/fileIntegrity.test.cjs, 34 checks: every digest case including same-size
substitution and disagreeing digests, the ed25519 forgery attempt, the canonical
line, explained vs unexplained, and the pacing - including that an unreadable
clock does not start a scan, which it did until the test said so.
Thanks to xet7 !
The security event log said what a guard DID - a request blocked, a filename
sanitized - which answers "is WeKan defending itself" and not the question an
admin asks next: who did that, and from where. It also could not tell a browser
that got confused from somebody working through the Hall of Fame one entry at a
time.
A CANARY is a tripwire at a point that only a permission-override attempt
reaches. Ordinary use never gets there - the code path runs when a request asks
for something the rules forbid - so a trip is not noise, and it is worth an
admin's attention with the actor attached. Design:
docs/Security/Remediation/WeKan.md §12.
Three properties, each enforced by a test:
SILENT. Tripping one changes NOTHING the caller can observe. tripCanary()
always returns false and tripCanaryDeny() always returns true, so a call site
reads as the refusal it replaces:
if (forbidden) return false; // before
if (forbidden) return tripCanary('card.vote-field', { userId });
The return value is decided before any reporting is attempted and the reporting
is wrapped, so a canary can never throw into, delay or disturb the request it
watches. The REST handler re-throws the ORIGINAL error object. This is not
politeness: a canary that announces itself is a map of which paths are watched,
and a probe would simply avoid them.
BOUNDED. A canary sits where an attacker can loop, so one row per attempt would
be a denial of service they get for free. models/lib/canaryTokens.js - pure, no
Meteor, no clock of its own - records the first trip of a (canary, actor) pair
at once, COUNTS the rest of the window, flushes one summary carrying the total
when the window closes, and stops writing after 60 summaries for a pair that
never gives up. The tracked-pair map is capped at 5000 and evicts the LEAST
RECENTLY seen, so a long-running attacker is not pushed out by a passing one and
a botnet varying its address cannot grow it. Idle pairs are swept on a timer. A
thousand attempts in a minute cost ONE row, and a suppressed trip costs one map
lookup. The username lookup is cached and capped, and neither it nor the insert
is awaited on the caller's path.
ATTRIBUTED. Every event carries userId, username, ip and count. The username is
denormalised at write time on purpose: it is what the account was CALLED when it
tried, so a rename does not rewrite history and a deletion does not erase it.
The address uses the same spoofing-safe resolver as the login throttle -
X-Forwarded-For only as far as HTTP_FORWARDED_COUNT says to trust it - or an
attacker could write a colleague's address into the security log by sending a
header. `detail` says what was attempted in the words of the feature, never the
payload.
Seventeen canaries are wired at the permission checks that refuse the attempts
behind BoardBleed, ParentBleed, ChecklistBleed, PathBleed and CommentBleed, plus
the direct vote/poker field writes and a stored path that points outside the
storage root. All server-side: no Playwright, no browser, nothing to install.
Admin Panel / Problems / Security gains Username, IP address and Attempts
columns, and both new columns are searchable - the thing an admin does with one
security event is pivot on it.
tests/canaryTokens.test.cjs (25 checks) drives the limiter through bursts, an
endless probe, eviction order and junk input; tests/canaryCoverage.test.cjs (18)
pins every canary to its call site and asserts none of them changes what the
caller gets back.
Thanks to xet7 !
The five linked-card cursors and the streaming attachment read join the
ParentBleed and PathBleed entries they extend; the comment-reaction ownership
fix and the security-test coverage guard get subsections of their own.
Thanks to Alpastx and xet7 !
"Are the security tests enough to prevent what is in the Hall of Fame" was
unanswerable. WeKan publishes 58 named vulnerabilities at
https://wekan.fi/hall-of-fame/ ; some suites name the one they belong to, most
did not, and the Hall of Fame lives in a different repository that CI never
checks out. Answering it meant reading 58 pages against 300 suites by hand,
which means it was never answered - and a regression test that quietly stops
existing is worth nothing, the failure mode tests/testsAreRegistered.test.cjs
already caught once when two *bleed suites drifted out of the mocha index.
So the list lives in the repository that has the tests, and
tests/securityRegressionCoverage.test.cjs keeps the two in step. Every published
vulnerability is either GUARDED - named by a suite that still exists - or
RECORDED, a gap with a written reason, which is the CHANGELOG's "TODO Later"
pattern applied to tests. The gap count is pinned, so a vulnerability cannot be
published with neither a test nor a note; a gap that turns out to be guarded
after all fails too, so coverage is never understated; and a file that merely
REGISTERS suites is not accepted as coverage, or the guard would pass itself.
Getting there meant naming vulnerabilities in the suites that already guarded
them, which is most of what this commit is:
- tests/securityMeifukun.test.cjs guards eight reports and named one. Its
sections are RedirectBleed, SourceBleed, LiveBleed, CasBleed, OidcBleed,
MetricsBleed, ImpersonateBleed and InviteBleed - seven vulnerabilities that
looked untested and were not.
- tests/noIdentityReplacement.test.cjs guards IdentityBleed and PatternBleed,
the same CodeQL finding twice.
- ExportBleed, CrashBleed, MimeBleed and the four LockoutBleed suites now say so.
- tests/crossBoardParentCardLeak.test.cjs also guards BoardBleed's cross-board
move deny, and now checks it on Lists and Swimlanes too, not only Cards.
The count that comes out of it: 29 of 58 published vulnerabilities have a named
regression test, and 29 are recorded gaps - mostly older fixes from before WeKan
tested its security fixes at all. They are not known to be unprotected; they are
known to be unchecked, which is a different and more honest statement, and each
one now says what it would take to close it.
Thanks to xet7 !
Found while auditing for more of GHSA-pqr4-rxgp-hv2m (CommentBleed - the REST
comment delete that checked board membership and nothing about WHOSE comment it
was). A CardCommentReactions document holds
{ cardCommentId, reactions: [ { reactionCodepoint, userIds: [...] } ] }
and the whole array is ONE field, whose allow rule was board membership for
insert, update and remove alike. So any member could $set `reactions` to
anything: add a colleague's userId to a reaction they never made, or remove one
they did. `toggleReaction()` only ever touches Meteor.userId(), so no legitimate
client sends anything else - the rule simply never said so.
Integrity rather than confidentiality: reactions are visible to the whole board
already. But it puts words in another person's mouth, and it is the same shape
as the advisory - a per-user object gated only by "are you on this board".
A deny rule now refuses an update that changes any OTHER user's presence in any
reaction. The decision is pure and lives in models/lib/reactionOwnership.js:
it compares MEMBERSHIP rather than array order, because the client rebuilds the
array on every toggle and a reordered array with the same membership is the same
set of reactions. The modifier forms that cannot be checked that way -
$push/$pull/$addToSet/$unset, or a dotted `reactions.0.userIds` - are refused
outright, since toggleReaction() always $sets the whole array and letting them
through would be a hole the size of the one being closed.
The membership rule it sits on top of is unchanged: read-only and no-comment
members still may not react at all.
tests/reactionOwnership.test.cjs covers both directions - adding and removing my
own reaction, the first reaction and the last one going away, a reordered array,
several of my own toggled at once - and the attacks: adding somebody else,
deleting somebody else's, wiping an array that was not only mine, moving
somebody else between reactions, an anonymous caller, and junk in either array.
Thanks to xet7 !
GHSA-jvv9-498p-hxrg was reported against the ANCESTOR cursor of the `board`
publication - a cross-board `parentId` made it publish a private board's cards
to another board's subscribers. Auditing for more of the same found the
linked-card cursors beside it with the identical hole and a wider blast radius:
five of them, publishing the linked card, its comments, its attachments, its
checklists and its checklist items.
A `cardType-linkedCard` names a card by id exactly as `parentId` does, and that
card may live on any board. Creating one requires read access to that board -
but the person who created it is not the person the publication is sending to.
Every subscriber of the board carrying the linked card was getting the source
card's full document and all of its children, whether or not they may see the
board it lives on.
The same answer as the ancestor cursor: a linked card whose source board the
subscriber cannot see is not sent. Cards on THIS board are their own answer,
since they are already going to this subscriber. The five cursors repeated the
same fifteen-line preamble verbatim; they share one helper now, memoized per
board, which is also what stops the sixth from being written without the check.
Two more findings from the same audit, in the same file and its neighbour:
- Three further hand-written copies of the board-visibility `$or` (the boards
publication, getAllBoardsPage, getAllBoardsTileData). All three were correct,
which is exactly the state RevokeBleed's copy was in before it drifted. They
call boardVisibilitySelectors() now, so there is no hand-written copy left in
the file.
- The STREAMING attachment export read `versions.original.path` with nothing but
an existsSync. The attachment allow rule refuses a client-supplied path, so
this is not the PathBleed hole - but it was the last place a stored path was
read without the containment check, and a path is only as trustworthy as every
way it could have been written.
tests/crossBoardParentCardLeak.test.cjs grows six checks: all five cursors go
through the helper, the helper asks the same visibility question, no cursor
builds its own linkedId list any more, the assigned-only member narrowing
survived the de-duplication, the helper is memoized, and no hand-written
visibility array remains.
Thanks to Alpastx and xet7 !
PathBleed, RevokeBleed, ParentBleed and CommentBleed, each linking its own fix
commit and its Hall of Fame page, under a CRITICAL SECURITY ISSUES header.
Thanks to Alpastx and xet7 !
Over DDP, deleting a comment is author-or-board-admin, and a board that sets
`restrictCommentEditing` takes even the admin's ability away (#5906). That rule
was enforced in a CardComments collection hook keyed off the Meteor userId, with
an early `if (!userId) return` for the genuine server-internal callers - board
copy, cleanup, migrations - that run with no authenticated user.
An HTTP request carries no Meteor userId into the invocation context. So
DELETE /api/boards/:boardId/cards/:cardId/comments/:commentId
which checked only `Authentication.checkBoardAccess` - board MEMBERSHIP - reached
the collection with no user, the hook took its "server-internal, trust it" path,
and any normal member could delete any comment on the board. HTTP 200, the
comment gone, restrictCommentEditing or not, while the same deletion over DDP
was correctly refused (CWE-639, CWE-863). Broken object-level authorization:
harassment and evidence destruction on a shared board.
The fix does not rely on the hook seeing something it cannot see. The handler
loads the comment - 404 when there is none - and applies the same rule itself,
with req.userId, before removing anything. That rule is now an exported function
(`assertCanMutateComment` in models/cardComments.js) that the hooks and the REST
handler both call, so DDP and HTTP cannot enforce different things; it carries a
403 statusCode so a refusal answers Forbidden rather than 500. The no-userId
path stays, documented for the internal callers it was written for.
tests/restCommentDeleteAcl.test.cjs pins that the handler loads the comment,
that the ACL runs BEFORE the removal, that membership alone is no longer
sufficient, that the DDP hooks still call the same function, and - so a future
edit route cannot repeat this - that no REST PUT for comments exists without the
same check.
Thanks to Alpastx and xet7 !
Fixes GHSA-pqr4-rxgp-hv2m.
A card's `parentId` may name a card on ANOTHER board, and setting it was
authorized only against the CHILD board's write ACL - nothing asked whether the
person setting it, or the people subscribed to the child board, were allowed to
see the other board at all.
The board publication walks the whole ancestor chain, because the
'prefix-with-full-path' subtask setting renders a subtask's complete path
(#3453), and it published the complete ancestor card DOCUMENTS to every
subscriber of the child board. So a member who could write on shared board B and
knew a card id on private board A could point a card at it and have A's card -
title, description, custom fields - delivered over DDP to people who are not
members of A (CWE-200, CWE-862).
Closed at both ends, because either alone leaves half the hole open.
The WRITE refuses a parent whose board the actor cannot see. That is one
question, asked in one place (server/lib/visibleBoardIds.js) with the same
selectors the publication and All Boards use - so an ACTIVE org, team or domain
share counts and a revoked one does not. It is enforced on the REST card create,
on the REST card update (the PUT the report used), and on DDP by a deny rule
covering both insert and update, beside the cross-board MOVE deny that
GHSA-gm7v-pc38-53jr added. A parent that does not exist is refused too. Clearing
or omitting a parent is untouched.
The PUBLICATION sends only the ancestors whose board the subscriber may see. The
board being published is its own answer, so a normal same-board subtask path is
unchanged; an ancestor on a board the subscriber cannot see is simply not sent,
and a chain that yields nothing publishes nothing.
This is the shape of the check the linked-card path already made - creating a
linked card requires read access to the source card's board - applied to the
field that did not have it.
tests/crossBoardParentCardLeak.test.cjs pins the filtered cursor, that the
unfiltered publish of every ancestor is gone, that both REST paths validate
before writing, that the deny rule covers insert as well as update, and that
clearing a parent and the pre-existing cross-board move deny still work.
Thanks to Alpastx and xet7 !
Fixes GHSA-jvv9-498p-hxrg.
`isActive: false` is how a board admin REVOKES a share with an organization, a
team or an email domain. The All Boards list honoured it - it matches shares
with `$elemMatch: { ..., isActive: true }` - so a revoked user watched the board
disappear from their list and everything looked correct.
The `board` publication did not. That is the publication that sends the board
document and its lists, swimlanes, cards, comments and attachments, and it
matched shares with a dotted selector instead:
$or.push({ 'orgs.orgId': { $in: orgsIds } });
$or.push({ 'teams.teamId': { $in: teamsIds } });
$or.push({ 'domains.domain': { $in: emailDomains } });
A dotted path matches an element of the array without saying anything about
that element's other fields, so isActive was never consulted. Anyone who still
knew the boardId - a bookmark, a browser tab, a note - could subscribe and
receive the whole private board after their access had been taken away
(CWE-639, CWE-863). A revoke the primary data publication does not honour is
not a revoke.
The rule was written out twice and the two copies disagreed, so it is written
once now: models/lib/boardVisibilitySelectors.js builds the `$or`, both
Boards.userBoards and the `board` publication call it, and every share kind is
matched with `$elemMatch` requiring `isActive: true`. `includePublic: false`
still drops the public clause and only that clause, which is what the search
over all boards needs.
tests/boardShareRevokeBypass.test.cjs pins each share kind's selector shape,
that no clause anywhere matches a share without requiring isActive, that the
three dotted selectors are not produced at all, and that junk in the id lists is
dropped rather than handed to Mongo. tests/searchBoardScope.test.cjs asked its
question of the old array literal; it now asks it of the builder - the
behaviour it protects is unchanged.
Thanks to Alpastx and xet7 !
Fixes GHSA-gwc4-fw7p-gw58.
Attachments blocked client-supplied `versions.*.path`; avatars did not. The
avatar allow rule was the single line `update: isOwner` - being the owner let
you write ANY field of your own avatar document, `versions.original.path`
included. That field is server-managed metadata: it says where on disk the
bytes are.
Board export then read it. For every member whose avatar is a local WeKan file,
the exporter opens `versions.original.path` and embeds the bytes as base64 in
`profile.avatarFile`, so an authenticated user could point their own avatar at
/etc/passwd - or at anything else under WRITABLE_PATH - export a board they are
a member of, and decode the file out of the JSON. Arbitrary file read as the
WeKan OS user (CWE-22, CWE-73), from any account that can own an avatar.
Both halves are closed.
The WRITE: the guards that attachments had are now in one module,
models/lib/fileVersionFields.js, and BOTH permission files import it - avatars
had gone without them precisely because each collection wrote its own copy.
Avatars refuse an insert carrying versions.*.path or .storage, refuse any update
touching the versions subtree, and restrict updates to the same field whitelist
attachments use. Being the owner is still required; it is no longer sufficient.
The READ: nothing is read from a stored path unless the path RESOLVES to
somewhere inside WeKan's own storage (models/lib/storagePathContainment.js).
That is the half that also holds when a path is poisoned some other way - a
document written before this fix, a restored backup, a bad migration. The check
sits at getBase64Data, the one place the export turns a stored path into bytes
for attachments and avatars alike, and at both avatar call sites. The download
path in fileStoreStrategy.js has always checked containment this way; its
private copy of the function is gone and it uses the shared one, so the download
and the export ask the same question.
tests/avatarVersionPathTraversal.test.cjs pins the payload being refused on
insert and on update, that renaming an avatar still works, that ..-escapes are
resolved rather than string-matched, that a sibling directory with the root as a
prefix is not inside it, and that no unguarded read of a stored version path
remains in the exporter.
Thanks to Alpastx and xet7 !
Fixes GHSA-4mxf-m8pq-xc9p.
An audit of releases/ against who actually calls each script. It holds 89 of
them, and they divide cleanly:
20 invoked by .github/workflows - CI checks out the repo and runs the FILE, so
these cannot move anywhere;
65 reachable from the build.sh / build.bat Releases menu, which is what the
menu is for;
1 sourced by another script (ensure-tools.sh);
3 referenced by nothing that runs.
Of those last three, wekan-entrypoint.sh is the Docker image's entrypoint - named
by the Dockerfile, not by a caller in this repo - so it stays.
old-build-bundle-arm64.sh and translations/old-pull-translations.sh are the other
two, and both were already recorded as superseded, by build-bundle-arm64.sh and
pull-translations.sh. Nothing referenced them but the exemption list that excused
them from being menu entries. They are deleted, and their exemptions with them.
Two more exemption notes were wrong rather than stale-adjacent:
fix-changelog-hashes.sh said "Setup -> Update git runs it", an option that no
longer exists - it is Setup -> git pull and git push now, both of which run it -
and resolve-fork-node-tag.sh excused a file that is not in the tree at all. An
exemption naming a missing script is an exemption nobody can check.
Checked after: every remaining releases/ script is either a menu entry or exempt
with a reason, and no exemption names a file that does not exist.
Thanks to xet7 !
"Update git" fetched, rebased, ran the CHANGELOG hash repair, printed git status
and left the rest to the reader - and the rest is where the mistakes were. A
rebase rewrites local commits, so CHANGELOG links written before it name commits
that no longer exist; the repair fixes the file but commits nothing, so the next
push carries a dirty tree; and a push rejected as non-fast-forward leaves a repo
that needs a pull the caller has to know to run. Three manual steps after an
option whose name did not say what it did.
Two options now, one per direction, each ending in a state that is either correct
or unchanged:
git pull - fetch; fast-forward when there is nothing local to replay (no
commit is rewritten, so no link can go stale); otherwise rebase,
then repoint the CHANGELOG links the rebase moved AND commit that,
so the tree is clean. A conflict is the one thing a script must not
paper over - a half-applied rebase IS the wrong state - so the
rebase is aborted, the repo is exactly as it was, and the message
says what to run by hand.
git push - repair the links BEFORE publishing them, since a stale link that
reaches GitHub 404s for every reader; say so when there is nothing
to push; and on the one rejection with an obvious answer, origin
moved, pull and retry ONCE. Never force: force-pushing a branch
somebody has pulled rewrites their history too, and a second
rejection is a different problem that retrying does not fix.
build.bat had a 41-line batch REIMPLEMENTATION of the old flow. It is gone: both
options there call `bash ./build.sh git-pull` / `git-push`, so there is one
implementation of this logic and Windows cannot drift from it. The same two are
CLI commands (./build.sh git-pull, git-push), so the menu, the .bat and the
command line are the same code.
Verified against throwaway repositories - an origin plus two clones - rather than
by reading: already-up-to-date does nothing; origin-only-moved fast-forwards and
rewrites no commit; both-sides-moved rebases, and a CHANGELOG link to a rewritten
commit is repointed to the replayed commit with the same subject and committed,
leaving the tree clean; a conflicting rebase aborts with HEAD, the working tree
and the index exactly as before and no rebase in progress; a rejected push pulls
and lands on the second try; and pushing with nothing to push says so.
Thanks to xet7 !
v10.72 was tagged and published at 08:32; the PatternBleed fix was committed at
09:05. Its entry went into the section directly above the previous release, and
by then that section had been renamed from "Upcoming" to "v10.72" by the release
commit - so the changelog claimed a released version contains a fix that is not
in its tag. Checked rather than assumed: the v10.72 tag points at "Bump versions
for v10.72", and the fix is not an ancestor of it.
There is a "# Upcoming WeKan ® release" section above v10.72 again, holding the
security subsection, and v10.72's first subsection reads "This release updates
the following dependencies:" once more instead of the "and updates" it was left
with. The Upcoming section carries the shape CLAUDE.md prescribes: the
**In short:** paragraph, then the binaries table - v10.72's rows, since nothing
here rebuilds them, and the paragraph says so - then the subsection, then the
closing thanks line.
The entry's commit link is repointed. `git pull` rebased the three commits after
the release onto it, which gave them new hashes, so the link written before the
rebase named a commit that no longer exists.
And a real bug in the guard that was supposed to notice all this:
/^(This release|and ) .*:$/
The space is INSIDE the alternative AND after it, so matching an "and" header
needs "and updates" with two spaces. Every `and ...:` subsection header
therefore failed to match, and the grouping check never reset its current group
at a subsection boundary: entries under a later subsection were attributed to the
last group line above them, which is exactly the mistake that loop exists to
catch. With it fixed, the section reports 22 grouped entries and one loose - the
one-entry security subsection, which CLAUDE.md keeps flat.
Thanks to GitHub CodeQL code scanning and xet7 !
CLAUDE.md's security process is the Hall of Fame as well as the CHANGELOG: the
name, the row in hall-of-fame/index.html and the page under it. I had written the
entry without them.
Thanks to GitHub CodeQL code scanning and xet7 !
Its own SECURITY ISSUES subsection, first in the release as CLAUDE.md prescribes,
so the dependency subsection that used to open it becomes an "and updates".
Thanks to GitHub CodeQL code scanning and xet7 !
Code scanning alert #431, tests/releaseNodeSources.test.cjs:
new RegExp(`^\s*${p.replace('-', '-')}\)\s+nodename=`, 'm')
It reads as "escape the hyphen before interpolating this into a regex" and does
nothing whatsoever, so the value went in raw. Nothing failed, because a hyphen
outside a character class needs no escaping - but the guard it looked like was
not there, and a platform name carrying a `.` or a `+` would have matched the
wrong row or thrown. CWE-116, and CodeQL is right to call it out: the usual cause
of the shape is a mistyped backslash escape, where '\"' is silently just '"'.
The platform name is escaped for real now, with the same escapeRegExp the other
guards in tests/ use.
tests/noIdentityReplacement.test.cjs catches the class rather than the instance,
because code scanning reports these days later in a web UI and the node suites
report in fifteen seconds. Three things it took to make it honest:
* it compares the two sides as VALUES, not as source text. '\"' IS '"' - that
is the entire point of CodeQL's example - so a text comparison would call
them different and miss the mistake the guard exists for.
* the two quote styles are separate alternatives, not a backreference with a
character class excluding both quotes: `replace('"', '\"')` contains a double
quote inside a single-quoted literal, so the first shape of the pattern could
not match the very example above. Caught by asserting the pattern against
that line rather than trusting it.
* comments are stripped and the guard skips its own file: this one and the file
it was written for both QUOTE the bad line to explain it, and a guard that
reports the sentence describing a bug as the bug gets switched off.
Verified in both directions: the repository is clean now, the same scan against
the previous commit reports the offending line, all three bad shapes are detected
and four working replacements - including a real escapeRegExp - are not.
Thanks to xet7 !
The two commits that fixed what the companion-repo move and the cross-board card
fix left failing, and the Playwright retry loop that could not reach its own
error. The run after them 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.
Thanks to xet7 !
Down from five failures to none. Both of these are the same kind of thing - a
check that described the world before a change, and a loop that could not reach
its own error.
swcLegacyHelpers still required the .gitignore comment block that named each
clone: `- node/ : clone of the Node.js ...`. That comment went with the entries
it described. What has to be explained now is the one directory that replaced
them, so .gitignore says what .tools/ holds - companion repos, cloned inside the
checkout so they work in a Flatpak sandbox that shares only this directory, with
the entries it replaced named 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. The rule about the shape of a per-repo entry stays: if one is ever
re-added it must be rooted and a directory, since a bare `node` matches anything
a package happens to call that.
The webkit failure was one test of 243, in the shared boardPage fixture rather
than in an assertion:
Test timeout of 60000ms exceeded while setting up "boardPage".
Error: page.waitForTimeout: 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. So the loop
could never reach its own error: Playwright killed the test first, and 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.
The loop is bounded by a deadline now, 45s, leaving room for the rest of the
fixture inside the 60s timeout. The first attempt keeps the 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 three
cases: never renders (gives up at 45s, 3 attempts), renders at 4s and at 19s
(both on the first attempt) - all inside the test timeout, where the old shape
overran it by 45 seconds.
Thanks to xet7 !
An EVERYTHING run found four node suites and one mocha test failing. All five
are consequences of the two changes before them, and each one is a guard doing
its job.
Three broke on the companion-repo move:
* meteorignoreScanScope listed the foreign checkouts by name - FerretDB, node,
mongo-tools, TSC, gitea, the two WeKan variants - one .meteorignore entry
each. They are in .tools/ now, one entry that covers all of them, so 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. It asks the property instead:
.tools is excluded, and nothing at the top of this checkout that is its own
git repository is left for Meteor to walk. The variant check became the
stronger version of itself - no directory with client/, server/ and models/
is reachable from the top - since a second WeKan is loaded EAGERLY, not
merely scanned.
* swcLegacyHelpers checked .gitignore for the same list; one entry now.
* cpuMonitorWiring reads FerretDB's Go source and still opened FerretDB/. It
reads .tools/FerretDB, and skips with a note when the clone is not there -
it is a separate repository, and not every checkout has cloned it.
dockerComposeBackends caught a real omission: the LDAP_GROUP_BASEDN block from
#5539 went into docker-compose.yml only, and every compose file's wekan service
must be identical - what a user reads when they edit their settings must not
depend on which backend they picked. The block is in all of them now.
The mocha failure is the one worth reading twice.
cards.dependencies.move.tests.js pinned move() issuing the inbound-dependency
cleanup itself: a multi-document update with a compound selector. That is
exactly what #6572 removed, because this helper runs in the client bundle where
Meteor allows updates only by id - it threw "Untrusted code may only updateAsync
documents by ID" on every cross-board move, before the move ran. So the test was
pinning the bug. It now asserts the contract that replaced it: move() clears the
moved card's own dependencies, and makes NO update that is not by id - not a
selector, not multi. `{ _id: x }` still counts as by id, since Meteor's rule is
"by id", not "not an object". The inbound half is the server hook, pinned by
tests/cardMoveUntrustedUpdate.test.cjs.
Verified against the tree: all four node suites' conditions hold, the four
compose files the guard compares are identical again, and the FerretDB source
the CPU guard reads is present at the new path with every pattern it asserts.
Thanks to xet7 !
An audit of the TODO Later backlog against the open issues and the current
source. Three no longer belong there.
#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,
where "Lauri Ojansivu commented on ..." is what a person wants to read; a webhook
consumer needs the identifier, received "Lauri Ojansivu" 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 what left this recorded as needing a maintainer 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.
#6542 - "Import finished. Users imported: 60, but only 25 are listed". The Admin
Panel People pane paginates and passes its total to the shared table page
(totalLabelKey: 'people-number'), so the 60 the importer reported is on screen
next to the 25 rows of the current page. That is what the report was about, and
it is already true in this source.
#6500 - Docker `latest` not loading in a browser: already closed upstream, so it
was only sitting in the backlog.
The rest of the backlog stays, each with its reason: they need an SMTP server, an
LDAP directory, a container, a browser to drag in, or a maintainer's decision on
an intended contract - none of which a source reading can settle.
tests/webhookUsername.test.cjs pins both halves of the webhook change: that the
username is there and set from the same user document as the display name, and
that `user` still means what it always did, including the notification text that
depends on it. Three of its seven assertions fail on the previous commit.
Thanks to xet7 !
Fixes#3113,
Fixes#6542.
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. The directory comes from the script's own
location rather than the caller's cwd, so a clone cannot land in whatever
directory somebody happened to be in.
EVERYTHING's FerretDB stage calls it rather than assuming stage 3 already
cloned - a whole-run must not depend on the order of its own stages -
db-conformance.sh builds from .tools/FerretDB and makes .tools before cloning,
and build.bat does the same on Windows: it clones into .tools\FerretDB with the
same SSH-then-HTTPS fallback instead of printing instructions and stopping, and
says so plainly when git itself is missing.
CLAUDE.md's directory list describes the new layout, including that a companion
repo is cloned on demand, so a fresh checkout needs no manual setup.
tests/toolsDirectoryRepos.test.cjs pins both halves: that nothing still reaches
for the old path and both scripts clone, and that a repo INSIDE .tools still
finds its way back out - FerretDB's own build.sh used to reach WeKan's log
directory by counting ../, which lands one level short from .tools; it searches
for the checkout now (fixed in the FerretDB repo, same commit message).
Thanks to xet7 !
#6430's remaining flicker and #5707's invitation email both need something this
environment does not have - a browser to drag in, and a real SMTP server - so
they are recorded with the reason and what was already established, rather than
guessed at.
Thanks to xet7 !
#5251 asked for the last change date of a list, to build an offline Android
client that syncs only what moved. Neither half existed: GET
/api/boards/:boardId/lists returned {_id, title} and no dates at all, and while
the single-list endpoint returns the whole document - modifiedAt included - that
date answers the wrong question. It moves when the LIST document changes (title,
sort, archived); a card being added, edited or archived does not touch it, and
those are the changes the client is asking about.
The list endpoint now reports both:
modifiedAt - the list document itself
cardsModifiedAt - the newest change among the cards in it, null when none
One query for the board's cards and a reduction in memory, not a query per list -
a query per list is what made this expensive enough to ask about. Archived cards
are included on purpose: archiving is one of the changes named in the issue.
The reduction is models/lib/listActivityDates.js, a pure helper, so it is
testable without Meteor and so the handler stays readable. It reads
dateLastActivity as well as modifiedAt and takes whichever is newer, because
boards written by older WeKan carry only the former; a card with no usable date
is skipped rather than counted as now or as zero; and a list whose cards carry no
date is ABSENT from the result, so the endpoint reports null rather than an
invented time - a client polling on a wrong date either never syncs or syncs
forever.
tests/listActivityDates.test.cjs exercises the helper directly: newest wins, per
list, legacy field, string and numeric dates, an undated card cannot drag the
answer backwards, and a negative case feeding it nulls, strings, numbers, a card
with no listId and an Invalid Date - none of which may throw, since the handler
passes whatever the database holds. It also pins the endpoint's shape: both
fields, one cards query, only the three fields it reduces over, and no
`archived: false` on that query.
Thanks to mimZD and xet7 !
Fixes#5251,
#5539: WeKan could not authenticate LDAP users whose groups sit in a different
ou than the users themselves.
Both group searches - getUserGroups, which feeds the login restriction, admin
status sync, group->role sync and org/team sync, and isUserInGroup - searched
this.options.BaseDN. That is the USER base. A directory that keeps ou=groups
beside ou=people, which is a common layout and the reporter's, 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 - there was no setting for it.
LDAP_GROUP_BASEDN says where. Unset, or set to a blank string, it falls back to
BaseDN, so a directory that keeps users and groups in one subtree behaves exactly
as it did; a present-but-empty env var is one somebody meant to fill in, not a
search base, and searching "" would silently search the directory root.
The three USER searches - searchUsers, getUserById, getUserByUsername - keep
BaseDN. Pointing those at a group subtree would break login for everyone, so the
test pins which searches moved and which did not, in both directions.
Documented beside the settings it belongs with, in docs/Login/LDAP.md and
docker-compose.yml: a setting nobody can find is one that does not exist.
tests/ldapGroupBaseDn.test.cjs pins all of it, including the fallback and the
documentation. Six of its nine assertions fail on the previous commit; the three
that pass there are the user searches, which is the point of asserting them.
Not verified against a directory - this sandbox has none. The defect is read from
the source: two group searches, one user base, no way to say otherwise.
Thanks to leandro-cyberproject and xet7 !
Fixes#5539,
The screenshots in #6465 (comment 5187985815, images only, each with the fault
circled):
"Checkbox is misplaced" - 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 the width that column
happens to have, it landed on the word. They are 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. The rescue-card-description 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 now the same shape every other checkbox row in that file uses:
.flex anchor, .materialCheckBox, span.
"(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 h2 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 in the heading, the counter stays where the markup already puts it. Same
treatment the quick-access header and the table view already give their viewers.
"Please move the button down. This is too close ... panic ;-)" - the backup
schedule's Save sat directly under the 1..28 day buttons, close enough to hit
while aiming for a date. The gap goes on the form-group that FOLLOWS the day
grid, so it applies where that grid is and nowhere else.
"The alignment is out of place" - the attachment move button. This one is
normalisation rather than a measured fix: the row is align-items: flex-end, so a
margin of the button's own offsets it from the controls it acts on; that margin
is zeroed and the button aligns with the controls explicitly. Whether that is the
whole of what was circled needs a browser, and the test says so.
tests/uiSpacingReports.test.cjs pins each cause, including the premise behind the
WIP fix (.viewer really is a 22px block). Six of its nine assertions fail on the
previous commit; the three that pass there are the premises, which is the point
of stating them separately.
Not verified in a browser: this sandbox has none. Each fix is read off the markup
and CSS against what the screenshots show.
Thanks to Alishara and xet7 !
#5174: an OIDC login against a provider that rejected the scopes failed with
"Error in OAuth Server: Cannot read property 'ocs' of null", which says nothing
about the refusal that caused it.
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 only first by accident of ordering, which is why 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.
Checked, in this order, each naming what failed:
* the token response is an object at all;
* it carries an access_token or an id_token - a 200 with neither is the
provider answering with a complaint, so the error lists the fields that DID
come back (keys only: the values are secrets, and JSON.stringify of a token
response in a log is how refresh tokens leak);
* userinfo is a non-null object, saying WHICH path produced nothing (claims
expected in the access token, or the userinfo endpoint) and pointing at
OAUTH2_REQUEST_PERMISSIONS, which is what was actually wrong in #5174;
* the ocs and metadata hacks unwrap to something - `ocs` present with no
`ocs.data` used to set userinfo to undefined and fail on the next line;
* B2C's userinfo["emails"][0] is an array before it is indexed: a tenant that
does not release the claim made that undefined[0];
* expires_in parses to a finite number - parseInt(undefined) is NaN, and NaN
propagated into the account's expiry silently. Falls back to the OAuth2
default hour.
tests/oidcNullUserinfo.test.cjs pins the order rather than the individual checks:
it asserts each guard sits BEFORE the read it protects, since an edit that reads
a claim earlier would restore the original bug without touching a check. It also
pins that the no-token error names keys and never stringifies the response. All
eight assertions fail on the previous commit.
Not reproduced against a real provider - this sandbox has none. The null path is
read from the source: getTokenContent returns null for a falsy token, and the
ADFS/B2C branch passes its result to the line that threw.
Thanks to Esther125 and xet7 !
Fixes#5174,
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 both set no size at all and inherited the document's 16px, and
the composer's Add button was a 50px-tall slab. So 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 now heading 16 > card title 14 >= add-card link 13, each stating
its own size instead of inheriting, and the Add button is a 34px control rather
than a slab. Mobile keeps its own larger sizes - the .mobile-view rules with
!important, and the @media blocks - because a phone is read at arm's length.
tests/listTypeScale.test.cjs pins the ORDER rather than the numbers, since that
is what "in relation to each other" means, plus 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. It reads the
base scale only, skipping @media blocks, and takes the LAST rule for a selector -
.list-header .list-header-name has two rules, and reading the first one finds the
one that sets no size at all. Verified against the previous commit: heading 14,
card title and add-card link unset, button 50px - it fails on all four counts.
Not verified in a browser: this sandbox has none. The sizes are read from the
source, and the relative order is what the guard holds.
Thanks to 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 ran - so the card stayed put.
models/cards.js is isomorphic, so its helpers run in the client bundle, and
client code calls card.move() directly (list.js, swimlanes.js, minicard.js,
globalSearch.js, sidebarFilters.js). 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
is rejected on the client 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 now a Cards.after.update hook in server/models/cards.js, where
a selector is allowed, and being server-side it also covers the REST API and
import paths that never called the helper at all. It pulls both stored shapes:
{ cardId, ... } objects, and the bare id strings older data still holds, which
normalizeDependencies hides on read.
The issue also reports that the REST workaround corrupts the card, and it does.
A PUT of boardId/listId/swimlaneId naming the DESTINATION board is not a board
move - isBoardMove needs newBoardId, newSwimlaneId and newListId - so that 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,
belonging to a list and a swimlane on a board it was not on, showing on neither,
undone only by a hand-written database update. Those two branches now verify the
target belongs to the board in the URL and otherwise refuse with 400 naming the
parameters to use, and they are skipped during a board move - they would rewrite
listId before the isBoardMove 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.
tests/cardMoveUntrustedUpdate.test.cjs pins both halves: every Cards update in
the isomorphic model is by _id and none is multi-document (it reads the whole
call, because multi is the third argument and a check that stopped at the first
one could never fail), the inbound cleanup is server-side and keyed on a real
board change, and the REST branches are board-checked and skipped during a move.
Verified against the previous commit: the model guard reports the compound
selector and the multi update, and 6 of the 8 REST assertions fail.
Thanks to jullbo and xet7 !
Fixes#6572,
The EVERYTHING run's WeKan stage failed on one node suite:
dbConformanceWiring, "build.bat: playwright-all must be logged". It is a real
gap, and splitting the whole-suite browser job into three uncovered it rather
than caused it.
`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 the name was supposed to cover, Tests -> 11 "Playwright ALL
browsers", wrote no log at all: one `playwright test --project=chromium
--project=firefox --project=webkit` call straight to the terminal, nothing left
to read afterwards. That is the exact thing the guard is about, and build.sh's
same option has always written one log per browser.
So the option now runs the three browsers one at a time - unchanged, and 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 that it covers all three.
Verified: every build.bat label reference resolves, all seven menus still print
exactly what they dispatch, the build.sh menu entries all still match a case arm,
and the whole-suite per-browser jobs are untouched. Mocha ran in this run for the
first time since it was fixed - 518 passing - and the other stages were green:
320 node suites with this one failure, 249/243/243 in the three browsers, the
conformance run and all of FerretDB's own tests.
Thanks to xet7 !
Renaming the menu entry broke it. 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 in the previous commit changed only one
of them. 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.
The arm now carries the same string as the menu entry, so EVERYTHING runs its
four stages again. Two guards so it cannot come back:
* tests/buildScriptParity.test.cjs 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's build.sh.
* the dispatcher has a *) arm that says which option has no handler and that
nothing was run, so if one ever does get past the test, the person in front
of the menu is told rather than dropped back to the shell.
build.bat was checked for the same thing and cannot fail this way: its menus
dispatch on the NUMBER typed, not on a sentence, and its EVERYTHING hands the run
to releases/run-everything.sh rather than reimplementing it. Verified anyway,
both directions: every printed menu number has an if-dispatch in all seven menus,
and every goto/call target resolves to a label. That is now pinned too.
Thanks to xet7 !
Also repoints six entries at the commits that exist on this branch. The board
export and mocha entries were written against a commit that was amended after
they were filled in, and the four snap entries against one that was amended the
same way, so each of them linked a commit the amend had orphaned. A CHANGELOG
entry cannot carry the hash of the commit it is in; it goes in its own commit
afterwards, linking the code commit.
Thanks to xet7 !
#6573: picking up a card made every list on the board expand horizontally to fill
the window, and dropping it snapped them back, which makes landing a card in the
intended list a guess. Two correct changes met. A list's custom width is an
inline --list-width custom property, and what turns it into a 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 - no js-swimlane class, no such id - so
.dragscroll was the only one of the three that matched, and every list's width
there hung on that one class. #6558 then taught a card drag to stop the board
panning under the same pointer, and it does that by REMOVING the dragscroll class
for the duration of the drag. So the drag deleted the class the width rule
matched on: flex: none and the width declarations switched off and the lists
re-flowed, for exactly as long as the drag lasted. It also explains the
workaround in the report - dragging one list's edge first sets that list's width
through the resize path, so that column stopped jumping while the others did not.
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 name it now.
tests/listWidthDuringDrag.test.cjs pins that every width rule still matches in
lists view with dragscroll gone - it fails on the previous CSS - and pins the
premise that suspending the pan really does remove that class.
build.sh -> Tests -> EVERYTHING is what a release is tested with, so a check that
is in the Tests menu and in no whole-run is a check that runs when somebody
remembers it. The floating-promises guard was that: menu entry 12, in no run. It
is the first of four stages now - seconds, so an unawaited permission check is
reported before an hour of browsers - and its return code is in the verdict like
the others. What 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, while EVERYTHING runs unattended and must
modify nothing - a run that rewrites .eslintrc.json is no longer testing the
commit it started from. The two checks are one shared function using grep rather
than ripgrep, so they also work where nothing may be installed.
build.bat ran the three browsers as one Playwright invocation into one
wekan-alltests-browsers.log, where neither "which browser failed" nor "what did
WebKit print" was answerable afterwards, and CLAUDE.md's "check the newest test
logs" names the per-browser files. It starts chromium, firefox and webkit as
three jobs with a log, a status and a summary row each now, with a per-browser
--output because Playwright clears its output directory at startup. EVERYTHING is
not reimplemented there: Windows hands the run to releases/run-everything.sh,
which calls build.sh --run-everything, so the new stage arrives on both platforms
at once. The parity guard pins all of it, including that the shared checks
install nothing.
Verified without node or meteor, which this sandbox has neither of: the extracted
checks were run for real against the tree (clean) and against fixtures with an
unawaited auth call, a missing rule and no .eslintrc.json at all (all three fail
as they should); the new and changed guards were ported assertion-for-assertion
to Python and run against the same files, including that the #6573 guard fails on
the pre-fix CSS; build.sh passes bash -n and every build.bat call now resolves to
a label.
Thanks to yulqen and xet7 !
Fixes#6573,
The newest test run had two failures.
tests/changelogFormat.test.cjs failed on "the binaries table follows the
summary": the Upcoming section written after v10.71 had lost the binaries table
that every release section carries. It is restored from what v10.71 shipped, and
two group lines that had wrapped onto a second line are one line again, which is
what the grouping guard requires.
The mocha stage did not run at all. client/lib/tests/boardTriggersClass.tests.js
reads boardTriggers.jade and boardTriggers.js off disk with fs and path, which
worked only while node-polyfill-webpack-plugin's browser shims were in the client
bundle; the @meteorjs/rspack 2.1.0 update dropped that plugin and the client
build stopped with "Cannot find module 'path' for matched aliased key 'path'".
That is not one suite failing - the bundle does not build, so every mocha suite
is skipped and the stage reports a crash where its results should be. The suite
was on the wrong side rather than wrong: it now lives in server/lib/tests, and
tests/clientTestsNoNodeBuiltins.test.cjs is the mirror of the existing guard that
keeps browser-only packages out of the server bundle.
Issue #6274, "export includes only comments from current year", and its fix in
PR #6275 were then checked against the current source. The fix is intact and
survived the streaming-export rewrite that came after it: both exporters select
comments by cardId and activities by boardId OR cardId. The follow-up its
reporter left on that PR - "the export is now missing the lists part" - 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
no export code changed; what was missing was the guard, and
tests/exportBoardSections.test.cjs pins the section list, that the two JSON
writers agree on it, that no exporter selects comments by board again, and the
two smaller fixes from that PR.
Not run here: this sandbox has no node, meteor or any other JS runtime, so the
new and changed guards were verified by porting them assertion-for-assertion to
Python and running that against the same files - changelogFormat 16/16, the
export and client-bundle guards clean. The mocha suites behind the fixed build
have not executed yet; the next run is what shows them.
Thanks to Tuphal, KhaoulaMaleh and xet7 !
Remove the 'commit only, do not push' guidance from both the maintainer-mode
intro and General practices; the maintainer decides when to push, directly on
the branch (no PR, no branches).
When a Launchpad remote build ends as 'Stopped' with no .snap, the job scans
the build log to tell a real credential failure from a build cancelled mid-run
(an OOM in the Meteor npm install, a build-farm reset). The scan pattern was
'unauthoriz|401|403', and a bare 401/403 matches any three digits 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 credentials
that were working: the build had reached 'Building:', which 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 real
authorization failure.
releases/release-all.sh and releases/version.sh read a command's output
into an array with `mapfile`. `mapfile` (a.k.a. `readarray`) is a bash 4+
builtin, and macOS still ships bash 3.2, so `./releases/release-all.sh`
failed on a Mac with `mapfile: command not found` and could not trigger a
release from there. `brew install mapfile` finds nothing because mapfile
is a shell builtin, not a program.
Each `mapfile -t VAR < <(cmd)` is replaced with the portable
`VAR=(); while IFS= read -r line; do VAR+=("$line"); done < <(cmd)`, which
builds the same array on bash 3.2 and on the bash 5 the Ubuntu release
runner uses. Converted: release-all.sh's RELEASED version list, and
version.sh's three reads - the Node.js files, the MongoDB files, and the
two newest CHANGELOG release lines.
Thanks to xet7 !
Every place that downloaded bsondump, mongodump, mongoexport, mongofiles,
mongoimport, mongorestore, mongostat and mongotop from wekan/mongo-tools now
downloads them from wekan/mongo-tools-patches: the amd64 base bundle, the
per-arch overwrite in the Linux bundles and in repack-bundle-for-arch.sh, the
Windows and macOS bundle jobs, check-arch-binaries.sh, require-binaries.sh,
test-download-urls.sh, the Dockerfile, both snapcraft files, the Flatpak and
AppImage workflows, the Sandstorm build-deps note and the CPU-platforms docs.
wekan/mongo-tools was a fork of a large Go project that changed none of its
source - its six commits were all the build workflow. wekan/mongo-tools-patches
keeps that build and drops the fork: it clones the newest upstream
mongodb/mongo-tools release, applies the patches in dist/ (there are none yet,
which is the honest state of it), and publishes the same <tool>-<arch>[.exe]
assets with a .sha256sum beside each - the same model wekan/node-patches
replaced the wekan/node fork with.
Nothing else changes: the asset names, the per-tool tolerance for an
architecture that has no binary, and the checksum verification are what they
were, because the build script came across unchanged.
The new checkout is excluded from git and from Meteor's scan the same way
node-patches and the old mongo-tools clone are.
Thanks to xet7 !
WeKan's bundle .zip, Docker image and snap now resolve their Node.js from
three sources in order - https://nodejs.org/dist/ , then
https://unofficial-builds.nodejs.org/download/release/ , then the
wekan/node-patches releases - instead of only from the retired wekan/node
fork. A platform none of the three publishes a Node.js for is NOT BUILT that
release rather than failing it, and returns by itself on the first run after
one appears. Sandstorm is amd64 only and untouched.
releases/resolve-node-source.sh is the one place that order and the
platform-name mapping live; the bundle jobs, the extra-architecture preflight
and the Dockerfile all ask it, so the image and the .zip of one CPU cannot be
built on Node.js from different places. The snap takes its node out of the
bundle, so it follows without a change of its own.
Thanks to xet7 !
The win32 and mac-x64 preflights checked .../releases/download/${NODE_VERSION}/…
with NODE_VERSION=24 (the bare MAJOR), which 404s - the fork tags by full version
(v24.19.0) - so both jobs ALWAYS skipped. They now resolve the newest v24.x fork
tag carrying the asset via a shared helper, releases/resolve-fork-node-tag.sh
(authenticated with the workflow GITHUB_TOKEN, so the GitHub API is not
rate-limited); the win32 preflight hands the resolved node_full to the rebuild.
And the win32 native-module rebuild is completed. npm_config_arch=ia32 alone was
not enough: an ia32 addon must LINK against an ia32 node.lib, and nodejs.org
dropped 32-bit Windows in Node 23, so node-gyp cannot fetch win-x86/node.lib. The
rebuild now assembles a nodedir node-gyp CAN use - node headers (arch-independent,
from nodejs.org) plus the fork's own node-win32.lib (now published beside
node-win32.exe) at the two paths node-gyp looks in - and points node-gyp at it
with npm_config_nodedir, so bcrypt cross-builds to ia32 against the fork's Node.
(The inline python that resolved the tag broke YAML - a multi-line heredoc cannot
be indented into a `run: |` block - which is why it is a helper script, reused by
both preflights and mirroring embed-verified-node.sh / check-arch-binaries.sh.)
Thanks to xet7 !
Every extra arch is built by running the fork's target-CPU node UNDER QEMU to
rebuild the native modules, and qemu-user does not run every Node.js binary
perfectly. ppc64le crashed at node startup deserializing its V8 snapshot:
# Check failed: IsFreeSpaceOrFiller(filler).
... v8::Isolate::Initialize ...
qemu: uncaught target signal 5 (Trace/breakpoint trap) - core dumped
install-node-for-arch.sh: line 144: npm install ... Error 133
so the ppc64le leg failed. With ppc64le (and riscv64) still marked REQUIRED, one
such emulated-CPU failure failed the whole build-extra-arches matrix job.
They join s390x/i386/armhf/armv7/loong64 as best-effort (optional: true): a leg
that cannot run this release SKIPS with a warning instead of failing, so the
matrix never fails on one exotic CPU, the docker job (which now builds only the
bundles that landed) is never dragged down, and the release ships whatever built.
amd64 and arm64 (native, not in this matrix) remain the required core. ppc64le
returns the release it runs cleanly again - its qemu/snapshot crash is a
node-fork/emulation issue to chase separately, not something this bundle build can
fix inline.
Thanks to xet7 !
Three separate native-bundle failures in one release run:
* mac-arm64 failed the major->tag resolution. embed-verified-node.sh and
check-arch-binaries.sh call api.github.com to find the fork's newest v24.x tag,
but no step passed a token, so the call was UNauthenticated - 60 requests/hour
per IP, shared across runners - and 403-rate-limited intermittently. That is why
amd64 (which ran first) resolved fine while mac-arm64 (later) did not. A
workflow-level GITHUB_TOKEN: ${{ github.token }} authenticates every such call
(5000/hour), fixing the native jobs and the extra-arch preflight alike.
* win64 failed because the fork published node-win64.exe WITHOUT its
node-win64.exe.sha256sum, and the embed helper 404'd on the sidecar. The
checksum is a belt-and-suspenders check on top of an already-authenticated HTTPS
download from the fork, so a MISSING one is now a warning and the binary ships
UNVERIFIED (a checksum that is present and does NOT match is still fatal). This
matches how check-arch-binaries.sh already treats a missing fork checksum.
* win32 failed at setup-node: "Unable to find Node version '24' for platform win32
and architecture x86" - there is NO 32-bit Windows Node 24 (the reason the fork
exists). It now runs the runner's x64 Node to DRIVE node-gyp and cross-builds
the native modules (bcrypt) to ia32 via npm_config_arch=ia32; the shipped
node.exe is still the fork's 32-bit node-win32.exe. tests/releaseBundleCoverage
is updated for the ia32 cross-build.
Thanks to xet7 !
The docker job `needs: build-extra-arches`, a matrix. When any leg failed - a
single exotic CPU like ppc64le - the whole matrix job was "failed", and a job
whose `needs` dependency failed is SKIPPED. So one failed extra-arch skipped the
ENTIRE multi-arch image, even though amd64/arm64 and the other arches were fine.
Two changes make the image resilient:
* The job now runs on `always() && needs.release.result == 'success'`, so a
partially-failed build-extra-arches no longer skips it. It still lists
build-extra-arches in `needs`, so it waits for the successful legs to upload
their bundles first.
* A new "Decide which platforms" step probes which wekan-<version>-<arch>.zip
bundles actually landed on the release. amd64 and arm64 are required (no image
without them, and the release would have failed already); the exotic arches
(ppc64le, s390x, riscv64, 386, arm/v7) are included only if their bundle is
present, and a missing one is a ::warning that drops just that platform. The
decided set drives the wait loop, buildx --platform "${PLATFORMS}" and the
push-verify want="${WANT}" from one place, so they always agree.
The result: a failed or best-effort-skipped exotic arch drops only itself from
the image and returns the next release that builds it; the image is never skipped
and never fails because of one CPU. tests/releaseDockerPlatforms.test.cjs and the
docker cross-check in tests/releaseSnapArches.test.cjs are updated for the
decided-set design (required amd64/arm64, optional exotics, loong64 excluded).
Thanks to xet7 !
build-amd64 (and the other native jobs) call
embed-verified-node.sh .build/bundle/node node-x64 "$NODE_VERSION"
but $NODE_VERSION is the pinned MAJOR, '24', while the wekan/node fork tags its
releases by full version, v24.19.0. So the helper built the URL
https://github.com/wekan/node/releases/download/24/node-x64
which 404s, and the amd64 bundle failed at the Node embed step.
embed-verified-node.sh now resolves a bare major to the newest fork tag v<major>.x
that actually carries the asset (the same GitHub-API walk check-arch-binaries.sh
uses), and takes a full tag (with or without the leading v) as given. If no
v<major>.x release has the asset it stops with a message naming the fork asset to
build, rather than 404-ing on a bad URL.
Note: this fixes the URL; a build still needs the fork to HAVE published the
asset. As of now the fork's v24.19.0 carries only node-mac-x64/node-win32.exe/
node-win64.exe - its node-x64/node-arm64/node-mac-arm64 and the Linux arches were
dropped when the "release all missing" run replaced the assets, so those bundles
cannot build until the fork republishes them (a wekan/node-side fix).
Thanks to xet7 !
The wekan/node fork builds Node.js for 13 platforms and WeKan takes its Node.js
only from the fork, so every one of those platforms should have a WeKan .zip
bundle. Twelve did; 32-bit Windows (win32) and Intel macOS (mac-x64) did not -
the fork builds node-win32.exe and node-mac-x64 (nodejs.org has neither a 32-bit
Windows nor, going forward, a place WeKan will take Intel macOS from), but there
was no job to turn them into bundles. This adds both.
build-win32 mirrors build-win64 on a windows runner, with two differences: it
installs a 32-bit (x86) Node via setup-node so the native modules it rebuilds
(bcrypt) are ia32, matching the fork's 32-bit node.exe the bundle ships, and it
takes ferretdb-win32.exe. build-mac-x64 mirrors build-mac-arm64 on a macos-13
Intel runner, taking node-mac-x64 and ferretdb-mac-amd64.
Both are BEST-EFFORT: a preflight step checks whether the fork has published the
platform's node binary for this version and, if not, SKIPS the job with a warning
instead of a red failure - the fork builds these on its own schedule, and the
bundle appears the run after the fork publishes the binary. Every build step is
gated on that skip. release-notes waits for both so their provenance rows land in
the table. tests/releaseBundleCoverage.test.cjs pins that all six native fork
assets and all seven extra-arch legs map to a bundle; the cpu-exec and snap-arch
guards learn about the two new non-Linux bundles and the armv7 leg.
Thanks to xet7 !
The wekan/node fork builds a generic ARMv7 Node.js (node-armv7) as well as
Debian's hard-float one (node-armhf); each is a real fork platform, so each gets
its own WeKan .zip bundle. This adds an armv7 leg to build-extra-arches: it runs
in the same linux/arm/v7 emulated container as armhf, takes its Node.js from the
fork's node-armv7, and shares FerretDB's armhf binary (FerretDB has no distinct
armv7). It is best-effort (optional), like the other 32-bit ARM/x86 bundles - if
the fork's node-armv7 is ever absent for this major it skips rather than failing
the matrix job. The result is wekan-<version>-armv7.zip alongside the armhf one.
tests/releaseArchSkipAndBaseAttach.test.cjs pins armv7 as a best-effort matrix
entry.
Thanks to xet7 !
WeKan now takes its Node.js from the wekan/node fork for EVERY platform and from
nowhere else - the .zip bundles, the emulated cross-builds and the multi-arch
Docker image alike. The reason is control: the fork is Node built from source, so
a Node bug can be PATCHED and rebuilt, which cannot be done with nodejs.org's or
unofficial-builds' opaque binaries, and one source means a set of artifacts is
never half built on one Node.js and half on another depending on the CPU. The
fork already covers the CPUs the other two do not build at all (32-bit x86/ARM,
loong64, ...), so it is the only source that fits every platform.
check-arch-binaries.sh no longer walks nodejs.org -> unofficial-builds -> fork;
it lists the fork's OWN releases for this major, newest first, and takes the
newest that carries node-<arch>, verifying it against that release's
node-<arch>.sha256sum. install-node-for-arch.sh drops the official/unofficial
tarball branch and always installs the fork's bare node binary (grafting npm -
arch-independent JavaScript - from the official amd64 tarball, a build-time tool,
not the shipped node). The Dockerfile maps every TARGETARCH to a fork asset
(node-x64/arm64/ppc64le/s390x/riscv64/i386/armhf) and installs it the same way.
s390x becomes best-effort (optional) like i386/armhf/loong64: the fork has not
published node-s390x for this major yet, so the preflight SKIPS it - a warning,
exit 0 - instead of failing the whole matrix job and skipping docker with it, and
it returns on its own the release after the fork publishes node-s390x. ppc64le
and riscv64 stay required - the fork builds them reliably.
tests/releaseArchSkipAndBaseAttach.test.cjs pins the fork-only sourcing (no
official/unofficial fallback for the node binary) and the s390x skip.
Thanks to xet7 !
The docker job built and pushed the multi-arch image for all seven platforms
correctly; the step AFTER it, which inspects each registry's manifest to prove
every platform actually arrived, then failed the run over a manifest that was
right.
buildx builds linux/arm/v7, but a registry records that manifest as architecture
"arm" with variant "v7" in a separate field. The verify step's imagetools
--format printed only `{{.Platform.OS}}/{{.Platform.Architecture}}`, so the entry
read back as bare "linux/arm", and the check for "linux/arm/v7" in want= never
matched it - "wekanteam/wekan:v10.67 has no linux/arm/v7 in its manifest (it has:
... linux/arm ...)". The image was complete; the check was blind to the variant.
The --format now appends `/{{.Platform.Variant}}` when a variant is present, so
32-bit ARM reads back as linux/arm/v7, and a trailing /v8 is normalised away so
arm64 (which a registry may record as arm64/v8) still matches want=, which names
it the way everyone writes it, linux/arm64. tests/releaseDockerPlatforms.test.cjs
pins the variant-aware format and the /v8 normalisation so an OS/arch-only format
cannot come back.
Thanks to xet7 !
WeKan takes its Node.js from the wekan/node fork for EVERY platform now, not
only the arches nobody else builds. The reason is control: the fork is Node
built from source, so a Node bug can be patched and rebuilt - which cannot be
done with nodejs.org's opaque binaries - and one source means a set of bundles
is never half built on one Node.js and half on another. The fork also already
covers the arches nodejs.org and unofficial-builds do not build at all (32-bit
x86/ARM, loong64, win32), so it is the only source that fits every platform.
The four native bundles (amd64, arm64, win64, mac-arm64) embedded a verified
nodejs.org tarball; embed-verified-node.sh now downloads the fork's bare
node-<asset> binary (node-x64 / node-arm64 / node-win64.exe / node-mac-arm64)
and verifies it against the node-<asset>.sha256sum the fork publishes beside it.
That is all a bundle needs - it ships the node binary and runs `node main.js`;
the BUILD still uses the runner's own node+npm, so no npm is grafted here (the
Docker image and the emulated cross-builds, which run npm, graft it separately).
Provenance records source wekan/node and the fork's SHA256.
tests/releaseNodeVerified.test.cjs pins the fork source for all four.
The extra-arch bundles, the Docker image and a new win32 bundle move to the fork
too, in following commits.
Adds two Docker platforms that were shipping as .zip bundles only.
Base image: ubuntu:26.04 -> debian:trixie. Ubuntu publishes no linux/386
image (which is why 386 could never build); Debian does, and debian:trixie
carries every arch this image targets - amd64, arm64, 386, arm/v7, ppc64le,
riscv64, s390x - so one base covers them all, and it is the same base the
per-arch .zip bundles already build in. (The Dockerfile installs no MongoDB
server - every arch defaults to the bundle's FerretDB - so nothing here is
Ubuntu-specific; sha checks switched from `shasum` to `sha256sum`, which is
coreutils and always present.)
Node.js: nodejs.org and unofficial-builds ship no Node 24 for 32-bit x86 or
ARM, but the wekan/node fork does - as a BARE node binary (node-i386 /
node-armhf) plus a .sha256sum, not a tarball. A new `fork` branch in the arch
case downloads that binary, verifies it against its .sha256sum, and grafts npm
+ npx from the official amd64 tarball (npm is arch-independent JavaScript that
runs on the fork's node). Debian's 32-bit ARM port is armhf (ARMv7 VFPv3-D16),
which is what linux/arm/v7 runs, so node-armhf is the match.
linux/386 and linux/arm/v7 go back into the buildx --platform list and the
want= verify list. linux/loong64 stays out: no Docker base publishes it and the
registries do not agree on its manifest yet. tests/releaseDockerPlatforms.test.cjs
now checks the debian base, that 386/arm/v7 are in and loong64 out, and that
every built platform has a matching branch in the Dockerfile arch case.
NOT built here (no buildx/emulation) - confirm on CI. Depends on the fork having
published node-i386 / node-armhf and the bundle having wekan-<v>-{i386,armhf}.zip
for the release.
With linux/386 gone, the multi-arch build got past the base image and then
failed inside the Dockerfile RUN on linux/arm/v7:
+ echo Unsupported architecture: arm
+ exit 1
Docker's TARGETARCH for linux/arm/v7 is `arm`, and the Dockerfile's arch case
handles only amd64/arm64/ppc64le/s390x/riscv64 - it deliberately has no `arm`
branch, because it installs Node.js from nodejs.org / unofficial-builds and
NEITHER ships a Node 24 for armv7l (the Dockerfile comment says so). But
linux/arm/v7 was still in the buildx --platform list, so the RUN hit the
"Unsupported architecture" guard and the whole build failed.
linux/arm/v7 is removed from --platform and from the want= verify list, joining
linux/386 and linux/loong64: all three ship as .zip bundles (armhf's Node comes
from the wekan/node fork there) but not as Docker images, because the image
sources its Node from nodejs.org/unofficial only. tests/releaseDockerPlatforms.test.cjs
now pins arm/v7 out of both lists too.
With loong64 no longer skipping build-extra-arches, the docker job ran again
for the first time in a while and failed at the base image:
#2 ERROR: no match for platform in manifest: not found
ubuntu:26.04: failed to resolve source metadata ... no match for platform
The image is FROM ubuntu:26.04, and Ubuntu publishes no i386 image, so
`docker buildx build --platform ...,linux/386,...` cannot resolve the base for
that one platform and the whole multi-arch build stops - the workflow's own
bundle-build comment already notes `docker run --platform linux/386
ubuntu:26.04` answers "no matching manifest".
linux/386 is removed from the build's --platform list and from the want= list
that verifies the pushed manifest, exactly as linux/loong64 already was: i386
ships as a .zip bundle (built on debian:trixie, which has 386) but not as a
Docker image. tests/releaseDockerPlatforms.test.cjs pins that 386 and loong64
are out of both lists, that the two lists match, and that the base is
ubuntu:26.04.
Group the Upcoming entries by area, per CLAUDE.md - a subsection with more
than one entry is grouped, not loose: "Extra-architecture bundles" (loong64)
and "The Launchpad snap builds" (the flatten and the licence-hint fix). The
In short now accounts for the third entry in a closing clause.
The Launchpad project xet7-craft-remote-build is set to MIT / X/Expat, but the
snap-launchpad failure hint printed "check its Licence, 'None specified' ... set
the licence to MIT" on EVERY failure - so an s390x build that Launchpad stopped
for another reason read as a licence problem that was already fixed, and the
time went to re-checking a correct setting.
The hint now says the licence should already be MIT (re-set it only if the page
shows "None specified") and, for the real remaining case, explains that a
"Stopped" build with a BUILDING (not FAILED) log that ends mid-step is Launchpad
cancelling the build as it runs - typically an OOM in the memory-heavy Meteor
npm install, or a transient build-farm reset, both of which the three retries
already cover. The large-history push that also broke these builds is fixed
separately by the repo flatten (previous commit).
The ppc64el (and before it v10.55 riscv64) snap-launchpad build failed with
"Git operation failed with: Could not push 'HEAD' to git.launchpad.net/...
snapcraft-wekan-<hash>", ~4-5 minutes into the push, on every one of the three
attempts. snapcraft remote-build pushes the project's git repository to
Launchpad and builds it there; it rejects a SHALLOW clone (which is why the
checkout is fetch-depth: 0), but it does not need the history, and WeKan's full
history is large enough that the push times out or is refused mid-upload.
After the full checkout, the repo is now re-initialised as ONE commit of the
tagged tree: git rev-parse --is-shallow-repository is still false, so
remote-build accepts it, but the push carries the source tree (tens of MB)
instead of the whole history (hundreds). The snap version comes from
snapcraft.yaml (verified in the next step), not git-describe, so dropping the
history changes nothing about what is built. tests/releaseSnapLaunchpadFlatten.test.cjs
pins the flatten, its order, and that the checkout stays full-depth.
The docker job needs build-extra-arches, and GitHub skips a job whose needed
job failed. build-extra-arches is a matrix, and its loong64 leg hard-failed:
no debian/ubuntu image publishes linux/loong64 (node-loong64 and
ferretdb-loong64 exist, the container does not), so the preflight stopped it.
One failed matrix leg makes the whole build-extra-arches job "failure", which
SKIPPED docker - and through docker the charts, ucs and nextcloud jobs that
need it. That is what left docker skipped in run #209.
loong64 is now optional: true, like i386 and armhf, and check-arch-binaries.sh
skips a best-effort arch whose BASE IMAGE is missing - not only one whose
Node.js is missing - with a warning and exit 0 emitting skip=true, gated the
same way every other best-effort skip is. So the matrix job succeeds and docker
runs; loong64 stays visible as a skip and returns to a real build the day a
linux/loong64 base image is published.
The release job in v10.63 and v10.64 shipped with no amd64/arm64 bundle, and
every downstream snap/docker/AppImage job 404'd on wekan-<version>-amd64.zip.
The cause was not softprops (v10.63's guess) but the checkout: the job
downloaded the bundles into the workspace and THEN ran actions/checkout for the
provenance script. The workspace is not a git repo at that point, so checkout's
first act is "Deleting the contents of '<workspace>'" to make room for a fresh
clone - and it does this even with `clean: false`, which only skips the
`git clean` in an already-checked-out repo. The just-downloaded
wekan-<version>-{amd64,arm64}.zip were deleted before the attach step, which
then failed with "wekan-<version>-amd64.zip is missing or empty".
The checkout now runs FIRST, into the empty workspace, and the bundles are
downloaded on top of the checked-out tree, where nothing removes them.
tests/releaseBundlesSurviveCheckout.test.cjs now pins the ORDER (checkout
before the bundle download) rather than clean: false, which was never enough.
Thanks to xet7 !
Opening an admin report by its address - /admin/problems/files typed,
bookmarked or refreshed - drew the column headers, "No results" and a
"1 / 1" pager over data that was plainly there, while the count method
reported five. 78b2f9ebc fixed one half of this (re-subscribe once the
login lands) but the report still came up empty, because the deeper cause
was untouched.
The onCreated autorun opens the pane and subscribes when problemsOpenPane
or the logged-in user changes. It called openReportPane()/loadReport()
directly in its reactive body, so it became reactive on cfg.count -
loadReport reads it through pageInfo - and loadReport's own count method
then did cfg.count.set(...). That re-ran the autorun, and a Meteor.subscribe
made inside an autorun is AUTO-CANCELLED when the autorun re-runs; the
re-run took the "same user, same pane" branch, did not re-subscribe, and
left the report with no subscription at all - "attachments in minimongo: 0".
Reached from the menu it worked, because switchMenu opens the pane from an
EVENT, not a computation, so that subscribe was never auto-managed; only
the URL path, which runs inside the autorun, hit it.
The autorun now reads only paneId and userId reactively and runs its body
inside Tracker.nonreactive, so cfg.count.set no longer re-runs it and the
subscribe is not auto-cancelled. The subscription's lifetime is managed by
loadReport (stops the previous) and a new onDestroyed (stops the last)
instead of the autorun. tests/adminReportsSubscriptionLifetime.test.cjs
pins the nonreactive body and the onDestroyed cleanup.
Thanks to xet7 !
Repoint the Upcoming section's commit links after the version-bump rebase
moved the two code commits to new hashes:
081d50b45 -> e802d52eed and
17d7493d6 -> 55463f685a .
Two release-build failures from the v10.63 run (logs in ../log/wekan3):
1. The release shipped NO amd64/arm64 bundle, so every snap job (native,
wekan-ondra, wekan-gantt-gpl) 404'd on wekan-10.63-amd64.zip. The base
bundles were the only ones attached by the central `release` job with
softprops `files:`, which does not fail on an unmatched file - so a run
that produced no zip created a release with none and reported success,
and softprops never listed the .sha256sum either. Every other bundle
self-attaches with `gh release upload --clobber`, loud on a missing file
and verified from the release side. The release job now attaches amd64
and arm64 the same way - failing the release if a base bundle is missing
instead of 404'ing ten downstream jobs - and checksums the exact bytes it
attaches so the base bundles get a .sha256sum like the rest.
2. build-extra-arches failed on i386 and armhf: Node has no linux-x86 build
at all, and no source builds Node 24 for armv7l, so there is nothing to
build a bundle against. They are now marked best-effort (optional: true);
when its Node.js is absent everywhere, check-arch-binaries.sh emits
skip=true (a warning, exit 0) and the build steps are gated on it, so the
arch is skipped rather than failing the run. It returns on its own the
first release after wekan/node publishes node-i386 / node-armhf. A
required arch whose Node.js is missing is still fatal.
tests/releaseArchSkipAndBaseAttach.test.cjs pins both; the base-attach guard
in tests/releaseBundlesSurviveCheckout.test.cjs is updated to the new path.
Thanks to xet7 !
The four native bundles (amd64, arm64, win64, mac-arm64) shipped Node.js
with `cp $(command -v node)` - the runner's node. It has no published
checksum, so the release provenance table could only say "no checksum
published" and nobody could tell WHICH Node.js build a platform carried -
the exact thing needed when a Node.js CVE lands. Worse, build-arm64 has
no setup-node, so ubuntu-24.04-arm's DEFAULT Node 22 went into the arm64
bundle instead of the pinned Node 24, silently.
Each native bundle now downloads the pinned Node.js for its OS+CPU from
nodejs.org and verifies it against SHASUMS256.txt
(releases/embed-verified-node.sh), and verifies FerretDB against the
.sha256sum wekan/FerretDB now publishes beside every binary. Provenance
records nodejs.org + the exact version + the verified SHA256; win64 and
mac-arm64 now upload provenance too, so every platform is traceable. The
emulated arches already did this via install-node-for-arch.sh.
tests/releaseNodeVerified.test.cjs pins that no native bundle goes back
to the runner's node or an unverified download.
Thanks to xet7 !
releases/expected-assets.sh looks for WeKan-<v>-<arch>.AppImage.sha256sum and
WeKan-<v>-<arch>.flatpak.sha256sum (asset name + .sha256sum, the same convention
the wekan-<v>-<arch>.zip.sha256sum bundles use). But the builds wrote
WeKan-<v>-<arch>.sha256sum - extension DROPPED - so "what the release is missing"
never found the checksum and rebuilt the AppImage and Flatpak on every run even
when they were already published. The checksum (and md5sum) now keep the
.AppImage / .flatpak extension, matching what the check expects.
This is the root cause of the snap / docker / AppImage 404s on
wekan-<version>-amd64.zip. The `release` job downloads the per-arch bundles to
the workspace root, then checks the repo out (for the provenance script) BEFORE
creating the GitHub Release. actions/checkout defaults to clean: true, whose
`git clean -ffdx` deleted those untracked zips - so the "Create GitHub Release"
step's `files: wekan-<version>-{amd64,arm64}.zip` matched nothing. softprops does
not fail on unmatched files, so the release was created (job "success") WITH NO
BUNDLES, and every downstream job that downloads a bundle 404'd. The checkout now
sets clean: false, so the bundles survive and are attached.
Also: the snapcraft `wekan` part downloaded the bundle with a single wget that
treated a 404 as fatal, so it also broke on the brief CDN lag right after an
upload. It now retries (--tries/--retry-on-http-error), like the curl downloads
elsewhere in the release.
tests/releaseBundlesSurviveCheckout.test.cjs pins that a checkout after the
bundle download keeps clean: false; it fails on the pre-fix workflow.
The snap i386 platform was removed because core24 cannot build it; this guard
still pinned i386 as a snap platform and a Launchpad arch. Updated to expect the
six buildable snap platforms (amd64, arm64, armhf, ppc64el, riscv64, s390x) and
to treat i386 like loong64 - an arch that has a wekan-<arch>.zip BUNDLE but is
deliberately not a snap - with the reason recorded in the test.
core24 has no i386 port, so `base: core24` cannot build an i386 snap (removed in
the previous commit). Debian still ships i386 and is glibc, so - unlike an
Alpine/musl base - WeKan's existing binaries run unchanged. Since the WeKan snap
only assembles the prebuilt wekan-<arch>.zip (and wekan-i386.zip already exists),
the only missing piece for an i386 snap is an i386-capable base.
snap-base-debian/ adds `wekan-base-debian13` (type: base), a trimmed Debian
trixie rootfs, starting with i386 - which builds NATIVELY on an amd64 runner (no
qemu, no Launchpad). It is isolated from the working core24 snapcraft.yaml so it
can never break the mainstream builds.
This is a documented SCAFFOLD, not a tested artefact: a custom base snap is
advanced snapcraft and needs a real `snapcraft pack` + local install + a WeKan
snap running on top before it is wired into CI or sent for the (manual) store
review. README.md carries the full plan, the local test loop, the publish/ACL
steps, and the known pitfalls.
The v10.60 release job succeeded (the release-notes shell-injection fix landed),
but four of the per-platform build jobs failed, each for its own reason:
- build-win64: the "Check the binaries" step ran `set -euo pipefail` under the
Windows default shell (PowerShell), which answered "A parameter cannot be found
that matches parameter name 'euo'". It is a bash script; it now says
`shell: bash`.
- build-mac-arm64: the zip step ran `sha256sum`, which does not exist on macOS
(that is the GNU coreutils name), so the job died with exit 127. Use
`shasum -a 256`, the BSD/macOS spelling, which writes the same format.
- build-extra-arches: the job downloaded wekan-<version>-amd64.zip but never
unpacked it, so the container mounted an EMPTY /bundle and the native-module
rebuild died with "ENOENT ... /bundle/programs/server/package.json" (exit 254).
Added the unzip the arm64/win64/mac-arm64 jobs already do.
- snap-native / snap-launchpad: snapcraft.yaml declared an i386 platform, but
core24 (Ubuntu 24.04) has no i386 port, so snapcraft rejected it with "trying
to build snap in one of ['i386'], but none of these build architectures are
supported" - a PARSE error that failed EVERY snap build, not only i386's.
Removed the i386 platform and its snap-launchpad matrix entry; i386 users are
served by the .deb and AppImage, and an i386 snap is not buildable on a modern
base.
loong64 still reports its documented "debian:trixie has no linux/loong64 image"
gap (there is no loong64 userland to rebuild in), which is intentional and left
visible.
These jobs do not build WeKan from source — they download the prebuilt release
zip (wekan-<version>-<arch>.zip) that release-all.yml publishes, and fail hard
if it is missing. So they failed whenever they ran before the release was
complete: dockerimage.yml / docker-publish.yml on a push or a tag pushed early
in the release, and AppImage.yml when release-all-missing asked it to wrap a
version whose base bundle was not published (`gh release download` → "no assets
to download"). The v10.59 release itself had failed (the release-notes shell
injection, fixed separately), so every one of these downstream jobs 404'd.
Each now checks whether the release asset it needs is published and SKIPS with a
notice (a green run) when it is not, instead of failing:
- dockerimage.yml / docker-publish.yml gate the image build on the pinned
version's amd64 zip being on the release;
- AppImage.yml flips its per-arch BUILD_THIS off when that arch's bundle is
absent, and its publish job treats "no AppImage, and no base bundle either" as
nothing-to-do rather than an error.
The real images/AppImages are still built by release-all.yml's own jobs once the
release is up; these standalone/gap-filling runs simply stop failing in the
meantime.
The release and release-notes jobs built the GitHub Release body with
`printf '%s\n' "${{ needs.prepare.outputs.changelog }}"`. Interpolated inline,
the CHANGELOG becomes part of the shell source before bash parses it, so every
backtick in it runs as a command. The v10.59 notes are full of `code` spans, so
the release job failed with "Incorrect: command not found",
"loginFailureDecision.js: Permission denied", "POST: command not found" and
published no release. The changelog now reaches both steps through the
environment (env: CHANGELOG), and the scripts use "$CHANGELOG", where the shell
treats it as data and never parses its backticks, $( ) or quotes.
tests/releaseNotesNoShellInjection.test.cjs pins that outputs.changelog is only
ever consumed as an env: assignment, never inline in a run: script; it fails on
both pre-fix printf lines.
Thanks to xet7.
The binary pre-check `bash releases/require-binaries.sh`, wired into the build
jobs, needs the repo on disk, but three jobs did not have it there:
- build-arm64 and build-mac-arm64 download a prebuilt bundle artifact and never
checked the repo out at all, so the "Check the binaries ..." step died with
exit 127 ("releases/require-binaries.sh: No such file or directory") before a
bundle was ever assembled. Each now checks the tag out first, before
download-artifact drops the bundle into the same workspace.
- build-win64 checks the repo out into src/ (for start-wekan.bat and
snapcraft.yaml) but called the script at the workspace root. It now calls
src/releases/require-binaries.sh, the same src/ path its start-wekan.bat copy
already uses.
tests/releaseBuildJobsCheckout.test.cjs pins, for every build job that runs
require-binaries.sh, that the job checks the repo out where the call looks for it
(root vs src/) and that a root checkout precedes download-artifact so it cannot
wipe the downloaded bundle. It fails on all three pre-fix breakages.
Thanks to xet7.
GHSA-2g94-9x3m-hv37. Two verified weaknesses on the login path chained into
account takeover, and both are fixed here.
1. The bundled wekan-accounts-lockout brute-force protection was completely
inert. Its two Accounts.validateLoginAttempt hooks decided whether an attempt
failed by comparing the error's reason string ('Incorrect password' /
'User not found'), but Meteor's accounts-base ships ambiguousErrorMessages
defaulting to true, so _handleError rewrites every credential failure to
"Something went wrong. Please check your credentials." before those hooks run.
The literals never matched, both hooks returned early, the failure counter
was never incremented, and no account ever locked. The hooks now decide from
the attempt's structural fields (new loginFailureDecision.js): any genuine
password failure counts, while the benign 'no-2fa-code' step (password already
correct, second factor still to come) never counts, so 2FA users are not
locked out of their own sign-in.
2. The login path leaked which accounts exist by timing: a real user runs bcrypt
(~50 ms) while a missing one returns in ~2 ms. A dummy bcrypt comparison
against a fixed cost-10 hash (loginTimingDefense.js) equalises the missing-user
path on the DDP login method (loginTimingNormalization.js, a handler that runs
ahead of the built-in one and never authenticates) and on the REST endpoint.
The REST POST /users/login twin also enumerated by message (a distinct
"...not found." string) and never ran the lockout hooks at all. It now fails
missing-user and wrong-password identically, runs the timing equaliser, and
throttles failed attempts per client address (loginAttemptThrottle.js), keyed
safely behind HTTP_FORWARDED_COUNT, resetting on success.
Tests: loginFailureDecision, loginTimingDefense, loginAttemptThrottle and
loginBruteForceEnumerationWiring (four new plain-Node suites).
Thanks to NinjaGPT and xet7 !
Fixes GHSA-2g94-9x3m-hv37.
`uses: ./.github/workflows/X.yml` fails at STARTUP - taking the calling
workflow down with it, so nothing runs and there is no job log to read - when
X.yml does not exist, has no `workflow_call` trigger, or is handed a `with:`
key it never declared. That is the same class of failure as the job-level
`if:` reading matrix, and just as invisible: the YAML is valid, and only
GitHub's loader objects.
It is worth pinning now because release-all-missing.yml calls AppImage.yml
and Flatpak.yml here, TSC's calls five workflows, and the `only` input they
all take was added by hand to each of them. One typo in a `with:` key would
stop the whole run with "error parsing called workflow".
Parsed by indentation rather than with js-yaml, which is not a dependency of
this repository. These files are written in a fixed shape, and the two things
this needs - which workflows a job calls with which `with:` keys, and which
inputs a workflow declares under workflow_call - are read from it directly.
Verified against all three shapes by breaking each in turn:
passes 'onlyy' which AppImage.yml does not declare
calls missing Nope.yml
AppImage.yml has no workflow_call
and checked across every repository first - TSC, wekan, both variants, node,
FerretDB, mongo-tools and gitea - where all reusable-workflow calls already
match.
Thanks to xet7 !
The `only` filter I added to the build workflows was written as a job-level
condition:
if: ${{ inputs.only == '' || contains(fromJSON(inputs.only), matrix.arch) }}
GitHub refuses to load a workflow that does this:
Invalid workflow file
(Line: 109, Col: 9): Unrecognized named-value: 'matrix'
`matrix` is available to a job's runs-on, env, name, container, services,
continue-on-error, timeout-minutes, strategy and steps - but NOT to
`jobs.<id>.if`, which is evaluated before the matrix is expanded. It looks
entirely reasonable, which is why it was written in five workflows in one
sitting.
It is worse than a job that does not run: a workflow that will not load takes
every workflow that CALLS it with it, so TSC's release-all-missing.yml failed
at startup with "error parsing called workflow" and built nothing at all.
The decision moves to the job's `env:`, which CAN see matrix, and every step
asks `if: ${{ env.BUILD_THIS == 'true' }}`. Steps that already had a
condition keep it, ANDed inside parentheses - matrix.emulated,
matrix.mode == 'windows' and the rest are unchanged.
Fixed in AppImage.yml and Flatpak.yml here, and in the same commit's worth of
changes in TSC (Deb, AppImage, Flatpak, Windows, Mac), wekan/node,
wekan-ondra and wekan-gantt-gpl. Twelve files.
tests/workflowExpressions.test.cjs is the guard, and it exists because a YAML
parser is perfectly happy with every one of these: the file is valid YAML and
only GitHub's expression evaluator rejects it, when the workflow is
dispatched. It pins that no job-level `if:` reads matrix or steps, that every
${{ }} is closed, and that a workflow declaring an `only` input actually
consults it - a filter that is declared and ignored would quietly build
everything, which is the failure that looks like success.
Verified by putting the bad line back: the guard fails with
"job-level if: cannot see matrix" and names AppImage.yml:107.
The closed-brace check counts differently than it first did, because three
real lines broke it: these workflows run docker with Go templates
(--format '{{range .Manifest.Manifests}}...'), which are full of `}}` that
close nothing of GitHub's. It strips complete expressions and looks for a
leftover opener instead of counting braces.
Thanks to xet7 !
release-all.yml is one run that bumps the version, tags, builds every
platform, and publishes Docker images, snaps, the website and the charts. It
is the right thing for MAKING a release and the wrong thing for FINISHING
one. When the v10.57 run ended with all six extra architectures missing -
four on a shell-quoting bug, two on a base image that is not built for their
CPU - there was no way to get those six except to run the whole thing again,
version bump and all.
This finishes a release that already exists. It never bumps a version, never
tags, and never touches Docker, snap, the website or the charts.
What makes it possible: only build-amd64 runs Meteor. Every other bundle
WeKan ships is that same bundle with its native modules rebuilt, its Node.js
swapped and its database binaries replaced. So this downloads the PUBLISHED
wekan-<v>-amd64.zip - verifying its checksum, since it is the bundle every
other architecture is cut from - and repacks it with
releases/repack-bundle-for-arch.sh, the same script release-all.yml's
build-extra-arches runs. A bundle added to a release months later is
therefore built exactly like the ones already on it.
It builds the six extra architectures and calls AppImage.yml and
Flatpak.yml, which are reusable workflows now and take the same `only`
filter.
It does NOT claim to build everything, and says which: amd64 is the Meteor
build, arm64/win64/mac-arm64 each need their own kind of runner and their own
steps, and the Sandstorm .spk is signed. When one of those is missing the run
names it and points at release-all.yml, as a warning rather than a failure.
What a complete release looks like is releases/expected-assets.sh, in one
place, with the kind of each asset saying which build makes it. Present means
the .sha256sum is there too - a bundle whose checksum upload failed is
half-published - except the .spk, which is signed rather than checksummed.
Verified against the real v10.57 release: every one of its five assets
matches a name this predicts, with nothing left unaccounted for. Three
states, through the plan's own logic:
v10.57 as published 6 extra + 4 AppImage/Flatpak to build; amd64, arm64,
win64 and mac-arm64 reported as needing
release-all.yml (they predate the checksums, which is
noted in the script)
only the 6 extra extra=[all six], appimage=[], flatpak=[], nothing
arches missing manual - the case this exists for
complete release everything empty, nothing built
Thanks to xet7 !
Opening a report BY ITS ADDRESS - /admin/problems/files typed, bookmarked or
refreshed - drew the column headers, "No results" and a "1 / 1" pager over
data that was plainly there. Reached by clicking the menu entry it worked,
so this only ever happened to the URL.
A full page load resumes the login from localStorage ASYNCHRONOUSLY, and the
route sets `problemsOpenPane` before that lands. The subscription was
therefore made with no user; the publication's isAdmin check answered
`this.ready()` with no rows; and nothing re-subscribed, because the autorun
that opens the pane did not depend on the user. The count METHOD, called
later from the same page, happily reported five - which is what made this
look like a publication bug rather than a timing one.
The autorun depends on Meteor.userId() now. openReportPane() returns early
when the pane is already open, so re-running it after the login would do
nothing at all - hence the second branch, which re-subscribes the report
that is already open now that there is a user to subscribe as.
Found by the e2e test's own diagnosis, added in the previous run for exactly
this purpose: "attachments in minimongo: 0; report_pages index: no
report-files index doc" - neither half arrived, which of the three
possibilities it lists is the one meaning the publication returned early or
never ran.
ALSO, a release-build bug of my own from earlier today:
build-extra-arches had no actions/checkout. It calls
releases/check-arch-binaries.sh and releases/record-provenance.sh and mounts
$(pwd)/releases into the container - none of which exist without one, so
every one of the six extra architectures would have stopped at "No such file
or directory". The checkout goes before the artifact download, because
actions/checkout cleans the workspace and would remove the bundle.
And releases/repack-bundle-for-arch.sh: the amd64 bundle turned into one
architecture's zip, lifted out of that job so it is one script rather than
one job's inline steps. Only build-amd64 runs Meteor; arm64, win64,
mac-arm64 and the six extra architectures are all this same bundle with its
native modules rebuilt, its Node.js swapped and its database binaries
replaced. That is what lets a release be completed one architecture at a
time without rebuilding the rest, and it had better be the same script or a
bundle added later would differ from the ones beside it.
Thanks to xet7 !
On a phone the left menu was drawn 260px wide inside a 157.5px grid track
and lay OVER the board icons. The grid itself was right: 42% of a 375px
phone is 157.5, plus the 8px gap puts the board column at x=165.5, which is
exactly where the boards start. The menu simply did not fit in its track.
260px is --wekan-left-menu-width, the DESKTOP width the drag-grip sets -
leftMenu.js does not even offer the grip below 800px. The menu carried it on
a phone anyway, because the phone rules never said otherwise and the
`max-width: 100%` on the base rule does not do what its comment claimed. A
percentage width on a grid item does not resolve against the track, so it
capped nothing. That is not a browser quirk: Chromium, Firefox and WebKit
all drew the same 260px.
`width: auto` in the phone rules instead. A grid item with an auto width
stretches to its grid area (justify-self defaults to stretch), so it fills
the track exactly with no percentage to resolve. The base rule keeps its
max-width, which is still right where this menu is not a grid item, and its
comment now says what it does rather than what it was hoped to do.
Measured, not guessed: decoding the failing test's screenshot pixel by pixel
shows the menu background #f7f7f7 running from x=3 to x=255 with the blue
board tiles painted on top of it from x=170, and the page background #dedede
only from x=260.
Also: when the Files report draws its headers and then "No results", the
test now says which half is missing. A row needs the page rows AND the
report_pages index that names them; the server count it already checks
proves only that the query matches. Rows but no index, index but no rows,
and neither are three different bugs that all show up as "element(s) not
found". That report is still failing - the server counts 5 and the pane
renders none, with every static link between them verified correct - so the
next run should name the layer instead of another read-through.
Thanks to xet7 !
Seven of the DnsBleed mocha tests failed with "Timeout of 2000ms exceeded" -
every one of the ALLOW cases, while every block case passed, which is what
hid it.
The stub was at fault, not fetchSafe. It faked http.IncomingMessage with a
bare EventEmitter that emitted 'data' and 'end' from a process.nextTick,
into the void if nothing was listening yet. No real response does that: an
IncomingMessage is a PAUSED Readable that buffers its body until a listener
attaches or it is resumed, so reading late cannot lose data.
That only started to matter when fetchSafe was split into resolving the
response and then reading it, which the redirect handling needs - it has to
see the status before deciding whether to read a body at all. The nextTick
queue drains BEFORE promise microtasks, so the fake had already fired 'end'
by the time the awaited continuation attached its listeners.
Verified rather than assumed, both halves:
- against a real server: a request whose 'data' listener is attached two
nextTicks, a setImmediate and 20ms after the response callback still
receives the whole body.
- against the real fetchSafe, driven by each stub in turn: the old
EventEmitter stub times out after 2s; the Readable-backed one returns
status=200, the body, the pinned IP 93.184.216.34 and Host
example.com.
The security assertions still hold through the new stub, checked the same
way: a 302 with the default maxRedirects:0 is refused, a host resolving to
127.0.0.1 is refused with ZERO requests sent, a followed redirect pins each
hop to its own resolved IP, the abandoned hop reports destroyed=true, and
Authorization and Cookie are dropped on the cross-origin hop.
destroy()/destroyed are now the stream's own rather than a hand-rolled flag,
so "fetchSafe abandoned this body" is asserted against real stream
behaviour.
Thanks to xet7 !
wekan/gitea, forked from go-gitea/gitea, is cloned to gitea/ here: a whole
Go + web application, 1,401 directories, 6,209 files, 384 MB. Nothing in
WeKan imports it.
Left out of .meteorignore it would have gone straight back into the build
scan - about a fifth again of everything Meteor legitimately walks in this
repository, and that scan is what ran the build out of memory. Left out of
.gitignore it would fill git status.
The guard added for exactly this caught it: with the entry missing from
.meteorignore, `every git repository cloned in here is excluded from BOTH
files` fails with
cloned repositories Meteor would still walk - add them to .meteorignore
actual: [ 'gitea' ]
It is named in the explicit list as well as found by the walk, because the
walk can only check a clone that is present - a checkout without gitea/ must
still not lose the entry.
Thanks to xet7 !
Excluding /_build/ and /_build-local-test/ in .meteorignore was wrong and
broke the build, with an error that does not mention .meteorignore at all:
error: Could not find mainModule for 'os' architecture:
_build/main-prod/server-meteor.js
Check the "meteor" section of your package.json file?
They look exactly like build output that should be ignored - they are
gitignored, and they are the first two entries of the 296 KB pattern list
that the heap snapshot turned up. They are not leftovers. rspack compiles
the app INTO _build/main-prod/ (and _build/test/ for a test run), and Meteor
then reads server-meteor.js and client-meteor.js from there as the
application's main modules: package.json names client/main.js and
server/main.js, and the rspack integration repoints mainModule at its own
output. Ignoring them hides the files Meteor is about to be handed.
They are 3 directories each, so there was nothing to win by excluding them
and a build to lose. The rest of the excludes - the Node.js fork checkout,
mongo-tools, TSC, the TSC website and the two variant WeKan checkouts - are
where the 5,860 directories were, and they stay.
The guard now asserts the OPPOSITE for these two, so the mistake cannot be
made again by anyone reading .gitignore and concluding the obvious thing.
The general "gitignored at the top level must be in .meteorignore" rule
gains them as documented exceptions: .gitignore happens to write them
unanchored today, so that rule skipped them by luck rather than by design,
and normalising .gitignore would have turned the guard itself into the thing
that breaks the build.
Thanks to xet7 !
The guard added with the .meteorignore fix derived its list FROM .gitignore,
so it only caught a clone that had got half way: one added to NEITHER file
was invisible to it. That is the state every one of these arrived in, so it
is the state worth failing on.
The check walks the tree looking for a directory with a .git of its own -
existsSync, because a submodule's .git is a file, not a directory - rather
than comparing against a list, so a clone nobody thought to name is caught
too. Anything it finds must be ignored by git (asked of `git check-ignore`,
not of a hand-parsed .gitignore) and, when it is a top-level directory,
listed in .meteorignore. It does not descend into a repository it has
already found: that repository's own submodules ride along with it.
A directory with a .git of its own is another project. It is not WeKan's
source and it is not WeKan's history, so it belongs in .gitignore to keep
`git status` readable and in .meteorignore to keep the build from walking
it.
Verified by planting a directory with a .git in it: the guard fails and
names it.
Thanks to xet7 !
`meteor build` ran out of JavaScript heap, and raising --max-old-space-size
never helped because the ceiling was not the variable.
A heap snapshot taken as the build died says what it was: of a 4.1 GB heap,
980 MB was 14,267,543 IgnoreRule objects, 667 MB sliced strings, 308 MB
concatenated strings and 274 MB in 945 copies of one 296 KB pattern list -
about 2.5 GB of ignore machinery, and only 945 directories in.
Meteor builds one ignore matcher per directory it descends into, each
carrying the whole accumulated pattern list. There were 6,867 directories
under the app; roughly 1,000 of them were WeKan. The rest were repositories
cloned in beside it - the Node.js fork checkout (4,132 directories, 45,527
files, 2.3 GB), mongo-tools, TSC, the TSC website, two more WeKan checkouts
- plus Meteor's own _build/ and _build-local-test/ output, which it was
scanning as source to produce. All of them were in .gitignore, which Meteor
does not read; it reads .meteorignore, which listed only .tools/, FerretDB/
and tests/. So the cost was very nearly seven times what the snapshot
caught.
The two variant checkouts were the worst of them: wekan-ondra and
wekan-gantt-gpl contain client/, server/ and models/, and Meteor loads
server/ and client/ eagerly - a second and third copy of the whole app
pulled into the build.
Excluding all of it takes the scan back to WeKan itself.
Also: build.sh exited 0 while printing "ERROR: the WeKan build failed".
build_wekan returns 1, the menu called it bare and threw that away, so
anything driving it non-interactively saw a green run and a missing bundle.
tests/meteorignoreScanScope.test.cjs pins the excludes, that they are
anchored to the repo root rather than matching a directory of that name at
any depth, and - for the next clone dropped in here - fails on any new
top-level directory that .gitignore excludes while .meteorignore does not,
which is exactly how each of these arrived.
Thanks to xet7 !
The capped snapshot worked - 6 GB written at a 4096 MB heap - and
releases/analyse-heapsnapshot.py (added here) says what is in it. Of 4,120 MB:
980 MB 14,267,543 x object IgnoreRule
667 MB 21,869,662 x sliced string
308 MB 10,105,340 x concatenated string
274 MB 945 x string "_build/test/** _build/main-dev/** node_modules/** .babelrc ..."
237 MB millions x string "t(?=$|\/$)", "s(?=$|\/$)", "e(?=$|\/$)" ...
Around 2.5 GB of the 4.1 GB heap is one thing: ignore-rule machinery. The
regexes are the `ignore` npm package's own rule compilation, IgnoreRule is its
rule class, and the 274 MB string is the pattern list it is compiling.
The arithmetic says what is happening. 274 MB across 945 copies is 296 KB per
copy - a pattern list of about 15,000 entries - and 14,267,543 rules across 945
constructions is 15,097 rules each. So a ~15,000-pattern ignore list is being
compiled from scratch 945 times, and the sliced and concatenated strings are the
paths it is built from. The repo has 3,030 tracked files and 104 top-level
entries, so 15,000 patterns is not "one per file": the list is accumulating
across those rebuilds.
That is the memory. It is not the minifier, not the heap ceiling, and not
anything in WeKan's own code - it is build tooling recompiling a growing ignore
list once per unit of work.
WHAT I COULD NOT DETERMINE HERE: which component does it. `ignore` is not a
WeKan dependency except through eslint, which does not run during a build, and
~/.meteor is not visible from this sandbox - so the caller is inside meteor-tool
or @meteorjs/rspack and needs a look from a shell that can read it:
grep -rl IgnoreRule ~/.meteor/packages/meteor-tool/*/mt-os.linux.*/dev_bundle/lib/node_modules/
releases/analyse-heapsnapshot.py is kept rather than left in /tmp, because this
will be wanted again. It streams, so it answers on a machine that cannot open
the file in DevTools, and it explains in its header why it reports self size
rather than retained size.
Two bugs in my own reporting, found by reading the output it produced:
* the failure said "allowed 15542 MB and reached 4280.0 MB" - it quoted the
COMPUTED limit while the snapshot mode had capped the build at 4096. It
quotes what the build was actually given now.
* it told you to run WEKAN_BUILD_HEAP_SNAPSHOT=1 even when that is what you
had just run. When a snapshot was requested it now points at the file and
the command that reads it.
Heap.*.heapsnapshot is gitignored: Node writes it into the repo root because
that is the build's working directory, and it is as large as the heap.
300 node suites pass, 0 failed.
The first heap-snapshot run was killed by the kernel - "Päätetty" - and left a
0-byte Heap.20260803.220733.2327163.0.001.heapsnapshot behind. The build log
ends mid-rspack with no GC dump at all, because the process never got to fail
its own way.
Writing a snapshot costs memory ON TOP of the heap being dumped. Asking a
15.5 GB heap to write a 15.5 GB snapshot, on a 30 GB machine with 12 GB already
in use, does not fit - the OOM killer arrives before the file does. So the flag
as written could not do the one thing it existed for.
WEKAN_BUILD_HEAP_SNAPSHOT=1 now caps the heap at 4096 MB rather than using the
computed limit. The build fails sooner, which is the point: a snapshot is not
more useful for being bigger. What is wanted is WHAT is retaining memory, and
whatever grows to 15 GB is already the largest thing on the heap at 4 GB - it
just gets there sooner and writes a file that fits in RAM and on disk.
WEKAN_BUILD_HEAP_SNAPSHOT_MB overrides it if 4096 turns out to be too early to
be representative.
The message says all of this, including that the build WILL fail and that a
full-size snapshot was already tried and killed, so nobody repeats it.
300 node suites pass, 0 failed.
The fourth run died the same way - 15526.3 MB of 15542 - and produced no more
information than the third, because the heap snapshot is opt-in and nothing told
anyone it existed. The flag was added in the last commit and the failure path
was not updated to mention it, which makes it a flag nobody will use.
The error now names the exact command:
WEKAN_BUILD_HEAP_SNAPSHOT=1 ./build.sh
and says what it produces, where, and how to read it.
It also says plainly what NOT to try, because the obvious move is wrong and has
now been wrong twice: the limit has already been raised once, 8192 to 15542, and
the build used the extra; removing the second JS minifier moved the peak by 6 MB.
Both of those are cheap to try again and neither will work, so the message says
so rather than leaving the next person to repeat them.
And the diagnosis is appended to the build log, not only printed. The point of
that log is that "check the newest test logs" answers the question - a diagnosis
that lives only in terminal scrollback is one the next reader does not have, and
the last four runs are exactly the case: the log ends on a V8 stack trace and
the explanation was somewhere above it on a terminal since closed.
Verified by rendering the message against the real log of the run that prompted
it: it reads back 15526.3 MB of 15542.
300 node suites pass, 0 failed.
The theory was that rspack minifies for production itself and standard-minifier-js
was running Meteor's minifier over that same output a second time, in exactly
the phase where `meteor build` runs out of memory. The run settles it:
with the minifier peak 15526.6 MB died after both rspack compiles
without it peak 15520.4 MB died after both rspack compiles
Six megabytes apart, which is noise, and it died in the same place. The minifier
was not the consumer, so it goes back. An unverified change to the release
artifact that demonstrably fixes nothing does not belong in the tree - and
because the build never completes, there was no way to confirm the resulting
bundle was still correctly minified either. Both .meteor/packages and
.meteor/versions are restored exactly as they were, byte for byte.
Three guesses is enough. build.sh takes WEKAN_BUILD_HEAP_SNAPSHOT=1 now, which
adds Node's --heapsnapshot-near-heap-limit=1 to the build so it writes a heap
snapshot just before it would die, rather than only dying. Off by default,
because the snapshot is about as large as the heap limit; when it is on the
build prints where the file lands and how to read it - Chrome DevTools, Memory,
Load, sorted by retained size.
That turns the next run into evidence instead of a fourth theory. Whatever is
retaining 15 GB in Meteor's bundle-writing phase will be at the top of that
list.
300 node suites pass, 0 failed.
Two builds died in the same place - after both rspack compiles report done, in
the phase where Meteor writes the production bundle - the second with twice the
heap of the first:
=> Compiled Rspack client app: done
=> Compiled Rspack server app: done
<--- Last few GCs ---> 8146 MB of 8192, then 15526 MB of 15542
So it was not a ceiling set too low. rspack is the bundler here and it minifies
for production itself - the asset-size warnings a build prints are about
rspack's own minified output, 4.797 MiB of client-rspack.js and its chunks.
standard-minifier-js then ran Meteor's minifier plugin over that same output a
second time, in exactly the phase that runs out of memory.
.meteor/packages says so in a comment rather than leaving the removal to look
like an accident, including what to check first if this turns out to be wrong: a
bundle that is broken rather than merely unminified twice means put it back.
The stale pin is dropped from .meteor/versions so the two files agree;
minifier-js stays, because it is a library other packages depend on rather than
the plugin that was running.
Four guards failed on this and on the last few commits, and all four were
pinning something that deliberately changed. Each keeps its assertion and gains
the reason:
changelogFormat - three Upcoming summaries of mine had no commit link,
which every summary in a release must have, and the
section was not grouped by area. That one was a real
defect in the CHANGELOG, not in the guard: the entries
are linked and grouped now, under "The extra-
architecture bundles", "Which Node.js a bundle carries"
and "Running the tests locally".
buildScriptParity - looked for `rm -rf .build` in the first 400 characters
of build_wekan. That window was an accident of how long
the comment above it happened to be, and it broke when
build_wekan gained an explanation of where its log
goes. It searches the whole function. It also needed
the five release-workflow helpers exempted from "every
script is a menu entry": they are called by a build job
with its matrix and environment, and would have nothing
to do on a developer's machine.
cpuExecWiring - looked for the qemu handling inline in release-all.yml.
It moved into releases/install-node-for-arch.sh, which
is the fix for the apostrophe-in-a-single-quoted-string
bug, so the guard looks there and also pins that the
workflow runs the file.
dbConformanceWiring - pinned ../log literally in three places. The root is
WEKAN_LOG_ROOT now, because ../log is not writable in a
Flatpak sandbox; what is pinned is that the same
question is asked and that WEKAN_LOGDIR still wins.
300 node suites pass, 0 failed.
NOT verified: the build itself - Meteor is not installable in this sandbox, so
whether dropping the minifier gets past the OOM is the next run's answer, and
whether the resulting bundle is correct is the thing to look at if it does.
The build failed again, and the log now says why on its own - but its last word
was a V8 stack trace, which reads as a crash rather than as the resource limit
it is. Running out of heap and failing to compile look identical at the level
build_wekan reports at, and they have nothing in common.
When the build log contains "JavaScript heap out of memory", the error now says
so, with the limit the build was given and the peak it actually reached, pulled
out of the last Mark-Compact line:
The build ran out of JavaScript heap. It was allowed 15542 MB and reached
15526.6 MB.
and points at the last line before the GC dump, which is the stage it got to.
It also says what NOT to do. Raising the number again is the obvious move and is
wrong once the figure is already large: this build used 15.5 GB, and the run
before it died the same way with half as much, so the ceiling is not what is
short. That belongs in the message rather than in somebody's memory.
Verified against the real log of the run that prompted it: the extraction finds
15526.6 MB from the final Mark-Compact line, and the sentence renders.
The build log added last commit earned its keep on the first run: the WeKan
stage failed again, and this time ../log/<datetime>/wekan-build.log says why,
on its last lines.
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
at 8146 MB of an 8192 MB limit, on a machine with 30 GiB of RAM. The build had
not run out of memory. It had run out of the ceiling this script gave it -
--max-old-space-size=8192, hard-coded at the top with a comment saying to lower
it if your machine has less RAM, which is only half the problem: a constant is
simultaneously too small on a large machine and too large on a small one, and
nobody edits it until a build dies.
It is half of total RAM now, clamped to [4096, 16384]:
8 GiB -> 4096 (the floor - a small machine is not handed something unusable)
16 GiB -> 8192 (exactly the old constant, so nothing changes there)
30 GiB -> 15542 (this machine, nearly double what just failed)
64 GiB -> 16384 (the ceiling - past this, a heap that big means something is
wrong rather than something is big)
Half is the share that leaves the rest of the machine usable while a build runs.
Total RAM comes from /proc/meminfo, or hw.memsize on macOS, and falls back to
assuming 16 GiB if neither answers. The chosen size is printed at startup so the
log says what the build was given, and exporting TOOL_NODE_FLAGS or NODE_OPTIONS
yourself still wins.
Computed unconditionally rather than only when neither variable is set, which
was the first version of this and was wrong: with only ONE of them exported, the
other would have been handed a bare "--max-old-space-size=".
The dev server on :3000 had the same 8192 written into its command line and now
uses the same figure, so the two cannot drift.
Verified by running the block with /proc/meminfo stubbed at 8, 16, 30 and 64
GiB, giving the four values above; and with TOOL_NODE_FLAGS exported alone, which
keeps the exported value and still gives NODE_OPTIONS a real one.
Every log build.sh writes goes into a log/<datetime>/ directory, and the root of
those was hard-coded to ../log - one level up from the repo. That is the right
default and stays the default: a test run then does not appear in git status,
and the docs and the release process read it there.
But the parent of the repo is not always there to write into. A Flatpak sandbox
shares only the repository directory, so `..` is missing or read-only and
`mkdir -p ../log` fails - after which every redirection in the script either
failed outright or quietly dropped its log into the repo root. That is the worst
of both: the run looks like it logged something, and git status fills up with
stray files that are not part of the tree.
WEKAN_LOG_ROOT is resolved once at startup and used everywhere the path was
built by hand: ../log when the parent is writable, ./log inside the repo when it
is not, and whatever you set if you set it. The shape is log/<datetime>/ either
way, so nothing that reads these logs has to know which happened, and the chosen
path is printed when a run starts - including a line saying why, when it is the
fallback.
Everywhere means everywhere: the whole-run directory, one_log for the individual
test options, the Playwright per-browser runs, the EVERYTHING orchestrator, and
the wekan-log.log the whole session is teed into. releases/db-conformance.sh
makes the same choice when it is run on its own rather than under build.sh, and
the thirteen menu entries that told the reader "logs in ../log/<datetime>/" now
say "log/<datetime>/" - those strings are also the case labels the menu
dispatches on, so both copies had to move together.
/log/ is gitignored, for the case it lands inside the repo.
Verified both branches by running the resolver in a temporary tree: with a
writable parent it chooses ../log and creates it; with the parent chmod'ed
read-only it prints the note, chooses ./log inside the repo, creates that, and
does NOT create ../log. build.sh, db-conformance.sh and run-everything.sh all
pass bash -n.
The newest test run ended
FAIL WeKan tests (sequential)
PASS Database conformance (all databases for this CPU)
PASS FerretDB tests (unit, vet, integration)
and ../log/2026-08-03_19-32-26/ held twelve conformance and FerretDB logs and
NOTHING about WeKan. Not one line about why WeKan never got as far as running a
test. The build writes to the terminal only, so the one question that run raised
was the one question its logs could not answer - and "check the newest test
logs" is how these runs are meant to be read.
What can be said from the tree it left: node_modules is 654 MB, so
`meteor npm install` finished; .meteor/local is 43 MB where a completed build is
gigabytes, and .build does not exist at all, so `meteor build .build --directory`
started and died partway. Why, nothing recorded. Meteor is not installable in
the sandbox this is being fixed from, so it cannot be reproduced here either.
build_wekan tees into wekan-build.log in the run's own log directory now, and
prints the path before it starts. An interactive build still scrolls past
exactly as it did.
Two more things found while in there.
build_wekan did not check whether the build worked. `meteor build` failing left
the function returning success, and the caller learned about it only afterwards
by noticing .build/bundle was missing - which is how this failure was reported,
one level away from the thing that failed. It checks the exit status and the
bundle, and says which log to read.
run_all_tests minted its own ../log/<datetime>/ even when a larger run had
already set WEKAN_LOGDIR. That is exactly the split-across-two-directories the
per-run directory exists to prevent; it landed in the same second as the outer
one this time, so nothing showed, and on a slower machine half a run's logs
would have gone somewhere else. It uses the outer directory when there is one
and exports its own when there is not, so every job of a run agrees on it
instead of each calling date() again.
Verified: build.sh passes bash -n, and the tee construct was exercised on its
own - a failing command inside the group produces exit 1 from the function, the
ERROR path, and a complete log file, rather than tee's own zero.
Two things a WeKan release did not say, and now does.
WHAT IT SHIPS. Each bundle zip now has a wekan-<version>-<arch>.zip.sha256sum
beside it on the release, so a download can be checked. The bundles were the one
thing in this chain with no checksum at all - the binaries inside them are
verified where their sources publish one, and the zip carrying them was not.
WHERE ITS BINARIES CAME FROM. A bundle is assembled out of a Node.js, a
FerretDB and the MongoDB tools, and WHICH source serves each varies per release
and per CPU: nodejs.org builds some architectures, unofficial-builds others, the
wekan/node fork the ones neither of them does, and not all of them publish a
checksum. None of that was visible anywhere except a build log that expires, so
"where did the node in the arm64 bundle come from, and was it checked" had no
answer.
Each build job records a row per binary - bundle, binary, source, version,
whether it was checked, the SHA256 - and the release notes open with the table.
A row saying "no checksum published" is not a failed check but a source that
publishes nothing to check against, and the table says so, because those are the
ones worth fixing at the source rather than mistaking for failures.
The table is written twice, and that is deliberate. The release is CREATED
before win64, mac-arm64 and the extra architectures are built - those jobs need
a release to upload their bundles to - so the table it is created with can only
cover amd64 and arm64. A new release-notes job runs after all of them and
rewrites the notes with every bundle in. It is always(), so a platform that
failed does not cost the release the table for the platforms that succeeded.
FerretDB is now verified too, where it can be: wekan/FerretDB publishes a
.sha256sum beside each binary as of the commit in that repository alongside
this one, and the extra-arches job checks the download against it. An older
release without one is recorded as unverified rather than failing the build -
the binary IS available, which is the test that matters.
Nothing new fails a build. A missing checksum is a warning, a missing MongoDB
tool is a warning; only a binary that does not exist at all stops the run, which
is what it did before.
Verified: every script parses under bash -n; release-all.yml is valid YAML with
its nineteen jobs and the new one wired to all five build jobs; and the table
renders correctly from real rows - three of them, two sources, one with a
checksum and one without - producing the markdown checked by eye.
Replaces the fork-first entry, which this supersedes: the order is official,
then unofficial, then the fork, by how verifiable each build is - and the
checksum is now actually checked, with a retry before it is fatal.
Thanks to xet7 !
The three sources do not offer the same assurances, and this was checked rather
than assumed:
nodejs.org SHASUMS256.txt, and SHASUMS256.txt.sig / .asc signed with
the Node.js release keys
unofficial-builds SHASUMS256.txt, no signature
wekan/node fork neither, until the commit in that repo alongside this one
So the order goes back to official, then unofficial, then the fork - descending
verifiability, with the fork as the backstop for what the other two do not
build. A binary somebody else signed is worth more than one this project built,
and the fork's own fixes are build-configuration fixes upstream's builds do not
need: they are what makes the CPUs upstream does NOT build compile at all.
Preferring a source because it publishes a checksum and then not checking it
would be preferring it for nothing, so the checksum is now looked up in the
preflight - from the source's own SHASUMS256.txt, or from the fork's per-binary
.sha256sum - and the download step refuses a file that does not match it.
A mismatch is retried before it is fatal. The overwhelmingly likely cause is a
truncated or corrupted transfer, a CDN edge that served a partial file or a
connection cut mid-stream, and that is fixed by asking again. What retrying must
not do is give up quietly: after three attempts the build stops, because at that
point the file being served is not the file that was published.
Where no checksum exists the log says so in as many words, rather than leaving
the reader to assume a check was made. FerretDB and the MongoDB tools are in
that position today - neither wekan/FerretDB nor wekan/mongo-tools publishes
checksums for its release binaries, which is the next gap to close.
Verified by exercising download_and_verify directly: a matching checksum
returns 0, a missing one returns 0 with the file named as unverified, and a
mismatching one retries three times and exits 1. And by resolving the real
checksums for every architecture against the live releases - s390x and ppc64le
from nodejs.org, riscv64 and loong64 from unofficial-builds - then downloading
the riscv64 tarball and confirming its hash matches the published one.
The fork is not "the CPUs nobody else builds" any more - it builds all thirteen
platforms WeKan ships a bundle for, and it carries fixes upstream does not: the
ICU genccode architecture name, the x86 /SAFESEH opt-out, the zlib SSE2 and NEON
flags, the V8 template disambiguator. Those are build-configuration fixes that
any platform benefits from, and a set of bundles should not be half built on one
Node.js and half on another depending on which CPU it is for.
So the probe order is fork, then nodejs.org, then unofficial-builds - the last
two now being fallbacks for the case the fork has not published a version yet,
rather than the other way round.
The rest of the rule is unchanged and still matters: the newest Node.js that
exists FOR THIS CPU, walking versions from newest down and asking all three
sources at each one. Preferring the fork does not mean pinning to whatever the
fork last built.
Checked against the live releases, which show the transition in progress - the
fork has v24.18.1 for the architectures it already built and has not yet run for
v24.19.0:
i386 v24.18.1 fork
loong64 v24.18.1 fork was coming from unofficial-builds before this
s390x v24.19.0 official fork has not built v24.19.0 yet
ppc64le v24.19.0 official likewise
riscv64 v24.18.1 unofficial likewise
armhf - none still nothing anywhere
Once node.yml has run for v24.19.0 with its new matrix, every one of these
resolves to the fork; until then the fallback chain keeps the bundles building
instead of blocking on it.
Splits the version-fallback out of the binary-check entry, because it is its own
change and its own reason: the fallback now applies to all three sources rather
than only the fork, which is what makes riscv64 build again.
Thanks to xet7 !
The version fallback added with the preflight check was only wired to the
wekan/node fork. nodejs.org and unofficial-builds were still pinned to the exact
newest release, so a CPU that HAD a perfectly good build, one patch version
back, was reported as having none at all.
riscv64 was exactly that. unofficial-builds publishes riscv64 up to v24.18.1 and
has not got to v24.19.0; the check looked only at v24.19.0, found 404s, and
stopped the job saying "no Node.js for riscv64 exists anywhere" - while
node-v24.18.1-linux-riscv64.tar.xz was sitting there.
The rule is now "the newest Node.js that exists for this CPU". It walks the 24.x
releases from newest down and asks all three sources at each one - official,
then unofficial, then the fork - and the first hit wins, which is by
construction the newest build that exists anywhere for that architecture. Those
are the same thing on amd64 and arm64 and regularly are not anywhere else:
nodejs.org builds a handful of architectures, unofficial-builds adds a few, the
fork builds the rest, each on its own schedule, so the further a CPU is off the
beaten path the further behind its newest build tends to be.
When the answer is behind the newest, the log says which version it got, from
where, which version it wanted, and what to build to bring it in line. It is a
warning, not an error: the alternative is no bundle at all for that CPU.
The walk stops after 12 releases. A CPU whose newest build is a dozen releases
old is not slightly behind, it is unmaintained, and saying so is more use than
quietly shipping something from last year.
Re-ran the check against the live releases for all six architectures:
s390x v24.19.0 official newest
ppc64le v24.19.0 official newest
riscv64 v24.18.1 unofficial was failing before this change
i386 v24.18.1 fork
loong64 v24.18.1 unofficial the fork has it too; unofficial is tried first
armhf - none exit 1, naming node-armhf
and checked armhf by hand across six releases back to v24.10.0 at all three
sources, to be sure "nothing anywhere" is the truth about armhf and not a bug in
the walk. It is: every one is a 404.
Four of the six now come out newer or working where they did not before, and the
one that still fails fails for a real reason.
The six extra-architecture jobs that all failed in the v10.57 run, grouped under
the area they belong to: the shell quoting bug that emptied the CPU name out of
the Node.js URL, the base image that is not built for i386 or loong64, and the
checks that now run before any of it and name the missing file.
Thanks to xet7 !
Every one of the six extra-architecture jobs failed in the v10.57 run, and none
of the six failures was about WeKan.
FOUR OF THEM - ppc64le, riscv64, s390x, armhf - died on the same 404, at a URL
with no architecture in it at all:
curl: (22) The requested URL returned error: 404
No Node.js for at official (https://nodejs.org/dist/v24.18.1/node-v24.18.1-linux-.tar.xz)
The container script was passed as `bash -c '...'` and contained apostrophes, in
comments like "# Where this CPU\'s Node.js comes from". A single-quoted shell
string cannot hold an apostrophe - \' does not escape it, it ENDS the string -
so the script was chopped into words at the first one, and ${NODE_ARCH} landed
in a region the RUNNER's shell expanded rather than the container's. The runner
has no NODE_ARCH, so it expanded to nothing. Reproduced here before changing
anything: that construct really does deliver only the fragment up to the
apostrophe and leave the rest as separate arguments.
The script is a file now, releases/install-node-for-arch.sh, mounted into the
container. A file has no quoting layer to get wrong, and `bash -n` can check it.
THE OTHER TWO - i386 and loong64 - died earlier still, on
docker: no matching manifest for linux/386 in the manifest list entries
ubuntu:26.04 publishes amd64, arm/v7, arm64, ppc64le, riscv64 and s390x. Not
386. Not loong64. The base image is named per architecture in the matrix now and
is debian:trixie, which publishes 386 as well - checked against the registry
rather than assumed.
loong64 still cannot be built, and that is now said rather than discovered: no
image on Docker Hub publishes linux/loong64 at any tag, so there is no loong64
userland to rebuild the native modules in. node-loong64 and ferretdb-loong64
both exist; the container does not. It stays in the matrix so it stays visible.
AND UNDERNEATH BOTH: nothing checked that the binaries these bundles are
assembled from had been published at all. A bundle takes FerretDB from
wekan/FerretDB, the MongoDB tools from wekan/mongo-tools, and on the CPUs nobody
else builds for, Node.js from wekan/node - and any of those can be absent
because a build has not finished. releases/check-arch-binaries.sh now runs
before anything is built and checks the base image, the Node.js, FerretDB and
the tools; releases/require-binaries.sh does the same for the amd64, arm64,
win64 and mac-arm64 bundles. Each missing file gets a line naming it and the
repository that should publish it, and the job stops there. The MongoDB tools
stay a warning: FerretDB is the database and the launcher does not need them.
It also handles wekan/node running behind. The fork builds the architectures
nobody else does, one release at a time, so the newest Node.js is regularly a
version it has not reached - today nodejs.org is at v24.19.0 and the fork at
v24.18.1. Asking for the exact newest version would fail for exactly the CPUs
the fork exists to serve, so the check falls back to the fork's newest release
that HAS that CPU and says out loud that the bundle is a patch version behind.
Verified by running the check against the real releases, for all six:
s390x Node.js v24.19.0, official exit 0
ppc64le Node.js v24.19.0, official exit 0
i386 Node.js v24.18.1, wekan/node exit 0
loong64 Node.js v24.18.1, wekan/node exit 0
armhf nothing anywhere exit 1, naming node-armhf
riscv64 nothing anywhere exit 1, naming node-riscv64
armhf and riscv64 are the two the fork has not finished; the log now says which
file to publish instead of a bare 404. i386 and loong64 resolve because
node-i386 and node-loong64 were added to the fork's v24.18.1 release.
NOT verified: the builds themselves, which need the runners. What was checked:
that release-all.yml is still valid YAML and every job has its check step; that
the three new scripts pass `bash -n`; that the quoting bug reproduces and the
file form does not; that debian:trixie really publishes linux/386 and
ubuntu:26.04 really does not, from the Docker registry; and the six preflight
results above, run against the live releases.
Two more ways to install WeKan, and neither of them rebuilds it.
release-all.yml already produces a bundle that runs offline with nothing
installed: the Meteor server, the bundled Node.js the release was built
against, the wekan/FerretDB v1 fork with its embedded pure-Go SQLite backend,
the wekan/mongo-tools binaries, the cpu-exec/qemu-user shim from #6458 and
start-wekan.sh to bring them up. That bundle is already relocatable and already
self-contained - it is the offline zip people download today. So both workflows
take wekan-<version>-<arch>.zip off the release and wrap it, which means the
AppImage and the Flatpak are byte-for-byte the same server, Node and FerretDB
as the zip beside them, rather than second and third builds that could drift
from it and from each other.
For the person installing, both mean the same thing: one file, no MongoDB to
install, no Node to install, no database server to run. FerretDB v1 keeps
everything in SQLite files.
AppImage.yml wraps the bundle in an AppDir with WeKan's own icon and an AppRun
that hands over to start-wekan.sh, so every variable that launcher documents -
WRITABLE_PATH, PORT, ROOT_URL, MONGO_URL, WEKAN_DB - still works. It defaults
WRITABLE_PATH to ~/.local/share/wekan, because an AppImage is read-only and may
be sitting on a USB stick, so the data cannot live beside it. After packing it
STARTS the AppImage and waits for WeKan to answer on its port before uploading
anything: an AppImage that packs is not an AppImage that runs, and a broken
bundle should be caught here rather than by whoever downloads it.
Flatpak.yml uses `flatpak build` rather than flatpak-builder, because the
bundle is the build and there is nothing to compile. Its sandbox gets
--share=network and --persist=.wekan and nothing else: WeKan is a server
reached in a browser, so it needs the network and a place for its data, not
X11, audio or the user's files. It produces both a single-file bundle per
architecture and a multi-architecture repository - the bundle is what you
install by hand, the repository is what makes `flatpak update` work - and the
repository is static files, so a website can serve it with no server to run.
The Flatpak repository is unsigned. Signing needs a GPG key, which is a secret
rather than something a workflow can invent, so the README written into the
repository sets out the four steps to sign it when a key exists: make the key,
put the private half in GPG_PRIVATE_KEY, add --gpg-sign to the export and the
update-repo, and publish the public half beside the .flatpakrepo.
Both cover x86_64 and aarch64, which is where the two constraints meet: AppImage
and the freedesktop runtime have those architectures, and WeKan publishes
amd64 and arm64 Linux bundles. Each builds on a runner of its own CPU, because
appimagetool packs for the architecture it runs on. If a bundle for another
architecture appears on a release, adding a matrix line wraps it the same way.
NOT verified: neither workflow has been run - that needs the runners. What was
checked: that both are valid YAML and their matrices resolve; that the icon
they use exists in public/; that the four files they assert on
(bundle/main.js, bundle/node, bundle/ferretdb, bundle/start-wekan.sh) are what
release-all.yml actually puts in the bundle; and that the newest release really
does carry wekan-10.57-amd64.zip and wekan-10.57-arm64.zip under those names.
CLAUDE.md sets the shape of a `<details>` block precisely - `<details>` and
`</details>` each on their own line, the whole `<summary>` on ONE line, a blank
line under it, the body, a blank line, the close - and three blocks were missing
the blank line before `</details>`: the Version block in `# Platforms`, the
`Carried to a future release.` block at the top of `# TODO Later`, and one entry
in v10.55.
They render either way, which is why they survived; the rule is there so every
block looks the same in the file, and a block that does not is the one somebody
copies next.
Also rewrapped the one line in the `# TODO Later` intro that had grown to 152
characters, against the 80 the file is wrapped at. It carries no link, so
nothing stopped it being wrapped.
Thanks to xet7 !
The TSC website (Secretchronicles/secretchronicles.github.io), cloned inside
this repo to be worked on beside the game itself. Like the others it is a whole
git repository of its own, so git would not commit it into this one, but it
showed up as untracked noise in every status.
Thanks to xet7 !
git@github.com:Secretchronicles/TSC, cloned inside this repo to be worked on.
It is a whole git repository of its own, so git would not commit it into this
one, but it showed up as untracked noise in every status.
Listed beside the other local-only working copies - node, FerretDB, sandstorm,
mongo-tools, mongosh and the two wekan variants - with the same one-line note
saying what it is a clone of.
Thanks to xet7 !
Two reported bypasses of the SSRF guard, so the section now leads with the
CRITICAL SECURITY ISSUES header and the documentation reorganisation becomes
"and reorganises the documentation:". Both entries are grouped under "The SSRF
guard - what it checks, and what it was deciding from", because that is the one
thing both reports found from two directions.
The "In short" paragraph is rewritten around them: what each bypass was, in one
clause each, then the documentation work accounted for in the closing sentence.
Also records the Sandstorm bridge guard reading the page at its new path, which
the docs reorganisation left pointing at a directory.
Thanks to RandomGenerator, tonghuaroot and xet7 !
Two separately reported bypasses of the same defence, fixed together because
they are the same mistake in the two halves of it: the guard decided from
something it did not control.
FollowBleed (GHSA-j9p2-jm73-p549, reported by RandomGenerator against v10.53) -
the live Trello import validated the attachment URL with validateAttachmentUrl(),
which is the fix that closed LiveBleed / CVE-2026-30844, and then downloaded it
with the platform fetch(), which FOLLOWS REDIRECTS. So the guard only ever saw
the request, and the target gets to answer:
1. the attacker puts http://<public-host>/attachment.txt on a Trello card
2. validateAttachmentUrl() resolves it, sees a public IP, allows it
3. that host answers 302 Location: http://127.0.0.1:18080/secret
4. fetch() follows, and the loopback body is stored as the imported
attachment, readable back through WeKan
Non-blind SSRF against loopback services, internal admin panels, cloud metadata
and anything else reachable from the container - the exact thing the validation
was added to stop, reached through the response instead of the request.
A guard on the URL alone cannot hold, so fetchSafe() now guards EVERY HOP.
`maxRedirects` (default 0) is how many redirects a caller is willing to follow:
0 keeps the old behaviour of refusing any 3xx outright, which is right for
outgoing webhooks and avatar downloads because a legitimate one never redirects.
A caller that must follow one passes a small number, and each hop goes through
the same protocol allowlist, blocked-range check and DNS pinning as the original
URL before a packet is sent to it. Credentials are dropped on a cross-origin
redirect, so following Trello's 302 to S3 cannot hand the API key and token to
whoever the redirect names.
All three live-import sinks - downloadAttachmentBase64, inlineBoardBackground,
inlineMemberAvatars - download through it. Refusing every redirect was not an
option: Trello's own attachment endpoint answers with a 302 to a signed S3 URL,
so that would have meant importing no attachments at all.
The offline importers had the same hole and were not in the report. They handed
the validated URL to Attachments.loadAsync(), and Meteor-Files downloads with
the platform fetch() too, so a pasted Trello or WeKan board export reached
127.0.0.1 by exactly the same 302. models/lib/importAttachmentDownload.js
validates, downloads through fetchSafe with every hop checked, and hands back
the bytes, which are stored with the writeAsync() both importers already use for
an attachment that arrived inline.
TransitBleed (GHSA-c5xr-mg26-vq5w, reported by tonghuaroot) - isIpBlocked() in
models/lib/attachmentUrlValidation.js is the one block-list behind BOTH the
input-time validator and the delivery-time guard, and its IPv6 half classified
an address by its spelling: startsWith('::ffff:'), startsWith('2001:db8'), and
the first hextet parsed out of the string. IPv6 has several standard ways to
write "this packet goes to an IPv4 address" and none of them looks like
`::ffff:`:
2002:a9fe:a9fe:: 6to4 (RFC 3056) -> 169.254.169.254
64:ff9b::c0a8:101 NAT64 (RFC 6052) -> 192.168.1.1
2001:0:... Teredo (RFC 4380) -> complement of the low 32 bits
0:0:0:0:0:ffff:7f00:1 IPv4-mapped, spelled out -> 127.0.0.1
On a host with a 6to4 relay or a NAT64 gateway, which is ordinary in cloud and
Kubernetes networks, the packet arrives at that IPv4 address. So
http://[2002:a9fe:a9fe::]/latest/meta-data/ read cloud metadata straight through
the guard whose whole job was to stop it, and even a plain IPv4-mapped address
written out in full missed the check.
The address is EXPANDED to its 16 bytes once and every check reads those bytes,
so notation cannot change the answer, and every transition form has its embedded
IPv4 extracted and re-checked with the IPv4 rules - 6to4, NAT64 (well-known and
RFC 8215 local-use prefixes), Teredo through both its server and its obfuscated
client address, IPv4-mapped, IPv4-translated, IPv4-compatible and ISATAP under
any routing prefix, not only the link-local one. The deprecated fec0::/10
site-local range is blocked too. A transition address wrapping a PUBLIC IPv4 is
still allowed, because a guard that blocks everything gets switched off.
Tests. tests/followbleed.test.cjs replays the reported attack against a stubbed
transport - a public host that 302s to 127.0.0.1 - and asserts the second hop is
never sent, then covers redirects to a hostname resolving to a private IP, to
metadata, to a non-http scheme, relative Location, the chain limit, credential
stripping across origins, 303/307 method handling, and that a legitimate
public-to-public redirect is still followed with each hop pinned. It also pins
the sinks: the only bare fetch() left in the live import is the hardcoded
api.trello.com call, and neither offline importer uses loadAsync any more.
tests/transitbleed.test.cjs checks every transition form in both directions.
Both are in the plain-node suite; the guard's own dnsbleed suite gained the same
cases. 300 node suites pass.
Thanks to RandomGenerator and tonghuaroot for the coordinated disclosures, and
xet7 !
The docs reorganisation (f628a698) turned
`docs/Platforms/FOSS/Container/Sandstorm` from a page into a directory, and
this test reads that page at run time to pin what it documents. So it did not
merely go stale - `fs.readFileSync` on a directory throws EISDIR and the suite
died before its assertions ran.
It reads the directory's landing page, `README.md`, which is where the
migration-bridge documentation ended up. Both assertions are unchanged and both
pass.
Thanks to xet7 !
Two entries for the two commits since the last changelog update, and the bug
subsection is grouped by area now that it holds three: the snap, and All Boards.
Grouping meant rewording two summaries - an entry under a group must not repeat
the group's own name, and both began with it.
The In short paragraph follows: it named two of the three fixes and now names
all of them.
The counts entry goes under All Boards with the left-menu one; the remote-build
diagnostics are release tooling rather than a bug in WeKan itself, so they get
their own subsection after the fixes, as CLAUDE.md orders them.
Thanks to xet7 !
Starred, Home, Templates and Archive have labels of four different lengths, and
on a phone the count chip sat immediately after its label - so the four numbers
landed at four different x positions and the column of counts read as ragged
rather than as a column at all.
That packing was deliberate (#6523): the number beside the text rather than
across a gap, on the grounds that a landscape phone's menu is wide enough for
the gap to look like a mistake. Reversed here, because the alignment is what a
reader is actually using - the counts are compared with each other down the
column, and four x positions is what stops that.
The row keeps the space-between and the flex: 1 label the base rule gives it, at
every width, so every count sits at the end of its own row and they line up. The
phone override is gone rather than re-tuned; there is no width at which the
ragged version was wanted.
Guarded both ways in mobileAllBoardsFit.test.cjs: the row must spread its two
children apart and the label must take the slack, AND no narrower width may take
either back - a phone override is exactly how they came to be ragged.
Thanks to xet7 !
The v10.55 riscv64 leg failed three times in sixteen minutes, and the whole of
what it printed was:
Git operation failed with: Could not push 'HEAD' to
git.launchpad.net/.../+git/snapcraft-wekan-<hash>
with refspec 'HEAD:refs/heads/main'
This is a different failure from the other two arches. It is not the staging bug
- that happens minutes into a build, on the builder. Here the build never
started: remote-build uploads the source to a Launchpad git repository first,
and the upload is what failed. ppc64el and s390x pushed fine in their own runs,
with the same credentials.
Why it is still unconfirmed: snapcraft swallows git's own error, writes it to
its execution log, names that log's path in the output, and nobody reads it. So
"rejected", "timeout", "auth" and "too big" all look identical from the job log
- which is why three runs have not narrowed it down.
Two changes, both about being able to tell next time:
The failed-attempt handler prints snapcraft's own execution log, not only the
Launchpad build log. The Launchpad one does not exist when the build never
started, which is exactly this case.
And the retry clears ~/.cache/snapcraft/remote-build before waiting. snapcraft
keeps a local clone of the Launchpad repository there, and a retry that reuses a
half-pushed one repeats the same failure - three identical attempts sixteen
minutes apart is what that looks like.
The leading suspect is recorded in ../log/snap/TODO.txt rather than guessed at
here: remote-build pushes HEAD:refs/heads/main with no force, WeKan's history
was rewritten between v10.54 and v10.55 by the release rebase, and a `main` left
behind by the previous run is then no ancestor of today's HEAD. If the log the
job now prints says "rejected", deleting that scratch repository is the fix, and
the longer-term answer is for the workflow to delete it or to use a fresh one
per run.
Thanks to xet7 !
The three commits since v10.55, each linking the commit it describes: the
platform expansion as a new feature, and the snap that could not finish staging
on any FerretDB-only architecture and the left menu lying over the boards on a
phone as bug fixes.
One style fix with it: the dependency bullets in v10.55 wrote their link text as
[update] where v10.54 and everything before it write [Update].
The hashes were checked rather than assumed - every commit link in the new
section resolves AND is an ancestor of HEAD, which is the property that matters
after a rebase: a hash that still exists locally but has left the branch is a
404 the moment it is pushed. The rest of the file was checked the same way and
needed nothing.
Left alone: hash LENGTH. The two newest releases happen to use 9-character
prefixes, which is what a sample of them says, but the file as a whole is 2811
forty-character hashes against 399 short ones - so the newest entries are the
outliers, not the rule, and normalising the file to match a sample of itself
would have rewritten every release ever published.
Thanks to xet7 !
Every non-amd64, non-arm64 platform already used FerretDB v1 - MongoDB ships no
server for any of them - but only three of them were built, because only three
had a Node.js runtime anyone published. wekan/node now builds the rest, so the
gap is gone and the release follows FerretDB's list.
New bundles: i386, armhf and loong64, beside the ppc64le, s390x and riscv64
already there. They go into the snap (i386 and armhf; loong64 is not a snap
architecture) and into the multi-arch image (linux/386 and linux/arm/v7;
buildx and the three registries do not agree on loong64 yet).
WHERE THE NODE.JS COMES FROM is now resolved at build time rather than declared:
nodejs.org, then unofficial-builds, then the wekan/node fork, in that order, and
the log says which one served. The first two ship a tarball; the fork ships the
bare binary it built, because that is the only part missing - so when the fork
serves, npm comes from the official amd64 tarball of the same version. npm is
JavaScript and runs on whatever node executes it, so an npm built for one CPU
drives a node built for another.
THREE VOCABULARIES meet in this matrix and they disagree: Node says x86 and
armv7l where Debian, snap and FerretDB say i386 and armhf, and snap says ppc64el
where everyone else says ppc64le. Each row now names all three - arch, node_arch
and ferretdb_arch - because a row that named one of them would download another
CPU's binary and nothing would notice until somebody ran it.
The MongoDB Database Tools are per-tool tolerant now: wekan/mongo-tools does not
publish every arch, and a missing mongodump is a missing convenience rather than
a broken bundle - FerretDB is the database. It removes the inherited amd64 one
instead of shipping it, because a tool for the wrong CPU is worse than no tool.
armel is the one FerretDB target left out, and deliberately: V8 has not
supported ARMv5 for many years, so there is no runtime to put in the bundle and
the fork cannot build one either.
A new guard reads the four lists that have to agree - the bundle matrix, the
snap platforms block and its arch mapping, the snap-launchpad matrix and the
Docker platform list - and checks every snap arch has a bundle behind it, every
bundle row names all three vocabularies, and buildx builds exactly the platforms
whose push is then verified. Two older guards pinned the five-platform snap list
and now pin the seven.
Written up in docs/Design/Autoupdate/Platforms.md.
Not verified by a real release run: no workflow here can be executed, and the
first run is what will say whether the loongarch64 cross toolchain is available
on the runner and whether each platform's Node.js resolves where expected.
Thanks to xet7 !
The s390x snap died in the STAGE step of both its Launchpad builds, right after
"Staging mongodb":
/build/snapcraft-wekan-<id>/stage/bin: Is a directory
IsADirectoryError: filename: '/build/.../stage/bin'
The mongodb part's stage-packages unpack an Ubuntu 24.04 merged-/usr layout,
which puts everything under usr/ and leaves `bin` in the part as a SYMLINK to
usr/bin. On amd64 and arm64 the part's build then downloads MongoDB and copies
its binaries into $SNAPCRAFT_PART_INSTALL/bin, which replaces that symlink with
a real directory. On s390x, ppc64el and riscv64 MongoDB ships no server, so the
build exits early - "No MongoDB server for ...; FerretDB-only arch" - and the
symlink survives.
mongodb is staged LAST of the parts that carry a bin/, so stage/bin is already a
real directory by then, from wekan, caddy and helpers. Copying a symlink onto an
existing directory is the IsADirectoryError, and it fails the whole snap rather
than that one part.
An override-stage turns a `bin` SYMLINK into an empty real directory before
staging, then stages normally. It tests for a symlink specifically, so the
arches where mongod really is there are untouched; it removes before mkdir,
because mkdir -p FOLLOWS a symlink and would change nothing; and an empty real
directory merges into stage/bin without changing anything else.
ppc64el is the same FerretDB-only branch and would hit this too - its own logs
never got that far, because snapcraft's monitor loop died fetching the
in-progress build log from launchpad.net with "[Errno 101] Network is
unreachable" while the build itself was still compiling, and each retry then
submitted a NEW build instead of recovering the running one. That half is
infrastructure, not this file; it is written up with the rest in
../log/snap/TODO.txt.
riscv64 takes the same branch, so this covers it as well.
Not verified against a real Launchpad builder: the fix is read off the failure,
and the s390x leg has to be re-run to confirm it now reaches "Snapped".
Thanks to xet7 !
Spec 43 (All Boards on a phone) still failed in all three browsers with the same
numbers as before: the menu 260px wide over a 157px column, lying across the
boards.
max-width: 100% was not enough, and that is the part worth knowing: a grid
item's default min-width is `auto`, which is its content's intrinsic minimum,
and a MINIMUM BEATS A MAXIMUM. The cap did nothing until the item was allowed to
shrink to it. `min-width: 0` is the same pair the right-hand column beside it
has carried all along, for the same reason and with the same comment. Nothing
inside the menu needs the intrinsic width held open - the workspace name already
ellipses through its own min-width: 0.
Two guards followed the format rather than the other way round. Both were
written against a release with forty entries and rejected v10.55 for being four
dependabot bullets and nothing else: CLAUDE.md keeps a dependency batch as plain
bullets, so a release can legitimately have no <details> at all, and padding one
bump into a block whose body repeats its summary would be the noise that rule
exists to prevent. They count entries OR bullets now.
The empty Files report (spec 41) is NOT fixed - it is made legible. The helper
that fills that table caught every exception and returned [] in silence, which
renders exactly like "there is nothing to show": headers, "No results", and
nothing anywhere recording that it threw. It logs now. The publication says one
line when it matches nothing, whatever DEBUG is set to, because a publication
answering with nothing and a publication never running look identical from the
outside - which is why three runs have not told us which this is.
And node/ - a 2.2G clone of the Node.js 24.x fork the runtime is built from -
joins the other local-only clones in .gitignore, rooted like they are.
Thanks to xet7 !
# An AppImage that packs is not an AppImage that runs. Start it, wait
# for WeKan to answer on its port, then stop it - so a broken bundle
# is caught here rather than by whoever downloads it.
export WRITABLE_PATH="$PWD/smoke-data" PORT=8080
# A CHECK THAT CANNOT RUN IS NOT A FAILED CHECK. The 32-bit builds are
# packed on a 64-bit runner, which executes them only if the kernel and
# loader for that word size are present - GitHub promises neither. If
# the binary cannot be executed AT ALL, the AppImage is as good as
# anything here knows and the release must not be withheld for it; say
# so and move on. Anything else - it starts and does not answer - is
# the bug this step exists for and still fails.
if ! APPIMAGE_EXTRACT_AND_RUN=1 "$app" --appimage-help >/dev/null 2>&1 \
&& ! APPIMAGE_EXTRACT_AND_RUN=1 "$app" --help >/dev/null 2>&1; then
if ! file "$app" | grep -q "$(uname -m)"; then
echo "::warning::this runner cannot execute a ${{ matrix.appimage_arch }} binary, so the AppImage was packed but not started. It is uploaded unchecked."
echo "::notice::No base bundle (wekan-${{ needs.release.outputs.version }}-{amd64,arm64}.zip) is on ${{ needs.release.outputs.tag }} yet, so there is nothing to wrap - skipping. release-all.yml builds the AppImages once the base bundles are published."
exit 0
fi
echo "::error::A base bundle is published but no architecture produced an AppImage - every build job failed. Their logs say why; nothing is uploaded."
exit 1
fi
- name:Upload them
env:
GH_TOKEN:${{ github.token }}
run:|
set -euo pipefail
if ! ls assets/*.AppImage >/dev/null 2>&1; then
echo "Nothing to upload (no base bundle was published to wrap)."
exit 0
fi
# --repo, because this job does NOT check the repository out - it only
# downloads artifacts - so `gh` has no git remote to infer the repo
# from and fails with "fatal: not a git repository". Every other gh
# call in this workflow already passes it; these two did not, so the
# AppImages were built, smoke-tested and uploaded as artifacts, and
echo "::warning::v${{ steps.plan.outputs.version }} is missing ${{ steps.plan.outputs.manual }}, which this workflow cannot build: amd64 is the Meteor build, arm64/win64/mac-arm64 each need their own runner, and the Sandstorm .spk is signed. Run release-all.yml for those."
# The six architectures nobody else builds for, repacked from the PUBLISHED
# amd64 bundle - which is what makes this possible without a Meteor build.
extra-arches:
name:${{ matrix.arch }}
needs:plan
if:${{ needs.plan.outputs.extra != '[]' }}
runs-on:ubuntu-24.04
timeout-minutes:300
strategy:
fail-fast:false
matrix:
include:
# The same table as release-all.yml's build-extra-arches, and it has
# to stay the same: `node_arch` is what the Node.js download is
# called (Node says x86 where we say i386, armv7l where we say
# armhf), `ferretdb_arch` is the asset name in the wekan/FerretDB
# release, and `image` must be a base image that PUBLISHES this
# platform - ubuntu:26.04 does not build 386, debian:trixie does.
if ! NODE_META="$(bash /tmp/resolve-node-source.sh "${WEKAN_ARCH}""${NODE_MAJOR}")";then
echo"No Node.js ${NODE_MAJOR}.x for ${WEKAN_ARCH} at nodejs.org, unofficial-builds.nodejs.org or wekan/node-patches, so this image cannot be built for it. Build node-${WEKAN_ARCH} in wekan/node-patches, or drop ${TARGETARCH} from the image's platform list." >&2
echo ==^> Running Playwright Chromium, Firefox and WebKit one at a time^(--workers=1^) on Meteor #1 [Node.js :3000, MongoDB :3001]. Full log: %RUN_LOGDIR%\wekan-alltests-browsers.log
"Snap|Release the snap for one version|releases/release-snap.sh|WeKan version, e.g. 10.50||"
"Snap|List the newest Snap Store revisions|releases/snap-store-revisions.sh|||"
"Snap|Release one store revision to edge, beta and candidate|releases/snap-store-release-revision-to-channels.sh|Snap Store revision number||"
"Snap|Release every snap, every architecture, to all four channels|releases/snap-release-all-channels.sh|Version, or empty for the newest||snap-release-all-channels"
"Snap|Switch the installed snap to the edge channel|releases/snap-edge.sh|||"
"Snap|Switch the installed snap to the stable channel|releases/snap-stable.sh|||"
"Snap|snapcraft help topics|releases/snapcraft-help.sh|||"
"Update git|Update git: fetch + rebase onto origin, fix CHANGELOG hashes, show status";;
"git pull|git pull: fetch, fast-forward or rebase onto origin, repoint the CHANGELOG commit links the rebase moved, and leave the repo unchanged if anything conflicts"\
"git push|git push: check the CHANGELOG commit links resolve before publishing them, push this branch to origin, and pull-then-retry once if origin moved meanwhile";;
"Dev server")
choose "Dev server"\
"localhost:3000|Run Meteor for dev on http://localhost:3000"\
@@ -1744,9 +2136,9 @@ while [ -z "$opt" ]; do
"Kill all dev servers|Kill all dev servers (free ports 3000/3001/3100/3101/4000/4001/8080)";;
"Tests")
choose "Tests"\
"EVERYTHING (sequential): WeKan tests + all databases + FerretDB tests|Run EVERYTHING sequentially: WeKan's own tests, then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB's own tests (unit, vet, integration) - one stage at a time, all logs in ../log/<datetime>/"\
"WeKan's own tests only, parallel|Run WeKan's own tests in parallel on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, concurrently. No database conformance and no FerretDB tests - logs in ../log/<datetime>/"\
"WeKan's own tests only, sequential|Run WeKan's own tests sequentially on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, one job at a time. No database conformance and no FerretDB tests - logs in ../log/<datetime>/"\
"EVERYTHING (sequential): the guard + WeKan tests + all databases + FerretDB tests|Run EVERYTHING sequentially: the floating-promises guard (seconds), then WeKan's own tests (mocha, the node suites, import regression, node E2E and all three browsers), then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB's own tests (unit, vet, integration) - one stage at a time, all logs in log/<datetime>/"\
"WeKan's own tests only, parallel|Run WeKan's own tests in parallel on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, concurrently. No database conformance and no FerretDB tests - logs in log/<datetime>/"\
"WeKan's own tests only, sequential|Run WeKan's own tests sequentially on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, one job at a time. No database conformance and no FerretDB tests - logs in log/<datetime>/"\
"Mocha (server-side)|Test Mocha unit + security + API-logic tests (server-side only, no browser)"\
"Import regression|Test import regression (tests/wekanCreator.import.test.js, fast, no server)"\
"Count tests by category|Count amount of tests by category"\
"Run all FerretDB tests - SEQUENTIAL|Run all FerretDB tests - SEQUENTIAL: unit, vet and the integration suite of the FerretDB subdirectory of this repo, one at a time, logs in ../log/<datetime>/"\
"All databases (sequential)|Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in ../log/<datetime>/)";;
"Run all FerretDB tests - SEQUENTIAL|Run all FerretDB tests - SEQUENTIAL: unit, vet and the integration suite of the FerretDB subdirectory of this repo, one at a time, logs in log/<datetime>/"\
"All databases (sequential)|Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in log/<datetime>/)";;
"Tools")
choose "Tools"\
"Save Meteor deps list|Save Meteor dependency chain to ../meteor-deps.txt"\
@@ -1863,12 +2255,23 @@ for _once in 1; do
;;
"Build WeKan")
build_wekan
# || exit 1, because build_wekan returns 1 on failure and a bare call
# throws that away: the menu breaks, the script falls off the end and
# exits 0. A build that printed "ERROR: the WeKan build failed" while
# reporting success to its caller is worse than one that just fails -
# anything driving this non-interactively (`printf '1\n2\n' | ./build.sh`,
# or CI) sees a green run and a missing bundle.
build_wekan ||exit1
break
;;
"Update git: fetch + rebase onto origin, fix CHANGELOG hashes, show status")
update_git
"git pull: fetch, fast-forward or rebase onto origin, repoint the CHANGELOG commit links the rebase moved, and leave the repo unchanged if anything conflicts")
git_pull
break
;;
"git push: check the CHANGELOG commit links resolve before publishing them, push this branch to origin, and pull-then-retry once if origin moved meanwhile")
git_push
break
;;
@@ -1877,9 +2280,9 @@ for _once in 1; do
kill_meteor_on_port 3000||break
#Not in use, could increase RAM usage: NODE_OPTIONS="--max_old_space_size=4096"
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1| tee ../log/wekan-log.log
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1| tee "$WEKAN_LOG_ROOT/wekan-log.log"
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1| tee ../log/wekan-log.log
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1| tee "$WEKAN_LOG_ROOT/wekan-log.log"
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL="$DEV_ROOT_URL" meteor run --port "$DEV_PORT" 2>&1| tee ../log/wekan-log.log
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL="$DEV_ROOT_URL" meteor run --port "$DEV_PORT" 2>&1| tee "$WEKAN_LOG_ROOT/wekan-log.log"
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://$IPADDRESS:$PORT meteor run --port $PORT 2>&1| tee ../log/wekan-log.log
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"DDP_TRANSPORT=uws DEBUG=trueWRITABLE_PATH=.. WITH_API=trueRICHER_CARD_COMMENT_EDITOR=falseROOT_URL=http://$IPADDRESS:$PORT meteor run --port $PORT 2>&1| tee "$WEKAN_LOG_ROOT/wekan-log.log"
"Run WeKan's own tests in parallel on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, concurrently. No database conformance and no FerretDB tests - logs in ../log/<datetime>/")
"Run WeKan's own tests in parallel on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, concurrently. No database conformance and no FerretDB tests - logs in log/<datetime>/")
run_all_tests parallel
break
;;
"Run WeKan's own tests sequentially on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, one job at a time. No database conformance and no FerretDB tests - logs in ../log/<datetime>/")
"Run WeKan's own tests sequentially on http://localhost:3000: Mocha, the node unit suites, import regression, node E2E and all three browsers, one job at a time. No database conformance and no FerretDB tests - logs in log/<datetime>/")
run_all_tests sequential
break
;;
@@ -2076,39 +2479,15 @@ for _once in 1; do
echo"Ensuring .eslintrc.json includes @typescript-eslint plugin and no-floating-promises rule"
echo"OK: Rule @typescript-eslint/no-floating-promises is configured in .eslintrc.json"
else
echo"WARNING: Rule @typescript-eslint/no-floating-promises is NOT configured in .eslintrc.json"
echo"Suggested: add @typescript-eslint/eslint-plugin and set '@typescript-eslint/no-floating-promises': 'error'"
fi
echo"Quick note: @typescript-eslint/no-floating-promises is a type-aware rule and may require further parser/project setup for full enforcement in all files."
echo
echo"Scanning for unawaited Authentication.checkBoardAccess/checkBoardWriteAccess in server/models"
if rg -n "$AUTH_CALL_PATTERN" server/models | rg -v "$AUTH_AWAIT_PATTERN";then
echo"WARNING: Found possible unawaited board auth checks above"
else
echo"OK: No unawaited board auth checks found"
fi
else
if grep -RInE "$AUTH_CALL_PATTERN" server/models | grep -vE "$AUTH_AWAIT_PATTERN";then
echo"WARNING: Found possible unawaited board auth checks above"
else
echo"OK: No unawaited board auth checks found"
fi
fi
# The checks themselves - the same function EVERYTHING runs, so the
# menu option and the whole-run stage can never drift apart. Above
# this line is the setting-up half, which only a person asking for
# this option gets: it installs packages and writes the rule into
# .eslintrc.json, and a test run must do neither.
floating_promises_checks
} 2>&1| tee "$LOG"
break
;;
@@ -2123,7 +2502,7 @@ for _once in 1; do
break
;;
"Run all FerretDB tests - SEQUENTIAL: unit, vet and the integration suite of the FerretDB subdirectory of this repo, one at a time, logs in ../log/<datetime>/")
"Run all FerretDB tests - SEQUENTIAL: unit, vet and the integration suite of the FerretDB subdirectory of this repo, one at a time, logs in log/<datetime>/")
# FerretDB is a subdirectory of this repo; releases/db-conformance.sh clones it
# when it is not there, and its own build.sh installs Go and the modules.
if[ ! -x FerretDB/build.sh ];then
@@ -2135,11 +2514,11 @@ for _once in 1; do
fi
;;
"Run EVERYTHING sequentially: WeKan's own tests, then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB's own tests (unit, vet, integration) - one stage at a time, all logs in ../log/<datetime>/")
"Run EVERYTHING sequentially: the floating-promises guard (seconds), then WeKan's own tests (mocha, the node suites, import regression, node E2E and all three browsers), then the database conformance run for every database with a Docker image for this CPU, then all of FerretDB's own tests (unit, vet, integration) - one stage at a time, all logs in log/<datetime>/")
run_everything
;;
"Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in ../log/<datetime>/)")
"Test all databases that have a Docker image for this CPU, SEQUENTIALLY: build newest FerretDB v1 from source, then run every FerretDB v1 query type against each database and compare that they all answer the same (results in log/<datetime>/)")
# Everything this needs - the FerretDB source, the Go toolchain, the module
# dependencies - is fetched or built by the script itself; see the note at its
# top for why it is sequential and why SAP HANA is opt-in.
@@ -2225,5 +2604,21 @@ for _once in 1; do
break
;;
# No arm matched. $opt is the menu entry's FULL DESCRIPTION, written twice -
# once in the choose() list far above, once as the arm here - so an edit to
# the menu text that misses the arm leaves an option that matches nothing.
# Without this, the case simply falls through, the `for _once` loop ends and
# the script EXITS: the option looks like it worked, printed nothing and ran
# nothing. That is what renaming "EVERYTHING (sequential)" did to it.
# tests/buildScriptParity.test.cjs fails on the mismatch itself; this is what
# the person in front of the menu sees if one ever gets past it.
*)
echo"build.sh: no handler for this menu option - that is a bug in build.sh." >&2
echo" The menu entry's description and its case arm must be the SAME string:" >&2
echo"$opt" >&2
echo" Nothing was run. Please report it, or run the same thing from the command line:" >&2
Then paste to webbrowser Wekan Add Board / Import / From previous export.
You can [save all MongoDB database content as JSON files](../Platforms/FOSS/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md). Files are base64 encoded in JSON files.
You can [save all MongoDB database content as JSON files](../Platforms/FOSS/Container/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md). Files are base64 encoded in JSON files.
Export board to Wekan JSON, and import as Wekan JSON can make some part of board to load, but you should check is some data missing.
@@ -765,7 +765,7 @@ makesRestore $1
## Docker Backup and Restore
[Docker Backup and Restore](../Platforms/FOSS/Docker/Export-Docker-Mongo-Data.md)
[Docker Backup and Restore](../Platforms/FOSS/Container/Docker/Export-Docker-Mongo-Data.md)
Download Wekan grain with arrow down download button to .zip file. You can restore it later.
[Export data from Wekan Sandstorm grain .zip file](../Platforms/FOSS/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
[Export data from Wekan Sandstorm grain .zip file](../Platforms/FOSS/Container/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
## <a name="cloudron">Cloudron
If those [Backup](Backup.md) ways are not easily found at [Cloudron](../Platforms/Propietary/Cloud/Cloudron/Cloudron.md), one way is to install [Redash](https://redash.io/) and then backup this way:
If those [Backup](Backup.md) ways are not easily found at [Cloudron](../Platforms/Propietary/SaaS/Cloudron), one way is to install [Redash](https://redash.io/) and then backup this way:
Redash works with this kind of queries:
```json
@@ -844,7 +844,7 @@ but downloading with API script still works:
7) Repeat steps 1-4 and 6 for every collection/table like boards,cards, etc
8) Remove from downloaded .json files extra query related data, so that it is similar like [any other Wekan database backup JSON files](../Platforms/FOSS/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
8) Remove from downloaded .json files extra query related data, so that it is similar like [any other Wekan database backup JSON files](../Platforms/FOSS/Container/Sandstorm/Export-from-Wekan-Sandstorm-grain-.zip-file.md)
9) Insert data to some other Wekan install with nosqlbooster like mentioned at page [Backup](Backup.md)
@@ -93,7 +93,7 @@ Related, for creating apps to other appstores: [Browser compatibility matrix](Br
3. Clone WeKan repo, add favicons etc from step 2 to wekan/public/ , and build WeKan bundle from source like at [Emoji](../Features/Editor/Emoji.md) . Note: Currently WeKan does not have feature for custom favicons, it would require a lot of work for 100+ favicons etc customizations.
4. Run bundle at server like [Offline](../Platforms/Propietary/Windows/Offline.md) or [Raspberry Pi](../Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
4. Run bundle at server like [Offline](../Platforms/Propietary/OS/Windows/Offline.md) or [Raspberry Pi](../Platforms/FOSS/HW/RaspberryPi/Raspberry-Pi.md)
5. Install Caddy like [Caddy Webserver Config](../Webserver/Caddy.md) , with this config for PWA at https://boards.example.com , there add assetlinks.json details you got when you downloaded Android .zip file from https://pwabuilder.com , see assetlinks issue about where at Play Console those 2 SHA256 keys https://github.com/pwa-builder/PWABuilder/issues/3867#issuecomment-1450826565
This page designs the **alternatives**: what it would take for **one** WeKan
@@ -23,8 +23,8 @@ Everything the question touches. Paths are from the repository root.
| File Path | File Type | Description |
| --- | --- | --- |
| `docs/Platforms/FOSS/Docker/Meteor3/README.md` | `.md` guide | The shipped topology: Caddy in front, one WeKan container per customer, one MongoDB, one database per customer. |
| `docs/Platforms/FOSS/Docker/Meteor3/multitenancy.md` | `.md` guide | The two supported transports for that topology, and why each instance in one network namespace needs its own internal `uws.port`. |
| `docs/Platforms/FOSS/Container/Docker/Meteor3` | `.md` guide | The shipped topology: Caddy in front, one WeKan container per customer, one MongoDB, one database per customer. |
| `docs/Platforms/FOSS/Container/Docker/Meteor3/multitenancy.md` | `.md` guide | The two supported transports for that topology, and why each instance in one network namespace needs its own internal `uws.port`. |
| `docker-compose-multitenancy.yml` | `.yml` compose file | The working example: per-tenant `PORT`, `ROOT_URL`, `MONGO_URL` database and `uws.port`. |
| `models/lib/universalUrlGenerator.js` | `.js` module | Builds attachment/avatar URLs that do **not** depend on `ROOT_URL`. Already tenant-safe, and the model the rest of the code should follow. |
| `server/routes/universalFileServer.js` | `.js` route | Serves `/cdn/storage/…` for any host. Two `Meteor.absoluteUrl()` calls are the only `ROOT_URL` dependency left in it. |
- Redbean amd64 binary works at Win/Mac/Linux/BSD/BIOS without any modifications, because it is based on Cosmopolitan https://github.com/jart/cosmopolitan
- Uses HTMX https://htmx.org at UI
- Saves to SQLite database
- Petclinic fork, using Blink to run at s390x [s390x](../../Platforms/FOSS/s390x.md#petclinic-s390x)
- Petclinic fork, using Blink to run at s390x [s390x](../../Platforms/FOSS/HW/s390x.md#petclinic-s390x)
@@ -43,7 +43,7 @@ When you run this, you see .jade template files where is search input box, and .
## Getting Started
- Currently Wekan development is done mainly at Ubuntu 20.10 64bit, but building Wekan does work on any Debian, Ubuntu, WSL Ubuntu 20.04, [Mac](../Platforms/Propietary/Mac/Mac.md). Sametime maybe on [Windows](../Platforms/Propietary/Windows/Windows.md) with `choco install -y meteor` and installing packages it then asks for with `meteor add ...` or `meteor npm install --save ...` and then running meteor again.
- Currently Wekan development is done mainly at Ubuntu 20.10 64bit, but building Wekan does work on any Debian, Ubuntu, WSL Ubuntu 20.04, [Mac](../Platforms/Propietary/OS/Mac). Sametime maybe on [Windows](../Platforms/Propietary/OS/Windows) with `choco install -y meteor` and installing packages it then asks for with `meteor add ...` or `meteor npm install --save ...` and then running meteor again.
- Look through old [pull requests](https://github.com/wekan/wekan/pulls)
- Read Wekan source code, you can use some git history viewer GUI like gitk
- Read [Meteor documentation](https://docs.meteor.com/) for [Meteor version](../../.meteor/release) in use, other versions mentioned at [Dockerfile](../../Dockerfile)
@@ -93,22 +93,22 @@ Files in this directory are served by meteor as-is to the client. It hosts some
* **[authentication.js](../../server/authentication.js)** add the `Authentication`object to Meteor that provides methods for checking access rights.
* **[lib/utils.js](../../server/lib/utils.js)** defines some checks used by [checklists.js](../../models/checklists.js)** model. <span style="color:red">*XXX: these methods are defined in server-only code by are used in models, which are visible by the client (in Checklists.allow)?*</span>
* **[notifications](../../server/notifications)**
* **[notifications.js](../../server/notifications/notifications.js)**: defines the `Notifications` object that supports [Activities](models/activities.js) and holds a list of functions to call when its `notify()` method is called along with convenience methods to subscribe, unsubscribe and a way to filter recipients according to user settings for notification;
* **[notifications.js](../../server/notifications/notifications.js)**: defines the `Notifications` object that supports [Activities](../../models/activities.js) and holds a list of functions to call when its `notify()` method is called along with convenience methods to subscribe, unsubscribe and a way to filter recipients according to user settings for notification;
* **[email.js](../../server/notifications/email.js)**: makes use of the notification system to send an email to a user;
* **[profile.js](../../server/notifications/profile.js)**: *stub*; will allow associating notifications with user ids to be consumed by mobile apps;
* **[notifications.js](../../server/notifications/notifications.js)**: adds the `watch()` Meteor server-only [method](https://guide.meteor.com/methods.html) that may watch boards, lists or cards using [models/watchable.js](../../models/watchable.js);
* **[outgoing.js](../../server/notifications/outgoing.js)**: adds the `outgoingWebhooks()` Meteor server-only [method](https://guide.meteor.com/methods.html) that can call external API <span style="color:red">*XXX: I guess*</span>
* **[publications](../../server/publications)** defines sets of records that are [published](https://docs.meteor.com/api/pubsub.html#Meteor-publish) by the server and how clients can subscribe to those:
* **[avatars.js](../../server/publications/avatars.js)**: [Avatars](models/avatars.js) collection for current user;
* **[boards.js](../../server/publications/boards.js)**: [Boards](models/boards.js) collection for current user, archived boards collection and individual board as a [relation](https://atmospherejs.com/cottz/publish-relations);
* **[avatars.js](../../server/publications/avatars.js)**: [Avatars](../../models/avatars.js) collection for current user;
* **[boards.js](../../server/publications/boards.js)**: [Boards](../../models/boards.js) collection for current user, archived boards collection and individual board as a [relation](https://atmospherejs.com/cottz/publish-relations);
* **[cards.js](../../server/publications/cards.js)**: a [Card](../../models/cards.js) by its id;
* **[fast-render.js](https://github.com/wekan/wekan/tree/main/server/publications/fast-render.js)**: configures [FastRender](https://github.com/kadirahq/fast-render) to use the board data; <span style="color:red">*XXX: FastRender docs say "Make sure you're using Meteor.subscribe and not this.subscribe"*</span>
* **[users.js](../../server/publications/users.js)**: provides a "mini-profile" for individual users and a [way](https://docs.meteor.com/api/collections.html#fieldspecifiers) to check if current user is admin.
- Read [Wekan new release ChangeLog](../../CHANGELOG.md)
- [Install Meteor.js](https://www.meteor.com/install). Note: Windows install is very slow, please use other option at [Windows wiki page](../Platforms/Propietary/Windows/Windows.md)
- [Install Meteor.js](https://www.meteor.com/install). Note: Windows install is very slow, please use other option at [Windows wiki page](../Platforms/Propietary/OS/Windows)
- Download newest bundle from https://releases.wekan.team
Loaded 100 of 410 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.