mirror of
https://github.com/wekan/wekan.git
synced 2026-11-09 03:45:00 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f89ad07b8 | ||
|
|
3fb9e4d1be | ||
|
|
5ca5b00a98 | ||
|
|
6cc84a07cd | ||
|
|
a7ace18af8 | ||
|
|
7e0fd84c3c | ||
|
|
4023654d84 | ||
|
|
d213e7b674 | ||
|
|
423ac8a145 | ||
|
|
913acde0ef | ||
|
|
1513b2e98c |
No files matched your search
@@ -4714,7 +4714,7 @@ jobs:
|
||||
with:
|
||||
snapcraft-args: pack
|
||||
|
||||
- name: Refresh stale managed APT indexes after a package 404
|
||||
- name: Refresh managed APT after a package 404 or a failed apt update
|
||||
if: steps.build-first.outcome == 'failure'
|
||||
run: sudo -u "$(id -un)" -E python3 "$GITHUB_WORKSPACE/releases/snap-refresh-managed-apt.py"
|
||||
|
||||
@@ -5610,7 +5610,7 @@ jobs:
|
||||
snapcraft-args: pack
|
||||
path: variant
|
||||
|
||||
- name: Refresh stale managed APT indexes after a package 404
|
||||
- name: Refresh managed APT after a package 404 or a failed apt update
|
||||
if: steps.build-first.outcome == 'failure'
|
||||
run: sudo -u "$(id -un)" -E python3 "$GITHUB_WORKSPACE/wekan/releases/snap-refresh-managed-apt.py"
|
||||
|
||||
|
||||
@@ -124,6 +124,72 @@ its reason.
|
||||
|
||||
</details>
|
||||
|
||||
# v13.03 2026-11-09 WeKan ® release
|
||||
|
||||
**In short:** The amd64 **snaps** no longer fail the release when the Ubuntu
|
||||
archive is briefly unreachable: the build waits for it and retries once.
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
**Release builds** - what the release workflow does when a build fails.
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/a7ace18af8">The amd64 snaps wait out an Ubuntu archive outage instead of failing</a>. Thanks to xet7.</summary>
|
||||
|
||||
v13.02's native amd64 snap and both amd64 snap variants failed within eight
|
||||
minutes of each other with "Failed to refresh package list: failed to run apt
|
||||
update" while Snapcraft installed build-packages; the arm64 snaps, served by
|
||||
`ports.ubuntu.com`, built. `releases/snap-refresh-managed-apt.py` recovered
|
||||
only a stale `.deb` 404, so the one guarded retry never ran. It now also
|
||||
recognises a failed apt update in the managed build instance, retries
|
||||
`apt-get update` there with a 30/60/120/240 s backoff, and lets the Snapcraft
|
||||
retry run only once the archive answers; a longer outage stays fatal. It
|
||||
prints apt's own error lines first, so a failing job says why apt failed.
|
||||
`tests/snap-managed-apt.py` pins the recovery, its bound, and that unrelated
|
||||
failures are still refused.
|
||||
|
||||
</details>
|
||||
|
||||
and has the following developer-tooling fix:
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/6cc84a07cd">The security regression inventory counts UnassignedArchiveBleed as guarded</a>. Thanks to tsurutanmen and xet7.</summary>
|
||||
|
||||
`tests/securityRegressionCoverage.test.cjs` failed once the Hall of Fame
|
||||
published UnassignedArchiveBleed, because its inventory did not have the name.
|
||||
It is guarded now by `tests/assignedCardMethods.test.cjs` and
|
||||
`tests/playwright/specs/assigned-card-methods.e2e.js`, which name it.
|
||||
|
||||
</details>
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v13.02 2026-11-09 WeKan ® release
|
||||
|
||||
**In short:** **UnassignedArchiveBleed** is fixed: a member with the
|
||||
**Normal, assigned only** role archives and converts only the cards assigned
|
||||
to them, through every card method.
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUE of [UnassignedArchiveBleed](https://wekan.fi/hall-of-fame/unassignedarchivebleed/):
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/wekan/wekan/commit/1513b2e98c">An assigned-only member archives and converts only the cards assigned to them</a>. Thanks to tsurutanmen and xet7.</summary>
|
||||
|
||||
[UnassignedArchiveBleed](https://wekan.fi/hall-of-fame/unassignedarchivebleed/),
|
||||
[GHSA-6xqf-jh8x-4wxc](https://github.com/wekan/wekan/security/advisories/GHSA-6xqf-jh8x-4wxc).
|
||||
The DDP methods `archiveSelectedCards` and `createBoardFromCard` asked only
|
||||
for the board's write capability, which the Normal, assigned only role has, so
|
||||
such a member who knew a card's id archived any card of the board - and its
|
||||
subtasks - and turned any card into a linked-board card. Both now also ask
|
||||
the card, as `copyCard` does; a selection is checked whole before the first
|
||||
card is archived. A refusal is shown in Admin Panel / Problems.
|
||||
`tests/assignedCardMethods.test.cjs` fails for any server method that takes a
|
||||
card id and decides by the board's write access alone.
|
||||
|
||||
</details>
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v13.01 2026-11-08 WeKan ® release
|
||||
|
||||
**In short:** Translations: the strings still in English are written in **33
|
||||
|
||||
+15
-23
@@ -5,6 +5,21 @@ each one changed and the project status are in
|
||||
Written by `node releases/changelog-short.mjs` from CHANGELOG-big.md:
|
||||
make changes there.
|
||||
|
||||
# v13.03 2026-11-09 WeKan ® release
|
||||
|
||||
The amd64 **snaps** no longer fail the release when the Ubuntu
|
||||
archive is briefly unreachable: the build waits for it and retries once.
|
||||
|
||||
[All changes in v13.03](CHANGELOG-big.md#v1303-2026-11-09-wekan--release)
|
||||
|
||||
# v13.02 2026-11-09 WeKan ® release
|
||||
|
||||
**UnassignedArchiveBleed** is fixed: a member with the
|
||||
**Normal, assigned only** role archives and converts only the cards assigned
|
||||
to them, through every card method.
|
||||
|
||||
[All changes in v13.02](CHANGELOG-big.md#v1302-2026-11-09-wekan--release)
|
||||
|
||||
# v13.01 2026-11-08 WeKan ® release
|
||||
|
||||
Translations: the strings still in English are written in **33
|
||||
@@ -32,26 +47,3 @@ GitHub's retired **macos-14** runner and was cancelled in the v12.98 release
|
||||
run. The **weba** instructions now live in the weba repository itself.
|
||||
|
||||
[All changes in v12.99](CHANGELOG-big.md#v1299-2026-11-06-wekan--release)
|
||||
|
||||
# v12.98 2026-11-06 WeKan ® release
|
||||
|
||||
**GroupLinkBleed** is fixed: a board admin can no longer skip the
|
||||
board methods by writing the board directly, to attach an organization or team
|
||||
they do not belong to or to add a member past the invitation checks.
|
||||
**Public board CSV and TSV exports** honour the selected columns, and the REST
|
||||
**card archive, restore and custom-field** endpoints answer an unmatched card
|
||||
with a JSON 404 instead of 500, and a REST **card update** can set sort 0, turn
|
||||
overtime off and clear a description. The **notifications drawer** gets its
|
||||
attachments again, and the developer instructions cover the **weba** Gambas
|
||||
companion repository.
|
||||
|
||||
[All changes in v12.98](CHANGELOG-big.md#v1298-2026-11-06-wekan--release)
|
||||
|
||||
# v12.97 2026-11-06 WeKan ® release
|
||||
|
||||
`build.sh` and the `releases/` scripts now work on **Arch Linux**
|
||||
and **Omarchy**, and the release builds no longer lose the **OpenBSD arm64
|
||||
bundle** to a smoke test that left WeKan running, or the **s390x snap** to
|
||||
Launchpad outages that were counted as build failures.
|
||||
|
||||
[All changes in v12.97](CHANGELOG-big.md#v1297-2026-11-06-wekan--release)
|
||||
+1
-1
@@ -38,7 +38,7 @@ FROM --platform=$BUILDPLATFORM debian:trixie AS bundle
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=13.01
|
||||
ARG VERSION=13.03
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY --chmod=755 releases/check-telemetry.py /tmp/check-telemetry.py
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v13.01.0"
|
||||
appVersion: "v13.03.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-13.01-win64.zip](https://github.com/wekan/wekan/releases/download/v13.01/wekan-13.01-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
1. [wekan-13.03-win64.zip](https://github.com/wekan/wekan/releases/download/v13.03/wekan-13.03-win64.zip) or newest available at https://github.com/wekan/wekan/releases/
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v26.x/win-x64/node.exe)
|
||||
|
||||
@@ -22,7 +22,7 @@ Right click and download files 1-4:
|
||||
|
||||
6. Double click `mongodb-windows-x86_64-7.0.34-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
|
||||
7. Unzip `wekan-13.01-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-13.03-win64.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -855,11 +855,11 @@
|
||||
"filter-no-due-date": "Da a ɛsɛ sɛ ewie nni hɔ",
|
||||
"filter-overdue": "Hwehwɛ ade yi",
|
||||
"filter-due-any": "Awiei da biara (nea enni da nso ka ho)",
|
||||
"filter-due-today": "Hwehwɛ ade yi",
|
||||
"filter-due-today": "Ɛsɛ sɛ ewie nnɛ",
|
||||
"filter-due-previous-week": "Ɛsɛ sɛ ɛba awiei nnawɔtwe a etwaam no",
|
||||
"filter-due-next-month": "Ɛsɛ sɛ ɛba awiei bosome a edi hɔ no",
|
||||
"filter-due-this-week": "Ɛsɛ sɛ ewie dapɛn yi",
|
||||
"filter-due-next-week": "Hwehwɛ ade yi",
|
||||
"filter-due-next-week": "Ɛsɛ sɛ ewie dapɛn a edi hɔ no",
|
||||
"filter-due-tomorrow": "Ɛsɛ sɛ ewie ɔkyena",
|
||||
"list-filter-label": "Yi mu Nhyehyɛe denam Ti asɛm",
|
||||
"filter-clear": "Hwehwɛ ade yi",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "In der Vorwoche fällig",
|
||||
"filter-due-next-month": "Im nächsten Monat fällig",
|
||||
"filter-due-this-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Nächste Woche fällig",
|
||||
"filter-due-tomorrow": "Morgen fällig",
|
||||
"list-filter-label": "Liste nach Titel filtern",
|
||||
"filter-clear": "Filter entfernen",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "In der Vorwoche fällig",
|
||||
"filter-due-next-month": "Im nächsten Monat fällig",
|
||||
"filter-due-this-week": "diese Woche fällig",
|
||||
"filter-due-next-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "nächste Woche fällig",
|
||||
"filter-due-tomorrow": "morgen fällig",
|
||||
"list-filter-label": "Liste nach Titel filtern",
|
||||
"filter-clear": "Filter entfernen",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "In der Vorwoche fällig",
|
||||
"filter-due-next-month": "Im nächsten Monat fällig",
|
||||
"filter-due-this-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Nächste Woche fällig",
|
||||
"filter-due-tomorrow": "Morgen fällig",
|
||||
"list-filter-label": "Liste nach Titel filtern",
|
||||
"filter-clear": "Filter entfernen",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "In der Vorwoche fällig",
|
||||
"filter-due-next-month": "Im nächsten Monat fällig",
|
||||
"filter-due-this-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Diese Woche fällig",
|
||||
"filter-due-next-week": "Nächste Woche fällig",
|
||||
"filter-due-tomorrow": "Morgen fällig",
|
||||
"list-filter-label": "Liste nach Titel filtern",
|
||||
"filter-clear": "Filter entfernen",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "An der viregter Woch fälleg",
|
||||
"filter-due-next-month": "Den nächste Mount fälleg",
|
||||
"filter-due-this-week": "Dës Woche fällig",
|
||||
"filter-due-next-week": "Dës Woche fällig",
|
||||
"filter-due-next-week": "Déi nächst Woch fälleg",
|
||||
"filter-due-tomorrow": "Morgen fällig",
|
||||
"list-filter-label": "Lëscht no Titel filtern",
|
||||
"filter-clear": "Filter ewechhuelen",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "Förföll förra veckan",
|
||||
"filter-due-next-month": "Förfaller nästa månad",
|
||||
"filter-due-this-week": "Förfaller denna vecka",
|
||||
"filter-due-next-week": "Förfaller denna vecka",
|
||||
"filter-due-next-week": "Förfaller nästa vecka",
|
||||
"filter-due-tomorrow": "Förfaller imorgon",
|
||||
"list-filter-label": "Filtrera lista efter titel",
|
||||
"filter-clear": "Rensa filter",
|
||||
|
||||
@@ -859,7 +859,7 @@
|
||||
"filter-due-previous-week": "Leswi faneleke ku hela vhiki leri hundzeke",
|
||||
"filter-due-next-month": "Leswi faneleke ku hela n’hweti leyi taka",
|
||||
"filter-due-this-week": "fanele mhaka vhiki",
|
||||
"filter-due-next-week": "fanele mhaka vhiki",
|
||||
"filter-due-next-week": "fanele vhiki leri taka",
|
||||
"filter-due-tomorrow": "fanele mundzuku",
|
||||
"list-filter-label": "sefa nxaxamelo hi nhlokomhaka",
|
||||
"filter-clear": "basisa sefa",
|
||||
|
||||
@@ -232,6 +232,9 @@ const CATALOG = {
|
||||
// added or soft-deleted attachments, through the REST API, on a card that
|
||||
// was no longer assigned to them. Only a caller with a kept card id reaches it.
|
||||
'authz.assigned-attachment-write': { category: 'authz', bleed: 'UnassignedAttachBleed', severity: 'medium', cwe: 'CWE-863' },
|
||||
// GHSA-6xqf-jh8x-4wxc: an assigned-only member archiving or converting a card
|
||||
// not assigned to them through archiveSelectedCards / createBoardFromCard.
|
||||
'authz.assigned-card-write': { category: 'authz', bleed: 'UnassignedArchiveBleed', severity: 'medium', cwe: 'CWE-863' },
|
||||
'authz.tenant': { category: 'authz', bleed: 'TenantBleed', severity: 'medium', cwe: 'CWE-862' },
|
||||
'authz.search-session': { category: 'authz', bleed: 'SessionBleed', severity: 'medium', cwe: 'CWE-639' },
|
||||
'authz.database': { category: 'authz', bleed: 'DatabaseBleed', severity: 'high', cwe: 'CWE-863' },
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v13.01.0",
|
||||
"version": "v13.03.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wekan",
|
||||
"version": "v13.01.0",
|
||||
"version": "v13.03.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v13.01.0",
|
||||
"version": "v13.03.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<title>WeKan REST API v13.01</title>
|
||||
<title>WeKan REST API v13.03</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
@@ -484,11 +484,11 @@ data-styled.g658[id="sc-hHttOM"]{content:"dKSNwp,"}/*!sc*/
|
||||
|
||||
<body>
|
||||
|
||||
<div id="redoc"><div hidden=""><script>try{var m=localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.add(m)}catch(e){}</script></div><div style="--navbar-height:0px;--navbar-stack-height:0px" class="sc-kIbBWU fFQMWf"><div class="sc-cjUTkN dGsweu menu-content"><div class="sc-jqwVmx jjzLa-d"><div data-testid="search-trigger" class="sc-cToEys dffrbr"><div class="sc-cRcubC ddjiE"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-cHazmW gTzQi"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-dNLPWG dtcefb"><ul data-component-name="Menu/Menu" class="sc-kBPbfe ctPApS"><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-epufQc fYmDZR"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="WeKan REST API" class="sc-eStdmu iyAVQU">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc itBMtf"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Production Security Concerns" class="sc-eStdmu liVEkC">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Login" class="sc-eStdmu liVEkC">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="AttachmentStorageSettings" class="sc-eStdmu liVEkC">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Settings" classLine truncated
|
||||
<div id="redoc"><div hidden=""><script>try{var m=localStorage.getItem("redoc.colorMode");if(m!=="light"&&m!=="dark")m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.add(m)}catch(e){}</script></div><div style="--navbar-height:0px;--navbar-stack-height:0px" class="sc-kIbBWU fFQMWf"><div class="sc-cjUTkN dGsweu menu-content"><div class="sc-jqwVmx jjzLa-d"><div data-testid="search-trigger" class="sc-cToEys dffrbr"><div class="sc-cRcubC ddjiE"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-component-name="icons/SearchIcon/SearchIcon" class="sc-cHazmW gTzQi"><path d="M14.5 13.7931L10.724 10.017C11.6313 8.9277 12.0838 7.5305 11.9872 6.11608C11.8907 4.70165 11.2525 3.37891 10.2055 2.423C9.15857 1.4671 7.78336 0.951637 6.36602 0.983845C4.94867 1.01605 3.5983 1.59345 2.59582 2.59593C1.59335 3.5984 1.01595 4.94877 0.983738 6.36612C0.95153 7.78347 1.46699 9.15867 2.4229 10.2057C3.3788 11.2526 4.70155 11.8908 6.11597 11.9873C7.53039 12.0839 8.92759 11.6314 10.0169 10.7241L13.793 14.5001L14.5 13.7931ZM2.00001 6.50012C2.00001 5.6101 2.26393 4.74007 2.7584 4.00005C3.25286 3.26003 3.95567 2.68325 4.77794 2.34266C5.6002 2.00207 6.505 1.91295 7.37792 2.08658C8.25083 2.26022 9.05266 2.6888 9.68199 3.31814C10.3113 3.94747 10.7399 4.7493 10.9135 5.62221C11.0872 6.49513 10.9981 7.39993 10.6575 8.22219C10.3169 9.04446 9.7401 9.74726 9.00008 10.2417C8.26006 10.7362 7.39003 11.0001 6.50001 11.0001C5.30694 10.9988 4.16312 10.5243 3.31949 9.68064C2.47587 8.83701 2.00133 7.69319 2.00001 6.50012Z"></path></svg><span>Loading...</span></div></div></div><div class="sc-dNLPWG dtcefb"><ul data-component-name="Menu/Menu" class="sc-kBPbfe ctPApS"><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="true" class="sc-epufQc fYmDZR"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="WeKan REST API" class="sc-eStdmu iyAVQU">WeKan REST API</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-default" role="listitem"><a class="sc-iJONRL iBHKJo" href="/section/production-security-concerns" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc itBMtf"><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Production Security Concerns" class="sc-eStdmu liVEkC">Production Security Concerns</span></span></div></li></a></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/login" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Login" class="sc-eStdmu liVEkC">Login</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/attachmentstoragesettings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="AttachmentStorageSettings" class="sc-eStdmu liVEkC">AttachmentStorageSettings</span></span></div></li></a><div style="display:none;height:0px;overflow:hidden" class="sc-hgJgCR dlNiXx"></div></div><div data-component-name="Menu/MenuItem" class="sc-gwVKcS fGSWQx menu-item-type-group" role="listitem"><a class="sc-iJONRL iBHKJo" href="/settings" data-discover="true"><li role="none" data-testid="menu-item-label" data-active="false" class="sc-epufQc hKPPiy"><div class="sc-jokKPH jtaKKl"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" color="--tree-content-color-default" data-component-name="icons/ChevronRightIcon/ChevronRightIcon" class="sc-iUlCMS kEEIsB"><path d="M11 8L6.00002 13L5.30002 12.3L9.60002 8L5.30002 3.7L6.00002 3L11 8Z"></path></svg></div><div class="sc-boLXHU jshptd"><span class="sc-bcyEQV hrIrOF"><span data-text="Settings" classLine truncated
|
||||
<script type="module">
|
||||
import { hydrate } from "https://cdn.redocly.com/redoc/v3.0.1/bundles/redoc.standalone.js";
|
||||
|
||||
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.01","description":"The REST API allows you to control and extend WeKan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production WeKan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `\u003cfield>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the global admin can call this. CountsLine truncated
|
||||
const __redoc_definition = {"openapi":"3.2.0","info":{"title":"WeKan REST API","version":"v13.03","description":"The REST API allows you to control and extend WeKan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production WeKan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}},"/api/admin/attachment-settings":{"get":{"operationId":"get_attachment_settings","summary":"Get the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. Returns the Admin Panel >\n Attachments settings document, including `limitSettings.attachmentsUploadBlocked`\n and `limitSettings.avatarsUploadBlocked` plus the size limits. Cloud-storage\n secret keys (S3 secretAccessKey, Azure accountKey/connectionString, GCS\n credentials) are masked and never returned; a boolean `\u003cfield>Set` marker\n shows whether a value exists.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}},"put":{"operationId":"update_attachment_settings","summary":"Update the attachment storage / upload-block settings (GlobalAdmin)","description":"Only the global admin can call this. The request body is a\n partial settings object (for example\n `{\"limitSettings\": {\"avatarsUploadBlocked\": true}}`). It is applied through\n the same update path as the `updateAttachmentStorageSettings` Meteor method,\n so validation and `$set` semantics are identical: cloud-storage secrets left\n blank are preserved. The masked, updated settings document is returned.\n","tags":["AttachmentStorageSettings","Settings"],"security":[{"UserSecurity":[]}],"responses":{"200":{"description":"200 response","content":{"application/json":{"schema":{"type":"Object"}}}}}}},"/api/admin/domains":{"get":{"operationId":"get_admin_domains","summary":"List email domains with their user counts (GlobalAdmin)","description":"Only the global admin can call this. CountsLine truncated
|
||||
const __redoc_options = {"skipBundle":true,"disableTelemetry":false};
|
||||
|
||||
hydrate(__redoc_definition, __redoc_options, document.getElementById('redoc'));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: WeKan REST API
|
||||
version: v13.01
|
||||
version: v13.03
|
||||
description: |
|
||||
The REST API allows you to control and extend WeKan with ease.
|
||||
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recover stale .deb URLs in Snapcraft's managed base or build instance."""
|
||||
"""Recover stale APT state in Snapcraft's managed base or build instance.
|
||||
|
||||
Two failures are recoverable, and nothing else is:
|
||||
- a .deb that 404s because the instance's package indexes are stale;
|
||||
- "Failed to refresh package list: failed to run apt update" while installing
|
||||
build-packages, which is the Ubuntu archive being briefly unreachable or
|
||||
mid-sync (v13.02: every amd64 snap failed within eight minutes while the
|
||||
arm64 ones, served by ports.ubuntu.com, built). That one is retried with a
|
||||
backoff until apt-get update succeeds in the same instance, so the Snapcraft
|
||||
retry only runs once the archive answers again.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
APT_UPDATE_FAILED = 'Failed to refresh package list: failed to run apt update'
|
||||
# Waits before each further apt-get update attempt for an archive outage.
|
||||
UPDATE_BACKOFF = (30, 60, 120, 240)
|
||||
APT_DIAGNOSTIC = re.compile(r'(?:^|\s)(?:E:|W:|Err:)|Hash Sum mismatch|Failed to fetch|Could not resolve|'
|
||||
r'Temporary failure|Connection failed|Unable to connect')
|
||||
|
||||
|
||||
def apt_diagnostics(text, limit=20):
|
||||
"""The apt error lines from a Snapcraft log, so the job says WHY apt failed."""
|
||||
text = text.replace('\\n', '\n')
|
||||
lines = [line.strip() for line in text.splitlines() if APT_DIAGNOSTIC.search(line)]
|
||||
return list(dict.fromkeys(lines))[-limit:]
|
||||
|
||||
|
||||
def project_instance(text):
|
||||
# craft-providers logs each LXC command with shlex.join. Build and stage
|
||||
# packages are handled in the project instance, after base provisioning
|
||||
# finishes. Never fall back to the base here: changing it cannot repair its clone.
|
||||
if 'Failed to run snapcraft in instance' not in text:
|
||||
raise ValueError('Package failure is not from a failed managed Snapcraft build')
|
||||
instances = set(re.findall(
|
||||
r'lxc --project snapcraft exec local:(snapcraft-[a-zA-Z0-9-]+) ', text))
|
||||
if len(instances) != 1:
|
||||
raise ValueError('Cannot identify one failed Snapcraft build instance in the log')
|
||||
return instances.pop()
|
||||
|
||||
|
||||
def is_update_failure(text):
|
||||
return APT_UPDATE_FAILED in text.replace('\\n', '\n')
|
||||
|
||||
|
||||
def failed_instance(text):
|
||||
text = text.replace('\\n', '\n')
|
||||
if is_update_failure(text):
|
||||
return project_instance(text)
|
||||
if not re.search(
|
||||
r'Failed to fetch[^\n]*\.deb[^\n]*(?:\n[^\n]*)?404', text):
|
||||
raise ValueError('Not a managed APT package 404; refusing to retry this build failure')
|
||||
raise ValueError('Not a managed APT package 404 or apt update failure; '
|
||||
'refusing to retry this build failure')
|
||||
if 'Failed to fetch package:' in text:
|
||||
if 'Failed to run snapcraft in instance' not in text:
|
||||
raise ValueError('Stage-package 404 is not from a failed managed Snapcraft build')
|
||||
# craft-providers logs each LXC command with shlex.join. Stage packages
|
||||
# are fetched in the project instance, after base provisioning finishes.
|
||||
# Never fall back to the base here: changing it cannot repair its clone.
|
||||
instances = set(re.findall(
|
||||
r'lxc --project snapcraft exec local:(snapcraft-[a-zA-Z0-9-]+) ', text))
|
||||
if len(instances) != 1:
|
||||
raise ValueError('Cannot identify one failed Snapcraft build instance in the log')
|
||||
return instances.pop()
|
||||
return project_instance(text)
|
||||
instances = re.findall(
|
||||
r'lxc --project snapcraft exec local:(base-instance-snapcraft-[a-zA-Z0-9-]+) '
|
||||
r'[^\n]*apt-get install', text) if 'Failed to install packages' in text else []
|
||||
@@ -32,8 +67,9 @@ def failed_instance(text):
|
||||
return instances[-1]
|
||||
|
||||
|
||||
def refresh(text):
|
||||
def refresh(text, sleep=time.sleep):
|
||||
name = failed_instance(text)
|
||||
waits = UPDATE_BACKOFF if is_update_failure(text) else ()
|
||||
prefix = ['lxc', '--project', 'snapcraft']
|
||||
def run(args):
|
||||
return subprocess.run(prefix + args, check=True, text=True,
|
||||
@@ -50,11 +86,24 @@ def refresh(text):
|
||||
try:
|
||||
# craft-parts uses /var/lib/apt for stage-package indexes as well.
|
||||
# Refresh them in the guest; downloaded packages and part state stay intact.
|
||||
print('Refreshing APT indexes in failed Snapcraft instance ' + name, flush=True)
|
||||
result = run(['exec', 'local:' + name, '--', 'env', 'DEBIAN_FRONTEND=noninteractive',
|
||||
'apt-get', '-o', 'Acquire::Retries=3',
|
||||
'-o', 'APT::Update::Error-Mode=any', 'update'])
|
||||
print(result.stdout, end='')
|
||||
update = ['exec', 'local:' + name, '--', 'env', 'DEBIAN_FRONTEND=noninteractive',
|
||||
'apt-get', '-o', 'Acquire::Retries=3',
|
||||
'-o', 'APT::Update::Error-Mode=any', 'update']
|
||||
for attempt, wait in enumerate((0,) + tuple(waits), 1):
|
||||
if wait:
|
||||
print(f'apt-get update still failing; waiting {wait}s for the archive '
|
||||
f'(attempt {attempt}/{len(waits) + 1}) ...', flush=True)
|
||||
sleep(wait)
|
||||
print('Refreshing APT indexes in failed Snapcraft instance ' + name, flush=True)
|
||||
try:
|
||||
result = run(update)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if attempt > len(waits):
|
||||
raise
|
||||
print((error.stdout or '').rstrip(), flush=True)
|
||||
continue
|
||||
print(result.stdout, end='')
|
||||
break
|
||||
finally:
|
||||
if stopped:
|
||||
run(['stop', 'local:' + name, '--timeout', '30'])
|
||||
@@ -73,6 +122,10 @@ if __name__ == '__main__':
|
||||
raise ValueError('No Snapcraft execution log found; original failure remains fatal')
|
||||
log = max(logs, key=lambda item: item.stat().st_mtime_ns)
|
||||
print('Inspecting ' + str(log), flush=True)
|
||||
refresh(log.read_text(encoding='utf-8', errors='replace'))
|
||||
text = log.read_text(encoding='utf-8', errors='replace')
|
||||
diagnostics = apt_diagnostics(text)
|
||||
if diagnostics:
|
||||
print('APT reported:\n ' + '\n '.join(diagnostics), flush=True)
|
||||
refresh(text)
|
||||
except (ValueError, OSError, subprocess.SubprocessError) as error:
|
||||
parser.exit(1, str(error) + '\n')
|
||||
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
||||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 1301,
|
||||
appVersion = 1303,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "13.01~2026-11-07"),
|
||||
appMarketingVersion = (defaultText = "13.03~2026-11-09"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
||||
@@ -72,6 +72,18 @@ function getTranslatedString(key, fallback, options) {
|
||||
// route used to check only READ access on the source board and clone the whole
|
||||
// source card, so a read-only or assigned-only member could link - and copy -
|
||||
// cards the method refuses them.
|
||||
// GHSA-6xqf-jh8x-4wxc: an assigned-only member's write on a card not assigned
|
||||
// to them. No view shows them such a card, so it is an attempt: recorded in
|
||||
// Admin Panel -> Problems as UnassignedArchiveBleed (medium: never disables).
|
||||
function recordUnassignedCardWrite(userId, source) {
|
||||
try {
|
||||
require('/server/lib/securityLog').record({
|
||||
key: 'authz.assigned-card-write', action: 'blocked', source, userId,
|
||||
detail: 'Assigned-only member changed a card not assigned to them.',
|
||||
});
|
||||
} catch (e) { /* logging must never break the guard */ }
|
||||
}
|
||||
|
||||
async function createLinkedCardFor(userId, sourceCardId, boardId, swimlaneId, listId, sort, source) {
|
||||
check(sourceCardId, String);
|
||||
check(boardId, String);
|
||||
@@ -162,6 +174,13 @@ Meteor.methods({
|
||||
if (!sourceBoard || !allowIsBoardMemberWithWriteAccess(this.userId, sourceBoard)) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
// GHSA-6xqf-jh8x-4wxc: the board's write capability is not enough. An
|
||||
// assigned-only member converts only a card assigned to them, as copyCard
|
||||
// allows them only theirs (models/lib/boardCardScope.js).
|
||||
if (!require('/models/lib/boardCardScope').assignedOnlyAllowsCard(sourceBoard, this.userId, card)) {
|
||||
recordUnassignedCardWrite(this.userId, 'method:createBoardFromCard');
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
// CreateBleed: the card becomes a new working board.
|
||||
const { assertMayCreateBoard } = require('/server/lib/boardCreationGuard');
|
||||
await assertMayCreateBoard(this.userId, { source: 'method:createBoardFromCard', type: 'board' });
|
||||
@@ -252,6 +271,16 @@ Meteor.methods({
|
||||
throw new Meteor.Error('invalid-card-selection');
|
||||
}
|
||||
|
||||
// GHSA-6xqf-jh8x-4wxc: the board's write capability is not enough. An
|
||||
// assigned-only member archives only cards assigned to them - every card
|
||||
// of the selection is checked before the first is archived, so a refused
|
||||
// selection changes nothing.
|
||||
const { assignedOnlyAllowsCard } = require('/models/lib/boardCardScope');
|
||||
if (!cards.every(card => assignedOnlyAllowsCard(board, this.userId, card))) {
|
||||
recordUnassignedCardWrite(this.userId, 'method:archiveSelectedCards');
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
// Validate the complete selection before changing the first card. Preserve
|
||||
// the board order used by Multi-Selection for deterministic activity order.
|
||||
const byId = new Map(cards.map(card => [card._id, card]));
|
||||
|
||||
+4
-4
@@ -19,7 +19,7 @@
|
||||
# base: core24 (not core26) · no build-base: devel · grade: stable
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
name: wekan
|
||||
version: '13.01'
|
||||
version: '13.03'
|
||||
base: core24
|
||||
confinement: strict
|
||||
grade: stable
|
||||
@@ -703,18 +703,18 @@ parts:
|
||||
armhf) WEKAN_ARCH="armhf" ;;
|
||||
*) echo "Unsupported architecture: ${CRAFT_ARCH_BUILD_FOR}"; exit 1 ;;
|
||||
esac
|
||||
WEKAN_ZIP="wekan-13.01-${WEKAN_ARCH}.zip"
|
||||
WEKAN_ZIP="wekan-13.03-${WEKAN_ARCH}.zip"
|
||||
echo "Cleaning environment first"
|
||||
rm -rf .build
|
||||
mkdir .build
|
||||
cd .build
|
||||
#wget https://github.com/wekan/wekan/releases/download/v13.01/wekan-13.01-amd64.zip
|
||||
#wget https://github.com/wekan/wekan/releases/download/v13.03/wekan-13.03-amd64.zip
|
||||
# Retry: a release asset can 404 for a short while right after it is
|
||||
# uploaded (CDN/propagation lag), and a plain wget treated that as
|
||||
# fatal. --retry-on-http-error covers the 404/5xx window; a genuinely
|
||||
# missing bundle still fails after the tries are spent.
|
||||
wget --tries=20 --waitretry=20 --retry-on-http-error=404,403,500,502,503 \
|
||||
"https://github.com/wekan/wekan/releases/download/v13.01/${WEKAN_ZIP}"
|
||||
"https://github.com/wekan/wekan/releases/download/v13.03/${WEKAN_ZIP}"
|
||||
unzip "${WEKAN_ZIP}"
|
||||
python3 ../releases/check-telemetry.py --bundle bundle
|
||||
rm "${WEKAN_ZIP}"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
// Guard: UnassignedArchiveBleed, GHSA-6xqf-jh8x-4wxc (2026-11-08). The DDP methods archiveSelectedCards
|
||||
// and createBoardFromCard asked only for the board's `write` capability. The
|
||||
// normal-assigned-only role has write: true but sees only its own cards, so a
|
||||
// member with that role archived - and hid - cards not assigned to them, and
|
||||
// turned any card into a linked-board card. Both now ask the card as well
|
||||
// (assignedOnlyAllowsCard), as copyCard does, and record a refusal.
|
||||
// Run: node tests/assignedCardMethods.test.cjs
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const read = file => fs.readFileSync(path.join(ROOT, file), 'utf8');
|
||||
const { assignedOnlyAllowsCard } = require('../models/lib/boardCardScope');
|
||||
const { memberCan } = require('../models/lib/boardRoleCapabilities');
|
||||
|
||||
const board = {
|
||||
_id: 'B',
|
||||
members: [
|
||||
{ userId: 'owner', isActive: true, isAdmin: true },
|
||||
{ userId: 'member', isActive: true, isNormalAssignedOnly: true },
|
||||
{ userId: 'normal', isActive: true },
|
||||
],
|
||||
};
|
||||
|
||||
// The decision both methods make now: board write AND the card.
|
||||
const mayChangeCard = (userId, card) => memberCan(board.members, userId, 'write') && assignedOnlyAllowsCard(board, userId, card);
|
||||
|
||||
test('the reported attack: an assigned-only member may not change a card not assigned to them', () => {
|
||||
const theirs = { _id: 'C', boardId: 'B', assignees: ['someone-else'] };
|
||||
// The old check alone let them through - that was the hole.
|
||||
assert.equal(memberCan(board.members, 'member', 'write'), true);
|
||||
assert.equal(mayChangeCard('member', theirs), false);
|
||||
assert.equal(mayChangeCard('member', { _id: 'D', boardId: 'B' }), false, 'a card with no assignees is not theirs either');
|
||||
});
|
||||
|
||||
test('their own card, and everybody else with write, still may', () => {
|
||||
assert.equal(mayChangeCard('member', { _id: 'C', boardId: 'B', assignees: ['member'] }), true);
|
||||
for (const userId of ['owner', 'normal']) {
|
||||
assert.equal(mayChangeCard(userId, { _id: 'C', boardId: 'B', assignees: [] }), true);
|
||||
}
|
||||
});
|
||||
|
||||
function methodBody(src, name) {
|
||||
const start = src.indexOf(`async ${name}(`);
|
||||
assert.ok(start >= 0, `${name} exists`);
|
||||
const next = src.slice(start + 1).search(/\n async [\w']+\(/);
|
||||
return next < 0 ? src.slice(start) : src.slice(start, start + 1 + next);
|
||||
}
|
||||
|
||||
test('archiveSelectedCards checks every card of the selection before archiving the first', () => {
|
||||
const body = methodBody(read('server/models/cards.js'), 'archiveSelectedCards');
|
||||
const check = body.indexOf('cards.every(card => assignedOnlyAllowsCard(board, this.userId, card))');
|
||||
assert.ok(check > 0, 'every card is checked');
|
||||
assert.ok(check < body.indexOf('.archive()'), 'before the first card is archived');
|
||||
assert.match(body, /recordUnassignedCardWrite\(this\.userId, 'method:archiveSelectedCards'\);\n\s+throw new Meteor\.Error\('not-authorized'\)/);
|
||||
});
|
||||
|
||||
test('createBoardFromCard checks the card before making a board or changing the card', () => {
|
||||
const body = methodBody(read('server/models/cards.js'), 'createBoardFromCard');
|
||||
const check = body.indexOf('assignedOnlyAllowsCard(sourceBoard, this.userId, card)');
|
||||
assert.ok(check > 0);
|
||||
assert.ok(check < body.indexOf('Boards.insertAsync'), 'before the board is made');
|
||||
assert.ok(check < body.indexOf('Cards.updateAsync'), 'before the card is changed');
|
||||
assert.match(body, /recordUnassignedCardWrite\(this\.userId, 'method:createBoardFromCard'\);\n\s+throw new Meteor\.Error\('not-authorized'\)/);
|
||||
});
|
||||
|
||||
test('a refusal is recorded as UnassignedArchiveBleed, and logging never breaks the guard', () => {
|
||||
const src = read('server/models/cards.js');
|
||||
const fn = src.slice(src.indexOf('function recordUnassignedCardWrite'), src.indexOf('async function createLinkedCardFor'));
|
||||
assert.match(fn, /try \{\n\s+require\('\/server\/lib\/securityLog'\)\.record\(\{\n\s+key: 'authz\.assigned-card-write', action: 'blocked', source, userId,/);
|
||||
assert.match(fn, /\} catch \(e\) \{ \/\* logging must never break the guard \*\/ \}/);
|
||||
assert.match(read('models/lib/securityCategories.js'),
|
||||
/'authz\.assigned-card-write':\s*\{ category: 'authz', bleed: 'UnassignedArchiveBleed', severity: 'medium', cwe: 'CWE-863' \}/);
|
||||
});
|
||||
|
||||
// The shape, anywhere on the server: a method that takes a card id and decides
|
||||
// by the write capability of that card's own board alone. Every such method
|
||||
// must also ask the card (assignedOnlyAllowsCard, mayCopyFromBoard, or
|
||||
// canEditCardOrLinkedCard, which asks it). A check of a DESTINATION board
|
||||
// (destBoard, destination, target) is a different question and not counted.
|
||||
function serverFiles(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true })) {
|
||||
const rel = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...serverFiles(rel));
|
||||
else if (entry.name.endsWith('.js')) out.push(rel);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('negative: no method taking a card id decides by board write access alone', () => {
|
||||
const offenders = [];
|
||||
for (const file of serverFiles('server')) {
|
||||
const src = read(file);
|
||||
const starts = [...src.matchAll(/\n\s+async '?([\w.]+)'?\(([^)]*)\)\s*\{/g)];
|
||||
starts.forEach((m, i) => {
|
||||
const params = m[2];
|
||||
if (!/\bcardIds?\b/.test(params)) return;
|
||||
const body = src.slice(m.index, i + 1 < starts.length ? starts[i + 1].index : undefined);
|
||||
const boards = [...body.matchAll(/allowIsBoardMemberWithWriteAccess\(this\.userId, (\w+)\)/g)].map(x => x[1]);
|
||||
const own = boards.filter(v => !/dest|destination|target/i.test(v));
|
||||
if (!own.length) return;
|
||||
if (!/assignedOnlyAllowsCard\(|mayCopyFromBoard\(|canEditCardOrLinkedCard\(/.test(body)) {
|
||||
offenders.push(`${file}: ${m[1]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
assert.deepEqual(offenders, [], 'these decide a card write by the board alone');
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
// #6887: in German the filter sidebar's "Due next week" option said "Diese
|
||||
// Woche fällig", the same as "Due this week", so the two options that return
|
||||
// different cards could not be told apart. The same copy sat in de-AT, de_DE,
|
||||
// de-CH, lb, sv and ts, and in ak "Due today" and "Due next week" were both
|
||||
// "search this".
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { translationTokens } = require('../releases/translations/placeholder-tokens.mjs');
|
||||
|
||||
const dir = path.join(__dirname, '../imports/i18n/data');
|
||||
const read = code => JSON.parse(fs.readFileSync(path.join(dir, `${code}.i18n.json`), 'utf8'));
|
||||
const en = read('en');
|
||||
const keys = Object.keys(en).filter(key => key.startsWith('filter-due-'));
|
||||
assert.ok(keys.includes('filter-due-this-week') && keys.includes('filter-due-next-week'), 'the due filter keys exist');
|
||||
|
||||
// Positive: the German labels say which week.
|
||||
for (const [code, thisWeek, nextWeek] of [
|
||||
['de', 'Diese Woche fällig', 'Nächste Woche fällig'],
|
||||
['de-AT', 'Diese Woche fällig', 'Nächste Woche fällig'],
|
||||
['de_DE', 'Diese Woche fällig', 'Nächste Woche fällig'],
|
||||
['de-CH', 'diese Woche fällig', 'nächste Woche fällig'],
|
||||
]) {
|
||||
const locale = read(code);
|
||||
assert.equal(locale['filter-due-this-week'], thisWeek, `${code}: this week`);
|
||||
assert.equal(locale['filter-due-next-week'], nextWeek, `${code}: next week`);
|
||||
}
|
||||
assert.equal(read('sv')['filter-due-next-week'], 'Förfaller nästa vecka', 'sv: next week');
|
||||
|
||||
// Negative: in no language do two due-date options share one label.
|
||||
const locales = fs.readdirSync(dir).filter(file => file.endsWith('.i18n.json')).map(file => file.replace('.i18n.json', ''));
|
||||
for (const code of locales) {
|
||||
const locale = read(code);
|
||||
const seen = new Map();
|
||||
for (const key of keys) {
|
||||
const value = locale[key];
|
||||
if (!value) continue;
|
||||
assert.deepEqual(translationTokens(value), translationTokens(en[key]), `${code}:${key}: exact placeholders`);
|
||||
assert.ok(!seen.has(value), `${code}: ${key} and ${seen.get(value)} are both ${JSON.stringify(value)}`);
|
||||
seen.set(value, key);
|
||||
}
|
||||
}
|
||||
console.log(`due filter labels distinct in ${locales.length} locales`);
|
||||
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
// UnassignedArchiveBleed, GHSA-6xqf-jh8x-4wxc (2026-11-08): an assigned-only member passed the
|
||||
// board-level write check of archiveSelectedCards and createBoardFromCard for
|
||||
// every card on the board, so they archived - and hid - cards not assigned to
|
||||
// them, and turned any card into a linked-board card. Both methods now ask the
|
||||
// card as well, and a refusal is recorded in Admin Panel -> Problems.
|
||||
const { test, expect } = require('../fixtures');
|
||||
const db = require('../helpers/db');
|
||||
const { loginWithToken } = require('../helpers/auth');
|
||||
|
||||
const BASE_URL = process.env.WEKAN_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
const call = (page, name, ...args) => page.evaluate(async ({ name, args }) => {
|
||||
try { return { result: await window.Meteor.callAsync(name, ...args) }; } catch (error) { return { error: error.error }; }
|
||||
}, { name, args });
|
||||
|
||||
test('an assigned-only member archives and converts only the cards assigned to them', async ({ page, user2, board }) => {
|
||||
const [theirs, mine] = db.find('cards', { boardId: board.boardId });
|
||||
const original = db.getBoard(board.boardId).members;
|
||||
db.updateOne('boards', { _id: board.boardId }, { $set: { members: [...original,
|
||||
{ userId: user2.id, isActive: true, isAdmin: false, isNormalAssignedOnly: true }] } });
|
||||
db.updateOne('cards', { _id: theirs._id }, { $set: { assignees: [] } });
|
||||
db.updateOne('cards', { _id: mine._id }, { $set: { assignees: [user2.id] } });
|
||||
const recorded = () => db.find('eventlog', { bleed: 'UnassignedArchiveBleed' })
|
||||
.filter(row => /archiveSelectedCards|createBoardFromCard/.test(row.source || ''))
|
||||
.reduce((sum, row) => sum + (row.count || 1), 0);
|
||||
const before = recorded();
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/sign-in`);
|
||||
await loginWithToken(page, user2.id, user2.token);
|
||||
|
||||
// The reported attack: a card that is not theirs.
|
||||
expect((await call(page, 'archiveSelectedCards', board.boardId, [theirs._id])).error).toBe('not-authorized');
|
||||
expect((await call(page, 'createBoardFromCard', theirs._id, 'x')).error).toBe('not-authorized');
|
||||
// A selection with one card not theirs changes none of it.
|
||||
expect((await call(page, 'archiveSelectedCards', board.boardId, [mine._id, theirs._id])).error).toBe('not-authorized');
|
||||
expect(db.findOne('cards', { _id: theirs._id }).archived).not.toBe(true);
|
||||
expect(db.findOne('cards', { _id: mine._id }).archived).not.toBe(true);
|
||||
expect(db.findOne('cards', { _id: theirs._id }).type).not.toBe('cardType-linkedBoard');
|
||||
// Each attempt is visible in Admin Panel -> Problems.
|
||||
await expect.poll(recorded).toBeGreaterThanOrEqual(before + 3);
|
||||
|
||||
// Their own card: still allowed.
|
||||
expect((await call(page, 'archiveSelectedCards', board.boardId, [mine._id])).result).toEqual({ archived: 1 });
|
||||
expect(db.findOne('cards', { _id: mine._id }).archived).toBe(true);
|
||||
} finally {
|
||||
db.updateOne('boards', { _id: board.boardId }, { $set: { members: original } });
|
||||
db.updateOne('cards', { _id: mine._id }, { $set: { archived: false } });
|
||||
}
|
||||
});
|
||||
@@ -221,6 +221,7 @@ const GUARDED = {
|
||||
importplacementbleed: ['tests/importPlacementBleed.test.cjs', 'tests/playwright/specs/security-import-placement.e2e.js'],
|
||||
samlsubjectbleed: ['tests/samlIdentityBinding.test.cjs'],
|
||||
stampbleed: ['tests/stampBleed.test.cjs', 'tests/playwright/specs/upload-stamp-bleed.e2e.js'],
|
||||
unassignedarchivebleed: ['tests/assignedCardMethods.test.cjs', 'tests/playwright/specs/assigned-card-methods.e2e.js'],
|
||||
unassignedattachbleed: ['tests/unassignedAttachBleed.test.cjs', 'tests/playwright/specs/unassigned-attach-bleed.e2e.js'],
|
||||
uploadbleed: ['tests/uploadBleed.test.cjs', 'tests/playwright/specs/upload-stamp-bleed.e2e.js'],
|
||||
};
|
||||
@@ -371,7 +372,8 @@ test('the whole published list is accounted for', () => {
|
||||
// 144 -> 145: CreateBleed (GHSA-qcfm-2g4w-wqcc, 2026-11-07), guarded. It went
|
||||
// unnoticed until the companion checkout moved to .tools/w/wekan.fi and this
|
||||
// file looked there: at the old path the comparison was skipped.
|
||||
assert.strictEqual(total, 145, 'the Hall of Fame and this list disagree on how many there are');
|
||||
// 145 -> 146: UnassignedArchiveBleed (GHSA-6xqf-jh8x-4wxc, 2026-11-08), guarded.
|
||||
assert.strictEqual(total, 146, 'the Hall of Fame and this list disagree on how many there are');
|
||||
});
|
||||
|
||||
test('the companion Hall of Fame names match the inventory when available', () => {
|
||||
|
||||
@@ -22,6 +22,16 @@ STAGE_LOG = (LOG.split('* Command standard error')[0]
|
||||
"Failed to fetch package: The item '/root/.cache/snapcraft/download/"
|
||||
"libcurl4t64_8.5.0-2ubuntu10.15_amd64.deb' could not be fetched: "
|
||||
'404 Not Found [IP: 91.189.91.82 80].\nFailed to run snapcraft in instance\n')
|
||||
# v13.02 amd64: build-packages could not run apt update in the project
|
||||
# instance while the amd64 archive was unreachable.
|
||||
UPDATE_LOG = ('Executing in container: /snap/bin/lxc --project snapcraft exec local:' + BUILD
|
||||
+ ' --cwd /root/project --mode non-interactive -- env CRAFT_MANAGED_MODE=1 snapcraft pack\n'
|
||||
':: Installing build-packages\n'
|
||||
':: E: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/noble-updates/InRelease '
|
||||
'Could not connect to archive.ubuntu.com:80 (185.125.190.81), connection timed out\n'
|
||||
':: W: Some index files failed to download. They have been ignored, or old ones used instead.\n'
|
||||
'Failed to refresh package list: failed to run apt update.\n'
|
||||
'Failed to run snapcraft in instance\n')
|
||||
|
||||
|
||||
class RecoveryTests(unittest.TestCase):
|
||||
@@ -46,6 +56,72 @@ class RecoveryTests(unittest.TestCase):
|
||||
with self.subTest(text=text), self.assertRaises(ValueError):
|
||||
recovery.failed_instance(text)
|
||||
|
||||
def test_apt_update_failure_selects_project_instance(self):
|
||||
self.assertEqual(recovery.failed_instance(UPDATE_LOG), BUILD)
|
||||
self.assertEqual(recovery.failed_instance(UPDATE_LOG.replace('\n', '\\n')), BUILD)
|
||||
for text in [UPDATE_LOG.replace('Failed to run snapcraft in instance', ''),
|
||||
UPDATE_LOG.replace(BUILD, 'other-container'),
|
||||
UPDATE_LOG + 'lxc --project snapcraft exec local:snapcraft-other-123 -- env\n',
|
||||
UPDATE_LOG.replace('failed to run apt update', 'failed to run apt upgrade'),
|
||||
'Build failed\nFailed to run snapcraft in instance\n'
|
||||
'lxc --project snapcraft exec local:' + BUILD + ' -- env\n']:
|
||||
with self.subTest(text=text), self.assertRaises(ValueError):
|
||||
recovery.failed_instance(text)
|
||||
|
||||
def test_apt_diagnostics_say_why(self):
|
||||
lines = recovery.apt_diagnostics(UPDATE_LOG)
|
||||
self.assertEqual(len(lines), 2)
|
||||
self.assertIn('Could not connect to archive.ubuntu.com', lines[0])
|
||||
self.assertIn('Some index files failed', lines[1])
|
||||
self.assertEqual(recovery.apt_diagnostics('Building\nStaged\n'), [])
|
||||
|
||||
def exercise_update(self, failures, status='Stopped'):
|
||||
"""apt-get update fails `failures` times, then succeeds."""
|
||||
calls, sleeps, left = [], [], [failures]
|
||||
def run(args, **kwargs):
|
||||
self.assertTrue(kwargs['check'])
|
||||
calls.append(args[3:])
|
||||
if args[3] == 'list':
|
||||
return subprocess.CompletedProcess(args, 0, json.dumps([{'name': BUILD, 'status': status}]))
|
||||
if args[3] == 'exec':
|
||||
self.assertEqual(args[-1], 'update')
|
||||
if left[0]:
|
||||
left[0] -= 1
|
||||
raise subprocess.CalledProcessError(100, args, 'E: Could not connect')
|
||||
return subprocess.CompletedProcess(args, 0, '')
|
||||
with patch.object(recovery.subprocess, 'run', side_effect=run):
|
||||
if failures > len(recovery.UPDATE_BACKOFF):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
recovery.refresh(UPDATE_LOG, sleep=sleeps.append)
|
||||
else:
|
||||
recovery.refresh(UPDATE_LOG, sleep=sleeps.append)
|
||||
return [c[0] for c in calls], sleeps
|
||||
|
||||
def test_apt_update_outage_waits_until_the_archive_answers(self):
|
||||
self.assertEqual(self.exercise_update(0), (['list', 'start', 'exec', 'stop'], []))
|
||||
calls, sleeps = self.exercise_update(2)
|
||||
self.assertEqual(calls, ['list', 'start', 'exec', 'exec', 'exec', 'stop'])
|
||||
self.assertEqual(sleeps, list(recovery.UPDATE_BACKOFF[:2]))
|
||||
|
||||
def test_apt_update_outage_that_never_ends_is_fatal_and_bounded(self):
|
||||
calls, sleeps = self.exercise_update(len(recovery.UPDATE_BACKOFF) + 1)
|
||||
self.assertEqual(calls.count('exec'), len(recovery.UPDATE_BACKOFF) + 1)
|
||||
self.assertEqual(calls[-1], 'stop')
|
||||
self.assertEqual(sleeps, list(recovery.UPDATE_BACKOFF))
|
||||
# Bounded well inside the 90-minute snap-variants job.
|
||||
self.assertLess(sum(recovery.UPDATE_BACKOFF), 15 * 60)
|
||||
|
||||
def test_package_404_is_refreshed_once_without_waiting(self):
|
||||
sleeps = []
|
||||
def run(args, **kwargs):
|
||||
if args[3] == 'list':
|
||||
return subprocess.CompletedProcess(args, 0, json.dumps([{'name': NAME, 'status': 'Running'}]))
|
||||
raise subprocess.CalledProcessError(100, args)
|
||||
with patch.object(recovery.subprocess, 'run', side_effect=run):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
recovery.refresh(LOG, sleep=sleeps.append)
|
||||
self.assertEqual(sleeps, [])
|
||||
|
||||
def exercise(self, status='Stopped', fail=False, name=NAME, log=LOG):
|
||||
calls = []
|
||||
def run(args, **kwargs):
|
||||
|
||||
@@ -5,7 +5,7 @@ const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
test('managed APT recovery handles package 404s and rejects unrelated failures', () => {
|
||||
test('managed APT recovery handles package 404s and apt update outages, and rejects unrelated failures', () => {
|
||||
const result = spawnSync('python3', ['-B', 'tests/snap-managed-apt.py'], {
|
||||
cwd: root, encoding: 'utf8', timeout: 10000,
|
||||
});
|
||||
@@ -20,7 +20,7 @@ test('native and variant Snap jobs allow exactly one guarded recovery', () => {
|
||||
// Pinned to a commit of v1 (tests/workflowHardening.test.cjs).
|
||||
assert.equal((job.match(/uses: snapcore\/action-build@[0-9a-f]{40} # v1\./g) || []).length, 2);
|
||||
assert.match(job, /id: build-first\s+continue-on-error: true/);
|
||||
assert.match(job, /Refresh stale managed APT indexes after a package 404\s+if: steps.build-first.outcome == 'failure'\s+run: sudo .*snap-refresh-managed-apt.py/);
|
||||
assert.match(job, /Refresh managed APT after a package 404 or a failed apt update\s+if: steps.build-first.outcome == 'failure'\s+run: sudo .*snap-refresh-managed-apt.py/);
|
||||
assert.match(job, /id: build-retry\s+if: steps.build-first.outcome == 'failure'\s+uses:/);
|
||||
assert.match(job, /BUILT_SNAP: \$\{\{ steps.build-first.outputs.snap \|\| steps.build-retry.outputs.snap \}\}/);
|
||||
assert.match(job, /test -n "\$BUILT_SNAP"\s+test -f "\$BUILT_SNAP"/);
|
||||
|
||||
Reference in new issue
Block a user