This file tells the coding assistant how to safely and efficiently work in this repository.
- 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.
- Backend: PHP 7.3 under
lpm-coreand related modules. - Frontend: Vanilla JS under
lpm-scripts, templates underlpm-themes, CSS inlpm-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) orlpm-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 vialpm-cli/migrate.php(seedocs/db-migrations.md). The pre-0.27.0 dump and change log are frozen in.dev/db/legacy/. CLAUDE.mdis a symlink to thisAGENTS.md— editAGENTS.mddirectly; writes through theCLAUDE.mdsymlink are refused.
- Two frameworks coexist: a legacy one bundled in
lpm-libs/gm-framework-v1.1.1.pharand the newerGMFramework\*underlpm-libs/framework/. The project is migrating off the legacy one. Watch for classes that still extend legacy bases (e.g.LPMOptions extends Optionsresolves to the phar'sOptions, which has nosaveOptions()/change()). - DB access: new code uses the V2 query builder
\GMFramework\DBQueryBuilderviaLPMBaseObjecthelpersbuildAndSaveToDbV2($sqlHash)andloadFromDV2()/loadAndParseV2(). Do NOT hand-write SQL strings or use the legacyDBConnect::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 likeoption/valueare safe) and escapes values; throw\GMFramework\ProviderSaveExceptionon 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 —
FROMis assembled from a list of table names — soROW_NUMBER() OVER (…)over a subquery has to be written by hand (seeIssue::getPositionInProjectFiltered(), and the pre-existingIssue::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.
- The one exception: a query the builder cannot express at all. It has no
window functions and no derived tables —
- Service layer (AJAX): JS calls
srv.<service>.<method>(args, onResult), which dispatch to a PHP service extendingLPMBaseService. There is NO dynamic dispatch: every method needs an explicit wrapper in the service map inlpm-scripts/lightning.js, otherwise the call throwsTypeErrorbefore any request. Readdocs/agents/frontend-js.mdbefore adding or changing a service call. - Model load misses return
false, notnull:Issue::load(),Issue::loadByIdInProject(),Project::load(), and similarStreamObject::singleLoad-backed loaders returnfalsewhen nothing is found. Guard withempty()/truthy checks (if (!$x)/empty($x)), NOT=== null— afalseresult slips past a!== nullguard 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 inPagesManager::__construct. Restrict a page to admins withreqRole = User::ROLE_ADMIN; the menu auto-filters bycheckUserRole(). Page/model classes are autoloaded vialpm-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->_patternand passing data withaddTmplVar(). - Active menu highlighting:
PagesManager::getLinks4Menu()marks a top-levelLinkcurrent when its pageuidequals the current page'sgetMenuSectionUid()(rendered inpage.htmlas the.activenav-link). A page's section defaults to its ownuid; a nested page with no menu entry of its own (notInMenu, e.g.ProjectPage,UserPage) declares which section it belongs to by setting$this->_menuSectionUidin 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'sLinkcurrent. - External HTTP API (
/api/v1): lives inlpm-core/modules/api/. Readdocs/agents/api.mdbefore adding, changing or removing an endpoint — it holds the routing/serialization contract and the three docs every endpoint change must update. - App options:
LPMOptionsis a singleton over thelpm_optionstable (option/value). Read withLPMOptions::getInstance()->prop, persist with the staticLPMOptions::save(['name' => $value, ...]). NotecookieExpireis 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. Readdocs/agents/ai-modules.mdbefore touching that directory or anything that generates or caches an AI artifact (adapters, prompts,sourceHash).
- Docker compose lives in
.dev/docker-env/.- Start: from
.dev/docker-env/rundocker-compose up(or-d). - Rebuild:
docker-compose up --buildifDockerfilechanges.
- Start: from
- Dev helper
.dev/bin/lpmwraps 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 rawdocker exec. The work tree it acts on is the one holding the.dev/bin/lpmyou 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 bundledcomposer.pharinlpm-libs/; a worktree has nocomposer.pharof its own, so the main checkout's copy is used). The autoloader is authoritative (optimize-autoloader+classmap-authoritativeincomposer.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 updatedlpm-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:.phpviaphp -lin the container,.jsvianode --checkon the host. Do NOT callnode --checkdirectly;lpm lintcovers 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-rolleddocker 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 fromlpm-config.inc.phpand the password is passed via the environment — never writedocker 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_MOUNTenv 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.
- Composer:
- PHP settings:
short_open_tag = On(seeREADME.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 inlpm-config.inc.php; ask the user for anything neither file answers.
originfetches over SSH (git.innim.ru:22, reachable only from the VPN) but pushes over HTTP on port 1414. Ifgit fetch originhangs 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 thegithubremote — do not retry SSH a second time.
- 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## Nextin 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-linkis 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 inCHANGELOG.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
PagePrintermethod that includes the template and expose it via an alias inlpm-core/aliases.inc.php(e.g.,lpm_print_goto_issue($project)), then call the alias in templates instead ofincludePattern()directly.
- Schema changes are migrations: create one with
.dev/bin/lpm migrate create <slug>(never hand-name the file — theYYYYMMDDHHMMSS_prefix defines apply order), apply with.dev/bin/lpm migrate apply. Readdocs/db-migrations.mdbefore writing one — it is the full contract (exec/execFile/t(LPMTables::X), noDELIMITER, no transactions, re-run-safe statements, never edit an applied migration, prefer additive changes). - Inside a migration raw SQL is expected — the
DBQueryBuilderrule above does not apply there. .dev/db/legacy/is frozen history — do NOT append tochanges-log.sqlor editdump.sqlany more.
- Stick to existing patterns in
lpm-scripts/*.jsand template files inlpm-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+ (subtlebg-*-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 firstnav-linkup with the brand/footer), drop the link padding (px-0) and space items withgap-*, 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.dialogwrapper inlpm-scripts/lightning.js— do not add jQuery UI dialogs or hand-rolled modals. Readdocs/agents/frontend-js.mdbefore 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>inlpm-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 inlpm-scripts/issues.js. - Don't put
titleon 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. Puttitleon the wrapping element (<a>/<button>— real line-box height) so the tip clears the top edge; keeparia-labelon that wrapper for the accessible name andaria-hidden="true"on the<i>. Do NOT "fix" tooltip spacing by adding a globaloffset— Bootstrap's delegated tooltip ignores per-elementdata-bs-offset(_getDelegateConfiguses only the shared config), and changing the shared offset shifts every tooltip in the app. window.lpmOptions(emitted byPagePrinter::jsOptions(), called via thelpm_print_js_options()alias inpage.htmlbefore 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 arraysvideoUrlPatterns/imageUrlPatterns(fromAttachmentVideoHelper/AttachmentImageHelper::URL_PATTERNS),roles({user, admin, moderator}), andissueUrlPattern(=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 (adata-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 thetitleon an inner<span>wrapping the icon (no component there, and a real line box — see the next bullet) and keep anaria-labelon the toggle for its accessible name — see the.menu inissue.html,main-menu-item.htmlandproject.html.lightning.jshides that inner tooltip onshow.bs.dropdown/show.bs.collapseso 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
.tooltipclass 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>"(optionaldata-copy-toast="<msg>"overrides the default "Скопировано" toast) — a global delegated handler inlpm-scripts/lightning.jscopies it and shows the toast, no per-widget JS needed. For dynamic/computed text uselpm.utils.copyToClipboard(text)/lpm.utils.copyRichToClipboard(html, plain)thenlpm.toast.show(...). - Bootstrap+jQuery gotcha:
lpm-scripts/lightning.jspolyfillsElement.prototype.hide()/show()to setstyle.display. Because jQuery invokes an element's native method matching a triggered event's base type, Bootstrap'shide.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/hidehooks that claim the default action so jQuery never makes that call. Do NOT add per-eventdisplayrestores inhide.bs.*/hidden.bs.*handlers: they are unnecessary, and two of them racing over the samestyle.displayis what broke the tooltip-on-inner-icon pattern. If ashow()/hide()polyfill is ever added for another element method, register the matching_defaulthook 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 likeLPMFile::isVideo()to check file types instead of MIME checks in templates, and prefer rendering viaPagePrintermethods. - At the top of each template, document all required external variables in a
Требуются:PHP comment, following the pattern used inlpm-themes/default/comment-text.html.
- 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 atlpm-themes/default/css/and that loads the reallpm-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 localfont-awesome7/css/all.min.css(note thecss/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-sizewidths 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@mediabreakpoints only apply if the repro has a<meta name="viewport" content="width=device-width, initial-scale=1">(the app'spage.htmlhas none, so its media queries evaluate at the default ~980px). To verify a collapsed/expanded state directly, add/remove the.showclass on the.collapseelement in the repro.
- Gotchas: headless Chrome's minimum layout viewport is ~500px wide, so
- 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>(containerlightning-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();— theLightningEngineconstructor wires params/auth/cache/PageConstructorWITHOUT routing (routing is inrun()), so domain calls work (Issue::load(...), service methods, and template rendering viaob_start(); PagePrinter::x(...); ob_get_clean()).LightningEngine::getHost()readsSITE_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.
- Backend feature/fix:
- Update PHP in
lpm-core/.... - Adjust templates in
lpm-themes/...if needed. - Wire JS in
lpm-scripts/...for UI interactions.
- Update PHP in
- Adding config:
- Runtime:
- template
lpm-config.inc.template.php(do not commit secrets); - local
lpm-config.inc.php(local only, not committed).
- template
- Docker:
- template
.dev/docker-env/.env.template; - local
.dev/docker-env/.env(local only, not committed).
- template
- Runtime:
- Logging:
- Write to
_private/logs/if enabled; ensure directory exists and is writable.
- Write to
- 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 must be in English
- Use Conventional Commits style:
type: summaryortype(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
refactorONLY 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 afeat(orfixif it repairs broken behavior), not arefactor. - 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.
- All entries in
CHANGELOG.mdmust 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.
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.
Sections of a release always appear in this order, and only the ones that have entries appear at all:
AddedChangedRemovedFixedSecurity
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.
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.
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.
- Use clickable paths (e.g.,
lpm-core/base/LightningEngine.php:42). No ranges. - Wrap commands, paths, and identifiers in backticks.
- 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.mdhas 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.mdcontains the dated section for{version}.
- Current branch is
- Verify version: set target in
lpm-core/version.inc.php(VERSION). - Update changelog:
- Move items from
## Nextto## {version} - {YYYY-MM-DD}inCHANGELOG.md. - Keep unrelated items under
## Nextfor future.
- Move items from
- DB changes:
- Nothing to do: migrations carry their own order and are applied on deploy. Do not edit anything in
.dev/db/legacy/.
- Nothing to do: migrations carry their own order and are applied on deploy. Do not edit anything in
- Commit on
develop:git add -A && git commit -m "release: {version}".
- Merge to
masterand 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-tagsgit remote | xargs -I R git push R develop
- Return to
develop:git checkout developand push if ahead.- Verify branch:
git rev-parse --abbrev-ref HEAD→develop. - Verify clean tree:
git status -sbshows no changes. - Verify tag exists:
git tag -l 'version/{version}'(optional remote check:git ls-remote --tags origin 'version/{version}').