Repository navigation
Releases: Zherikhov/hamstrack
Release list
v0.18.3
Full Changelog: v0.18.2...v0.18.3
v0.18.2
Upgrading to 0.18.2 — the deploy now reads production back, and refuses on a mismatch
Wave 1 of the quality retrospective: the mechanisms that make the rules from 0.18.1 actually run.
Most of this release is build-time machinery with no runtime surface. One item is not, and it is
the reason this note exists.
If you deploy from GitHub Actions, do this first
The workflow now takes the target instance from a repository variable named INSTANCE_ID
(Settings → Secrets and variables → Actions → Variables, not Secrets). Without it the deploy
step refuses by name in a few seconds rather than failing later against AWS. A variable rather than
a secret is deliberate: a masked value would make a typo look correct in the log.
The deploy verifies itself
After applying configuration, the deploy now reads the running box back and fails on a
mismatch. Five checks: memory ceilings, environment keys, application identity, Grafana health and
configuration-drift freshness. There is a --verify-only mode that re-reads without deploying, a
metric per check, and an alert that reads them per check rather than as a roll-up.
Details.
Third-party output from the box is no longer republished into the public Actions log. Only lines
the deploy script wrote itself are shown; everything else is counted and named, with the command to
read it privately.
Everything else is the build
Rules that refuse a test which cannot fail, an enumeration harness so a guard covers every door of
its kind rather than the one that was reported, the documented gotchas turned into executable
architecture rules, a contract requiring every timer and alert to name what observes it, a lint
rule set for the interface, and a guard that refuses a suite which quietly ran less of itself than
the repository contains.
For self-hosters building from source: the build is stricter than before and will refuse things it
used to accept. None of it affects a running instance.
Full Changelog: v0.18.1...v0.18.2
v0.18.1
The process half of the 2026-09 quality retrospective
Nothing in this release changes how the application behaves. It changes how the application is
built, and it is the half that had to land before the mechanisms in 0.18.2 could.
A retrospective over 194 defects was codified. The review found three recurring causes, and
each one now has a rule attached to it rather than a lesson somebody has to remember:
- the unit of work is the category, never the single instance — a bound applied to one door is
applied to all of its siblings in the same change, or the change ships the test that enumerates
them; - evidence is measured, never read — a test counts as evidence only after it has been seen
failing against the defect it guards; - the process definitions age like code, so they are checked like code.
The reviewer bench was rebuilt. Twenty-eight imported general-purpose helpers were deleted
after an audit found they had been used zero times in 292 tickets, while twenty-four of them held
write access. Two new specialists replaced them: one for the production and deployment surface, one
that measures the interface in a real browser. Those two areas had no reviewer at all, and half of
the most serious defects lived in them.
All 38 architecture decision records were translated to English and the directory is now
tracked, so the reasoning behind each fork in the design is readable by anyone who clones the
repository.
Full Changelog: v0.18.0...v0.18.1
v0.18.0
Upgrading to 0.18.0 — launch hardening, and several things you must read first
This is not a feature release. It is the set of things that were false and would have been found
by strangers: production with no second copy of anyone's work, an unbounded invite mailer, no
Content-Security-Policy, a first-touch flow handing out dead links, and an Elastic-License product
calling itself open source.
It outgrew its own scope — 47 issues against the 26 it was organised with. The items below are the
ones that change behaviour you can observe. The generated list of commits further down does not.
Read before you pull
Account addresses become case-insensitive. Email uniqueness used to rest on an application-side
fold with no database guarantee, which means two accounts differing only in case may already
exist. The upgrade adds the constraint, and it will not start if such a pair is present. There
is one query to run before you pull:
Account addresses become case-insensitive in 0.18.0.
A custom field keyed like a later-added query name was silently shadowed, and could not be
renamed out of the collision. If you have such a field, the upgrade tells you:
Shadowed custom field keys from 0.18.0.
New bounds, each of which can turn a slow request into an error
Connection acquisition is bounded. One principal could be entitled to more connection-seconds
per minute than the pool actually has.
Details.
PostgreSQL is bounded and tuned. Among other things, the planner was being told there was 4 GB
of cache on a host with 1909 MB of RAM.
Details.
Free text is bounded. Over-long values used to answer 500; five email fields had no length
cap at all.
Details.
Attachment storage is capped per workspace.
Details.
Expensive reads are bounded by concurrency, not only by rate.
Details.
Security and privacy
A Content-Security-Policy where there was none. A per-account throttle on password-reset and
resend-verification mail. A cap on invitation mail, which any verified account could previously
send to arbitrary addresses without limit. Account deletion, which until now was a promise in
the interface and nothing behind it. And a beta notice that says what happens to your data.
Account-critical mail is no longer handed to the executor before its token row commits, and a
rolling deploy during a mail backlog no longer drops up to a hundred committed messages.
Accessibility
Text contrast failed WCAG 1.4.3 on 480 rendered elements, and a single design token caused a third
of them. Taxonomy colours are now chosen against a contrast guard; ten of the fifteen seeded ones
were unreadable as text.
Operations
Production had no backups. It has them now, and the restore has been rehearsed. Daily snapshots
had been running against a detached volume for five days with nothing noticing. Nine alert rules
had been delivering to alerts@example.com, and the comment beside them called that "no delivery".
Grafana crash-looped on any alert rule identifier over 40 characters, which meant production had no
alerting at all. Production was also running July's compose file, so two shipped features were not
actually running.
Licensing
Hamstrack is source-available under the Elastic License 2.0, not open source under the OSI
definition. The product had been describing itself the other way, and no longer does.
Full Changelog: v0.17.0...v0.18.0
v0.17.0
Upgrading to 0.17.0 — what changes for you
This release tightens three resource limits and adds one database constraint. Each item below
changes behaviour you can observe; the generated list of merged pull requests further down does not.
Every database statement is now bounded (DB_STATEMENT_TIMEOUT_MS, default 10 s).
Before 0.17.0 a single query could run for ever while holding one of the ten pooled
connections; ten of them stopped the instance. From now on a statement still running after
10 seconds is cancelled and the request answers 422 with
errorType: STATEMENT_BUDGET_EXCEEDED and no Retry-After — an identical retry costs
identical time. On a large install this can turn a slow report, search or member removal
into an error; a report can be narrowed, a member removal cannot. If it happens, raise
DB_STATEMENT_TIMEOUT_MS in .env (minimum twice DB_LOCK_TIMEOUT_MS, so ≥ 6000) and
docker compose up -d. Database migrations are deliberately not bounded.
Lock waits are bounded with it, and this half is a status-code change. A write that
collides with a long-running change — most often removing a member with a lot of assigned
work — used to wait indefinitely and eventually succeed. It now gives up after
DB_LOCK_TIMEOUT_MS (3 s) with 409 + Retry-After, which is retryable and which clients
should retry. The two bounds ship together on purpose: statement_timeout counts lock-wait
time, so bounding statements alone would have answered that same collision with the
non-retryable 422 above. Details:
Statements are bounded from 0.17.0.
The JVM heap is now bounded (APP_MEMORY_LIMIT, default 1g → 512 MB heap). Before
0.17.0 the JVM claimed ~25% of host RAM, so on a host larger than 2 GB this is less
heap than you had — a 4 GB host drops from ~1 GB to 512 MB, an 8 GB host from ~2 GB.
There is no error; it shows up as reports and searches getting slower. Set
APP_MEMORY_LIMIT in .env to about half the host (4 GB → 2g, 8 GB → 4g) and
docker compose up -d. On a host of 2 GB or less you gain heap and need do nothing.
Details:
The heap is bounded from 0.17.0.
issues.type_id and issues.status_id now have foreign keys. The database enforces that
no issue can point at a status or issue type that does not exist. If your database already
contains such a row, the upgrade will not start — Flyway fails V19__issues_taxonomy_fk.sql
and the container exits. This is deliberate: those rows are already broken (the issue renders
as a blank board column and disappears from every status filter), and the fix needs a decision
only you can make. To check before upgrading, and to find any offenders afterwards:
SELECT i.id, i.workspace_id, i.project_id, i.number, i.type_id
FROM issues i LEFT JOIN issue_types t ON t.id = i.type_id
WHERE t.id IS NULL;
SELECT i.id, i.workspace_id, i.project_id, i.number, i.status_id
FROM issues i LEFT JOIN statuses s ON s.id = i.status_id
WHERE s.id IS NULL;Both must return no rows. If either does not, repoint those issues at a catalog row that
does exist — pick one your project already offers, e.g.
UPDATE issues SET status_id = '<a real status id>' WHERE id = '<the issue id>'; — then
upgrade. Nothing else changes: a delete that would strand an issue was already refused by the
application, and remains so.
A delete the database refuses is a 409, not a 500. Related: catalog deletes that
collide with a reference now answer 409 with
errorType: REFERENCE_CONSTRAINT_VIOLATION instead of a stack trace.
Notifications are scoped to a workspace. The in-app inbox filtered by user alone, so
somebody removed from a workspace kept reading its issue titles and comment excerpts
indefinitely — that text is stored in the notification row itself, so no permission check
could hide it after the fact. From 0.17.0 the list, the unread count and the live stream all
filter by current workspace membership. Rows are hidden, not deleted: re-add the person
and their inbox returns with its read state intact.
The upgrade adds a NOT NULL workspace_id and fills it by reading each notification's
link. A row whose link cannot be read is copied to notifications_unresolvable_v20 and
removed from the inbox — that table is created only if there is something to put in it, and
is yours to DROP once you have looked at it. The count is knowable only before the
upgrade, so if you want it, take it first:
SELECT count(*) FROM notifications n
LEFT JOIN workspaces w
ON w.id = substring(n.link from '^/w/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/')::uuid
WHERE w.id IS NULL;A non-zero answer is almost always rows whose workspace was removed outside the application:
until this release there was no foreign key here, so those accumulated silently and were
already unshowable. Removing them is the correct outcome and is not a reason to hold the
upgrade. Details:
Notifications are scoped to a workspace from 0.17.0.
v0.16.0
Search and the backlog stop giving wrong answers quietly
Six items, and they are one family: the product returning an empty or truncated result with no
signal that anything was withheld. That is the failure a user cannot debug, because it looks
exactly like an ordinary "nothing found".
A backlog section above 100 issues was silently truncated on refresh. You saw a section, it
was short, and nothing said the rest existed.
A workspace search could not be narrowed to one project, because the query language had no
project field. You could search everything or nothing in between.
closedAt was returned by the API and could not be queried. It is now a field you can filter
and sort on, like the other timestamps.
Text search lower-cased with the server's default locale. On a Turkish locale that turns a
capital I into a dotless ı, so a search for a word containing I matched nothing. Folding is now
locale-independent.
A key pasted with a non-breaking space did not match. Copying ABC-12 out of a chat window
often brings an invisible character with it, and parent = "ABC-12" then found nothing.
One refactor underneath the five
The query layer resolved field names in two separate implementations. Every fix above would
otherwise have been made twice, and the two copies would have drifted apart again. They were
collapsed into one resolver first, and the rest of the release sits on top of it.
Full Changelog: v0.15.0...v0.16.0
v0.15.0
Reports
Five reports and an insights panel. The whole set is built on one idea: a report that cannot be
trusted at a glance is worse than no report, so each of these says what it is measuring and
refuses to imply more.
Flow — created against resolved over a window, so you can see whether the queue is growing.
Cycle and lead time, with aging work in progress. Not just what finished, but what is still
open and how long it has been open, which is the half that usually goes unlooked-at.
Sprint burn-up with a real scope line. Scope changes are drawn, not hidden: issues added to
and removed from a sprint are recorded as events, so a sprint that grew looks like a sprint that
grew. The sprint review record keeps that history after the sprint closes.
Velocity as a forecast band, never a scoreboard. It is drawn as a range, and it is deliberately
not presentable as a per-person number.
An insights panel on search results. Any search you can express becomes a small set of
aggregates over exactly the issues it matched. This is the dashboard replacement.
Export. CSV of the plotted series, PNG with fixed axes so two exports are comparable, and
shareable URLs — a report you can hand to somebody is a report they can check.
What was deliberately refused
Burndown, the cumulative flow diagram and a configurable gadget dashboard are not here, and
that is a decision rather than a backlog item. The reasoning is written down in
docs/design/reports-proposal.md §3.
Full Changelog: v0.14.0...v0.15.0
v0.14.0
Upgrading to 0.14.0 — roles and permissions replace "everyone can edit everything"
Before this release any member of a workspace could edit issues in every project in it. There was
no way to say otherwise. 0.14.0 replaces that with a permission model, and it is the largest
behavioural change since the board.
Authorization is now one resolution and one check. A request resolves the caller's permissions
once, and each protected action asks for exactly the permission it needs. A refusal names the
permission it wanted rather than saying "forbidden".
Roles are rows, not code. Built-in roles are shared across workspaces; a workspace can define
its own. Creating a custom role is always a duplicate of an existing one, because the rule for
handing out permissions is that you can only grant what you already hold — and a role composed
from an empty checklist could be assigned to nobody.
Projects gained an access mode. OPEN lets non-members inherit a default role; STRICT means
membership is explicit. There is a default project role and a default workspace role behind that,
so a new project is not a blank wall.
Workspace member management exists. Until now a member could be added and never changed or
removed. Roles can be changed and members removed, with the grant ceiling checked at both ends
of the change, so an administrator cannot demote an owner.
A project can no longer be stranded. Removing the last person who can manage a project's
members used to leave it unmanageable by anyone, including the workspace owner. Nine separate
doors now refuse that.
What to expect after upgrading
Nothing is locked down that was open before: existing members keep an equivalent role, so the
first thing you notice is that the screens now say who can do what. Tightening is a decision you
make afterwards, in People and Roles.
The interface reads permissions rather than role names, so a custom role gets the same UI as a
built-in one with no further work.
Full Changelog: v0.13.2...v0.14.0
v0.13.2
Patch release: seven things that failed quietly
No new features. Every item here is something that went wrong without saying so.
An unknown address in the app rendered a blank page. There is now a not-found screen, so a
mistyped or stale link tells you what happened instead of showing nothing.
Searching for a name with a trailing space never matched. Name lookup in the query language
did not normalise whitespace, so a value pasted from elsewhere silently found nothing.
closedAt was returned by the API and documented nowhere. It is now in the OpenAPI spec and
in both API references.
A dialog was not announced as a dialog. The saved-filter dialog carried no modal semantics, so
screen readers did not present it as one.
Workspace owners and admins could not reach project Settings from the navigation rail, even
though they are allowed to use it.
Two build and release fixes worth knowing about
-Dfrontend.skip=true had never actually skipped the frontend build. The flag existed and did
nothing. It now works, which makes a backend-only build materially faster.
A release tag only reached production if it was pushed before the main build. Pushing the
branch first and the tag second meant the tag's build lost a race and the deploy chain broke. The
two now run as independent builds, which is why pushing a branch and a tag together produces two
builds rather than one.
Full Changelog: v0.13.1...v0.13.2
v0.13.1
What a project does is declared, not guessed
Before 0.13.1 the app decided what a project was by looking at its data. A project with a sprint
row "did Scrum", so it got a sprint board. A project with a version row "did releases", so it got
a releases tab. Four such heuristics existed, and each was wrong the moment someone created one
row to try something out.
Delivery capabilities are now columns on the project — board, releases, estimation —
set by a person and changed by a person. The preset name you see (Kanban, Scrum, Releases) is
derived from those columns server-side and cannot be written directly.
Creating a project asks you to choose, and says the choice is reversible, because it is.
Project settings gained a Delivery tab where the three capabilities are turned on and off.
A capability that is off still has a way in. Every one of them has an affordance visible while
it is off, so a feature you have never used is reachable rather than invisible.
Capabilities never change a status code. They narrow what the interface offers you, never what
the server answers. A project with releases turned off still accepts and returns version data, so
a saved link keeps working.
If you use saved filters or search
Two field keys were retired in this change. Searches and saved filters that use the old names keep
working through aliases, and the suggestion list is now capability-aware: it offers you the keys
that mean something for the project you are looking at.
Full Changelog: v0.13.0...v0.13.1