You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/07/06 14:00:29 UTC

(allura) 01/03: [#8612] PasteScript removed by Claude

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch db/8612
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 536d86f56816e0141abd0a312b62c58df5ac28c6
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Thu Jul 2 17:35:45 2026 -0400

    [#8612] PasteScript removed by Claude
    
    What replaced what
    
    - paster <cmd> → allura <cmd> (same arguments). New dispatcher at Allura/allura/command/cli.py, registered as a [project.scripts] console script. It discovers commands from a new allura.commands entry-point group, so it works from any directory (no more egg-info/cwd magic or [tool.paster] plugin lists). allura help lists everything grouped by tool.
    - paste.script.command.Command → a small replacement written directly into allura/command/base.py (Command + BadCommand, ~50 lines). It keeps the same optparse-based API (standard_parser(), run(), min_args/max_args, summary, etc.), so all existing command subclasses (Allura, ForgeChat, ForgeTracker, ForgeBlog) needed no logic changes — just import swaps in smtp_server.py and forgechat/command.py.
    - paster setup-app → new allura setup-app command (allura/command/setup_app.py) that calls allura.websetup.setup_app() directly, replacing the paste.script.appinstall machinery and the paste.app_install entry point. AlluraTest/alluratest/controller.py now uses it too (that was the last paste.script import).
    - paster serve → gunicorn, which was already how Docker runs the app (gunicorn --paste development.ini -b :8080 --reload). The dead [server:main] section was removed from development.ini. PasteDeploy stays (gunicorn's --paste, loadapp, appconfig all still use it); the Paste package also stays since it's used directly (fileapp, ErrorMiddleware, converters).
    - The copydir concern turned out to be indirect only — nothing imports it; it disappeared along with the paste.script imports.
    
    Migration doc
    
    Added a "Version 1.20.0 (unreleased)" section to CHANGES with upgrade instructions: the paster→allura rename table, the note to update cron/systemd/monitoring invocations, the gunicorn serve replacement, [server:main] removal, and the entry-point group rename for third-party tool authors. Docs (installation, install_each_step, administration incl. the program-output directives, contributing, extending, platform_tour, message_bus), docker-compose files, and the 035/036 migration shell  [...]
    
    Notes from verification
    
    - Found and fixed a latent bug the removal exposed: base.py used logging.config.fileConfig while only importing logging — it previously worked only because paste.script happened to import logging.config first.
    - requirements.in/requirements.txt: PasteScript removed by hand (matching what pip-compile would output). The setuptools<82 pin must stay — I verified colander still imports pkg_resources at import time, so I updated its comment.
    - Tests: test_commands.py (33), ForgeBlog/ForgeChat/ForgeTracker command tests, test_root.py, test_site_admin.py (exercises the web task-manager → run_command path), test_validators.py — all pass, ruff clean. I also exercised the CLI directly: help listing across all four packages, --help per command, bad-arg and unknown-command exit codes.
---
 Allura/allura/command/base.py                      | 55 ++++++++++++++--
 Allura/allura/command/cli.py                       | 73 ++++++++++++++++++++++
 Allura/allura/command/create_neighborhood.py       |  2 +-
 Allura/allura/command/script.py                    |  2 +-
 Allura/allura/command/set_neighborhood_features.py |  2 +-
 Allura/allura/command/setup_app.py                 | 47 ++++++++++++++
 Allura/allura/command/smtp_server.py               |  3 +-
 Allura/allura/controllers/root.py                  |  2 +-
 Allura/allura/tests/test_tasks.py                  |  2 +-
 Allura/development.ini                             | 15 ++---
 Allura/docs/development/contributing.rst           |  4 +-
 Allura/docs/development/extending.rst              |  4 +-
 Allura/docs/getting_started/administration.rst     | 60 +++++++++---------
 Allura/docs/getting_started/install_each_step.rst  |  6 +-
 Allura/docs/getting_started/installation.rst       | 11 ++--
 Allura/docs/platform/message_bus.rst               |  2 +-
 Allura/docs/platform/platform_tour.rst             |  2 +-
 Allura/pyproject.toml                              | 10 ++-
 AlluraTest/alluratest/controller.py                | 15 +----
 CHANGES                                            | 30 +++++++++
 ForgeBlog/pyproject.toml                           |  2 +-
 ForgeChat/forgechat/command.py                     |  3 +-
 ForgeChat/pyproject.toml                           |  2 +-
 .../forgetracker/command/fix_discussion.py         |  2 +-
 ForgeTracker/pyproject.toml                        |  2 +-
 docker-compose-prod.yml                            |  6 +-
 docker-compose.yml                                 |  4 +-
 requirements.in                                    |  3 +-
 requirements.txt                                   |  6 --
 scripts/add_user_to_group.py                       | 10 +--
 scripts/migrations/035-field-encryption.sh         |  2 +-
 scripts/migrations/036-field-encryption-cleanup.sh |  2 +-
 32 files changed, 283 insertions(+), 108 deletions(-)

diff --git a/Allura/allura/command/base.py b/Allura/allura/command/base.py
index cf495fbf4..3a5347dd6 100644
--- a/Allura/allura/command/base.py
+++ b/Allura/allura/command/base.py
@@ -15,11 +15,13 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+import optparse
 import os
-import logging
+import logging.config
+import sys
+import textwrap
 
 import tg
-from paste.script import command
 from paste.deploy import appconfig
 from paste.deploy.converters import asbool
 from tg.support.registry import Registry
@@ -37,7 +39,7 @@
 
 @task
 def run_command(command, args):
-    """Run paster command asynchronously"""
+    """Run an allura Command asynchronously"""
     mod, cls = command.rsplit('.', 1)
     mod = __import__(mod, fromlist=[str(cls)])
     command = getattr(mod, cls)
@@ -60,11 +62,56 @@ def __doc__(cls):
         return cls.parser.format_help()
 
 
-class Command(command.Command, metaclass=MetaParserDocstring):
+class BadCommand(Exception):
+    def __init__(self, message: str, exit_code: int = 2):
+        self.message = message
+        self.exit_code = exit_code
+        super().__init__(message)
+
+
+class Command(metaclass=MetaParserDocstring):
+    # replacement for the old paste.script.command.Command, same API for the parts we use
     min_args = 1
     max_args = 1
     usage = '[<ini file>]'
+    summary = ''
+    description = None
     group_name = 'Allura'
+    return_code = 0
+
+    def __init__(self, name: str):
+        self.command_name = name
+
+    @classmethod
+    def standard_parser(cls, verbose: bool = True) -> optparse.OptionParser:
+        parser = optparse.OptionParser()
+        if verbose:
+            parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0)
+        return parser
+
+    def run(self, args: list) -> int:
+        self.parse_args(args)
+        if self.min_args is not None and len(self.args) < self.min_args:
+            raise BadCommand(f'You must provide at least {self.min_args} arguments')
+        if self.max_args is not None and len(self.args) > self.max_args:
+            raise BadCommand(f'You must provide no more than {self.max_args} arguments')
+        self.verbose = getattr(self.options, 'verbose', 0)
+        result = self.command()
+        if result is None:
+            return self.return_code
+        else:
+            return result
+
+    def parse_args(self, args: list) -> None:
+        usage = f' {self.usage}' if self.usage else ''
+        self.parser.usage = f'%prog [options]{usage}\n{self.summary}'
+        self.parser.prog = self._prog_name()
+        if self.description:
+            self.parser.description = textwrap.dedent(self.description)
+        self.options, self.args = self.parser.parse_args(args)
+
+    def _prog_name(self) -> str:
+        return f'{os.path.basename(sys.argv[0])} {self.command_name}'
 
     @classmethod
     def post(cls, *args, **kw):
diff --git a/Allura/allura/command/cli.py b/Allura/allura/command/cli.py
new file mode 100644
index 000000000..ed412fb46
--- /dev/null
+++ b/Allura/allura/command/cli.py
@@ -0,0 +1,73 @@
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+"""The `allura` command-line script.  A replacement for the old `paster` script from PasteScript.
+
+Commands are registered under the "allura.commands" entry point group, by Allura itself and by any
+tool packages (e.g. ForgeBlog, ForgeChat).
+"""
+
+from collections import defaultdict
+import importlib.metadata
+import sys
+
+from allura.command.base import BadCommand
+
+ENTRY_POINT_GROUP = 'allura.commands'
+
+
+def load_commands() -> dict[str, importlib.metadata.EntryPoint]:
+    return {ep.name: ep for ep in importlib.metadata.entry_points(group=ENTRY_POINT_GROUP)}
+
+
+def print_help(eps: dict[str, importlib.metadata.EntryPoint]) -> None:
+    print('Usage: allura COMMAND [command_options]')
+    print("Run 'allura COMMAND --help' for details on a command.")
+    by_group = defaultdict(list)
+    for name, ep in sorted(eps.items()):
+        cmd_class = ep.load()
+        summary = (cmd_class.summary or '').strip().splitlines()
+        by_group[cmd_class.group_name].append((name, summary[0] if summary else ''))
+    for group in sorted(by_group):
+        print(f'\n{group or "Other"} commands:')
+        for name, summary in by_group[group]:
+            print(f'  {name:35} {summary}')
+
+
+def main(argv: list | None = None) -> int:
+    if argv is None:
+        argv = sys.argv[1:]
+    eps = load_commands()
+    if not argv or argv[0] in ('help', '-h', '--help'):
+        print_help(eps)
+        return 0
+    command_name = argv[0]
+    ep = eps.get(command_name)
+    if ep is None:
+        print(f'Unknown command: {command_name}\n')
+        print_help(eps)
+        return 2
+    cmd_class = ep.load()
+    try:
+        return cmd_class(command_name).run(argv[1:])
+    except BadCommand as e:
+        print(e.message)
+        return e.exit_code
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/Allura/allura/command/create_neighborhood.py b/Allura/allura/command/create_neighborhood.py
index 936c6daf2..68b82794f 100644
--- a/Allura/allura/command/create_neighborhood.py
+++ b/Allura/allura/command/create_neighborhood.py
@@ -60,7 +60,7 @@ class UpdateNeighborhoodCommand(base.Command):
         '\t<value> - boolean value to install/uninstall Home tool\r\n' \
         '\t    must be True or False\r\n\r\n' \
         '\tExample:\r\n' \
-        '\tpaster update-neighborhood-home-tool development.ini Projects True'
+        '\tallura update-neighborhood-home-tool development.ini Projects True'
     parser = base.Command.standard_parser(verbose=True)
 
     def command(self):
diff --git a/Allura/allura/command/script.py b/Allura/allura/command/script.py
index 27b841de0..614cf8cd1 100644
--- a/Allura/allura/command/script.py
+++ b/Allura/allura/command/script.py
@@ -33,7 +33,7 @@ class ScriptCommand(base.Command):
     min_args = 2
     max_args = None
     usage = '<ini file> <script> ...'
-    summary = 'Run a script as if it were being run at the paster shell prompt'
+    summary = 'Run a script with the Allura environment set up'
     parser = base.Command.standard_parser(verbose=True)
     parser.add_option('--profile', action='store_true', dest='profile',
                       help='Dump profiling data to <script>.profile')
diff --git a/Allura/allura/command/set_neighborhood_features.py b/Allura/allura/command/set_neighborhood_features.py
index 75bfba032..8f13b5073 100644
--- a/Allura/allura/command/set_neighborhood_features.py
+++ b/Allura/allura/command/set_neighborhood_features.py
@@ -25,7 +25,7 @@
 from ming.odm import session
 
 # Example usage:
-# paster set-neighborhood-features development.ini
+# allura set-neighborhood-features development.ini
 # 4f50c898610b270c92000286 max_projects 50
 
 
diff --git a/Allura/allura/command/setup_app.py b/Allura/allura/command/setup_app.py
new file mode 100644
index 000000000..ebfae89bf
--- /dev/null
+++ b/Allura/allura/command/setup_app.py
@@ -0,0 +1,47 @@
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+import configparser
+import logging.config
+import os
+
+from paste.deploy import appconfig
+
+from allura import websetup
+from . import base
+
+
+class SetupAppCommand(base.Command):
+    # replacement for the old `paster setup-app` from PasteScript
+    min_args = 1
+    max_args = 1
+    usage = '<ini file>'
+    summary = 'Set up the application: create initial database objects, neighborhoods, etc.'
+    parser = base.Command.standard_parser(verbose=True)
+
+    def command(self):
+        config_spec = self.args[0]
+        section = 'main'
+        if '#' in config_spec:
+            config_spec, section = config_spec.split('#', 1)
+        config_file = os.path.join(os.getcwd(), config_spec)
+        ini_parser = configparser.ConfigParser()
+        ini_parser.read([config_file])
+        if ini_parser.has_section('loggers'):
+            logging.config.fileConfig(config_file, disable_existing_loggers=False)
+        conf = appconfig(f'config:{config_file}#{section}')
+        websetup.setup_app(self, conf, {})
diff --git a/Allura/allura/command/smtp_server.py b/Allura/allura/command/smtp_server.py
index 3945dbf53..feab1c9a7 100644
--- a/Allura/allura/command/smtp_server.py
+++ b/Allura/allura/command/smtp_server.py
@@ -17,7 +17,6 @@
 
 import faulthandler
 import tg
-from paste.script import command
 
 import allura.tasks
 from allura.command import base
@@ -34,7 +33,7 @@ class SMTPServerCommand(base.Command):
     max_args = 1
     usage = '<ini file>'
     summary = 'Handle incoming emails, routing them to taskd'
-    parser = command.Command.standard_parser(verbose=True)
+    parser = Command.standard_parser(verbose=True)
     parser.add_option('-c', '--context', dest='context',
                       help=('The context of the message (path to the project'
                             ' and/or tool'))
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index fd0337538..c3352dd71 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -99,7 +99,7 @@ def _check_security(self):
         c.project = c.app = None
         c.user = plugin.AuthenticationProvider.get(request).authenticate_request()
         assert c.user is not None, ('c.user should always be at least User.anonymous(). '
-                                    'Did you run `paster setup-app` to create the database?')
+                                    'Did you run `allura setup-app` to create the database?')
         if not c.user.is_anonymous():
             c.user.track_active(request)
 
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 953a44ac1..49bd8b4cb 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -122,7 +122,7 @@ def test_post_event_explicit_flush(self):
         assert M.MonQTask.query.get(task_name='allura.tasks.event_tasks.event', args=['my_event2'])
 
     def test_post_event_from_script(self):
-        # simulate post_event being called from a paster script command:
+        # simulate post_event being called from an 'allura script' command:
         with mock.patch.dict(tg.request.environ, PATH_INFO='--script--'):
             g.post_event('my_event3')
             # event task is flushed to db right away:
diff --git a/Allura/development.ini b/Allura/development.ini
index a76316620..f68c0c641 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -31,7 +31,7 @@
 ; Or inherit from this file by starting your .ini file like this:
 ;    [app:main]
 ;    use = config:development.ini#main
-; Then just use the new file name instead of development.ini for any paster commands you run.
+; Then just use the new file name instead of development.ini for any allura commands you run.
 
 
 [DEFAULT]
@@ -58,12 +58,9 @@ forgemail.return_path = noreply@localhost
 
 
 ;
-; Settings for `paster serve` command
+; To run the web app, use gunicorn, which reads this file via its --paste option:
+;    gunicorn --paste development.ini -b :8080 --reload
 ;
-[server:main]
-use = egg:Paste#http
-host = 0.0.0.0
-port = 8080
 
 
 ;
@@ -357,7 +354,7 @@ webhook.repo_push.max_hooks = {"git": 3, "hg": 3, "svn": 3}
 ; To make all pages use ssl:   (also set session.secure above)
 ;force_ssl.pattern = .
 ; And to permit some URLs to be accessed over http anyway:
-;    /_test_vars is used when running `paster shell`
+;    /_test_vars is used by test environments
 ;no_redirect.pattern = ^/nf/\d+/_(ew|static)_/|^/rest/|^/nf/tool_icon_css|^/auth/refresh_repo|^/_test_vars
 
 
@@ -624,7 +621,7 @@ solr.commitWithin = 10000
 ; convert existing label and custom field data to more appropriate solr types.
 solr.use_new_types = true
 
-; Incoming email settings.  Used when you run: paster smtp_server development.ini
+; Incoming email settings.  Used when you run: allura smtp_server development.ini
 ; address to listen to
 forgemail.host = 0.0.0.0
 forgemail.port = 8825
@@ -709,7 +706,7 @@ hide_private_info = true
 ; Enable or disable external RSS feed importing in ForgeBlog tool.
 ; Default is "false". This feature requires GPL library "html2text". Install it with following command:
 ;   pip install -e git://github.com/brondsem/html2text.git#egg=html2text
-; You will also need to run `paster pull-rss-feeds development.ini` in a cron job to fetch them.
+; You will also need to run `allura pull-rss-feeds development.ini` in a cron job to fetch them.
 forgeblog.exfeed = false
 
 ;
diff --git a/Allura/docs/development/contributing.rst b/Allura/docs/development/contributing.rst
index 84d80ef77..39ba5b60c 100644
--- a/Allura/docs/development/contributing.rst
+++ b/Allura/docs/development/contributing.rst
@@ -183,7 +183,7 @@ foreground::
     # web
     pkill -f gunicorn; gunicorn --reload --paste development.ini -b :8080
     # taskd
-    pkill "^taskd"; paster taskd development.ini --nocapture
+    pkill "^taskd"; allura taskd development.ini --nocapture
 
 Then make a request to the web app, and when your line of code is hit, a debug
 session will start on the console where the process is running.
@@ -200,7 +200,7 @@ as ``pudb`` are also available.
        docker compose run --rm web pip install ipdb
        docker compose stop web taskd
        docker compose run --rm --service-ports web gunicorn --reload --paste Allura/docker-dev.ini -b :8088
-       docker compose run --rm taskd paster taskd docker-dev.ini --nocapture
+       docker compose run --rm taskd allura taskd docker-dev.ini --nocapture
 
 
 Testing
diff --git a/Allura/docs/development/extending.rst b/Allura/docs/development/extending.rst
index 7a9484927..d0291c30c 100644
--- a/Allura/docs/development/extending.rst
+++ b/Allura/docs/development/extending.rst
@@ -61,8 +61,8 @@ For example if you have ForgeImporter set up, but want to disable the GitHub imp
     disable_entry_points.allura.project_importers = github
     disable_entry_points.allura.importers = github-tracker, github-wiki, github-repo
 
-Other entry points are used to provide ``paster`` commands and ``easy_widget`` configuration,
-which are not part of Allura but are used by Allura.
+Other entry points are used to provide ``allura`` commands (the ``allura.commands`` group)
+and ``easy_widget`` configuration, which are not part of Allura but are used by Allura.
 
 
 Event Handlers
diff --git a/Allura/docs/getting_started/administration.rst b/Allura/docs/getting_started/administration.rst
index 3e8217fda..013b25a1a 100644
--- a/Allura/docs/getting_started/administration.rst
+++ b/Allura/docs/getting_started/administration.rst
@@ -87,38 +87,38 @@ Overview
 Allura has many commands and scripts that can be run from the server commandline to
 administrate Allura.  There are also tasks that can be run through the :code:`taskd` system
 in the background.  These tasks can be submitted via the web at
-http://MYSITE/nf/admin/task_manager  Some paster scripts have been set up
+http://MYSITE/nf/admin/task_manager  Some commands have been set up
 so that they are runnable as tasks too, giving you the convenience of starting
 them through the web and letting :code:`taskd` execute them, rather than from a server
 shell.
 
-Commands can be discovered and run via the :code:`paster` command when you are in the
-'Allura' directory that has your .ini file.  For example::
+Commands can be discovered and run via the :code:`allura` command.  For example,
+in the 'Allura' directory that has your .ini file::
 
-     paster help
+     allura help
     ... all commands listed here ...
 
-     paster create-neighborhood --help
+     allura create-neighborhood --help
     ... specific command help ...
 
-     paster create-neighborhood development.ini myneighborhood myuser ...
+     allura create-neighborhood development.ini myneighborhood myuser ...
 
 
-Scripts are in the :file:`scripts/` directory and run slightly differently, via :code:`paster script`.  An extra
-:kbd:`--` is required to separate script arguments from paster arguments.  Example::
+Scripts are in the :file:`scripts/` directory and run slightly differently, via :code:`allura script`.  An extra
+:kbd:`--` is required to separate script arguments from allura arguments.  Example::
 
-     paster script development.ini ../scripts/add_user_to_group.py -- --help
+     allura script development.ini ../scripts/add_user_to_group.py -- --help
     ... help output ...
 
-     paster script development.ini ../scripts/add_user_to_group.py -- --nbhd /u/ johndoe Admin
+     allura script development.ini ../scripts/add_user_to_group.py -- --nbhd /u/ johndoe Admin
 
 To run these when using docker, prefix with :code:`docker compose run taskd` and use :file:`docker-dev.ini` like::
 
-    docker compose run --rm taskd paster create-neighborhood docker-dev.ini myneighborhood myuser ...
+    docker compose run --rm taskd allura create-neighborhood docker-dev.ini myneighborhood myuser ...
 
 Or with the docker *production* setup::
 
-    docker compose run --rm oneoff paster create-neighborhood /allura-data/production.ini myneighborhood myuser ...
+    docker compose run --rm oneoff allura create-neighborhood /allura-data/production.ini myneighborhood myuser ...
 
 
 Tasks can be run via the web interface at http://MYSITE/nf/admin/task_manager  You must know
@@ -142,59 +142,59 @@ Available scripts and commands are:
 create-neighborhood
 -------------------
 
-.. program-output:: paster create-neighborhood development.ini --help | fmt -s -w 95
+.. program-output:: allura create-neighborhood development.ini --help | fmt -s -w 95
    :shell:
 
 
 ensure_index
 ------------
 
-.. program-output:: paster ensure_index development.ini --help
+.. program-output:: allura ensure_index development.ini --help
 
 
 ircbot
 ------
 
-.. program-output:: paster ircbot development.ini --help
+.. program-output:: allura ircbot development.ini --help
 
 
 reindex
 -------
 
-.. program-output:: paster reindex development.ini --help
+.. program-output:: allura reindex development.ini --help
 
 
 set-neighborhood-features
 -------------------------
 
-.. program-output:: paster set-neighborhood-features development.ini --help | fmt -s -w 95
+.. program-output:: allura set-neighborhood-features development.ini --help | fmt -s -w 95
    :shell:
 
 
 set-tool-access
 ---------------
 
-.. program-output:: paster set-tool-access development.ini --help | fmt -s -w 95
+.. program-output:: allura set-tool-access development.ini --help | fmt -s -w 95
    :shell:
 
 
 taskd
 -----
 
-.. program-output:: paster taskd development.ini --help
+.. program-output:: allura taskd development.ini --help
 
 
 task
 -----
 
-.. program-output:: paster task development.ini --help | fmt -s -w 95
+.. program-output:: allura task development.ini --help | fmt -s -w 95
    :shell:
 
 
 taskd_cleanup
 -------------
 
-.. program-output:: paster taskd_cleanup development.ini --help | fmt -s -w 95
+.. program-output:: allura taskd_cleanup development.ini --help | fmt -s -w 95
    :shell:
 
 
@@ -209,7 +209,7 @@ Requires `html2text`, a GPL library.
 ::
 
     cd ../ForgeBlog
-    paster pull-rss-feeds development.ini --help
+    allura pull-rss-feeds development.ini --help
 
 
 disable_users.py
@@ -220,7 +220,7 @@ disable_users.py
 .. argparse::
     :module: allura.scripts.disable_users
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/disable_users.py --
+    :prog: allura script development.ini allura/scripts/disable_users.py --
 
 
 .. _delete-projects-py:
@@ -235,7 +235,7 @@ More convenient way to delete project is :ref:`this site admin page <delete-proj
 .. argparse::
     :module: allura.scripts.delete_projects
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/delete_projects.py --
+    :prog: allura script development.ini allura/scripts/delete_projects.py --
 
 
 refreshrepo.py
@@ -246,7 +246,7 @@ refreshrepo.py
 .. argparse::
     :module: allura.scripts.refreshrepo
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/refreshrepo.py --
+    :prog: allura script development.ini allura/scripts/refreshrepo.py --
 
 
 reindex_projects.py
@@ -257,7 +257,7 @@ reindex_projects.py
 .. argparse::
     :module: allura.scripts.reindex_projects
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/reindex_projects.py --
+    :prog: allura script development.ini allura/scripts/reindex_projects.py --
 
 
 reindex_users.py
@@ -268,7 +268,7 @@ reindex_users.py
 .. argparse::
     :module: allura.scripts.reindex_users
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/reindex_users.py --
+    :prog: allura script development.ini allura/scripts/reindex_users.py --
 
 
 create_sitemap_files.py
@@ -279,7 +279,7 @@ create_sitemap_files.py
 .. argparse::
     :module: allura.scripts.create_sitemap_files
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/create_sitemap_files.py --
+    :prog: allura script development.ini allura/scripts/create_sitemap_files.py --
 
 
 clear_old_notifications.py
@@ -290,7 +290,7 @@ clear_old_notifications.py
 .. argparse::
     :module: allura.scripts.clear_old_notifications
     :func: get_parser
-    :prog: paster script development.ini allura/scripts/clear_old_notifications.py --
+    :prog: allura script development.ini allura/scripts/clear_old_notifications.py --
 
 publicize-neighborhood.py
 -------------------------
@@ -300,7 +300,7 @@ publicize-neighborhood.py
 .. argparse::
     :filename: ../../scripts/publicize-neighborhood.py
     :func: parser
-    :prog: paster script development.ini ../scripts/publicize-neighborhood.py --
+    :prog: allura script development.ini ../scripts/publicize-neighborhood.py --
 
 
 .. _site-notifications:
diff --git a/Allura/docs/getting_started/install_each_step.rst b/Allura/docs/getting_started/install_each_step.rst
index 67168b9c6..46d5b5e90 100644
--- a/Allura/docs/getting_started/install_each_step.rst
+++ b/Allura/docs/getting_started/install_each_step.rst
@@ -189,7 +189,7 @@ Allura uses a background task service called "taskd" to do async tasks like send
 .. code-block:: bash
 
     (env-allura)~$ cd ~/src/allura/Allura
-    (env-allura)~/src/allura/Allura$ nohup paster taskd development.ini > /var/log/allura/taskd.log 2>&1 &
+    (env-allura)~/src/allura/Allura$ nohup allura taskd development.ini > /var/log/allura/taskd.log 2>&1 &
 
 
 A few more steps, if using git
@@ -214,13 +214,13 @@ For development setup:
 
 .. code-block:: bash
 
-    (env-allura)~/src/allura/Allura$ paster setup-app development.ini
+    (env-allura)~/src/allura/Allura$ allura setup-app development.ini
 
 For production setup:
 
 .. code-block:: bash
 
-    (env-allura)~/src/allura/Allura$ ALLURA_TEST_DATA=False paster setup-app development.ini
+    (env-allura)~/src/allura/Allura$ ALLURA_TEST_DATA=False allura setup-app development.ini
 
 This shouldn't take too long, but it will start the taskd server doing tons of stuff in the background.  Once this is done, you can start the application server:
 
diff --git a/Allura/docs/getting_started/installation.rst b/Allura/docs/getting_started/installation.rst
index d6c3daf37..7704b1ed8 100644
--- a/Allura/docs/getting_started/installation.rst
+++ b/Allura/docs/getting_started/installation.rst
@@ -85,11 +85,11 @@ Initialize database with test data:
 
 .. code-block:: bash
 
-    docker compose run --rm taskd paster setup-app docker-dev.ini
+    docker compose run --rm taskd allura setup-app docker-dev.ini
 
 .. note::
 
-   If you want to skip test data creation you can instead run: :code:`docker compose run --rm -e ALLURA_TEST_DATA=False taskd paster setup-app docker-dev.ini`
+   If you want to skip test data creation you can instead run: :code:`docker compose run --rm -e ALLURA_TEST_DATA=False taskd allura setup-app docker-dev.ini`
 
 Start containers in the background:
 
@@ -299,11 +299,12 @@ For any other wsgi server (e.g. mod_wsgi with Apache, or waitress) you will need
 
 .. code-block:: python
 
+    from logging.config import fileConfig
+
     from paste.deploy import loadapp
-    from paste.script.util.logging_config import fileConfig
 
     config_file = '/PATH/TO/Allura/development.ini'
-    fileConfig(config_file)
+    fileConfig(config_file, disable_existing_loggers=False)
     application = loadapp('config:%s' % config_file)
 
 
@@ -326,7 +327,7 @@ If you are not running docker, run:
 
 .. code-block:: bash
 
-    nohup paster smtp_server development.ini > /var/log/allura/smtp.log &
+    nohup allura smtp_server development.ini > /var/log/allura/smtp.log &
 
 By default this uses port 8825.  Depending on your mail routing, you may need to change that port number.
 And if the port is in use, this command will fail.  You can check the log file for any errors.
diff --git a/Allura/docs/platform/message_bus.rst b/Allura/docs/platform/message_bus.rst
index 8211936c5..e6161892e 100644
--- a/Allura/docs/platform/message_bus.rst
+++ b/Allura/docs/platform/message_bus.rst
@@ -93,7 +93,7 @@ parameter passed to the event handler.
 Running the Task Daemon
 -----------------------
 
-In order to actually run the asynchronous tasks, we have written a paster command
+In order to actually run the asynchronous tasks, we have written an allura command
 `taskd`.  This creates a configurable number of worker processes that watch for
 changes to the `MonQTask` collection and execute requested tasks.  `taskd` can be
 run on any server, but should have similar access to the MongoDB databases and
diff --git a/Allura/docs/platform/platform_tour.rst b/Allura/docs/platform/platform_tour.rst
index de18a7f78..2298c10d7 100644
--- a/Allura/docs/platform/platform_tour.rst
+++ b/Allura/docs/platform/platform_tour.rst
@@ -167,7 +167,7 @@ Asynchronous Processing
 
 Much of the actual functionality of Allura comes from code that runs
 *outside* the context of a web request, in the `taskd` server (invoked by
-running :command:`paster taskd development.ini`).  Asynchronous processing is performed
+running :command:`allura taskd development.ini`).  Asynchronous processing is performed
 by two types of functions, *tasks* and *events*, differentiated as follows:
 
 Task
diff --git a/Allura/pyproject.toml b/Allura/pyproject.toml
index 801fe2945..d9f293808 100644
--- a/Allura/pyproject.toml
+++ b/Allura/pyproject.toml
@@ -46,15 +46,12 @@ dependencies = []
 ]
 
 
-[tool.paster]
-paster-plugins = ['PasteScript', 'TurboGears2', 'Ming']
+[project.scripts]
+allura = "allura.command.cli:main"
 
 [project.entry-points."paste.app_factory"]
 main = "allura.config.middleware:make_app"
 
-[project.entry-points."paste.app_install"]
-main = "paste.script.appinstall:Installer"
-
 [project.entry-points.allura]
 profile = "allura.ext.user_profile:UserProfileApp"
 admin = "allura.ext.admin:AdminApp"
@@ -114,7 +111,8 @@ filesystem-googleauth = "allura.lib.multifactor:GoogleAuthenticatorPamFilesystem
 mongodb = "allura.lib.multifactor:MongodbRecoveryCodeService"
 filesystem-googleauth = "allura.lib.multifactor:GoogleAuthenticatorPamFilesystemRecoveryCodeService"
 
-[project.entry-points."paste.paster_command"]
+[project.entry-points."allura.commands"]
+setup-app = "allura.command.setup_app:SetupAppCommand"
 taskd = "allura.command.taskd:TaskdCommand"
 taskd_cleanup = "allura.command.taskd_cleanup:TaskdCleanupCommand"
 task = "allura.command.taskd:TaskCommand"
diff --git a/AlluraTest/alluratest/controller.py b/AlluraTest/alluratest/controller.py
index 80b83aa49..671ef2bc4 100644
--- a/AlluraTest/alluratest/controller.py
+++ b/AlluraTest/alluratest/controller.py
@@ -29,7 +29,6 @@
 from formencode import variabledecode
 from paste.deploy import loadapp
 from paste.deploy.converters import asbool
-from paste.script.appinstall import SetupCommand
 from tg import tmpl_context as c
 import tg
 from tg.wsgiapp import RequestLocals
@@ -43,6 +42,7 @@
 
 from allura import model as M
 from allura.command import CreateTroveCategoriesCommand
+from allura.command.setup_app import SetupAppCommand
 import allura.lib.security
 from allura.lib.app_globals import Globals
 from allura.lib import helpers as h
@@ -91,17 +91,8 @@ def setup_basic_test(config=None, app_name=DFL_APP_NAME):
     except AttributeError:
         conf_dir = os.getcwd()
     test_file = os.path.join(conf_dir, get_config_file(config))
-    """
-    # setup our app, from TG quickstart example:
-    from tg.util import Bunch
-    from gearbox.commands.setup_app import SetupAppCommand
-    cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch())
-    cmd.run(Bunch(config_file='config:{}'.format(test_file), section_name=None))
-    """
-    # setup our app without depending on gearbox (we still depend on Paste anyway)
-    # uses [paste.app_install] entry point which call our setup_app()
-    cmd = SetupCommand('setup-app')
-    cmd.run([test_file, '--quiet'])
+    cmd = SetupAppCommand('setup-app')
+    cmd.run([test_file])
 
     ew.TemplateEngine.initialize({})
 
diff --git a/CHANGES b/CHANGES
index acca37359..0c0bd7963 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,33 @@
+Version 1.20.0  (unreleased)
+
+Upgrade Instructions
+
+  Install updated dependencies by running:
+    pip install -r requirements.txt --no-deps --upgrade --upgrade-strategy=only-if-needed
+  And re-install the Allura packages (e.g. re-run rebuild-all.bash, or `pip install -e` each package dir) so the
+  new `allura` command-line script and its commands are registered.
+
+  The `paster` command is gone (the unmaintained PasteScript package is no longer used).  All commands are now run
+  with a new `allura` script instead.  It takes the same arguments, e.g.:
+    paster taskd development.ini            ->  allura taskd development.ini
+    paster setup-app development.ini        ->  allura setup-app development.ini
+    paster script development.ini foo.py -- --opts  ->  allura script development.ini foo.py -- --opts
+    paster help                             ->  allura help
+  Update anything that runs these commands: init/systemd scripts, cron jobs (e.g. pull-rss-feeds), monitoring, etc.
+  Docker compose services in this repo are already updated: pull the latest docker-compose files if you use them.
+
+  `paster serve` is also gone.  Run the web app with gunicorn (already the case in the Docker and standard setups):
+    gunicorn --paste development.ini -b :8080 --reload
+  The now-unused [server:main] section can be removed from your .ini file.
+
+  If you maintain a custom Allura tool package that registered commands under the "paste.paster_command" or
+  "paste.global_paster_command" entry point groups, register them under the "allura.commands" group instead.
+  Command classes should subclass allura.command.Command as before; code importing paste.script must be updated.
+
+Breaking Changes
+ * The `paster` command is replaced by the `allura` command (PasteScript dependency removed)
+
+
 Version 1.19.1  (June 2026)
 
 Upgrade Instructions
diff --git a/ForgeBlog/pyproject.toml b/ForgeBlog/pyproject.toml
index 5b3b3acf8..40ee23ddb 100644
--- a/ForgeBlog/pyproject.toml
+++ b/ForgeBlog/pyproject.toml
@@ -44,5 +44,5 @@ dependencies = [
 [project.entry-points.allura]
 blog = "forgeblog.main:ForgeBlogApp"
 
-[project.entry-points."paste.paster_command"]
+[project.entry-points."allura.commands"]
 pull-rss-feeds = "forgeblog.command.rssfeeds:RssFeedsCommand"
diff --git a/ForgeChat/forgechat/command.py b/ForgeChat/forgechat/command.py
index 75ca4e8ff..c1f594667 100644
--- a/ForgeChat/forgechat/command.py
+++ b/ForgeChat/forgechat/command.py
@@ -27,7 +27,6 @@
 from datetime import datetime, timedelta
 
 import tg
-from paste.script import command
 from paste.deploy.converters import asint
 
 from ming.odm import ThreadLocalODMSession
@@ -46,7 +45,7 @@ class IRCBotCommand(allura.command.Command):
     max_args = 1
     usage = '<ini file>'
     summary = 'For the ForgeChat tool.  Connect to all configured IRC servers and relay messages'
-    parser = command.Command.standard_parser(verbose=True)
+    parser = allura.command.Command.standard_parser(verbose=True)
     parser.add_option('-c', '--context', dest='context',
                       help=('The context of the message (path to the project'
                             ' and/or tool'))
diff --git a/ForgeChat/pyproject.toml b/ForgeChat/pyproject.toml
index 13409ebb0..f4833c687 100644
--- a/ForgeChat/pyproject.toml
+++ b/ForgeChat/pyproject.toml
@@ -44,5 +44,5 @@ dependencies = [
 [project.entry-points.allura]
 Chat = "forgechat.main:ForgeChatApp"
 
-[project.entry-points."paste.global_paster_command"]
+[project.entry-points."allura.commands"]
 ircbot = "forgechat.command:IRCBotCommand"
diff --git a/ForgeTracker/forgetracker/command/fix_discussion.py b/ForgeTracker/forgetracker/command/fix_discussion.py
index 8601ec9b9..be28f3cec 100644
--- a/ForgeTracker/forgetracker/command/fix_discussion.py
+++ b/ForgeTracker/forgetracker/command/fix_discussion.py
@@ -34,7 +34,7 @@ class FixDiscussion(base.Command):
 
     Usage:
 
-    paster fix-discussion ../Allura/development.ini [project_name_or_id]
+    allura fix-discussion ../Allura/development.ini [project_name_or_id]
 
     If used with optional parameter will fix trackers for specified project,
     else will fix all trackers in all projects.
diff --git a/ForgeTracker/pyproject.toml b/ForgeTracker/pyproject.toml
index 2bb6d70e4..df47c3bb8 100644
--- a/ForgeTracker/pyproject.toml
+++ b/ForgeTracker/pyproject.toml
@@ -50,5 +50,5 @@ tickets_24hr = "forgetracker.site_stats:tickets_stats_24hr"
 [project.entry-points."easy_widgets.resources"]
 ew_resources = "forgetracker.config.resources:register_ew_resources"
 
-[project.entry-points."paste.paster_command"]
+[project.entry-points."allura.commands"]
 fix-discussion = "forgetracker.command.fix_discussion:FixDiscussion"
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
index 6c2a4cf0c..31ab5f21e 100644
--- a/docker-compose-prod.yml
+++ b/docker-compose-prod.yml
@@ -58,7 +58,7 @@ services:
     image: allura-web
     working_dir: /allura/Allura
     environment: *env
-    command: paster taskd /allura-data/production.ini
+    command: allura taskd /allura-data/production.ini
     healthcheck:
       test: ["CMD", "pgrep", "-f", "taskd"]
     volumes: *volumes
@@ -68,7 +68,7 @@ services:
     restart: always
 
   # This is a single-purpose container that does not auto-restart, good for running commands like:
-  # docker-compose run --rm oneoff paster ensure_index /allura-data/production.ini
+  # docker-compose run --rm oneoff allura ensure_index /allura-data/production.ini
   oneoff:
     image: allura-web
     working_dir: /allura/Allura
@@ -109,7 +109,7 @@ services:
     working_dir: /allura/Allura
     environment: *env
     volumes: *volumes
-    command: paster smtp_server /allura-data/production.ini
+    command: allura smtp_server /allura-data/production.ini
     healthcheck:
       test: ["CMD", "python", "-c", "import socket; socket.create_connection(('localhost', 8825), 5).close()"]
     ports:
diff --git a/docker-compose.yml b/docker-compose.yml
index 089345ce0..4c5399831 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -36,7 +36,7 @@ services:
     image: allura-web
     working_dir: /allura/Allura
     environment: *env
-    command: paster taskd docker-dev.ini
+    command: allura taskd docker-dev.ini
     volumes: *volumes
     links:
       - mongo
@@ -85,7 +85,7 @@ services:
     working_dir: /allura/Allura
     environment: *env
     volumes: *volumes
-    command: paster smtp_server docker-dev.ini
+    command: allura smtp_server docker-dev.ini
     ports:
       - "8825:8825"
     links:
diff --git a/requirements.in b/requirements.in
index 43af7b8bf..1208a8f6c 100644
--- a/requirements.in
+++ b/requirements.in
@@ -31,7 +31,6 @@ paginate
 passlib
 Paste
 PasteDeploy
-PasteScript
 Pillow
 # profanity filter for feedback
 profanityfilter
@@ -71,5 +70,5 @@ docutils
 gunicorn
 pre-commit
 
-# while we still use PasteScript we need older setuptools to provide 'pkg_resources'
+# some dependencies (e.g. colander) still need setuptools to provide 'pkg_resources'
 setuptools<82
diff --git a/requirements.txt b/requirements.txt
index fd130d5d3..054af5b24 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -183,12 +183,7 @@ paste==3.10.1
     # via
     #   -r requirements.in
     #   easywidgets
-    #   pastescript
 pastedeploy==3.1.0
-    # via
-    #   -r requirements.in
-    #   pastescript
-pastescript==3.7.0
     # via -r requirements.in
 pillow==12.2.0
     # via -r requirements.in
@@ -278,7 +273,6 @@ setuptools==81.0.0
     #   -r requirements.in
     #   colander
     #   paste
-    #   pastescript
     #   pysolr
 sgmllib3k==1.0.0
     # via feedparser
diff --git a/scripts/add_user_to_group.py b/scripts/add_user_to_group.py
index 75be9a60f..a09909b1d 100644
--- a/scripts/add_user_to_group.py
+++ b/scripts/add_user_to_group.py
@@ -23,19 +23,19 @@
 
 Example:
     # Add admin1 to Admin group for the entire /p/ neighborhood:
-    $ paster script production.ini ../scripts/add_user_to_group.py -- admin1 Admin
+    $ allura script production.ini ../scripts/add_user_to_group.py -- admin1 Admin
 
     # Add admin1 to Member group on project /p/allura:
-    $ paster script production.ini ../scripts/add_user_to_group.py -- admin1 Member allura
+    $ allura script production.ini ../scripts/add_user_to_group.py -- admin1 Member allura
 
     # Add admin1 to Developer group on project /berlios/codeblocks:
-    $ paster script production.ini ../scripts/add_user_to_group.py -- admin1 Developer codeblocks --nbhd=/berlios/
+    $ allura script production.ini ../scripts/add_user_to_group.py -- admin1 Developer codeblocks --nbhd=/berlios/
 
     # Add admin1 to Admin group for entire berlios neighborhood:
-    $ paster script production.ini ../scripts/add_user_to_group.py -- admin1 Admin --nbhd=/berlios/
+    $ allura script production.ini ../scripts/add_user_to_group.py -- admin1 Admin --nbhd=/berlios/
 
     # Replace admin1 with admin2 in all individual projects in berlios neighborhood:
-    $ paster script production.ini ../scripts/add_user_to_group.py -- admin2 Admin ALLPROJECTS --nbhd=/berlios/ --replace-users admin1
+    $ allura script production.ini ../scripts/add_user_to_group.py -- admin2 Admin ALLPROJECTS --nbhd=/berlios/ --replace-users admin1
 
 """
 
diff --git a/scripts/migrations/035-field-encryption.sh b/scripts/migrations/035-field-encryption.sh
index 0a5d99b6e..bdf34b638 100755
--- a/scripts/migrations/035-field-encryption.sh
+++ b/scripts/migrations/035-field-encryption.sh
@@ -32,7 +32,7 @@ fi
 
 export PYTHONWARNINGS="${PYTHONWARNINGS:-ignore:pkg_resources is deprecated as an API:UserWarning}"
 
-CMD="paster script $INI ../scripts/convert_encrypted_field.py"
+CMD="allura script $INI ../scripts/convert_encrypted_field.py"
 
 $CMD forgediscussion.model.forum.Forum monitoring_email && \
 $CMD forgeblog.model.blog.BlogPostSnapshot author.username && \
diff --git a/scripts/migrations/036-field-encryption-cleanup.sh b/scripts/migrations/036-field-encryption-cleanup.sh
index 42c40a54b..8b3c3d6c8 100755
--- a/scripts/migrations/036-field-encryption-cleanup.sh
+++ b/scripts/migrations/036-field-encryption-cleanup.sh
@@ -32,7 +32,7 @@ fi
 
 export PYTHONWARNINGS="${PYTHONWARNINGS:-ignore:pkg_resources is deprecated as an API:UserWarning}"
 
-CMD="paster script $INI ../scripts/convert_encrypted_field.py -- --remove-unencrypted"
+CMD="allura script $INI ../scripts/convert_encrypted_field.py -- --remove-unencrypted"
 
 $CMD forgediscussion.model.forum.Forum monitoring_email && \
 $CMD forgeblog.model.blog.BlogPostSnapshot author.username && \