Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
After auto-resolving (or interactively resolving) conflicts, the
tool used to leave the renames as uncommitted changes for the user
to fold back into the right commits by hand. With more than a
trivial branch, that's tedious and error-prone.
For each `(old, new)` rename, find the most recent commit between
`@{u}` and `HEAD` that added `old`, build a `fixup!` commit
targeting it via `git commit --only` (so any unrelated already-
staged changes the user had don't get folded in), and then run `git
rebase --autosquash --autostash @{u}` so the renames land in the
commits that introduced each local migration. Skip the squash for
any renamed file with no in-branch add (untracked or already
committed elsewhere); leave that change in the working tree.
Refuse upfront if `@{u}..HEAD` contains any merge commit, since
`git rebase -i --autosquash` would silently linearize them and
`--autostash` would make recovery awkward.
`--no-rebase` skips the squash for users who want to handle commit
placement themselves.
The interactive prompt previously asked the user to order every
conflicting migration, and the renumberer bumped each loser by one
position at a time. A branch with a single new migration that
collides several positions behind the upstream tip therefore had to
walk through one prompt per intervening commit.
Use `git ls-tree @{u}` to detect which conflicting migrations are
branch-local. When `@{u}` resolves and every conflict group has
exactly one local file, renumber all local migrations at-or-past
the first conflict contiguously past the upstream tip, in NNNN
order: the new NNNNs fill in from `upstream_tip + 1` and increment
by 1, with each file's in-app dep rewritten to the previous file
in the new chain. Non-conflicting local migrations between the
first conflict and the new tip are renumbered too, so the chain
stays intact and there's no NNNN gap.
When `@{u}` can't be resolved, fall back to the existing interactive
prompt per group. If `@{u}` is available but a conflict has 0 or
2+ local files (backport or order-ambiguous), error out and let
the user resolve manually.
Pull the conflict-detection scan into a `find_conflict_groups`
helper that returns one list per colliding NNNN prefix, sorted by
prefix. Have `main()` operate on the lowest-numbered group, re-glob
after each pass, and stop when no group is left. Sets up the
follow-up commit's per-group decision (auto-resolve vs. interactive
prompt).
Interactive callers now get one prompt per conflict group rather
than a single combined prompt for every conflicting file at once,
which is also a smaller blast radius if the user gives a wrong
order.
* Move the body of the `if __name__ == "__main__":` block into a
`main()` function and lift `MIGRATIONS_TO_SKIP` to module scope
near the other module-level definitions at the top of the file.
* Replace the misnamed `stack: list[str]` with a `seen_prefixes:
set[str]`, and de-indent the rename branch by `continue`-ing on
the "first occurrence" path.
* Use `removesuffix(".py")` instead of `replace(".py", "")`.
* Rewrite `validate_order` as a single permutation check, with an
error message that says what was expected and what was received.
* Drop the unused `files_list` parameter from `resolve_conflicts`
and replace its `range(len(...))` with `enumerate`.
The previous regex `[\d]+(_[a-z0-9]+)+` matched any digit run
followed by `_word` segments, anywhere in the file. That includes
unrelated tokens like the Python integer literal `1_000` and
cross-app dependency tuples such as `("auth", "0001_initial")`,
both of which were silently rewritten to the renumbered migration's
new predecessor name.
Match only tuples whose app label matches the app being renumbered,
and rewrite just the migration-name string. Refuse outright to
renumber a file with multiple in-app dependency tuples (a migration
merge), since rewriting all of them to the same predecessor would
produce a self-referential migration; those need to be renumbered
by hand.
The prior type of `service` hid that `services?.[0]` is
undefined for embedded bots without stored `config_data`,
forcing a defensive trailing `assert(service && ...)` at the
outgoing render. It also duplicated the union already
exported from `bot_data`.
Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Satyam Bansal <sbansal1999@gmail.com>
This matches the naming convention of the existing
OUTGOING_WEBHOOK_BOT_TYPE_INT constant, since both hold integer values.
Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Mukul Goyal <96649866+Mukul1235@users.noreply.github.com>
`start_arg_parser` reads like a helper that mutates a parser, when it
actually builds the argument parser for the start/restart-server
scripts. Rename it for consistency with the sibling
`upgrade_script_arg_parser` helper.
The wrapper accepts a `refname` plus its own `--remote-url`/`--local-ref`
flags and quietly forwards anything else through to upgrade-zulip-stage-3.
That meant `upgrade-zulip-from-git --help` advertised none of the
operationally important options (`--skip-restart`, `--skip-puppet`,
`--audit-fts-indexes`, etc.), and admins had to read the stage-3 source
to discover them.
Extract stage-3's forwardable options into a shared
`upgrade_script_arg_parser` helper in zulip_tools, used as a parent
parser in both stage-3 and the git wrapper. The wrapper now parses these
options itself (so they appear in `--help`) and reconstructs them when
invoking stage-2.
When a user partially selects text inside a rendered <time> element,
Chrome's clipboard serializer strips the <time> wrapper from the
paste HTML along with the .timestamp-content-wrapper span, losing
the `datetime` attribute needed to reconstruct the `<time:ISO>`
markdown.
We expand the selection range to cover the full <time> element (same
trick the KaTeX path uses to keep its annotation), and at copy time
wrap the localized date text in a fresh `<span data-datetime="...">`.
The paste rule recovers the markdown via the surviving `<time>` tag
(cross-element selections) or the wrapping `<span data-datetime>`
(in-time selections, where Chrome has stripped the <time>).
Fixes: https://chat.zulip.org/#narrow/channel/138-user-questions/topic/Copy-paste-ability.20of.20global.20times/with/2462937
`do_events_register` registers an event queue before fetching
the initial state.
Earlier, if a device is removed in that window (or updated
then removed), the queued `device/remove` (and any preceding
`device/update`) event targets a device_id which doesn't exist
in `state["devices"]`. It resulted in KeyError in `apply_event`.
This commit fixes the runtime error, we check early
if the key exists.
Signed-off-by: Prakhar Pratyush <prakhar@zulip.com>
In message_list_view.populate_group_from_message, the tutorial
referred to in the comment for the case when a stream message's
`stream_id` returns an undefined, instead of a StreamSubscription
object, was removed in commit be7f6db854.
Since we now can assert that a StreamSubscription object is
returned for the message in question, we can populate the
MessageGroup fields via the fields in that object, instead of
calling various functions that check for the same object.
Removes the undefined case from stream_data.can_resolve_topics
as well.
Markdown `` syntax renders to
a bare <img class="inline-image"> element.
These images can't be displayed in the emails as the
request from the mail server can't be authenticated.
So recipients saw broken images.
For emails, we replace each <img class="inline-image">
with an <a> pointing at `data-original-src` to fix the bug.
When `alt` is empty, we fall back to the URL-decoded
basename of `data-original-src`.
Signed-off-by: Prakhar Pratyush <prakhar@zulip.com>
zoom_in previously inferred its stream from
topic_list.active_stream_id(), which only works when the topic list
is already active for the target stream. Pass the stream_id in
explicitly so the caller controls which stream is zoomed, and narrow
to that channel's feed first when it isn't already the current
narrow.
Preparation for filtering topics across all channels from the left
sidebar, where zoom-in may target a stream that isn't currently
narrowed.
get_zoomed_topic_search_term reads the zoomed-in topic filter input
(#topic_filter_query), which exists only in the more-topics modal.
Rename it to make that explicit, differentiate it from the upcoming
top-level left sidebar topic search, and assert that it's only
called while zoomed.
filter_topics_left_sidebar runs for both the zoomed topic list and
the inline topic lists under streams, so read its search term from
the zoomed input when zoomed and the left sidebar search box
otherwise, preserving existing behavior. The remaining callers are
all zoomed-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3% of generated messages now include a text markdown
file attachment, created via the upload API.
Fixes part of #14991.
Co-authored-by: Andrew Wang
<73965466+wandrew0@users.noreply.github.com>
The "Mark as read" banner click handler previously only marked
messages currently loaded in the message list as read, missing
unreads outside the locally fetched range. For example, in the
DM feed (is:dm), older DM conversations with unreads would not
get marked as read.
Fix this by using the server-side narrow API
(bulk_update_read_flags_for_narrow) to mark every message
matching the current narrow as read, the same pattern used by
mark_stream_as_read and mark_topic_as_read.
This banner can appear in any non-conversation feed view
(channel feed, DM feed, combined feed, mentions, starred,
search, etc.), so the fix applies broadly.
The narrow API was added in b1ca1fd606, after the BUG comment
in 6afdf2410d flagged this limitation.
Reported at: https://chat.zulip.org/#narrow/channel/9-issues/topic/DM.20mark.20all.20as.20read.20only.20marking.20some.20unreads/with/2463479
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The download URL for a realm export tarball previously contained
only a random suffix, making it easy to download the wrong file
when multiple exports were available in the admin UI -- or when
an admin manages exports across multiple organizations.
Embed both the realm's string_id (when non-empty) and the UTC
timestamp of the export in the tempdir prefix, producing tarballs
named like zulip-export-<string_id>-2026-05-25-09-30-45-<rand>.tar.gz
(or zulip-export-2026-05-25-09-30-45-<rand>.tar.gz for the root
realm, whose string_id is empty). The timestamp format matches
the convention used by `./manage.py backup`; the random suffix is
retained to disambiguate same-second exports.
The prefix is built by a new `export_tarball_prefix()` helper in
`zerver/lib/export.py`, shared by the `export` and
`export_single_user` management commands and the deferred-work
queue worker that handles admin-UI export requests.
Fixes https://chat.zulip.org/#narrow/channel/9-issues/topic/discrepancies.20in.20data.20export/with/2467409
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
In process_message_files, the loop over a message's `files` list
reassigned has_image on every iteration of the Slack-hosted branch,
so a non-image file following an image flipped the flag back to
False. Affected imported messages ended up with has_image=False.
When a message is re-rendered, we currently don't apply the edit/move
button classes to the newly created DOM element.
In this commit, when a message is re-rendered, the edit/move button
classes are applied.
The empty-right branch of change_state previously set the folder
filter dropdown only when left_side_tab === "all". This came from the
old structure where the folder filter set was buried inside the
section === "all" branch, but it caused a preexisting bug: clicking
"View channels" from a folder popover as a guest passed
left_side_tab = "subscribed" (since guests can't use the "All" tab),
which skipped the folder-filter setup — so guests saw all their
subscribed channels rather than the folder they clicked.
Drop the left_side_tab guard. The folder filter is just a dropdown
value; applying it whenever folder_id is supplied makes the filter
work across all left tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
URL slot 1 (`#channels/<slot1>/...`) was overloaded as a left-panel
tab, the create-channel sentinel ("new"), or a channel ID, all passed
to `change_state` as one `section: string`. Parse it at the hashchange
boundary into `right_panel: "new" | number | undefined` and a separate
`left_side_tab`, and dispatch `change_state` on `right_panel` in an
if/else over the three cases.
`right_side_tab` is now strictly the in-channel tab (general /
personal / subscribers / permissions), no longer overloaded with
"new". Opening the create form preserves the user's current left tab
instead of resetting it.
Fixes#27730.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The left-panel toggler had keys "subscribed", "available", and
"all-streams"; the URL hash sections are "subscribed", "available",
and "all". The "all" vs "all-streams" mismatch required a one-line
mapping in URL parsing. Rename the toggler key to "all" so the
toggler and URL hash sections use the same vocabulary.
No functional changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The folder-create flow at #channels/folders/<id>/new calls change_state
and launch with right_side_tab = "". The create-form branch in
change_state never reads right_side_tab, so this empty string was
vestigial. Pass undefined to match the parameter's `string | undefined`
type and signal "no value" explicitly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Improve alias matching in the Code Playground settings typeahead.
Typing an alias such as py, py3, or python3 now surfaces the canonical
language option instead of suggesting a custom language entry.
This ensures canonical languages are prioritized when matched via aliases.
Fixes#24045.
Most of what makes a Zulip migration tricky to write follows from
how Zulip Cloud deploys: staging and production share a database,
staging deploys first and runs the migration against the shared
DB, production deploys some time later, and Django processes
restart in a rolling fashion. Together these mean migrations must
be safe for the previous release's code to keep running against,
and that staging-time problems are also live production problems.
The previous version of this page didn't describe any of that,
and as a result didn't motivate most of the rules contributors
need to follow. Rework the page around the deploy model so each
rule traces back to a property of how migrations actually run on
Cloud.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filtering across all of a channel's topics (rather than just the
cached ones) requires clicking "Show all topics," which is not
discoverable. This adds a search icon in the expanded channel's
row, to the left of the "+ new topic" button, that opens the
"show all topics" view with the filter input focused — giving
users a clear visual affordance for searching topics within a
channel.
The icon only appears on the expanded channel row, mirroring the
zoom-in mechanism, which only operates on the active channel.
The grid container for the left-sidebar controls now flows in
columns so multiple icons sit side-by-side rather than stacking.
Discussion:
https://chat.zulip.org/#narrow/channel/101-design/topic/Filter.20topics.20within.20channel.20button/with/2369643
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`openInputFieldOnKeyUp` in the search typeahead was used to open the
search input when the user started typing while the search box was
closed.
In commit 75dab825fe, we removed the
ability to type into closed search box, making this code path
unreachable.
We remove this option altogether from bootstrap_typeahead
module.
When the destination of an extracted file is a symlink to a
directory (e.g. in the docker-zulip container, where
/home/zulip/uploads -> /data/uploads), modern GNU tar replaces the
symlink with a real directory before extracting through it, as a
security precaution. As a result, uploads (and potentially
configuration files) get restored improper (and, in the case of
docker-zulip, non-persistent) paths.
Pass `--keep-directory-symlink` to both tar invocations so the
extractor follows symlinks-to-directories rather than replacing them.
The non-docker case is unaffected (these paths are real directories
in a standard install), and the security mitigation is not
meaningfully weakened: destinations are pinned to /etc/zulip,
/home/zulip/uploads, and zproject, and the archive is one the
operator just produced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHECK DB FOR UNIQUE CONSTRAINTS BEFORE DEPLOYING!
Adds a UniqueConstraint to the CustomerPlan model so that a Customer
can only be associated with one CustomerPlan with a status of
NEVER_STARTED.
Adds a UniqueConstraint to the CustomerPlanOffer model so that a
Customer can only be associated with one CustomerPlanOffer with a
status of CONFIGURED.
The support admin actions/functions for creating these plan offers
and plans now also have an atomic block (durable=True) around
creating those database objects.
Since each child process lives in a separate memory block, they can't
access variables declared in the main process. So each process will
write attachment record to a copy of `total_attachment_record` instead
of the one we declared, making the script output empty attachmnet.json
file if --processes > 1.
This updates `total_attachment_records` to be a `ListProxy`. The
attachment IDs are also now fixed outside of the parallel context
manager for the same reason.
Some third-party importer can't reliably compute the has_image and
has_link attributes, so this makes sure all third-party messages are
imported with correct has_image and has_link attributes.
Import can't pass message object to `markdown_convert`, so when
rendering content for messages from third-party exports, these
attributes are not corrected/verified.
This returns the has_* attribute as part of the rendering result so that
import can later use it to fix exported messages' has_* attributes.
Test case that checks this is `test_has_image` in `test_message_fetch`.
Image HTML element would be converted to `` format by
convert_html_to_text, so previously we were converting such syntax into
`!`.
Tippy schedules show() via setTimeout to honor the hover delay. With
our default appendTo: "parent", the mount path resolves "parent" to
reference.parentNode. If the reference was removed from the DOM
during the delay (e.g., the user hovers a recipient-bar icon, then
narrows away before the 100ms delay elapses, causing the feed to
re-render and detach the icon), parentNode is null and tippy
crashes on parentNode.contains(popper).
Fix this with two complementary changes to the default config:
1. A default onShow that destroys the instance and cancels the show
when the reference is no longer in the DOM. This keeps the DOM
clean: no popper is mounted at all.
2. An appendTo function that falls back to document.body when
reference.parentElement is null. This acts as a structural
backstop, since tippy's onShow is replaced (not composed with) by
per-instance onShow handlers; any tooltip overriding onShow would
otherwise lose the isConnected check.
Reported in Sentry as ~50 events from 41 users over 90 days; the
stack trace lands in the setTimeout callback at the
parentNode.contains line, consistent with this scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>