Seven integration tests covering the Label pilot:
- Create_Read_Update_Delete — full round-trip through POST/GET/PUT/
DELETE, asserts body + status at each step.
- List_ReturnsItems — GET /labels, asserts items[] is non-empty and
contains a known fixture; this is the regression catcher for the
generic-any silent-empty trap the spike hit.
- ForbiddenErrorShape — user1 reading user13's private label returns
403 problem+json with the RFC 9457 type/title/status/detail shape.
- ValidationErrorShape — POST with empty title fails Huma's
minLength:1 check with 422 problem+json + structured per-field
errors locating `title`.
- ETagReturns304 — first GET captures ETag, second GET with
If-None-Match returns 304.
- PATCHMergePatch — AutoPatch-synthesised PATCH with partial
application/merge-patch+json body updates one field and leaves
the others untouched; a follow-up GET confirms preservation.
- OpenAPISpecDescribesAllFive — the unauthenticated
/api/v2/openapi.json surfaces GET+POST on /labels and GET+PUT+
DELETE on /labels/{id}.
Wires five hand-written huma.Register calls for Label CRUD onto the
existing /api/v2 group: list, read, create, update, delete. Uses
concrete type cast on ReadAll to avoid the generic-any silent-empty
trap. The read operation exposes an ETag via a header-tagged output
struct field and honours conditional.Params so clients can get 304
Not Modified on subsequent reads.
Also closes a prior-phase gap: SetupTokenMiddleware was intended to
run on the /api/v2 group (per task B4 of the plan) but was never
wired. Attach it now and teach the skipper to consult
unauthenticatedAPIPaths so spec + docs remain public.
The /api/v1 group sets Cache-Control: no-store to prevent browsers
from heuristically caching JSON responses. /api/v2 was missing the
same header, which could lead to stale reads. Extracted the inline
middleware into a shared noStoreCacheControl helper and applied it
to both groups.
Huma's AutoPatch synthesises a PATCH counterpart for every PUT, and both
verbs collapse to the same "update" permission. PATCH is still skipped
during collection (it would clobber PUT under the shared key), but the
matcher now accepts it as an alias for the stored PUT route on the same
path, so token holders aren't forced to use PUT exclusively.
Sub-phase G validation caught that a token scoped to e.g.
`labels.read_one` was rejected on /api/v2/labels because the route
collector only stripped /api/v1/ from paths and did not know about
v2's REST-style verbs (POST create, PUT/PATCH update, inverted
from v1 where PUT creates and POST updates).
Introduce a shadow apiTokenRoutesV2 map keyed under the same
(group, permission) names as the v1 entries. Route collection now
routes v2 paths into this shadow map and CanDoAPIRoute consults
both tables, so the same permission bit authorizes the v1 and v2
endpoints for the same resource without changing the data shape
served at /api/v1/routes (which the frontend token UI depends on).
Also teach getRouteDetail about PATCH so Huma's AutoPatch-synthesized
PATCH routes collapse to the `update` permission instead of being
dropped.
Promotes huma/v2 to a direct dep (now imported by pkg/routes/api/v2 and
pkg/modules/humaecho5) and bumps clipperhouse/displaywidth to v0.11.0,
which is required for compatibility with uax29/v2 v2.7.0 that huma pulls
in. Other version bumps are go-mod-tidy consequences of MVS.
The scratch image shipped /tmp owned by 1000:1000 and writable only by
UID 1000, so containers run under a different user (e.g. Unraid's
99:100, OpenShift random UIDs, or any `user:` override) could not create
the temp file used for data exports, failing with:
error creating temp file: open /tmp/vikunja-export-*.zip: permission denied
The builder-stage `chmod 1777 /tmp` did not survive into the final image
(see #2316, which had to add --chown to make it writable for UID 1000),
so the world-writable intent was lost. Force the mode at copy time with
BuildKit's --chmod=1777, restoring a normal sticky, world-writable /tmp
that works for every UID.
Closesgo-vikunja/vikunja#2755
A GetWithValue deserialization error in RememberFor was returned as fatal.
On a Redis upgrade the metrics counters live under the same keys as before
but were stored as plain int64, so the first decode into the new envelope
would fail and the metric would break permanently. Treat such errors as a
miss and recompute/overwrite so the cache self-heals.
Instead of priming a counter at startup and keeping it in sync via events,
each entity count is now read directly from the database and cached for
30s (countCacheTTL). The cache is the correctness guarantee: counts are at
most one TTL stale and self-healing, so they can never permanently drift.
This fixes vikunja_user_count never updating after registration (#2650):
the count no longer depends on every mutation path dispatching an event.
The tray icon was loaded from desktop/build/icon.png, but build/ is
electron-builder's default buildResources directory, whose contents are
not packaged into the app. The icon therefore existed when running from
source but was missing in every released build, leaving an empty tray
icon.
Load the icon from the packaged app root instead and add icon.png there,
rendered from the circular logo.svg so it has transparent corners rather
than the square full-bleed source artwork.
Fixes#2668
The postinstall scripts generated the jwt secret with:
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1
This relies on SIGPIPE to terminate the infinite `cat /dev/urandom`
once `head` has read its single line. Inside a dpkg/apt maintainer-script
context the SIGPIPE disposition is not reliably delivered, so
`cat /dev/urandom` spins forever, the postinstall never returns, and the
whole `dpkg -i` / upgrade hangs.
Read a bounded 512 bytes with `head -c` instead so nothing depends on
SIGPIPE to terminate. 512 random bytes yield ~124 alphanumerics on
average, so the trailing `head -c 32` reliably produces a full 32-char
secret while staying dependency-free.
Fixes#2660
vikunja's nfpm.yaml packs ./config.yml.sample as /etc/vikunja/config.yml.
The release-binaries action already regenerates it for the zip bundles,
but release-os-package runs on a fresh runner without that file, so
nfpm aborted with "matching ./config.yml.sample: file does not exist"
on every vikunja os-package matrix shard (the veans shards skip this
step entirely).
Add a vikunja-only step to regenerate it before nfpm runs.
The s3-action expands the upload glob into paths without a leading
"./", but strip-path-prefix was set to "./dist/os-packages/" (or
"./veans/dist/os-packages/"). The prefix never matched, so packages
landed at /<project>/<version>/<project>/dist/os-packages/<file>
instead of /<project>/<version>/<file>.
Drop the "./" prefix to match the working DIST_PREFIX pattern in
release-binaries.
The release-binaries and release-os-package composite actions were
comparing the raw release-version input against the literal "main" to
decide whether to use "unstable" for filenames and the S3 directory.
Callers always pass `steps.ghd.outputs.describe`, which is a value
like `v2.3.0-408-ge053d317` on non-tag builds — so the check never
fired and unstable artifacts landed under `/<project>/<describe>/`
with `<project>-<describe>-...` filenames.
Drive the switch from `github.ref_type == 'tag'` instead, matching the
pattern the desktop and config-yaml jobs in release.yml already use.
The raw describe value still flows into RELEASE_VERSION so the binary
and package metadata keep the precise commit reference.
GitHub's action manifest parser evaluates `${{ ... }}` expressions inside
`description:` block scalars, and `secrets` isn't a valid context in a
composite action — so the literal example text in the docstring caused
manifest validation to fail before any step ran.
build_mage_bin is only consumed by publish-repos in release.yml, so it
doesn't belong in the test workflow. Move it to release.yml as a
prep job and add it to publish-repos's needs list.
Reviewer asked us to stop over-configuring the release-binaries and
release-os-package composite actions — they're called only with
vikunja or veans, so per-project paths, artifact names, cache keys, S3
target, and version-or-unstable can all be derived inside the action
from the project name. The xgo-out-name input goes away too.
Vikunja-specific pre-build (downloading frontend_dist, generating
config.yml.sample) now happens inside the action, gated on the project
input. Callers no longer need those preamble steps.
Secrets stay as inputs — composite actions can't read \`\${{ secrets.* }}\`
directly; passing them through is the simplest workaround.
Each callsite shrinks to ~13 lines of mostly-secret pass-through plus
2-4 lines of real parameters.
The release pipeline lives entirely in build/magefile.go now, so the
per-project Release namespaces in vikunja's magefile.go and
veans/magefile.go are dead weight. Drop them.
Update the Dockerfile in the same commit so the apibuilder stage
invokes `cd build && mage release:xgo vikunja <target>` — the parent
magefile no longer exposes that target.
Replace the inline bodies of binaries, veans-binaries, os-package, and
veans-os-package jobs with calls to the new release-binaries and
release-os-package composite actions. Each call site is now ~25 lines
of inputs instead of ~75 lines of duplicated mage+upx+gpg+s3 plumbing.
publish-repos switches from the parent's ./mage-static to the
prebuilt build_mage_bin artifact so it can drive build/'s repo metadata
targets inside the publish-repos containers.
publish-repos runs inside ubuntu/fedora/archlinux containers that don't
ship a Go toolchain. Compile build/magefile.go into a static binary in
the test workflow (mirroring the existing mage_bin job for the parent
magefile) and upload it as the build_mage_bin artifact so publish-repos
can chmod+x and run it without setup-go.
Two reusable composite actions wrap the CI side of the release pipeline:
- release-binaries: setup-go, install mage + upx, cache xgo, invoke
`mage release:build <project>` from build/, GPG-sign the zip bundles,
upload to S3, store binaries and zips as workflow artifacts.
- release-os-package: download a binaries artifact, install mage,
`mage release:prepare-nfpm-config <project> <arch>`, stage the binary,
nfpm pack (with rpm signing inline and archlinux signing after), upload
to S3, store the package as an artifact.
Both actions are parameterized on project name, output paths, artifact
names, S3 target, and GPG/S3 secrets — adding a third Go binary to the
monorepo just means defining its project in build/magefile.go and adding
a four-line call site in release.yml.
New build/ Go module hosts the full release pipeline (xgo cross-compile,
upx, sha256, zip bundles, nfpm templating, deb/rpm/apk repo metadata)
for every Go binary in the monorepo. Parametric on project name —
`mage release:build vikunja` and `mage release:build veans` both flow
through the same code.
The module is intentionally self-contained: it depends on nothing but
stdlib + mage, and duplicates the small filesystem helpers (copyFile,
moveFile, sha256File) rather than importing them from a project
magefile. That keeps the release tooling free to evolve without
touching project code.
Cross-compile veans for the same OS/arch matrix as the main vikunja
binary, wrap each into a signed zip, build deb/rpm/apk/archlinux
packages via nfpm, and merge those into the existing dl.vikunja.io
package repos so `apt install veans` works from the same source.
- veans/magefile.go: Release namespace (xgo cross-compile, upx, sha256,
per-target zip bundle, nfpm.yaml templating).
- veans/nfpm.yaml: minimal — binary at /usr/local/bin/veans, no service
or postinstall.
- .github/workflows/release.yml: veans-binaries + veans-os-package
jobs, veans artifacts merged into publish-repos and create-release.
S3 layout mirrors vikunja under /veans/<version>/.
Resolves a medium-severity XSS in Address6 HTML-emitting methods
(GHSA / Dependabot alert #224). Vulnerable range: <=10.1.0,
patched in 10.1.1. The package is pulled in transitively through
socks -> socks-proxy-agent in the Electron build chain
(devDependency only), but we add a pnpm override to ensure the
patched version is used everywhere. The frontend workspace already
has the equivalent override.
Resolves Dependabot alert #233: qs.stringify crashes with TypeError on
null/undefined entries in comma-format arrays when encodeValuesOnly is
set (DoS, medium severity).
Updates transitive dependency via pnpm update from 6.15.0 to 6.15.2.
Adds a pnpm override for `tmp` in both the `frontend` and `desktop`
workspaces to force the patched version (0.2.6). The previous transitive
resolutions (`tmp@0.0.33` via external-editor in frontend, `tmp@0.2.3`
via tmp-promise in desktop) are vulnerable to a path traversal via
unsanitized prefix/postfix that enables directory escape.
Addresses Dependabot alerts #234 (desktop) and #235 (frontend).
Three helpers I added recently have no e2e coverage because the
suite always passes --bot-username with a valid name and
--yes-buckets to skip prompts.
Nine tests in a new bootstrap_test.go:
- TestValidateBotUsername — table-driven, 18 rows: valid shapes
(bot-foo, bot-foo-bar, bot-foo123, bot-foo_bar, bot-foo.bar,
bot-a), invalid prefix (foo, Bot-foo, ""), invalid chars
(spaces, commas, uppercase, !, embedded space), the reserved
link-share-N pattern, and the bare "bot-" edge.
- TestConfirmOverwriteExistingConfig — file-missing path, the
OverwriteExistingConfig=true short-circuit, every interesting
prompt answer (y, yes, Y, Yes, " yes " → proceed; n, "",
garbage → CodeConflict with path in message; prompter error
→ CodeUnknown wrapping the original via errors.Is).
- TestBootstrapBuckets_{AllPresent,AutoApprove,PromptDeclined,
PromptAborted,PromptUnknownCap,PromptAccepted} — drive the
function against a stub httptest server (bucketServer helper)
that records ListBuckets responses and CreateBucket payloads,
with a scripted queuePrompter for the prompt-driven cases.
Covers the alias-match short circuit, the auto-approve path,
the new declined/aborted/retry-cap paths, and the y-accepted
path.
Local helpers (queuePrompter for scripted answers with injectable
error; bucketServer for the stubbed bucket endpoints) stay in the
test file — no production code changes.
The e2e suite bypasses the OAuth flow via --token, so the callback
handler's error branches had zero coverage. Eight tests appended to
oauth_test.go drive the handler directly:
- happy path: code+state arrive on the channel; response is HTML
- authz-server error path: ?error=access_denied&error_description=…
bubbles up as a non-nil err containing the description (not the code)
- only-code fallback: when error_description is missing, the error
message falls back to the error code
- empty code: handler captures it; waitForCallback's job to reject
- non-GET method: 405 with Allow: GET, nothing pushed to channel
(defense against forged POST from a same-origin page)
- wrong path: 404, nothing pushed
- HTML-escaping: an error containing <script>…</script> renders as
<script> — XSS regression guard
- nil-err success page: 200 with 'veans is authorized'
Plus generateState shape coverage (length, charset, uniqueness)
to match the existing TestGeneratePKCE_*.
Sanity-checked the XSS test by deleting the html.EscapeString call —
it fails with raw <script> in the body. Restored.