Skip to content

Latest commit

 

History

History
217 lines (186 loc) · 29.2 KB

File metadata and controls

217 lines (186 loc) · 29.2 KB

AGENTS.md – Working Instructions for the AI Assistant

This file tells the coding assistant how to safely and efficiently work in this repository.

Scope & Priorities

  • Safety first: keep changes minimal, focused, and reversible.
  • No unrelated refactors or dependency bumps unless explicitly requested.
  • Prefer surgical fixes that address the root cause without side effects.
  • Update docs/user‑visible notes only when behavior changes or when asked.

Repo Basics

  • Backend: PHP 7.3 under lpm-core and related modules.
  • Frontend: Vanilla JS under lpm-scripts, templates under lpm-themes, CSS in lpm-themes/default/css.
  • Config: lpm-config.inc.php (runtime), Docker env in .dev/docker-env/.
  • Fixed, non-secret, non-deployment constants (display limits, size caps) live in lpm-core/consts.inc.php (peers: MAX_FILE_SIZE_MB, MAX_IMAGE_SIZE_MB, COPY_YEAR). Prefer this over overloading domain classes (e.g. Issue) or lpm-config.inc.php (which is for per-deployment/runtime config and secrets).
  • Data: app files in lpm-files/; logs under _private/logs/ (create and ensure writable if needed).
  • DB: schema changes are migrations in lpm-core/modules/db/migrations/, applied via lpm-cli/migrate.php (see docs/db-migrations.md). The pre-0.27.0 dump and change log are frozen in .dev/db/legacy/.
  • CLAUDE.md is a symlink to this AGENTS.md — edit AGENTS.md directly; writes through the CLAUDE.md symlink are refused.

Architecture Notes

  • Two frameworks coexist: a legacy one bundled in lpm-libs/gm-framework-v1.1.1.phar and the newer GMFramework\* under lpm-libs/framework/. The project is migrating off the legacy one. Watch for classes that still extend legacy bases (e.g. LPMOptions extends Options resolves to the phar's Options, which has no saveOptions()/change()).
  • DB access: new code uses the V2 query builder \GMFramework\DBQueryBuilder via LPMBaseObject helpers buildAndSaveToDbV2($sqlHash) and loadFromDV2()/loadAndParseV2(). Do NOT hand-write SQL strings or use the legacy DBConnect::queryt()/preparet(). Hash form: ['INSERT' => $assoc, 'INTO' => LPMTables::X, 'ODKU' => ['field']] (upsert), ['UPDATE' => ..., 'SET' => [...], 'WHERE' => [...]], ['DELETE' => ..., 'WHERE' => [...]]. The builder backticks table/column names (reserved words like option/value are safe) and escapes values; throw \GMFramework\ProviderSaveException on failure. Canonical connection: LPMGlobals::getInstance()->getDBConnect().
    • The one exception: a query the builder cannot express at all. It has no window functions and no derived tables — FROM is assembled from a list of table names — so ROW_NUMBER() OVER (…) over a subquery has to be written by hand (see Issue::getPositionInProjectFiltered(), and the pre-existing Issue::loadList()/countFiltered() around it). Forcing such a query through the builder would mean restating the whole selection and ordering a second time, which is worse than the raw SQL: two definitions of the same thing drift apart silently. When you take this exception, say so in a comment at the query, keep the ordering/selection expression in ONE place and reuse it, and still throw the provider exceptions (\GMFramework\ProviderLoadException) so callers cannot mistake a failure for an empty result.
  • Service layer (AJAX): JS calls srv.<service>.<method>(args, onResult), which dispatch to a PHP service extending LPMBaseService. There is NO dynamic dispatch: every method needs an explicit wrapper in the service map in lpm-scripts/lightning.js, otherwise the call throws TypeError before any request. Read docs/agents/frontend-js.md before adding or changing a service call.
  • Model load misses return false, not null: Issue::load(), Issue::loadByIdInProject(), Project::load(), and similar StreamObject::singleLoad-backed loaders return false when nothing is found. Guard with empty()/truthy checks (if (!$x) / empty($x)), NOT === null — a false result slips past a !== null guard and fatals on the next method call.
  • Page routing: pages extend LPMPage; constructor is (uid, title, needAuth, notInMenu, pattern, label, reqRole), and are registered manually in PagesManager::__construct. Restrict a page to admins with reqRole = User::ROLE_ADMIN; the menu auto-filters by checkUserRole(). Page/model classes are autoloaded via lpm-core/classes.dump (auto-regenerated on cache miss, not git-tracked), so new classes are picked up without manual registration in the autoloader. Render a template by setting $this->_pattern and passing data with addTmplVar().
  • Active menu highlighting: PagesManager::getLinks4Menu() marks a top-level Link current when its page uid equals the current page's getMenuSectionUid() (rendered in page.html as the .active nav-link). A page's section defaults to its own uid; a nested page with no menu entry of its own (notInMenu, e.g. ProjectPage, UserPage) declares which section it belongs to by setting $this->_menuSectionUid in its constructor (e.g. ProjectsPage::UID) — keep this in the page, not in a central map. Submenu active state is per-current-page: getSubMenu() marks the current subpage's Link current.
  • External HTTP API (/api/v1): lives in lpm-core/modules/api/. Read docs/agents/api.md before adding, changing or removing an endpoint — it holds the routing/serialization contract and the three docs every endpoint change must update.
  • App options: LPMOptions is a singleton over the lpm_options table (option/value). Read with LPMOptions::getInstance()->prop, persist with the static LPMOptions::save(['name' => $value, ...]). Note cookieExpire is stored in days but exposed in seconds in memory (×86400 on load) — edit it in days and write days back.
  • AI integration: lpm-core/modules/ai/ holds a provider-neutral adapter layer and the artifact builders. Read docs/agents/ai-modules.md before touching that directory or anything that generates or caches an AI artifact (adapters, prompts, sourceHash).

Local Dev Environment

  • Docker compose lives in .dev/docker-env/.
    • Start: from .dev/docker-env/ run docker-compose up (or -d).
    • Rebuild: docker-compose up --build if Dockerfile changes.
  • Dev helper .dev/bin/lpm wraps common container commands (composer, lint, php, run, db, status, exec, shell) so they run against the app's PHP 7.3 with one approval instead of per-command. Prefer it over raw docker exec. The work tree it acts on is the one holding the .dev/bin/lpm you invoked, not the directory you stand in — each git worktree has its own copy, so from a worktree call that worktree's copy; relative arguments are resolved against the script's tree, and a cross-tree call prints a warning to stderr and still runs. That tree is mapped against the directory the container actually mounts at /var/www/html, so lint/run really check the worktree's files; if the tree isn't visible in the container the command refuses loudly instead of silently using another checkout.
    • Composer: .dev/bin/lpm composer install (runs bundled composer.phar in lpm-libs/; a worktree has no composer.phar of its own, so the main checkout's copy is used). The autoloader is authoritative (optimize-autoloader + classmap-authoritative in composer.json): vendor classes resolve via a full classmap with NO runtime filesystem fallback. After ANY dependency change (add/update/remove) you MUST regenerate it — .dev/bin/lpm composer dump-autoload -o -a — and commit the updated lpm-libs/vendor/composer/autoload_*.php; otherwise newly referenced vendor classes silently fail to load. vendor/ is committed and deploy is manual from git, so the optimized files must be in the commit.
    • Lint: .dev/bin/lpm lint <path...> — dispatches by extension: .php via php -l in the container, .js via node --check on the host. Do NOT call node --check directly; lpm lint covers JS too and applies the same worktree-aware path resolution.
    • Run a PHP file: .dev/bin/lpm run <file.php> [args...] — copies ANY host PHP file (including a scratchpad file outside the repo) into the container, runs it with the work tree root as cwd, prints the output and removes the temp file. Use it for CLI harnesses instead of hand-rolled docker cp + docker exec.
    • Dev DB: .dev/bin/lpm db "SELECT ..." runs SQL in the dev DB; without an argument it opens a mysql shell (or reads SQL from stdin). DB name and credentials come from lpm-config.inc.php and the password is passed via the environment — never write docker exec ... mysql -uroot -p<pass> <db> by hand.
    • Stand state: .dev/bin/lpm status — project containers, app port, dev DB name, SITE_URL, and how the current work tree maps into the container.
    • Arbitrary: .dev/bin/lpm exec <cmd> / .dev/bin/lpm php <args> / .dev/bin/lpm shell.
    • Built-in help: .dev/bin/lpm help.
    • Override container/mount via LPM_CONTAINER / LPM_DB_CONTAINER / LPM_MOUNT env vars (defaults: lightning-pm, <LPM_CONTAINER>-db-1, container path derived from the actual bind mount). An explicitly set value always wins over autodetection — needed when a separate stand is started from a worktree.
  • PHP settings: short_open_tag = On (see README.md).
  • Long-running commands need an explicit increased timeout: composer, docker-compose up (especially --build) and migrations regularly exceed the default 120 s and get pushed to the background mid-run.
  • Do NOT read .dev/docker-env/.env — access to it is blocked. The variable names and defaults are in .dev/docker-env/.env.template, runtime values the app actually uses (DB_NAME, credentials) are in lpm-config.inc.php; ask the user for anything neither file answers.

Git Remotes

  • origin fetches over SSH (git.innim.ru:22, reachable only from the VPN) but pushes over HTTP on port 1414. If git fetch origin hangs and dies on a ~75 s timeout, you are outside the VPN: fetch through the push URL (git fetch http://greymag@git.innim.ru:1414/innim/LightningPM.git) or through the github remote — do not retry SSH a second time.

Editing Rules (for the assistant)

  • Use patch-based edits only; do not run destructive shell commands without approval.
  • Do not modify lpm-libs/vendor/ or introduce new dependencies without explicit request.
  • Keep PHP 7.3 compatibility; avoid newer language features.
  • Match existing style and structure; follow patterns present in nearby files.
  • Member order in classes: static methods come first — before properties/constants and the constructor. Place a new static method up top with the others, not after the constructor.
  • Docblocks describe the contract (params, return, thrown exceptions, observable behavior), NOT internal implementation (which query builder is used, upsert/ODKU mechanics, storage details, casting tricks). The same applies to field docblocks — document what the field means, not how it is stored.
  • When changing behavior, update inline PHPDoc/comments and CHANGELOG.md — the entry goes under ## Next in the same commit as the code, and needs no separate request from the user. If the change is not user-facing (refactor, chore, docs), say so explicitly in your report instead of adding an entry.
  • Comments in code (PHP, templates, CSS, JS) state a contract or a non-obvious technical constraint — e.g. "class back-link is used by JS", "hidden inputs must stay direct siblings of the buttons", "a Bootstrap tooltip can't attach to an element that already hosts another component". Never comment on why a layout/design looks the way it does, how it looked before the change, or what would look off without it ("without the placeholder the card looks unloaded", "dates on their own row looked lost") — that is churn: it describes a passing state of the task, not the code. Such reasoning belongs in the commit message; the user-facing effect belongs in CHANGELOG.md.
  • In frontend JS within project pages, assume shared globals (srv, showError, redirectTo, bootstrap) are present; avoid redundant existence checks unless adding code outside the app context.
  • For UI components, prefer adding a PagePrinter method that includes the template and expose it via an alias in lpm-core/aliases.inc.php (e.g., lpm_print_goto_issue($project)), then call the alias in templates instead of includePattern() directly.

DB Changes

  • Schema changes are migrations: create one with .dev/bin/lpm migrate create <slug> (never hand-name the file — the YYYYMMDDHHMMSS_ prefix defines apply order), apply with .dev/bin/lpm migrate apply. Read docs/db-migrations.md before writing one — it is the full contract (exec/execFile/t(LPMTables::X), no DELIMITER, no transactions, re-run-safe statements, never edit an applied migration, prefer additive changes).
  • Inside a migration raw SQL is expected — the DBQueryBuilder rule above does not apply there.
  • .dev/db/legacy/ is frozen history — do NOT append to changes-log.sql or edit dump.sql any more.

Frontend Conventions

  • Stick to existing patterns in lpm-scripts/*.js and template files in lpm-themes/.
  • Bootstrap 5 is used; overrides live in lpm-themes/default/css/bootstrap-reset.css. The version is exactly 5.1.3 (lpm-themes/default/css/bootstrap.min.css) — utilities added in 5.2+ (text-bg-*) and 5.3+ (subtle bg-*-subtle / text-*-emphasis) are NOT available; use 5.1 equivalents (e.g. bg-secondary + text-white).
  • Negative-margin utilities are DISABLED in this build ($enable-negative-margins: false) — ms-n*, mt-n*, m-n* etc. don't exist and silently do nothing. To pull/align an element left of the container edge (e.g. to line a first nav-link up with the brand/footer), drop the link padding (px-0) and space items with gap-*, or restructure — do NOT reach for negative margins. gap-* utilities ARE present.
  • For icons FontAwesome 7 is used (free version).
  • Keep JS modular and colocated with related UI screens when possible.
  • Try to use Bootstrap 5 components and utilities before adding custom CSS.
  • For dialogs/modals, prefer the lpm.dialog wrapper in lpm-scripts/lightning.js — do not add jQuery UI dialogs or hand-rolled modals. Read docs/agents/frontend-js.md before building one (rich dialogs, scoping to .modal.show, inline validation errors).
  • jQuery UI is not used — the project is fully on Bootstrap 5 for dialogs, tabs, the completion-date picker, and tooltips. Don't add it back.
  • The global tooltip is one delegated Bootstrap tooltip on <body> in lpm-scripts/lightning.js (selector [title]:not([data-tooltip]):not([data-bs-toggle]), [data-bs-toggle="tooltip"][title]:not([data-tooltip]), container: 'body'), so any [title] element (including dynamically added ones) gets a tooltip. Its priority-change refresh lives in lpm-scripts/issues.js.
  • Don't put title on a bare FontAwesome <i> icon: its rendered box height is sub-pixel (~0.8px, the glyph is a ::before), so the tooltip anchors flush and overlaps the icon, making it hard to click. Put title on the wrapping element (<a>/<button> — real line-box height) so the tip clears the top edge; keep aria-label on that wrapper for the accessible name and aria-hidden="true" on the <i>. Do NOT "fix" tooltip spacing by adding a global offset — Bootstrap's delegated tooltip ignores per-element data-bs-offset (_getDelegateConfig uses only the shared config), and changing the shared offset shifts every tooltip in the app.
  • window.lpmOptions (emitted by PagePrinter::jsOptions(), called via the lpm_print_js_options() alias in page.html before the app scripts) exposes server constants to JS — url/themeUrl/gitlabUrl, image/file limits (issueImgsCount, issueFilesCount, issueFileMaxSizeMb), aiRequestTimeout (= AiIntegration::getRequestTimeout()), priorityGroupStep (= Issue::PRIORITY_GROUP_STEP, the step of scrum-board priority grouping), URL-detection pattern arrays videoUrlPatterns/imageUrlPatterns (from AttachmentVideoHelper/AttachmentImageHelper::URL_PATTERNS), roles ({user, admin, moderator}), and issueUrlPattern (= OwnUrlHelper::getIssueUrlPattern(); group 1 = project uid, group 2 = idInProject). Use it (e.g. new RegExp('^' + lpmOptions.issueUrlPattern + '$') to detect/parse an issue URL client-side) instead of re-deriving these values.
  • Bootstrap 5 allows only ONE component instance per element (Data.set). A [title] element that also hosts another Bootstrap component (a data-bs-toggle="dropdown"/"collapse"/"modal"/… toggle) therefore CANNOT also get a Tooltip — the instance is silently not stored and the tip never hides (stays stuck open). Such toggles are excluded from the global tooltip (hence the selector above; data-bs-toggle="tooltip" is re-included because it hosts no conflicting component). To give such a toggle a styled tooltip anyway, put the title on an inner <span> wrapping the icon (no component there, and a real line box — see the next bullet) and keep an aria-label on the toggle for its accessible name — see the . menu in issue.html, main-menu-item.html and project.html. lightning.js hides that inner tooltip on show.bs.dropdown/show.bs.collapse so it doesn't linger over the opened menu/panel, and the inner element stays visible while the menu/panel is open — nothing extra is needed to keep it on screen.
  • Don't use the .tooltip class for custom widgets — Bootstrap's tooltip element owns it. Give a homegrown hover widget its own class name instead.
  • Copy-to-clipboard: for a static value, put it on any element as data-copy="<value>" (optional data-copy-toast="<msg>" overrides the default "Скопировано" toast) — a global delegated handler in lpm-scripts/lightning.js copies it and shows the toast, no per-widget JS needed. For dynamic/computed text use lpm.utils.copyToClipboard(text) / lpm.utils.copyRichToClipboard(html, plain) then lpm.toast.show(...).
  • Bootstrap+jQuery gotcha: lpm-scripts/lightning.js polyfills Element.prototype.hide()/show() to set style.display. Because jQuery invokes an element's native method matching a triggered event's base type, Bootstrap's hide.bs.*/show.bs.* events would make jQuery call .hide()/.show() on the event target — hiding deselected tabs, dropdown toggles, tooltip anchors. This is neutralised once, next to the polyfills, by $.event.special.show/hide hooks that claim the default action so jQuery never makes that call. Do NOT add per-event display restores in hide.bs.*/hidden.bs.* handlers: they are unnecessary, and two of them racing over the same style.display is what broke the tooltip-on-inner-icon pattern. If a show()/hide() polyfill is ever added for another element method, register the matching _default hook with it.
  • Keep templates minimal: templates in lpm-themes/ should only contain markup-related code. Move business logic and data shaping into PHP classes/services. For example, use model helpers like LPMFile::isVideo() to check file types instead of MIME checks in templates, and prefer rendering via PagePrinter methods.
  • At the top of each template, document all required external variables in a Требуются: PHP comment, following the pattern used in lpm-themes/default/comment-text.html.

Validation

  • There is no project-wide automated test suite. Validate by:
    • Static review and targeted runtime checks where feasible.
    • Running the app in Docker when requested to verify critical paths.
    • For frontend behavior/appearance, a fast check is a headless Chrome/Chromium screenshot: <chrome> --headless=new --disable-gpu --screenshot=out.png --virtual-time-budget=2500 file://<repro>.html, then read the PNG. Build a minimal repro whose <base href> points at lpm-themes/default/css/ and that loads the real lpm-scripts/libs/* (jQuery, bootstrap.bundle.min.js) so the real CSS cascade and JS behavior are reproduced without the running app. If the checked element carries a FontAwesome icon, link the local font-awesome7/css/all.min.css (note the css/ segment — the CSS resolves its ../webfonts/ relative to it, and a CDN link may be unreachable from the sandbox): without it the glyphs render empty and the repro under-reports the real width.
      • Gotchas: headless Chrome's minimum layout viewport is ~500px wide, so --window-size widths below 500 still lay out at 500px while the PNG is the narrower width — right-aligned content (e.g. a navbar toggler) gets cropped off the image edge; screenshot at ≥500px to see it. Responsive @media breakpoints only apply if the repro has a <meta name="viewport" content="width=device-width, initial-scale=1"> (the app's page.html has none, so its media queries evaluate at the default ~980px). To verify a collapsed/expanded state directly, add/remove the .show class on the .collapse element in the repro.
  • Run PHP checks (e.g. php -l) inside the running Docker container, not host PHP. The host may run a newer PHP (e.g. 8.x) that flags PHP 7.3-valid syntax (like $str{...} offsets) as errors — false positives. Lint via .dev/bin/lpm lint <repo-relative-path> (container lightning-pm, PHP 7.3, repo mounted at /var/www/html).
  • For runtime verification of backend logic without a browser session, write a CLI harness that bootstraps the app: require_once '/var/www/html/lpm-core/init.inc.php'; new LightningEngine(); — the LightningEngine constructor wires params/auth/cache/PageConstructor WITHOUT routing (routing is in run()), so domain calls work (Issue::load(...), service methods, and template rendering via ob_start(); PagePrinter::x(...); ob_get_clean()). LightningEngine::getHost() reads SITE_URL (not $_SERVER), so it is CLI-safe. Run it with .dev/bin/lpm run <path/to/harness.php> — the file may live outside the repo (e.g. in a scratchpad). Query the dev DB with .dev/bin/lpm db "<SQL>"; it resolves the database name and credentials itself. Clean up any test rows the harness writes.
  • Run composer only within the container if needed and approved.

Common Tasks Cheat Sheet

  • Backend feature/fix:
    1. Update PHP in lpm-core/....
    2. Adjust templates in lpm-themes/... if needed.
    3. Wire JS in lpm-scripts/... for UI interactions.
  • Adding config:
    • Runtime:
      • template lpm-config.inc.template.php (do not commit secrets);
      • local lpm-config.inc.php (local only, not committed).
    • Docker:
      • template .dev/docker-env/.env.template;
      • local .dev/docker-env/.env (local only, not committed).
  • Logging:
    • Write to _private/logs/ if enabled; ensure directory exists and is writable.

Approval & Safety

  • Networked commands, dependency installs, or destructive actions require explicit approval.
  • Prefer reading and patching files over shell mutations.
  • Never commit secrets. Do not hardcode tokens or passwords.

Commit Messages

  • Commit messages must be in English
  • Use Conventional Commits style: type: summary or type(scope): summary.
  • Prefer a concise one-line summary, add detailed descriptions ONLY for important or big changes.
  • Common types: feat, fix, docs, refactor, test, chore, release.
  • Use refactor ONLY for behavior-preserving changes (pure code rearrangement, no change to UI, appearance, or behavior). Replacing one UI component with another (e.g. jQuery UI → Bootstrap) changes appearance/behavior and adds logic — that is a feat (or fix if it repairs broken behavior), not a refactor.
  • DO NOT add description of meaningless changes like "update changelog" unless this is ONLY committed change.
  • Again: do not mention changelog updates unless this is the only change.

Changelog Language

  • All entries in CHANGELOG.md must be written in Russian.
  • Entries describe the user-facing change/behavior only — no implementation details (CSS selectors, class names, function names, root-cause internals). Put the "how" in the commit message/code.
  • Be terse — no filler. Don't pad an entry by contrasting against the old state (e.g. avoid "вместо прежнего широкого блока …"); just state the change.
  • One entry = one sentence (plus an issue link, where there is one). Entries have been drifting long — if an entry runs to a second or third sentence, it is over-detailed, not thorough. Cut it back; the detail belongs in the commit message.
  • Name the change, don't enumerate its parts. Skip the list of UI elements it is made of (badge, frame, warning, button, counter), the per-case branches, the settings' default values and the "and also …" tail. A reader must learn what is now different for them, not how it was assembled — the rest they will see in the app.
  • If one change genuinely covers several independent user-visible things, prefer separate one-sentence entries over one entry with a list.

Picking the section

Decide by what stood in that place before the change, never by how the issue was worded:

  • nothing stood there → Added;
  • the same thing stood there and worked as designed, and now behaves differently → Changed;
  • the same thing stood there and did something it was never meant to do → Fixed.

Only the third case is Fixed. New functionality keeps landing in Fixed because issues are almost always filed as complaints — «пользователь не понимает, что произошло», «подсказка не работает», «задача навсегда остаётся в красном». That framing describes why the work started, not what the product does now, and it must not decide the section. Note that the commit type does not decide it either: a fix commit routinely produces an Added or Changed entry, because repairing a feature that never worked at all is new functionality for the reader.

Section order — fixed, never by arrival

Sections of a release always appear in this order, and only the ones that have entries appear at all:

  1. Added
  2. Changed
  3. Removed
  4. Fixed
  5. Security

The first three describe what the product is now made of, the last two what was repaired. Removed sits next to Changed because dropping a feature is a change in what exists. Security comes last as a deliberate callout — an ordinary security fix still belongs in Fixed; use Security only when the reader must act (rotate a token, re-check access). This matches the Keep a Changelog order, so Deprecated — if ever needed — goes between Changed and Removed.

The order has been drifting release to release; it is fixed now, so a reader finds the same shape every time.

Order inside a section — most important first

Within a section entries are ordered by how much the change matters to the reader, not by when it was merged. The one that changes the most people's day goes on top.

API entries

Changes to the external HTTP API are marked and kept together, because they concern a different audience than the rest of the product: nobody reading about the interface needs to wade through endpoint changes, and nobody integrating needs to guess which entries are theirs.

  • Start the entry with the API: prefix, then the description: - API: у задачи отдаётся подстатус — …. Do not write «Через внешнее API можно …» — the prefix carries that.
  • Keep all API entries of a section in one block at the end of that section, after the entries about the interface. Being last is not a judgement of importance; it keeps one audience's entries from splitting the other's.
  • Inside the API block the usual rule applies: the more important entry goes higher.

File Reference Style (for assistant responses)

  • Use clickable paths (e.g., lpm-core/base/LightningEngine.php:42). No ranges.
  • Wrap commands, paths, and identifiers in backticks.

Done Checklist (before handing off)

  • Changes are minimal, coherent, and consistent with style.
  • No stray debug statements or unused code.
  • Docs updated if behavior changed (or user requested).
  • For a feat/fix, CHANGELOG.md has a one-sentence entry under ## Next.
  • Provided short verification steps or commands, if applicable.
  • If a release was performed:
    • Current branch is develop.
    • Tag version/{version} exists locally and is pushed.
    • CHANGELOG.md contains the dated section for {version}.

Release Process

  • Verify version: set target in lpm-core/version.inc.php (VERSION).
  • Update changelog:
    • Move items from ## Next to ## {version} - {YYYY-MM-DD} in CHANGELOG.md.
    • Keep unrelated items under ## Next for future.
  • DB changes:
    • Nothing to do: migrations carry their own order and are applied on deploy. Do not edit anything in .dev/db/legacy/.
  • Commit on develop:
    • git add -A && git commit -m "release: {version}".
  • Merge to master and tag:
    • git checkout master && git merge --no-ff develop -m "merge: release {version}".
    • git tag -a version/{version} -m "Release {version}".
  • Push:
    • git push origin master develop --follow-tags
    • Multiple remotes:
      • git remote | xargs -I R git push R master --follow-tags
      • git remote | xargs -I R git push R develop
  • Return to develop:
    • git checkout develop and push if ahead.
    • Verify branch: git rev-parse --abbrev-ref HEAD → develop.
    • Verify clean tree: git status -sb shows no changes.
    • Verify tag exists: git tag -l 'version/{version}' (optional remote check: git ls-remote --tags origin 'version/{version}').