Commit Graph
23264 Commits
Author SHA1 Message Date
59cd9d6282 Clone missing enterprise checkout in Cloud Agent install (#38117)
* Clone enterprise in Cloud Agent install when the multi-repo layout is missing.

Git-triggered automations only add mattermost/enterprise to the GitHub token scope; they do not check it out, so the install hook clones it when needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reject bare Git repos when locating the enterprise checkout.

git rev-parse --is-inside-work-tree exits 0 for bare repositories while
printing false, so require a real work tree. Document the $HOME/enterprise
fallback in AGENTS.md.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Honor config.override.mk during Enterprise detection and skip unusable sibling clone dests.

make evaluated BUILD_TAGS before reading the override, so a $HOME/enterprise fallback still shipped Team Edition; a bare ../enterprise also aborted install instead of falling back.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Match Enterprise branches in Cloud Agent clones

Prefer the corresponding Enterprise branch for PR-triggered environments while keeping clone publication safe and Cloud-only guidance scoped under .cursor.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Enterprise clone publication

Use an atomic no-clobber move instead of maintaining lock state in the ephemeral Cloud Agent environment.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-09 17:20:12 -04:00
786d22bce3 Land the React 19 fixes that stand on their own, ahead of the upgrade (#38390)
* fix(webapp): stop Card forwarding expand props to non-Card children

Card cloned every valid element child with expanded/disableExpandAnimation,
so a plain <div> child (e.g. the delete-policy warning notices in
policy_details) received them as unknown DOM attributes and React logged
'Received `true` for a non-boolean attribute `expanded`'. Only Header and
Body consume these props, so only forward to those.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* fix(webapp): key react-intl message chunks the way react-intl 8 does

React 19 marks elements cloned by React.Children.map as still needing a key,
which defeats the cloneElement-based workaround in react-intl 7 and makes every
rich-text message log a missing-key error. Backport the fix from react-intl 8,
which wraps each element chunk in a keyed Fragment instead.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* fix(webapp): stop rendering images with an empty src

React 19 reports an empty src as an error because the browser re-requests the
current page. Give the Jest image mock a non-empty value, fall back to the bot
icon when an Avatar has no url, and render the brand image preview from state
once the FileReader resolves instead of mutating an empty img.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): assert notification icons against the mocked image URL

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): stop opening nested act scopes around user interactions

React 19 rejects an act(...) scope entered while it is already flushing, and it
also errors when userEvent flips IS_REACT_ACT_ENVIRONMENT off inside an outer act.
Let userEvent manage its own act scope, and dispatch events fired from inside
layout effects directly.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): wait on the settled state instead of an already-present element

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): await rendered rows instead of the fetch spy in list tests

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): snapshot loaded lists instead of their loading placeholders

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* test(webapp): await rendered output instead of action spies

Under React 19 the commit that follows a resolved promise lands one flush
later than these assertions assumed, so each test read the DOM (or a
renderHook result published from an effect) from the previous commit:

- policy_details: the Delete policy card renders before fetchPolicy
  resolves, so the has-resources subtitle must be awaited.
- table_editor: becoming disabled wraps the button in WithTooltip, which
  remounts it, so a reference captured before the flip stays enabled
  forever; re-query inside waitFor.
- channel_invite_modal: searchUsers resolving is two renders ahead of the
  option appearing, since only the effect mirroring computed options into
  groupAndUserOptions feeds MultiSelect.
- use_rewrite: isProcessing starts false, so waiting on it is satisfied by
  the initial render; wait on originalMessage instead.
- team_details: onSave counts affected members over several awaits before
  opening the confirmation modal.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* chore(webapp): satisfy lint in the touched tests

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* fix(webapp): clear the brand image preview when a new file is selected

The preview effect only cleared brandImagePreview when brandImage was
absent, so replacing one selection with another left the previous data
URL on screen while the new FileReader was still pending -- and left it
there for good if that read never completed.

Clear the preview at the top of the effect instead, so it resets on
every brandImage change rather than only on the transition to empty.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
2026-09-09 16:05:26 -04:00
1c738a29fc Hide the System Console Recaps page unless EnableAIRecaps is on (#38331)
* Hide the System Console Recaps page unless EnableAIRecaps is on

The page was only gated on sysconsole_read_ai_recaps. After the
permissions migration that grants that to system_admin, Recaps
appeared even when the feature flag was off.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Update admin sidebar snapshots after hiding Recaps by default

With EnableAIRecaps off, the Recaps System Console item no longer
renders. Refresh the existing sidebar snapshots to match.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-09 19:57:06 +00:00
Edgar Bellot MicóandClaude Sonnet 5 f45e25d54b MM-69997: Fix bulk import account handling (#37718)
* MM-69997: Fix bulk import account handling

See MM-69997 for more details.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* MM-69997: Adjust ordering in bulk import account handling

See MM-69997 for more details.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 12:16:03 +02:00
Alejandro García Montoro a40861c414 MM-70511: Add (CreateAt, Id) indices on FileInfo, Channels (#38398)
* Add (CreateAt, Id) indices on FileInfo, Channels

* make generated

* Update expected indices in the Channels table
2026-09-09 11:03:41 +02:00
ea98f361ca MM-70543: Cap single image previews at 700px wide (#38406)
Co-authored-by: Nevyana Angelova <nevyangelova@Nevy-Macbook-16-2025.local>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-09 11:38:12 +03:00
Jesse Hallam bca77a7764 Select the French language label the code actually renders (#38407)
site_statistics_spec.js picks French from the display language dropdown by
its label, and asked for 'Français (Beta)'. i18n.ts has said
'Français (Alpha)' for some time, so cy.findByText never matched the option
it meant to click.

The other spec that touches this dropdown selects 'English (US)' and
'English (Australia)', neither of which carries a quality suffix, so this is
the only stale selector of its kind.
2026-09-09 13:38:53 +08:00
e75e823ff2 E2E/Playwright: Migrate active Cypress tests (batch 1 of 4) (#37454)
* E2E/Playwright: Migrate active Cypress tests batch 1

* E2E/Cypress: Retire migrated prod tests batch 1

* E2E/Playwright: Address Cypress batch 1 review

* E2E/Playwright: Fix inline image spec lint

* E2E/Playwright: Consolidate batch 1 page objects

* E2E/Playwright: Verify selected file preview names

* E2E/Playwright: Reuse merged POMs in Cypress batch 1

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: saturnino <saturnino@mattermost.com>
2026-09-09 03:46:40 +00:00
sabrilandMattermost Build 050c5c8040 chore: update dependencies in cypress (#38016)
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-09 02:36:23 +00:00
fe9a36e80e E2E/Playwright: rolling upgrade tests (#38367)
* E2E/Playwright: keep server state across a container swap

`/mattermost/config` and `/mattermost/data` are Dockerfile VOLUMEs, so
replacing the container factory-reset the config and dropped uploads. Store
the config in Postgres and bind-mount the data directory to
`.mattermost_data/` instead.

The server writes there as UID 2000 into 0750 dirs, so on Linux the host
user cannot delete them: resetting on a fresh boot threw EACCES and failed
global setup on every second boot, and so on every retest. Clear from a
container when the host cannot.

Adds the library pieces the upgrade projects need — image swapping, phase
log capture with boot env redacted — and fixes the specs this exposed.

* E2E/Playwright: add the upgrade-path harness

The suite only ever ran against a server booted fresh on the new image, so
an upgrade-only regression would surface first in a customer's upgrade.

Three projects perform an in-place upgrade: `upgrade-from` seeds an older
image and records a baseline, `upgrade-swap-to` replaces only the server
container, `upgrade-to` re-verifies the baseline. API-first, since older
images ship older web apps.

From-versions come from the published support policy at run time — the last
three minors plus any active ESR, strictly older than the version under test.

* E2E/Playwright: run rolling upgrades in CI, one status per from-version

An entry workflow resolves the from-version matrix and calls a template once
per version, in parallel. Each run posts its own commit status, so a failure
names the version it came from; an empty matrix posts `upgrade-from-none`.

Every worker upgrades its own server, then draws from the full suite against
it with no re-preparation in between — an upgrade needing the suite's setup
re-run is not a passing upgrade.

Runs on release cut and on demand. Not on merge: it costs `matrix x workers`
runners. On PRs, only when the diff touches the harness or these workflows.

* E2E/Playwright: address review findings

- Keep the to-phase loader read-only. It patched EmailSettings, FileSettings,
  SiteURL, AccessControlSettings and PluginSettings on the upgraded server
  before any assertion, so config the upgrade dropped was repaired instead of
  reported — the opposite of what this harness is for.
- Return the raw post ID for a combined post. `<postId>:<timestamp>` has the
  same shape as a pending_post_id, so `getId` retried and then threw on
  posts that were already permanent.
- Point `npm run clean` at `.mattermost_data`; it still removed the old
  `local_storage` name.
- Trigger rolling upgrades on `package-lock.json` too, and document the
  full path set.
- Quote the license placeholder in the local run instructions: bare
  `<key>` is a shell redirection, not a value.
- Quote `$GITHUB_OUTPUT` in the two rolling-upgrade workflows.

* E2E/Playwright: retarget the rolling-upgrade auto-trigger

Every file under `lib/` matched, so any page-object edit fired the pipeline
— `matrix size x workers` runners, each performing its own upgrade. Narrow
the harness side to what actually defines an upgrade run: `upgrade-specs/`,
the matrix resolver, and the rolling-upgrade workflows.

Add the server surfaces an upgrade has to survive, which had no trigger at
all: schema and config migrations, the app-layer data migrations, and the
version list the from-version matrix is derived from. A new numbered
migration is exactly what `upgrade-to`'s "schema migrations match baseline"
check exists to catch.

* E2E/Playwright: use repo-root paths in the trigger table

`model/version.go` and `app/migrations.go` were ambiguous; spell them from
the repo root so a contributor can match them against a diff.

* E2E/Playwright: align rolling-upgrade trigger docs with the implemented matcher

The trigger prose and table in e2e-tests-workflows.md still listed
e2e-tests/playwright/lib/ and playwright.config.ts as auto-trigger paths,
and marked merge-to-master/release-* as an auto-trigger. Neither matches
e2e-tests-ci.yml's check-changes step: those two paths are unmatched and
require the manual opt-in checkbox, and check-changes is never even
called with run_rolling_upgrades=true on a merge (e2e-tests-on-merge.yml
never passes it). Align both descriptions with the implemented matcher
and with README.md's already-correct table.

Co-authored-by: sabril <saturninoabril@users.noreply.github.com>

* E2E/Playwright: fix write-after-end crash in collectLogs() teardown

container.logs() follows a running container, so a stack left running
(PW_TESTCONTAINERS_REUSE=true) never emits 'end' on its own. The 10s
stall timeout then called outFile.end() while the source was still
piping into it, so the next chunk landed on the closed destination and
crashed the process with an unhandled ERR_STREAM_WRITE_AFTER_END. This
intermittently killed rolling-upgrade worker jobs (seen failing the
upgrade-from-release-11.10 lane on this PR).

Unpipe and destroy the source before ending outFile, and add an error
handler on outFile, so no write can land after it closes.

Co-authored-by: sabril <saturninoabril@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: sabril <saturninoabril@users.noreply.github.com>
2026-09-09 10:14:57 +08:00
Jesse Hallam 3b88476e31 Add a test pinning supportedLocales to the shipped catalogs (#38408) 2026-09-08 19:07:22 -03:00
Jesse Hallam 5e6f509f36 Add deterministic i18n checkers for the server and webapp locale catalogs (#38231)
* Add deterministic i18n checkers for both surfaces

Nothing in CI reads the locale catalogs. webapp/channels has an
i18n-verify-translations script -- `formatjs verify --structural-equality` --
but no workflow calls it, and it has no extra-key check. The server has nothing
at all. So a translation that throws at format time, invents a variable the
source does not have, or carries an id en.json retired is invisible until a
user in that language hits it.

Webapp: scripts/check_icu.mjs, wired up as the existing i18n-verify-translations
script. It imports @formatjs/icu-messageformat-parser, declared as a dependency
of channels and pinned to 2.11.4 -- the copy react-intl 7.1.14 resolves -- so
"parses here" really does imply "parses there". The pin is load-bearing: three
versions coexist in the tree, and a bare import resolved to the 2.4.0 that
babel-plugin-formatjs hoists to the root, the build-time transformer's grammar,
seven minors behind the parser that actually renders the catalogs, and a dedupe
could have moved it again without notice. The header records that the pin has
to move with react-intl.

The checks are in two layers. Layer 1 is the runtime guarantee and always
fails: valid JSON, string values, key parity with en.json, every value parses,
and no variable or tag the source does not have (an unknown variable throws
MISSING_VALUE at format time; an unknown tag renders as raw markup). Layer 2 is
fidelity: a dropped variable or tag, a variable demoted to a different type --
{count, plural, ...} becoming a bare {count} still parses and renders, it just
silently stops pluralizing -- and an ASCII apostrophe before < or {, which in
ICU opens a quoted literal that swallows the tag and everything after it. That
last one still parses, so this check is the only thing that catches it.

Type parity compares the set of types each variable is used as rather than a
single type per variable. A name is routinely used in two roles at once --
"{count, number} new {count, plural, one {message} other {messages}}" -- and a
type-per-name map mis-flags every one of those.

The comparison is asymmetric, because only one direction is a defect. Demotion
loses the plural. Promotion is how a translator makes a sentence grammatical:
English needs a plural far less often than the languages it is translated into
-- "{count} items were deleted" is one sentence in English and four in Russian
-- and react-intl reads the value out of the same values object either way. A
PROMOTIONS table names the roles that are safe to widen into and everything
else stays fatal. The formatting types stay out of it: {x, number} over a
string renders NaN and {x, date} over a non-date renders "Invalid Date", so
reaching for one the source did not use is a defect rather than a grammar fix.

Server: `mmgotool i18n verify` and `make i18n-verify`. The strongest check
available there is the loader itself: go-i18n's LoadTranslationFile is what the
server runs at startup, so a file it rejects is a file that breaks the server.
Each locale loads into its own bundle rather than the package-global one, so
one file cannot mask a defect in another depending on the order they happen to
be walked in, and the checks can be run in isolation. On top of the loader, id
parity, {{.Field}} parity in both directions, and exactly the CLDR plural
categories the locale uses -- stricter than the webapp, because go-i18n rejects
a category the locale does not have.

Plural shape is compared against en.json in the collapsing direction too, since
flattening a pluralized entry into a single string still loads and still
renders and just silently stops pluralizing. The opposite direction is left
alone: whether a count reaches go-i18n is a property of the call site, not of
en.json -- the batched email title is a plain string in English and pluralized
in six locales, and email_batching.go passes len(notifications)-1 -- so a
language that has to inflect where English does not is translating correctly.

Empty plural forms are flagged as well. countEmptyItems and
removeEmptyTranslations both test the raw JSON for `""`, so every plural map
slips past them, and go-i18n builds no template for an empty form, finds none
at format time, and returns the translation id. Empty single-string
translations render that same raw id and are deliberately left alone: several
hundred already exist across es, fr and it, and `i18n clean-empty` already
removes exactly that shape.

There is no allowlist. A translation that deviates from its source is either
wrong, or the source is encoding English grammar the other languages cannot
follow -- and both of those are worth fixing rather than recording. Every key
that wanted an exemption while this was being written turned out to be the
second kind, and the four source strings responsible landed upstream in #38228,
so the corpus passes with nothing exempted. If a case ever turns up that
genuinely is a property of a target language, the mechanism is ten lines and
should be scoped to key and locale, which a real example would show how to do.

Both take a flag (--warn-missing-keys / --warn-missing-ids) that downgrades
missing entries to warnings, since key parity is a separate problem from
whether the entries that exist are correct.

Pulling in go-i18n resolves the minimum versions its go.mod requires --
pelletier/go-toml v1.2.0 and yaml.v2 v2.2.1, both with known advisories. Pinned
to the versions the server module already resolves, v1.9.5 and v2.4.0, so the
tool module and the server agree rather than each carrying its own copy.

The server tests cover each check the command makes: the runtime loader gate,
id parity in both directions and under --warn-missing-ids, template token
parity, plural shape against en.json in both directions, CLDR categories per
locale, and empty plural forms. They also pin the two behaviours the command
walk depends on, that en.json, non-JSON files and subdirectories are skipped.
Writing them turned up that the "no CLDR plural spec" branch is unreachable:
the loader derives the language with language.Parse, which only returns one
when GetPluralSpec is non-nil for the same tag, so a file that loaded always
has a spec. It is left in place as an invariant guard and labelled as one, with
the test asserting what an unrecognized filename actually does.

The webapp tests drive the real script in a subprocess, since its contract is
its exit code: 0 clean, 1 defects found, 2 misuse. They cover well-formedness,
key parity in both directions and under --warn-missing-keys, variable and tag
parity including names nested inside plural branches, the argument role rules
in both directions, and the apostrophe check with its doubled and typographic
escapes. The first test asserts that the resolved ICU parser is the version
react-intl depends on, which is the property the rest of the suite rests on and
the one that silently regressed before. It fails against a node_modules tree
installed before that pin landed; npm ci fixes it.

Both are wired into the i18n workflow, which until now only ever looked at
which files changed. The existing check-files job stays as it is -- reporting a
direct edit to a non-English catalog is still worth doing, and it answers a
different question from whether the edit is correct.

Both run with missing entries downgraded to warnings (--warn-missing-keys /
--warn-missing-ids). Key parity is a real gap, 55,164 entries on the webapp and
21,748 on the server, but it is a separate problem from whether the entries
that do exist are valid, and a hard parity gate is only fair once a contributor
adding a string has a documented way to produce the other 21 locales.

The workflow triggers on server/i18n/** and webapp/channels/src/i18n/**, and on
the checkers themselves -- check_icu.mjs, mmgotool, and the workflow files. It
is the only workflow that invokes either checker, so without those paths a PR
touching one of them never ran it and a broken checker could land green. That
matches how webapp-ci.yml already lists its own workflow and composite action.

The catalogs do not pass yet; the three commits that follow repair them.

* Fix the locale catalog defects the checkers found

Running the new checkers over the shipped catalogs turns up 2,474 webapp errors
and 1,495 server errors. They are not stylistic: 98 webapp strings fail to parse
with the parser react-intl runs, so they throw the moment a user in that
language opens the screen. vi has 36 of them, ro 18, fa 15. Another 395 name a
variable or tag the source does not have, which throws MISSING_VALUE or renders
raw markup, and on the server 57 interpolate a {{.Token}} that is not there and
render "<no value>".

Two kinds of change here, and no new translation of anything.

Repaired mechanically, where the intended text is unambiguous:

  webapp  74  variable or tag the source renamed -- {fullUsers} -> {fullSeats},
              {displayname} -> {displayName}, {linkApi} -> {linkAPI}. Paired
              only when each unknown name has one clear counterpart.
          17  an apostrophe opening an ICU quoted literal. After a letter it is
              an elision, so it becomes the typographic apostrophe -- French
              l'<link>; otherwise it is a quote mark and every lone apostrophe
              in the string is doubled, which is the ICU escape and keeps an
              opening and closing pair matched.
          15  a translated variable name -- {потребителско име} for {username}.
              These are MALFORMED_ARGUMENT, so the string never parsed at all.
           3  a pseudo-tag the source quotes deliberately, '<blank>', copied
              without the quoting and so an UNCLOSED_TAG.
  server  22  a plural branch for a category the locale does not have. go-i18n
              rejects those outright; pt-BR, es, fr and it carry a "many" that
              its CLDR spec does not define.
          14  a {{.Token}} the source renamed.

Removed, because the entry translates an English string that no longer exists:

  webapp  1285  keys en.json no longer has
           702  translations whose structure no longer matches the source
  server   809  ids en.json no longer has
           497  translations whose tokens or plural categories no longer match

Removing is the conservative option, not the destructive one. react-intl falls
back to the English defaultMessage for a key a catalog does not carry, and the
server falls back to the id; what these entries render today is a stray
'<your-mattermost-url>' literal, unrendered markdown, a missing hyperlink, or an
exception. The catalogs are already 55,164 webapp and 21,748 server entries
short of parity, so this does not change what a translator has to do next -- it
changes what a user sees in the meantime. Every removed entry is recoverable
from history if someone wants to repair rather than retranslate it.

Nothing here is exempted, because there is nothing to exempt with. Six keys
wanted an allowlist entry over the course of this work, and every one turned out
to be the same thing: an English sentence assembled from parts, forcing
translators into a shape the source did not have. The four source strings
responsible are fixed in the commit this branch is stacked on, which is why the
checker ships without an allowlist at all.

Both checkers now pass with --warn-missing-keys / --warn-missing-ids. Key parity
is a separate problem and is left as warnings.

* Fix the French batched email title plural forms

The entry used the CLDR category "many", which the go-i18n plural spec for
French does not define, and left "other" empty. French resolves any count of
two or more to "other", so every French batched email digest covering more
than one message shipped with an empty subject line.

Move the plural text into "other" and drop the unused category.

* Remove the half-translated plural entries from six locale catalogs

Eighteen entries across bg, es, fa, fr, it and ro filled "one" and left the
remaining categories empty. An empty form is worse than an absent translation:
go-i18n builds no template for it, finds none at format time, and returns the
translation id, so a Spanish user inviting two people who are already members
saw "api.command_invite.user_already_in_channel.app_error".

Deleting the entry lets the en fallback serve real English instead. No
translation is invented here -- the singular text goes away with the entry,
because a catalog that answers only for count == 1 cannot be served safely.

* Use singular agreement in the Italian and Romanian out-of-channel mention strings
2026-09-09 04:36:16 +08:00
d2cec875b8 [MM-68252] Close the schedule message menu after picking a time on mobile web (#38288)
* [MM-68252] Close the schedule message menu after picking a time on mobile web

In mobile view the menu renders as a modal that only dismisses once the
click reaches the list wrapping the menu items, so the stopPropagation
call in handleOnSelect left the menu open after scheduling and let the
same draft be scheduled again.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Cover the mobile menu modal's dismiss-on-select behavior

Pins the invariant the fix relies on: a plain menu item closes the mobile
modal, while an item that opts out of closing on select does not.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Add E2E coverage for the mobile schedule message menu

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-09-08 13:32:41 -04:00
7eebdb8f94 Migrate from react-beautiful-dnd to @hello-pangea/dnd (#38320)
* refactor(webapp): import JSX from react instead of the global namespace

React 19 removes the global JSX namespace, so JSX.Element and friends have to
come from react itself. The scoped JSX namespace already exists in
@types/react 18 as an alias of the global one, so this compiles identically
today and stays a no-op until the React 19 upgrade lands.

Generated with:
  npx types-react-codemod@latest scoped-jsx ./channels/src ./platform \
    --ignore-pattern "**/{node_modules,dist,build}/**"

Then brought in line with lint: the codemod spaces its braces, and where a
file already had a separate react type import it left two react imports.
Note that eslint --fix cannot resolve the latter on its own; its
import/no-duplicates autofix merges a default import with a type import and
produces invalid syntax, so the JSX specifier was moved by hand instead.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* refactor(webapp): spell out ReactElement's any props

React 19 changes ReactElement's default props type from any to unknown.
Stating any explicitly keeps these call sites behaving as they do today once
that lands. @types/react 18 already defaults P to any, so this is a no-op
refactor for now.

Generated with:
  npx types-react-codemod@latest react-element-default-any-props \
    ./channels/src ./platform --ignore-pattern "**/{node_modules,dist,build}/**"

Followed by eslint --fix for the codemod's brace spacing.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* refactor(webapp): replace the deprecated ReactNodeArray type

ReactNodeArray is removed in React 19. It is declared as
ReadonlyArray<ReactNode> in @types/react 18, so the replacement is equivalent
today.

Generated with:
  npx types-react-codemod@latest deprecated-react-node-array \
    ./channels/src ./platform --ignore-pattern "**/{node_modules,dist,build}/**"

Followed by eslint --fix, which rewrites the codemod's ReadonlyArray<T> into
the readonly T[] form this repo's array-type rule requires.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* refactor(webapp): migrate from react-beautiful-dnd to @hello-pangea/dnd

react-beautiful-dnd has been unmaintained since 2022 and does not support
React 19, which makes it a blocker for that upgrade. @hello-pangea/dnd is the
maintained fork, with an unchanged API surface, and declares
react: ^18.0.0 || ^19.0.0, so this lands and is verified on React 18 today.

Two workarounds go away with it:

- patches/react-beautiful-dnd+13.1.1.patch, 109 lines of vendored edits that
  stripped connect()'s `pure: true` option, removed in react-redux 8.
- the "react-beautiful-dnd": {"react-redux": "9.2.0"} npm override that forced
  the library onto a react-redux it did not declare support for.

The fork also drops a duplicate nested copy of redux, plus use-memo-one and
memoize-one, from the tree.

The library renames its generated DOM attributes from data-rbd-* to data-rfd-*,
which is the one behavioural change to watch. SidebarList reads the categories
droppable back out of the DOM by that attribute to freeze the scroll box during
a drag, so a stale selector there makes querySelectorAll return nothing and
throws "Cannot read properties of undefined (reading 'style')" on drag start,
breaking sidebar drag and drop entirely.

The existing coverage could not catch that: it mocks document.querySelectorAll
to return a stub element, so the selector string is never exercised. Added a
test that drives onBeforeCapture and onDragStart against the real rendered DOM
instead, and confirmed it fails with exactly that TypeError when the data-rbd-*
selector is restored.

Snapshots are regenerated for the attribute rename. Module federation shares
'@hello-pangea/dnd' in place of 'react-beautiful-dnd' so product modules keep
resolving to the host's single instance.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
2026-09-08 14:37:18 +00:00
80fc4302ae Apply React 19 type codemods ahead of the upgrade (#38313)
* refactor(webapp): import JSX from react instead of the global namespace

React 19 removes the global JSX namespace, so JSX.Element and friends have to
come from react itself. The scoped JSX namespace already exists in
@types/react 18 as an alias of the global one, so this compiles identically
today and stays a no-op until the React 19 upgrade lands.

Generated with:
  npx types-react-codemod@latest scoped-jsx ./channels/src ./platform \
    --ignore-pattern "**/{node_modules,dist,build}/**"

Then brought in line with lint: the codemod spaces its braces, and where a
file already had a separate react type import it left two react imports.
Note that eslint --fix cannot resolve the latter on its own; its
import/no-duplicates autofix merges a default import with a type import and
produces invalid syntax, so the JSX specifier was moved by hand instead.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* refactor(webapp): spell out ReactElement's any props

React 19 changes ReactElement's default props type from any to unknown.
Stating any explicitly keeps these call sites behaving as they do today once
that lands. @types/react 18 already defaults P to any, so this is a no-op
refactor for now.

Generated with:
  npx types-react-codemod@latest react-element-default-any-props \
    ./channels/src ./platform --ignore-pattern "**/{node_modules,dist,build}/**"

Followed by eslint --fix for the codemod's brace spacing.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

* refactor(webapp): replace the deprecated ReactNodeArray type

ReactNodeArray is removed in React 19. It is declared as
ReadonlyArray<ReactNode> in @types/react 18, so the replacement is equivalent
today.

Generated with:
  npx types-react-codemod@latest deprecated-react-node-array \
    ./channels/src ./platform --ignore-pattern "**/{node_modules,dist,build}/**"

Followed by eslint --fix, which rewrites the codemod's ReadonlyArray<T> into
the readonly T[] form this repo's array-type rule requires.

Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
2026-09-08 09:22:53 -04:00
Alejandro García MontoroandMattermost Build a66e9c034b MM-70511: Optimize FileInfo and Channels bulk-indexing queries (#38206)
* Add (CreateAt, Id) indices on FileInfo, Channels

* Optimize FileInfo and Channels indexing queries

* make generated

* Update expected indices in the Channels table

* Remove indices

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-08 11:56:56 +00:00
Andre Vasconcelos 0d79d72074 Bumping prepackaged version of the zoom plugin to v1.13.1 (#38339) 2026-09-08 14:49:13 +03:00
Edgar Bellot Micó 9c8d4164ab MM-70074: Fix output formatting issue in mmctl (#37794) 2026-09-08 10:25:55 +02:00
d611ca8f4a chore: always render with createRoot and drop the EnableConcurrentReact flag (#38391)
The flag has defaulted to true since it was enabled server-wide, so concurrent
React is already what every user gets. Calling createRoot unconditionally makes
that the only path, which also removes the ReactDOM.render call that React 19
deletes, and leaves the flag with nothing left to switch: it goes from the web
app, the server's root.html script injection and its CSP hash, and the e2e
config.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
2026-09-08 06:59:26 +00:00
Eva Sarafianou 904ee27f26 docs: emit redirect stubs at both <from>/index.html and <from>.html (#38389)
active-redirects.json entries whose `from` lives outside the docs
CloudFront function's frozen legacy-Sphinx prefix set currently 404 in
production. The stub file exists in S3 at <from>/index.html but the CFF
(mattermost-iac-docs-prod -> 05-cdn -> clean-url-rewrite.js) only serves
that key for the frozen legacy prefixes; for any other first path
segment it falls back to <from>.html, which doesn't exist for
plugin-client-redirects stubs.

Add a postbuild step that duplicates every stub at the sibling flat
key. The legacy branch keeps hitting <from>/index.html unchanged; the
fallback branch now finds <from>.html. Real content pages are never
overwritten (the script skips any path where <from>.html already
exists), .html-suffixed from-values are skipped (CFF Phase A
canonicalizes them before Phase B routes them), and .. traversal in a
malformed from is rejected before any filesystem call.

Smoke-tested against a local build: 741 stubs duplicated, 0 real
content pages touched, byte-identical meta-refresh content.
2026-09-08 09:41:56 +03:00
Ben SchumacherandClaude Opus 4.8 03a30e9183 [MM-70592] Fix read-only settings for section-based plugins without a footer (#38384)
The enable setting is hidden, so its config value only reaches the form
state when the setting is part of the schema. In the sections branch it
was added only when the schema had a footer, so a section-based plugin
without one never seeded PluginSettings.PluginStates.<id>.Enable, and
stateIsFalse read undefined and disabled every setting even while the
plugin was running.

Always unshift the hidden enable setting in the sections branch, and
assert editability across all three schema shapes (flat, sections with a
footer, sections without one).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 08:41:03 +02:00
Eva Sarafianou 597d9c4296 docs/site: add legacy Sphinx URL redirects for 281 URLs, fix 3 broken targets (#38387)
* docs/site: add legacy Sphinx URL redirects for 281 URLs, fix 3 broken targets

Adds 281 new entries to active-redirects.json for legacy Sphinx URLs that
currently return 404 on docs.mattermost.com, and corrects 3 existing entries
whose 'to' pointed at pages that were moved after the initial migration.

Sources verified from:
- source .rst/.md files in mattermost/docs Sphinx repo
- redirects_map in source/redirects.py (sphinx_reredirects extension)
- old-Sphinx CDN at d3r1ild9aa68qq.cloudfront.net (still serving legacy URLs)

Every target was live-probed against docs.mattermost.com (200, no soft-404).

Release Note:
```release-note
NONE
```

* docs/site: fix two malformed redirect targets

- /guides/install-deploy-upgrade-scale: strip extra leading slash
  (//deployment-guide/... -> /deployment-guide/deployment-architecture).
- /overview/index.html: strip stray space ("/ " -> "/") so the
  target is the docs root instead of a whitespace-encoded path.

Both hand-edited values in the source CSV; the initial verify pass
missed them because HTTP silently collapses '//' and encodes ' ' -> '%20'
which still lands on the root. Added a small validator that flags
non-'/'-prefixed, leading-'//', or whitespace-in-path 'to' values, plus
duplicate 'from' values.

Release Note:
```release-note
NONE
```

* docs/site: drop trailing slash on /developer/before-you-get-started target

Docusaurus is configured with trailingSlash: false; all other 'to' values
in active-redirects.json omit the trailing slash. Normalize this one entry
for consistency.

Release Note:
```release-note
NONE
```
2026-09-07 18:47:45 +03:00
unified-ci-app[bot] f301b5310e Update latest version to 12.0.0 (#38378)
Automatic Merge
2026-09-07 16:12:42 +03:00
Amy Blais 53aaf6a4a8 Move changelog generation automation to server repo (#38113)
Automatic Merge
2026-09-07 15:42:35 +03:00
unified-ci-app[bot] 3ed4cb03b3 chore: Update NOTICE.txt file with updated dependencies (#38376)
Automatic Merge
2026-09-07 15:13:08 +03:00
2266df0c33 [MM-68319] Stop capping OutgoingIntegrationRequestsTimeout at 30 seconds (#38103)
* [MM-68319] Stop capping OutgoingIntegrationRequestsTimeout at 30s

Outgoing integration requests apply
ServiceSettings.OutgoingIntegrationRequestsTimeout as a per-request
context deadline, but the clients making them also carried the default
30s http.Client.Timeout, so any configured value above 30 seconds was
silently capped.

Add HTTPService.MakeClientWithTimeout so the shared outgoing webhook
client and the post action clients can be built without an end-to-end
timeout of their own, leaving the configured deadline in charge. Other
clients keep the default RequestTimeout.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* [MM-68319] Cover outgoing integration client timeouts with tests

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* [MM-68319] Fall back to the configured timeout without a request deadline

Post action clients take their deadline from the request context. Keep a
request that arrives without one bounded by
OutgoingIntegrationRequestsTimeout instead of letting it run unbounded,
and assert the effective deadline behaviourally rather than only reading
the client timeout.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* [MM-68319] Tighten outgoing integration timeout test assertions

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Rename MakeClientWithTimeout to MakeClientWithoutTimeout and drop post-action fallback

Address review feedback on getPostActionClient: every caller of DoActionRequest
sets a context deadline derived from OutgoingIntegrationRequestsTimeout, so the
no-deadline fallback that re-applied a client-level timeout was over-protective.
Removing it means all callers construct the client without a timeout, so the
HTTPService method now takes no timeout argument and is named for that intent.

* Address PR feedback: 0 answered, 1 resolved, 0 declined

Restore the configured-timeout fallback in getPostActionClient: when the
request context carries no deadline, bound the client at
ServiceSettings.OutgoingIntegrationRequestsTimeout instead of running
unbounded. Production callers always set a deadline, but the exported
DoActionRequest can be reached by deadline-less callers (tests,
out-of-tree), which previously regressed from the 30s MakeClient bound to
no bound at all.

* Address PR feedback: cancel outgoing integration requests on shutdown

The shared outgoingWebhookClient no longer imposes a client timeout, and
outgoing webhook requests run inside Srv().Go goroutines that Shutdown waits on.
Their contexts descended from context.Background(), so with a large configured
OutgoingIntegrationRequestsTimeout graceful shutdown could block for the whole
timeout while a hung endpoint kept the request open.

Add a server-lifetime context to PlatformService (GoContext) that is cancelled
during shutdown before waiting on tracked goroutines, and derive the outgoing
integration request contexts (post actions, dialogs, outgoing webhooks, slash
commands) from it so shutdown cancels them promptly instead of blocking.

* Address PR feedback: 2 answered, 1 resolved, 0 declined

* Address PR feedback: keep MakeClientWithoutTimeout off exported HTTPService

Revert the added HTTPService.MakeClientWithoutTimeout interface method to avoid
a breaking change to the public server/public module. The two call sites now
use MakeClient and clear the client's Timeout after construction instead.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-07 13:13:21 +02:00
Pablo VélezandMattermost Build b4b70b9218 MM-70570 - Fix flaky ESC blur test by enabling fake timers (#38316)
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-07 12:34:00 +02:00
15abf3275f [MM-67612] Show the default channel URL as read-only instead of failing the save (#38110)
* Lock the default channel URL in the channel name form field

The server rejects any change to the default channel's `name`
(`api.channel.update_channel.tried.app_error`, HTTP 400), but the webapp
still offered a URL Edit button on town-square in both the Channel
Settings Info tab and the legacy Rename Channel modal. Saving failed with
the generic "There are errors in the form above", which does not tell an
admin why the update was refused.

Present the URL read-only with an explanation for the default channel.
The display name stays editable, since the server does allow that.

`URLInput` gains generic `readOnly` and `helpText` props; when read-only
it renders neither the Edit/Done button nor the editable input, including
when an error would otherwise force it open.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Add E2E coverage for the default channel URL lock

Cover both directions against a real server: the default channel offers no
URL edit and still saves its display name, and an ordinary channel can
still have its URL changed.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Use shared Button for the URL input edit control

Replace the bespoke Edit/Done <button> with Button from
@mattermost/shared so the control follows the webapp style guide.
Keep quaternary + extra-small sizing, and override .btn chrome so the
compact link appearance is unchanged.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Lock the channel URL when ChannelNameFormField is read-only

When the user cannot manage channel properties, the display name is
already disabled. Pass that same readOnly flag through to URLInput so
ordinary channels do not still offer Edit.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-09-07 11:20:14 +02:00
Jesse Hallam cd9bcb005e chore(ci): warm Go module and build cache daily, restore read-only in CI (#38285)
* chore: extract install-<tool> Makefile targets

Pull each `go install` of a build tool into its own `install-<tool>`
target so the installs are declared once and reused as prerequisites,
and add `install-ci-tools` plus a `go-cache` target that pre-warms the
Go module and build caches.

Also collapse the duplicate `vet-api` target declarations into one.

* chore(ci): add restore-go-cache composite action

* chore(ci): mount warmed Go cache into run-in-buildenv

* chore(ci): add daily Go module/build cache warm workflow

* chore(ci): restore warmed Go cache read-only across server CI jobs

* Simplify Go cache key to glob-match all go.mod and go.sum files

* make GOBIN explicit

* Pass Go cache paths as step outputs, not GITHUB_ENV

restore-go-cache wrote GOMODCACHE/GOCACHE/GOBIN to GITHUB_ENV so later
steps would pick them up. zizmor flags any GITHUB_ENV write as a possible
code-execution vector, since an attacker-controlled value there can set
LD_PRELOAD or NODE_OPTIONS for every subsequent step.

Nothing untrusted reaches these particular values, but the write is also
unnecessary: expose them as action outputs and map them into the one step
that actually needs them, matching what 8f6d17d875 did for the buildenv
actions.

Only server-test-template's shard split consumes them outside the action.
Everything else either runs in the buildenv container, which gets the
container paths via explicit -e, or reads RUNNER_TEMP directly. That step
runs `go work` and shard-split.js's `go test -list` on the host, so it
takes GOMODCACHE and GOCACHE via env; GOBIN is omitted because the host
never invokes the tools in server/bin.

Also drops the `${{ github.workspace }}` interpolation in favour of the
runner's GITHUB_WORKSPACE, so no expression is expanded into the script.

zizmor now reports no findings for the action, with no ignore comment or
config entry needed.

* Note that server/bin is only a warm start

The cache key covers the Go version and go.mod/go.sum, so it doesn't
invalidate on a tools/mattermost-govet source edit or a Makefile version
pin bump. That reads like a staleness bug until you notice every consumer
re-runs its install-<tool> prerequisite, and `go install` rebuilds on
content, so a stale binary is overwritten before anything uses it.

Say so where the key is described, since it came up in review.
2026-09-07 09:14:04 +00:00
94d6a20395 MM-70228: Adjust LDAP test connection validation (#37913)
* MM-70181: Adjust LDAP test connection validation

* MM-70181: Generalize AD/LDAP bind password re-entry message

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* MM-70181: Narrow LDAP test connection validation fields

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-07 09:42:14 +02:00
Jesse Hallam 8b5045c27b Ensure we only warm the node_modules cache from the master branch (#38342) 2026-09-07 06:47:59 +00:00
d7880054f7 [MM-70278] Add enable button for disabled plugin settings (#37588)
* Add enable button for disabled plugin settings

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Simplify disabled plugin enable action

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Refresh plugin settings after enabling

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Use action button for plugin enable state

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Add plugin uninstall action to settings

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Move plugin actions under metadata

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Update plugin action search index tests

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Make disable plugin action neutral

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Guard plugin actions with unsaved settings

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Add plugin status under metadata

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Use colored plugin status panel

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Polish plugin status and action errors

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Place plugin status on separate line

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Assert plugin settings status in e2e

Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>

* Fix icon spacing in plugin status panel

PluginMetadataPanel__status previously relied on the ambient
.form-horizontal .alert .fa margin-right rule from _admin-console.scss,
which only applies when the panel is nested inside a .form-horizontal
form. Since the panel renders outside that context, the status icon
touched the message text.

* Sync live plugin status on plugin settings page

The plugin settings page read pluginStatuses straight from the store
with nothing to keep it fresh, so it only ever showed whatever status
was last fetched elsewhere (e.g. a previous Plugin Management visit)
and never reflected a plugin transitioning through starting/stopping/
failed states after an enable/disable action taken from this page.

Wrap the container with usePluginStatusesSync(), mirroring how
plugin_management/index.tsx subscribes to PluginStatusesChanged.

* Fix stylelint property order in plugin metadata panel

order/properties-order requires width before align-items.

* Give plugin run state and action buttons their own box

Wrap the status alert and enable/disable/uninstall buttons in a
bordered panel, similar to the footer treatment on the system user
detail card, so the runtime status and actions read as a distinct
unit from the plugin metadata line above and the settings form below.

* fix lint

* Move plugin metadata into action panel

Co-authored-by: ben.schumacher <ben.schumacher@mattermost.com>

* Put plugin status in the header and restyle settings actions.

Keep enable/disable as a labeled toggle with a kebab for uninstall, and move start failures into a section notice instead of the status pill.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add plugin homepage action and tighten status notice spacing.

Expose More about this plugin from the actions menu when a homepage URL exists, and remove title-to-body gap on the plugin failure SectionNotice only.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Merge master into cursor/plugin-enable-button-88bc

Resolves conflicts in the admin console plugin settings refactor:
- i18n/en.json: keep both new upload-related keys
- admin_console_plugin_index.test.ts: keep both new test cases
- custom_plugin_settings/index.tsx (renamed from index.ts): keep this
  branch's leftover-section design, but stop attaching a duplicate
  footer to it now that schema_admin_settings renders the schema
  footer once for the whole sections list; also fix hasAllCustomSectionsDisabled
  vacuously returning true for an empty sections array
- schema_admin_settings.tsx: apply master's buildSettingsList extraction
  while keeping this branch's header suppression for plugin schemas
- custom_plugin_settings/index.test.tsx: keep both branches' new tests
  and reconcile wording of the disabled-plugin warning banners to
  match master's "...and click Save." copy update, applied consistently

* Remove orphaned admin.plugin.upload i18n key

Its usage in plugin_management.tsx was removed by master's plugin
upload dropzone UX change (#37569); the merge kept the code removal
but left the now-unused string behind in en.json, failing
i18n-extract:check.

* Fix flaky menu-item click assertion in enable_plugin_button test

The homepage-open assertion could run before the menu item's click
handler resolved, causing an intermittent 0-calls failure in CI.

* Address review feedback: update stale copy and add plugin actions tooltip

The plugin-disabled banners still told admins to enable the plugin and
click Save, but enable/disable now applies immediately via the new
toggle. Also add a tooltip to the icon-only plugin actions menu button
so sighted users get a visual hint of what it does.

* Rename PluginMetadataPanel showName prop to hideName

Defaulting an optional boolean to true means an omitted prop silently
changes behavior; defaulting to false is the safer, more idiomatic
choice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [MM-70278] Add e2e coverage for plugin settings page actions

Extends Cypress coverage to exercise the new immediate enable/disable
toggle, uninstall confirmation + redirect, and unsaved-changes guard
on the plugin settings page, which the existing spec did not touch.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Matthew Birtch <2040554+matthewbirtch@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 08:32:59 +02:00
Angelos Kyratzakos 8608859279 fix(e2e): repoint markdown image tests off the deleted docs.mattermost.com asset (#38330)
docs.mattermost.com migrated from Sphinx to Docusaurus, which removed the
/_images/ static path. https://docs.mattermost.com/_images/icon-76x76.png now
returns 404, so the image proxy fails and the webapp renders the broken-image
placeholder instead. That breaks four specs on every run, independent of the
change under test:

  - markdown/markdown_image_spec.js "with in-line images 1"
  - messaging/inline_markdown_image_link_open_link_spec.js MM-T188
  - files_and_attachments/image_link_preview_spec.js MM-T2389
  - fixtures/markdown/markdown_inline_images_1.md

The icon is gone from the docs site entirely (no /img/, /static/img/,
/img/brand/ or /assets/images/ equivalent), so point these at an image we
actually own: the in-repo fixture mattermost-icon_128x128.png, served over
raw.githubusercontent.com. That is the convention several sibling specs
already use, and it removes the dependency on the docs site's asset layout.

Size assertions move from 76px to 128px to match the new fixture. 128px stays
under the 350px max-height cap on .markdown-inline-img, so the image still
renders at its natural size. The height tolerance tightens from closeTo(76, 76)
- which accepted anything from 0 to 152 and would have passed on a
zero-height broken image - to closeTo(128, 2).
2026-09-07 10:04:07 +08:00
Harshil SharmaandClaude Opus 5 0019ecc531 (1) - Feature post delivery audit setting (#37882)
* Added server and client side frameowkr for configuring settings

* Type fixes

* Added test mocks

* Review fixes

* Restored gotool go modules

* CI

* (2) - Post tracking audit event implementation (#37919)

* Initial implementation for generating delivery tracking

* Initial implementation for generating delivery tracking

* Some performance improvements

* Some performance improvements

* Record list.Posts instead of list.Order

* Fixed email batch delivery recording

* Fixed a couple of pending delivery mechanisms

* Caching fixes

* another few fixes

* Stub DeliveryTracking in store mocks used by cache purge

InvalidateAllCachesSkipSend now also clears the delivery tracking
store's caches. The partial store mocks in these tests mirror the store
list that function touches, so the added call panicked with an
unexpected DeliveryTracking() during test cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Recorded post delivery event after JSON encoding succeeds

* Updated decisionmkaing point in email delivery tracking

* Plugin hook handling

* Added the Learn More button on delivery tracking section

* Added in inline code comment for explaining a decision

* Restored go modules

* Updated indirect status of a go dependency

* Removed learn more button

* fixed a test

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 07:19:40 +05:30
Jesse Hallam 502cf7e3e5 Bump @floating-ui/react, MUI (within v5) and react-day-picker for React 19 support (#38337) 2026-09-05 18:14:18 +00:00
b3946ef5e2 [MM-69862][MM-69867] Lock Definition and Applies-to for plugin-owned attributes (#38268)
* [MM-69862][MM-69867] Lock Definition and Applies-to for plugin-owned attributes

Plugin-owned attributes currently redirect away from the edit page entirely,
with no way to inspect them. This renders the page instead, with every
Definition control (Display Name, Unique Name, Type, Options) shown in its
existing disabled state and a "Managed by {plugin}" panel replacing the
LDAP/SAML picker; Applies-to's Add-resource triggers are hidden and existing
resource rows stay visible but read-only. Also enables the listing's Edit
action for plugin-owned rows, since the page it now leads to can render them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Make lock tooltips keyboard-accessible

WithTooltip's useFocus only fires on its cloned child, so a disabled control
wrapped for a lock tooltip needs the wrapper span itself to be focusable, or
keyboard/screen-reader users can never reach the tooltip explaining the lock
(mouse hover only). Fixes this for the 3 new Applies-to row toggles plus the
3 pre-existing lock wraps in the same file (Type, Unique Name, external
source) that had the same gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Label the listing's action "View attribute" for plugin-owned rows

The edit page is now reachable but fully locked for these rows, so "Edit
attribute" sets the wrong expectation before the admin even clicks. Swaps
to "View attribute" with an eye icon, matching Edit's own convention of
pairing a distinct label with what the destination page actually offers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix orphaned-plugin lock copy and a plugin-status-fetch race

Type/Unique-Name/Applies-to lock tooltips still said "managed by a plugin"
(present tense) for an orphaned attribute, contradicting the "(no longer
installed)" copy the Managed-by panel already showed one card up -- none of
the three lock-reason computations read isOrphaned, only isPluginOwned. Adds
a pluginOrphaned reason/copy variant to all three, and a shared
firstMatchingReason() helper so the "which lock reason wins" priority chain
exists once instead of being copy-pasted per lockable field.

Also: the "Plugin settings" link could briefly render in its optimistic
(non-orphaned) form before the plugin-inventory fetch settles, since
isOrphaned reads false during that window regardless of the real state.
Threads pluginInventoryLoaded into AttributePluginSource and gates the link
on it too, so it stays hidden until the orphan check is actually known.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-04 15:36:32 -07:00
1b6a579759 Sync Cloud Agent Go toolchain with server/.go-version (1.26.7) (#38298)
* Sync Cloud Agent Go toolchain with server/.go-version (1.26.7)

The checked-in Dockerfile was still pinned at Go 1.25.9 after several
.go-version bumps. Agents that boot from a default/personal image can
also land on an older distro Go (this environment had 1.22.2), which
cannot build server/.

Pin the image to 1.26.7 and make the install hook install whatever
server/.go-version requires when the running toolchain does not match.

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>

* Align Cloud Agent Node toolchain with .nvmrc

Mirror the Go self-heal: read the required version from .nvmrc (or
CLOUD_AGENT_NODE_VERSION), install it via nvm when the running node
does not match, symlink node/npm/npx onto /usr/local/bin, and persist
nvm on PATH. A major.minor pin such as 24.11 matches 24.11.x.

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>

* Revert Cloud Agent toolchain notes from AGENTS.md

Keep version-pin documentation in .cursor/README.md so the root
AGENTS.md stays unchanged.

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>

* Persist the active NVM_DIR in cloud-agent-install.sh

Write the resolved NVM_DIR into .bashrc so a custom nvm location
survives later shells instead of always resetting to $HOME/.nvm.

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
2026-09-04 17:13:56 -04:00
Jesse Hallam 791fc09b5d Test and lint mmgotool in CI (#38338)
* Fix the pre-existing lint failures in mmgotool

Nothing lints this module yet, so these accumulated: an unchecked Close on the
en.json write path, two unused walk parameters, a nil initialiser and three
redundant breaks. The Close is the only behaviour change -- en.json is
regenerated wholesale, so a failed flush would otherwise truncate it silently.

* Test and lint mmgotool in CI

tools-ci.yml only ever covered mattermost-govet, and each tool under tools/ is
its own Go module that the server and mmctl suites cannot reach -- so mmgotool
was tested and linted nowhere. Matrix both jobs over the two modules and give
mmgotool the Makefile and golangci config the matrix expects.
2026-09-04 20:35:33 +00:00
Jesse HallamandMattermost Build 1807e4b681 [MM-70273] Validate and limit mention_keys notify prop (#38121)
* [MM-70273] Validate and limit mention_keys notify prop

Add MentionKeysExceedLimits validation to User.IsValid and bulk import,
and pass MaxCount/MaxLength params so error messages render the actual limits.

* Fix golangci-lint modernize: use strings.SplitSeq in parseMentionKeys

* Restore i18n strings lost during merge with origin/master

MM-70291 added global_relay custom header strings after this branch was cut.
The merge conflict resolution dropped them; restore them now.

* Don't cap mention keys in PreUpdate so IsValid can reject them

* fixup! Don't cap mention keys in PreUpdate so IsValid can reject them

* fixup! Restore i18n strings lost during merge with origin/master

* use joinMentionKeys for clarity

* Cap mention keys while parsing and bound the raw value

* Test mention key normalization and limit boundaries

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-04 16:49:16 -03:00
cursor[bot] 9905a827e2 Skip flaky TestLicenseFromBytesEnvironmentMismatch (MM-70560) (#38297)
Automatic Merge
2026-09-04 22:42:35 +03:00
Jesse Hallam 43b2ae87e0 Stop encoding English grammar in four i18n source strings (#38228)
* Allow direct modifications to non-English translation files

i18n-ci-template.yml existed to fail any PR that touched a non-English
translation file unless it came from the weblate account, back when
translations arrived exclusively through Weblate.

Translations can now be authored in-repo and land in the same PR as the
English string they translate, so that gate rejects exactly the change it
should be letting through. The changed-file detection stays -- a direct edit to
a locale catalog is still worth surfacing on the PR -- but it emits a warning
annotation instead of failing, and the weblate-account bypass is gone with
nothing left to bypass.

* Remove EnableExperimentalLocales and the experimental locales

The 42 webapp and 33 server locale catalogs outside the supported 22 were
never selectable except behind EnableExperimentalLocales, and they had
rotted: hundreds of thousands of missing keys between them, several files
100% missing, and strings that fail to parse with the runtime ICU parser and
would throw if a user ever selected that language. We are not shipping those
languages, so the catalogs go.

With the locale set trimmed to the supported 22 the setting was left inert.
It fed getAllLanguages(true), which built entries from langIDs -- the list of
locale files on disk -- and then spread the curated languages map over the
top. With langIDs now a subset of that map, every experimental entry was
shadowed and the flag changed nothing: getAllLanguages(true) deep-equalled
getAllLanguages().

Rather than leave an admin console toggle that does nothing, this removes the
setting end to end: the model field and its default, the client prop, the
TypeScript config types, the admin console control and its two strings, the
playwright default config, and the configuration docs.

Downstream of that, getAllLanguages no longer takes a parameter, and
getLanguageInfo reads the supported map directly instead of going through the
experimental path -- so it now resolves the supported 22 rather than whatever
JSON happens to sit in the locale directory.

That in turn orphaned the langIDs and langLabels exports from
gen_lang_imports.mjs, and langmap.mjs existed only to populate langLabels, so
all three go too. imports.ts is regenerated and now imports exactly the 21
supported non-English catalogs.

The two orphaned admin strings are dropped from en.json and from the 13
translated catalogs that carried them.

Existing config.json files keep working: config is decoded without
DisallowUnknownFields, so a stale EnableExperimentalLocales key is ignored.

The v10 changelog entry for the setting stays: it is a historical record of a
release that did ship it.

* Stop encoding English grammar in four i18n source strings

Four messages made themselves impossible to translate faithfully, by building an
English sentence out of parts and asking every other language to reassemble it
the same way. Each one forces translators into a structure the source does not
have, which is a defect in the source, not in the translations.

  This is {article} {minimumPlanRequiredForFeature} feature, ...

`article` was 'an' or 'a', chosen in TypeScript from the plan name. Most target
languages have no indefinite article at all; those that do inflect it for gender
and case, which cannot be decided before the noun. Every locale that kept the
placeholder renders a stray English "a" mid-sentence -- German ships "Dies ist a
Professional Funktion" today. Rephrased so no determiner is needed:

  This feature requires {minimumPlanRequiredForFeature}, ...

and the `article` value, and the LicenseSkus import it needed, are gone.

  {count} messages exported.
  {count} warning(s) encountered, see warning.txt for details

A bare count welded to a fixed noun, plus the "(s)" hack. Ukrainian, Polish and
Russian need one/few/many/other and had nowhere to put the agreement, so
translators wrapped the variable in a plural the source did not declare. Now
each is a plural in the source, which costs English nothing and gives every
locale the structure it needs.

  ... {countReturned, number} {countReturned, plural, one {result} other {results}} found out of {totalCount} ...

The number was split from its noun into a separate argument, inconsistently --
{countReturned, number} beside a bare {totalCount}. ICU's # does both jobs at
once and applies the locale's number formatting, which is what zh-CN was
reaching for when it added {totalCount, number} of its own accord.

  Save and remove {amount, plural, one {user} other {users}}

A plural selector on a count the button never shows, so de, ja, vi and zh-TW put
the number back. This one is the least clear-cut of the four -- the English is
defensible as written -- and it changes the button to "Save and remove 3 users".
It is a one-line revert if that is not wanted.

Changing a message orphans its translations, so the 21 catalogs are realigned
here rather than left to fail. No new translation of anything:

  16  wrapped in {n, plural, other {...}} for a locale with a single plural
      category, where that is a no-op on the rendered string
   6  folded a standalone {n} or {n, number} into the # of the plural beside it,
      which renders identically
   6  en-AU set to the regenerated English, which is what it already held
  69  dropped, because the entry translates a sentence that no longer exists

The dropped entries fall back to English and rejoin the translation backlog.
Some, like the German LDAP string that splits a noun across a plural to supply
only its suffix, cannot be refolded onto # without rewriting the translation.

Checked with the deterministic ICU lint: these six keys now produce zero errors
with the exception list disabled entirely, and the tree's error count is two
lower than before rather than higher. i18n-extract:check passes, and the message
export settings tests, including the getJobDetails cases that exercise two of
these strings, still pass with their snapshots.

* Update MM-T1168 for the pluralized export count

admin.complianceExport.messagesExportedCount became an ICU plural, so a
single-message export now renders "1 message exported." rather than the
ungrammatical "1 messages exported." the assertion still expected.

The other assertions on this string all pass counts of 2 or more, which
select the same "other" branch as before and keep passing unchanged.
2026-09-04 13:49:38 -03:00
Jesse Hallam e9379690d5 Remove EnableExperimentalLocales and the experimental locales (#38219)
* Allow direct modifications to non-English translation files

i18n-ci-template.yml existed to fail any PR that touched a non-English
translation file unless it came from the weblate account, back when
translations arrived exclusively through Weblate.

Translations can now be authored in-repo and land in the same PR as the
English string they translate, so that gate rejects exactly the change it
should be letting through. The changed-file detection stays -- a direct edit to
a locale catalog is still worth surfacing on the PR -- but it emits a warning
annotation instead of failing, and the weblate-account bypass is gone with
nothing left to bypass.

* Remove EnableExperimentalLocales and the experimental locales

The 42 webapp and 33 server locale catalogs outside the supported 22 were
never selectable except behind EnableExperimentalLocales, and they had
rotted: hundreds of thousands of missing keys between them, several files
100% missing, and strings that fail to parse with the runtime ICU parser and
would throw if a user ever selected that language. We are not shipping those
languages, so the catalogs go.

With the locale set trimmed to the supported 22 the setting was left inert.
It fed getAllLanguages(true), which built entries from langIDs -- the list of
locale files on disk -- and then spread the curated languages map over the
top. With langIDs now a subset of that map, every experimental entry was
shadowed and the flag changed nothing: getAllLanguages(true) deep-equalled
getAllLanguages().

Rather than leave an admin console toggle that does nothing, this removes the
setting end to end: the model field and its default, the client prop, the
TypeScript config types, the admin console control and its two strings, the
playwright default config, and the configuration docs.

Downstream of that, getAllLanguages no longer takes a parameter, and
getLanguageInfo reads the supported map directly instead of going through the
experimental path -- so it now resolves the supported 22 rather than whatever
JSON happens to sit in the locale directory.

That in turn orphaned the langIDs and langLabels exports from
gen_lang_imports.mjs, and langmap.mjs existed only to populate langLabels, so
all three go too. imports.ts is regenerated and now imports exactly the 21
supported non-English catalogs.

The two orphaned admin strings are dropped from en.json and from the 13
translated catalogs that carried them.

Existing config.json files keep working: config is decoded without
DisallowUnknownFields, so a stale EnableExperimentalLocales key is ignored.

The v10 changelog entry for the setting stays: it is a historical record of a
release that did ship it.
2026-09-04 12:21:12 -04:00
Nuno Simões bbd208cffd chore(ci): bump pr-test-analysis toolkit ref to pick up claude-code-action v1.0.210 (#38335)
Points at mattermost/mattermost-test-automation-toolkit
b51a481e3fe6332053be09800d59da1a8c3e3e2e, which upgrades
anthropics/claude-code-action from v1.0.70 → v1.0.210.
v1.0.210 includes anthropics/claude-code-action#1580 (shipped in
v1.0.188), which passes no-cache: true to oven-sh/setup-bun. The
upstream setup-bun key is not ref-aware, so every second-and-subsequent
run against the same PR ref hit a 409 on save, and @actions/cache
treated the HTML error body as transient and burned ~20-30s on 5
retries plus emitted a noisy warning per PR push. Disabling the cache
is a net wallclock win — the 35 MB Bun binary downloads in 2-3s.
Refs:
- https://github.com/anthropics/claude-code-action/pull/1580
- https://github.com/anthropics/claude-code-action/issues/1252
- https://github.com/anthropics/claude-code-action/releases/tag/v1.0.210
2026-09-04 17:58:37 +02:00
Eva SarafianouandCursor d78e09b9a5 fix(check-external-links): follow 301s, route GET-fallback through the loop, skip Zendesk (#38319)
* docs: add 10 more missing legacy redirect entries to active-redirects.json

Fixes 404s for legacy Sphinx URLs that mattermost.com/pl/* shortlinks
still redirect to (10 shortlinks → 9 unique target paths under legacy IA
prefixes, plus /developer/bot-accounts for the docs-subdomain merge).
Bumps _meta.active 747 → 757.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check-external-links): follow 301s and skip Zendesk-hosted URLs

Two related fixes to the CI link checker:

1. Remove the 301 short-circuit in processResponse so 301 responses are
   followed to their final destination just like 302s. The previous
   behavior masked real 404s at the end of redirect chains — in
   particular, mattermost.com/pl/* shortlinks whose docs.mattermost.com
   destinations no longer exist post-migration were reported as OK.

2. Add support.mattermost.com to a new UNCRAWLABLE_HOSTS skip set.
   Zendesk fronts these URLs with Cloudflare bot management that
   inspects TLS fingerprints (JA3/JA4) and blocks non-browser clients
   regardless of User-Agent spoofing. The URLs are all valid in a
   browser; broken help-center articles surface via support tickets.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check-external-links): route GET-fallback responses through the redirect loop

When HEAD returns 405/403, we retry with GET. Previously the fallback
branch returned processResponse's result directly, which bypassed the
outer loop's Location handling. Combined with the earlier removal of
the 301 short-circuit, a GET that returned a 3xx would come back to
the caller as {redirect: true} and be treated as broken with no error
text.

Reassign 'response' in the fallback branch so the same code path below
processes and follows the response like any other HEAD result.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check-external-links): release response bodies before continuing chain

We only read status and headers from each response but never consume the
body. Node's fetch (undici) keeps the underlying socket reserved until
the body is consumed or cancelled, so under concurrency this pinned
sockets in the pool longer than necessary.

Wrap the per-iteration logic in try/finally so the body is always
released, and cancel the HEAD response body before replacing it with
the GET fallback. HEAD responses have a null body (safe no-op) but the
GET fallback returns real bodies we never read.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 17:48:03 +03:00
53e6ffa876 Block link previews after redirects to restricted domains (#36665)
* Block link previews after redirects to restricted domains

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Allow localhost in link metadata redirect test

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Correct redirect metadata test setup

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Strengthen link metadata redirect tests

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Avoid draining rejected redirect responses

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Preserve default redirect limit in link metadata client

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-09-04 09:58:29 -04:00
3bd85d0a93 [MM-69882] Decouple pin/unpin from ServiceSettings.PostEditTimeLimit (#38296)
* Decouple pin/unpin from ServiceSettings.PostEditTimeLimit

The post edit time limit was extended to cover pin state, so posts older
than ServiceSettings.PostEditTimeLimit could no longer be pinned or
unpinned. Scope the limit back to post content (message, props, files) so
pin state can always be changed, and drop the pinning no-op shortcut that
only existed to soften that check.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Broaden pin/unpin time limit coverage

Derive the content check from PostPatch.IsEmpty so a future patch field is
gated by the edit time limit by default, and cover the helper directly, an
unpin through updatePost, a non-author pin/unpin, and EditAt staying put.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Guard PostPatch.IsEmpty against silently exempting new fields

The edit time limit now derives its content check from IsEmpty, so cover
IsEmpty directly next to the struct it mirrors and assert the field count
so a new field cannot slip past unnoticed.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Tighten test comments and use reflect.TypeFor

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-09-04 16:24:29 +03:00
Nuno Simões 97813196cd docs: unescape HTML entities inside markdown code spans (#38329)
* docs: unescape HTML entities inside markdown code spans
RST-to-MDX migration left &lt;/&gt; placeholders inside backticks, which
rendered literally. Restore <placeholder> syntax in code spans only.

* correct the url schema
2026-09-04 11:48:51 +02:00
240b9bed8b [MM-70274] Add permissions migration so upgraded installs can access the System Console Recaps page (#38058)
* [MM-70274] Add permissions migration for AI Recaps sysconsole permissions

The sysconsole_read_ai_recaps and sysconsole_write_ai_recaps permissions
were only added to the SysconsoleReadPermissions/SysconsoleWritePermissions
slices. Those slices are applied to system_admin by the one-time
add_system_console_permissions migration, which has already run on every
existing install, so upgraded servers never received the new permissions
and the System Console Recaps page stayed hidden for system admins.

Add a dedicated add_ai_recaps_permissions migration that grants both
permissions to the system_admin role, matching fresh-install defaults.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Add tests for the AI Recaps permissions migration

Cover the transformation itself (system_admin gains both permissions, other
roles untouched, idempotent on re-run) and guard that the migration is
actually registered in the migration list, which is the mistake that left
existing installs without the permissions.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Tighten the AI Recaps migration registration test

Assert the exact permission count so a migration key that collides with an
already-completed key, which would never run on a real upgrade, fails the
test. Drop the unreachable store expectations.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-09-04 00:00:47 -04:00
01fd4cfe26 Stabilize flaky Cypress and Playwright E2E specs (#38300)
* Stabilize flaky Cypress and Playwright E2E specs

Fix Reset password menu targeting, mention-badge isolation, invite
enablement, local image aspect-ratio fixtures, and Playwright waits
for members RHS, BoR posts, file attachments, and ABAC menus.

Co-authored-by: saturnino <saturnino@mattermost.com>

* Fix BoR concealed post locator after reveal

Co-authored-by: saturnino <saturnino@mattermost.com>

* Harden Cypress locators and image fixtures

Use existing aspect-ratio fixtures, invite chips, and emoji search so
the flake specs wait on the right UI instead of avatars or long timeouts.

Co-authored-by: saturnino <saturnino@mattermost.com>

* Read Cypress smtp and db URLs from the environment

Co-authored-by: saturnino <saturnino@mattermost.com>

* Replace Playwright E2E magic timeouts with readiness checks

Drop half-minute and 30s waits in favor of visible/attached/enabled
assertions and file-preview counts so specs do not rely on brittle
timeouts.

Co-authored-by: saturnino <saturnino@mattermost.com>

* Fix compact-view edited indicator flake in MM-T103

Co-authored-by: saturnino <saturnino@mattermost.com>

* Wait for the image post before asserting aspect ratio

Co-authored-by: saturnino <saturnino@mattermost.com>

* Shorten image post readiness comment

Co-authored-by: saturnino <saturnino@mattermost.com>

* Save edited posts via the Save control; expand image embeds

Co-authored-by: saturnino <saturnino@mattermost.com>

* Expand collapsed image embeds before ratio asserts

Co-authored-by: saturnino <saturnino@mattermost.com>

* Fix post-edit Save locator and collapsed image embed click

Co-authored-by: saturnino <saturnino@mattermost.com>

* Wait for file attach before expanding image embeds

Co-authored-by: saturnino <saturnino@mattermost.com>

* Keep the mention notification stub after channel switch

Co-authored-by: saturnino <saturnino@mattermost.com>

* Shorten mention-spy readiness comment

Co-authored-by: saturnino <saturnino@mattermost.com>

* Pin MM-T103 to the posted text instead of last post

Co-authored-by: saturnino <saturnino@mattermost.com>

* Force-click the remounted post edit menu item

Co-authored-by: saturnino <saturnino@mattermost.com>

* Requery the post box after re-render; wait for domain config

Co-authored-by: saturnino <saturnino@mattermost.com>

* Assert restricted-domain config before signup and invite

Co-authored-by: saturnino <saturnino@mattermost.com>

* Send MM-T103 posts via the Send button

Co-authored-by: saturnino <saturnino@mattermost.com>

* Harden Add People empty-state and file-chooser waits

Assert the real no-matches copy instead of a stale class, wait for
Add to be enabled, and attach files via the filechooser event so
uploads do not depend on a long preview timeout.

Co-authored-by: saturnino <saturnino@mattermost.com>

* Dismiss a leftover emoji picker before opening another

Co-authored-by: saturnino <saturnino@mattermost.com>

* Wait for upload and post-create APIs before UI asserts

Co-authored-by: saturnino <saturnino@mattermost.com>

* Click the Add-email option and invite from a new team

Co-authored-by: saturnino <saturnino@mattermost.com>

* Reload login after client config shows creation disabled

Co-authored-by: saturnino <saturnino@mattermost.com>

* Create the invite team before restricting email domains

Co-authored-by: saturnino <saturnino@mattermost.com>

* Clear leftover domain restriction before creating the invite team

Co-authored-by: saturnino <saturnino@mattermost.com>

* Pass the uploaded post ID when opening the image RHS

Co-authored-by: saturnino <saturnino@mattermost.com>

* Use # and * E2E comments on the stabilized specs

Co-authored-by: saturnino <saturnino@mattermost.com>

* Attach uploads via the hidden file input

Co-authored-by: saturnino <saturnino@mattermost.com>

* Wait for the deletion report file before opening details

Co-authored-by: saturnino <saturnino@mattermost.com>

* Poll site-statistics guest counts after creating guests

Co-authored-by: saturnino <saturnino@mattermost.com>

* Free leftover CPA fields before hitting the 20-field cap

Co-authored-by: saturnino <saturnino@mattermost.com>

* Keep the polled guest-count baseline for the increment assert

Co-authored-by: saturnino <saturnino@mattermost.com>

* Delete only this suite's leftover UAAE CPA fields

Co-authored-by: saturnino <saturnino@mattermost.com>

* Wait for the upload API before asserting the file preview

Co-authored-by: saturnino <saturnino@mattermost.com>

* Format the guest-count poll for Prettier

Co-authored-by: saturnino <saturnino@mattermost.com>

* Type at-mentions so profile-popover autocomplete can search

Co-authored-by: saturnino <saturnino@mattermost.com>

* Open the content-review thread to assert the deletion report

Co-authored-by: saturnino <saturnino@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: saturnino <saturnino@mattermost.com>
2026-09-04 07:52:03 +08:00
Eva SarafianouandCursor 4086261f00 docs: fix 10 legacy redirect gaps and two bugs in the external link checker (#38318)
* docs: add 10 more missing legacy redirect entries to active-redirects.json

Fixes 404s for legacy Sphinx URLs that mattermost.com/pl/* shortlinks
still redirect to (10 shortlinks → 9 unique target paths under legacy IA
prefixes, plus /developer/bot-accounts for the docs-subdomain merge).
Bumps _meta.active 747 → 757.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check-external-links): follow 301s and skip Zendesk-hosted URLs

Two related fixes to the CI link checker:

1. Remove the 301 short-circuit in processResponse so 301 responses are
   followed to their final destination just like 302s. The previous
   behavior masked real 404s at the end of redirect chains — in
   particular, mattermost.com/pl/* shortlinks whose docs.mattermost.com
   destinations no longer exist post-migration were reported as OK.

2. Add support.mattermost.com to a new UNCRAWLABLE_HOSTS skip set.
   Zendesk fronts these URLs with Cloudflare bot management that
   inspects TLS fingerprints (JA3/JA4) and blocks non-browser clients
   regardless of User-Agent spoofing. The URLs are all valid in a
   browser; broken help-center articles surface via support tickets.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check-external-links): route GET-fallback responses through the redirect loop

When HEAD returns 405/403, we retry with GET. Previously the fallback
branch returned processResponse's result directly, which bypassed the
outer loop's Location handling. Combined with the earlier removal of
the 301 short-circuit, a GET that returned a 3xx would come back to
the caller as {redirect: true} and be treated as broken with no error
text.

Reassign 'response' in the fallback branch so the same code path below
processes and follows the response like any other HEAD result.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 20:30:43 +00:00