Builds and Releases - sebiboga/testlink-upgraded GitHub Wiki
Modernization of Builds & Releases Management (lib/plan/buildView.php +
lib/plan/buildEdit.php) β GitHub issue #585.
The legacy Smarty screens are replaced by a standalone Dashio page
(gui/templates/plans/buildsView.html) backed by a plain-PHP REST BFF
(api/builds/index.php), following the same pattern as the other modernized
screens. The aside menu entry Test Plan β Builds / Releases now points to
the new HTML screen (switched in lib/functions/common.php).

| Feature | Legacy behavior | Modern implementation |
|---|---|---|
| List builds of current test plan |
testplan_create_build required (rightsAnd) |
GET /api/builds/?tplan_id=N (403 otherwise) |
| Create build (name, rich notes, active, open, release date) |
do_action=do_create + duplicate-name crossCheck + invalid-date check |
POST /api/builds/ (409 on duplicate, 400 on invalid date) |
| Edit build in modal |
do_action=edit / do_update
|
GET /api/builds/{id} prefill + PUT /api/builds/{id}; the stored closure date is shown beside the Open checkbox whenever the build is closed (showOrHideElement('closure_date', β¦), Refs #1124) |
| Active/Inactive toggle per row |
setActive / setInactive
|
POST /api/builds/{id}/flags {"active":bool} |
| Open/Closed lock toggle per row |
open / close; closing stamps today as closure date |
same flags route; stamping/clearing only on real transitions |
| Delete build with confirm dialog |
do_delete; needs exec_delete when executions exist |
DELETE /api/builds/{id} (409 with localized message otherwise) |
| Copy tester assignments from a source build | create-form selector newest-first with assignment counts + optional exec-status filter | same options served by BFF; source build validated to belong to the plan |
| Closure date semantics | preserved through hidden field on edit; stamped on close | historical date preserved across updates (build::update() wipes it β BFF restores it); the historical date is surfaced in the edit modal next to the Open checkbox (Refs #1124) |
| Audit trail |
audit_build_created/saved/deleted events |
identical audit events with project name resolved from context |
Create Build modal (name required, notes, active/open checkboxes, release date; copy-assignments section appears once the plan already has builds):

Edit Build modal (prefilled from API):

Edit Build modal of a closed build β the stored closure date is rendered beside the Open checkbox and toggles live with it (Refs #1124):

- Everything:
testplan_create_build(same as legacycheckRights()). - Deleting a build that already has executions additionally requires
exec_delete, exactly like legacydoDelete().
- Error messages are returned as stable codes (
warning_duplicate_build,invalid_release_date, β¦) and localized client-side via thebv.*i18n keys present in all 10 locale bundles. - Notes render as sanitized HTML (DOMPurify), matching the legacy rich-editor storage.
- Security review fixes applied: source-build must belong to the target plan, closure-date preservation, unknown exec-status codes ignored, JSON errors for all failure paths.
Full feature-parity audit of lib/plan/buildView.php + lib/plan/buildEdit.php
(+ dashio buildView.tpl/buildEdit.tpl) against the modern screen + BFF
(gui/templates/plans/buildsView.html + api/builds/index.php), executed in
the 2026-09-06 parity run.
-
build::get_by_id()returnsbool(false)β notnullβ when a build does not exist, so theis_null($b)guard in the four build-fetch routes (GET/api/builds/{id}, POST/api/builds/{id}/flags, PUT/api/builds/{id}, DELETE/api/builds/{id}) fell through intoresolveBuild()and answered a misleading "Invalid Test Project ID" 404. Guarded withif (!$b)β proper "Build not found" 404 (browser/API verified onid=9999). -
POST create validated the copy-assignments source build after
createFromObject(), so an invalidsource_build_idreturned 400 while an orphan build row stayed persisted (and the PHP8is_null($b)on afalsevalue emitted an E_WARNING into the Event Viewer). Source build is now validated up front, before creation β no orphan row, no PHP warning.
| Feature | Legacy behavior | Modern implementation |
|---|---|---|
| List CF columns |
buildView.php:56-110 builds cfieldsColumns/cfieldsType from build_mgr->get_linked_cfields_at_design() and per-build getCustomFieldsValues()
|
BFF GET /api/builds/ now returns cfields_columns ({id,name,label,type}) + a cf map per build; buildsView.html inserts a dynamic <th> per column and one value cell per row |
TL_BUILDVIEW_HIDECOL / _<prefix>
|
A config CF whose possible_values is a ` |
`-list of CF names to hide; the config CF itself is hidden too |
| CF inputs in create/edit |
buildEdit.php html_custom_field_inputs(); doCreate()/doUpdate() persist via design_values_to_db(...,'build')
|
buildsView.html modal loads GET /api/builds/cfields?tplan_id=&build_id= and renders all design CF controls; POST/PUT persist through the existing saveBuildCfields() -> cfield_build_design_values
|
| Date/datetime values | stored as mktime() epochs |
converted epoch -> ISO server-side (epochToIsoDate()), converted back to the session locale format on save (isoToLocaleDate()) |
| Full-replacement safety | legacy HTML form always submitted every field | the modal sends cfields only when it rendered inputs, so a plain rename never wipes values |
Verified in the browser: list shows BVC Environment / BVC Release Date (with bvc_component hidden by TL_BUILDVIEW_HIDECOL_BVC), create persists qa / backend / 2026-07-20, edit prefills staging / 2026-06-15, no JS console errors. i18n key bv.cfSection added to all 10 bundles. Not caused by this work: a pre-existing POST-create audit E_WARNING ($ctx['tproject_name']) is tracked in #1900.
Build design CF columns in the list (dynamic headers + per-build values; configured columns hidden):

Create/Edit modal with the restored build custom-field inputs:

Legacy buildEdit.tpl:52-57 rendered a Show event history icon next to the
operation title whenever the user held mgt_view_events (buildEdit.php:106),
opening showEventHistoryFor('{build_id}','builds'). The modern edit modal now
carries the same control (platformsView pattern): a #evHistoryWrap button in
the modal body, gated on rights.canViewEvents (the BFF already exposes it at
api/builds/index.php:716-724), submitting object_id/object_type=builds to
eventviewer.html. It is hidden in the create modal (no build id yet).
Edit modal with the event-history entry point:

Event viewer opened from the button β filtered to builds #1:

Verified as admin (canViewEvents=true: button shown, viewer lists the build's
AUDIT rows) and as a leader user without mgt_view_events
(canViewEvents=false, button absent). i18n key bv.showEventHistory added to
all 10 bundles (verify_i18n_coverage.sh exit 0). No new Event Viewer
Error/Warning entries.
| Issue | Type | Gap |
|---|---|---|
| #1122 | task | build design-time custom fields β closed |
| #1123 | task | edit-screen event-history icon β closed |
| #1124 | task | closed_on_date value never displayed β closed |
All three gating gaps above are closed, so the retired legacy Builds & Releases cluster is gone. Removed (4 files / 480 lines):
| File | Was |
|---|---|
lib/plan/buildView.php (81) |
redirect-only shim (#1791) |
lib/plan/buildEdit.php (66) |
redirect-only shim (#1787) |
gui/templates/dashio/plan/buildView.tpl (158) |
dead Smarty template |
gui/templates/dashio/plan/buildEdit.tpl (175) |
dead Smarty template |
The #1787/#1791 shims existed only because the old renderers fataled on the
removed build::getCustomFieldsValues(); with the modern
gui/templates/plans/buildsView.html + buildEdit.html + api/builds/index.php
at parity they are dead weight. lib/general/frmWorkArea.php had no remaining
reference; testplan::getViewActions() names only lib/plan/planView.php
(separate task #1120). Removed URLs lib/plan/buildView.php /
lib/plan/buildEdit.php now answer 404. gui/templates/tl-classic/plan/*
are dead code (TLSmarty pins gui/templates/dashio/) and were left in place;
provenance comments + builds schema untouched.

- Rich-text notes (
web_editor) vs modern plain textarea β matchesprojectsView.html/planView.htmlprecedent. - Legacy custom pagination
[20,40,60,100]/default 10 vs modern10/[10,25,50,100]β deliberate cross-screen DataTables convention. -
copy_to_all_tplans+commit_id/tag/branch/release_candidateare API/JSONB-only in both legacy (buildEdit.phpnever rendered inputs;:129-145just reads REQUEST) and modern (BFF honors them on POST/PUT) β parity, no UI gap. - Copy-to-all-plans is a no-op-by-design today: builds are project-scoped (#503;
buildshastestproject_id, notestplan_id), so a name created on one plan already exists project-wide andcheck_build_name_existence()skips every sibling.
Admin run, seeded BV1121 project (plans + builds in varied states):

Guest user (role 3, no testplan_create_build) gets a graceful rights toast and
an empty control-less table β matches legacy rightsAnd = testplan_create_build:

20/20 test cases PASS (append-only tmp/TLU_Test_Cases.md): ASIDE link switched
(common.php:1932), project-scoped build visibility, create/edit/delete modals,
active toggle, openβclosed closure-date stamping, duplicate/empty/invalid-date
errors, 404 "Build not found", invalid-source no-orphan, no-rights 403, copy
assignments, copy-to-all-plans no-op, i18n 46 bv.* keys Γ 10 bundles, clean
events table + JS console.