Compare commits

...

7 Commits

Author SHA1 Message Date
49ebbc34fa fix: do not update package status if it is unchanged
In order to prevent timestamp bumps, filter by status is added
2026-02-24 15:33:06 +02:00
e376f1307f docs: remove required flag from email.template_full option 2026-02-22 02:57:34 +02:00
415fcf58ce Release 2.20.0rc4 2026-02-21 12:52:51 +02:00
17a2d6362c style: replace """ with " for single line strings 2026-02-21 12:51:58 +02:00
e6275de4ed fix: handle class overrides for retry polices correctly
Previous implementation didn't work as intended because there was still
override in init. Current implementation instead of playing with
guessing separates default and instance setttings. Also update test
cases to handle this scenario correctly
2026-02-21 12:40:11 +02:00
4d009cba6d Release 2.20.0rc3 2026-02-20 20:56:05 +02:00
f6defbf90d fix: rollback samesite option to Lax, because of broken OAuth 2026-02-20 20:54:37 +02:00
16 changed files with 63 additions and 44 deletions

View File

@@ -2,7 +2,7 @@
pkgbase='ahriman' pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web') pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.20.0rc2 pkgver=2.20.0rc4
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2026\-02\-20" "ahriman 2.20.0rc2" "ArcH linux ReposItory MANager" .TH AHRIMAN "1" "2026\-02\-21" "ahriman 2.20.0rc4" "ArcH linux ReposItory MANager"
.SH NAME .SH NAME
ahriman \- ArcH linux ReposItory MANager ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS .SH SYNOPSIS

View File

@@ -245,7 +245,7 @@ _shtab_ahriman_init_options=(
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email]:packager:" "--packager[packager name and email (default\: None)]:packager:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:" "--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
@@ -526,7 +526,7 @@ _shtab_ahriman_repo_init_options=(
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email]:packager:" "--packager[packager name and email (default\: None)]:packager:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:" "--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
@@ -583,7 +583,7 @@ _shtab_ahriman_repo_setup_options=(
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email]:packager:" "--packager[packager name and email (default\: None)]:packager:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:" "--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
@@ -757,7 +757,7 @@ _shtab_ahriman_service_setup_options=(
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email]:packager:" "--packager[packager name and email (default\: None)]:packager:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:" "--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
@@ -792,7 +792,7 @@ _shtab_ahriman_setup_options=(
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:" {--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:" "--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:" {--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email]:packager:" "--packager[packager name and email (default\: None)]:packager:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:" "--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:" "--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)" "*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"

View File

@@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.20.0rc2" __version__ = "2.20.0rc4"

View File

@@ -251,7 +251,7 @@ class Setup(Handler):
content = f"PACKAGER='{packager}'\n" content = f"PACKAGER='{packager}'\n"
if makeflags_jobs: if makeflags_jobs:
content += """MAKEFLAGS="-j$(nproc)"\n""" content += "MAKEFLAGS=\"-j$(nproc)\"\n"
uid, _ = paths.root_owner uid, _ = paths.root_owner
home_dir = Path(getpwuid(uid).pw_dir) home_dir = Path(getpwuid(uid).pw_dir)

View File

@@ -69,8 +69,8 @@ class OAuth(Mapping):
Returns: Returns:
str: login control as html code to insert str: login control as html code to insert
""" """
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{ return f"<a class=\"nav-link\" href=\"/api/v1/login\" title=\"login via OAuth2\"><i class=\"bi bi-{
self.icon}"></i> login</a>""" self.icon}\"></i> login</a>"
@staticmethod @staticmethod
def get_provider(name: str) -> type[aioauth_client.OAuth2Client]: def get_provider(name: str) -> type[aioauth_client.OAuth2Client]:

View File

@@ -292,6 +292,7 @@ class PackageOperations(Operations):
(:package_base, :status, :last_updated, :repository) (:package_base, :status, :last_updated, :repository)
on conflict (package_base, repository) do update set on conflict (package_base, repository) do update set
status = :status, last_updated = :last_updated status = :status, last_updated = :last_updated
where status != :status
""", """,
{ {
"package_base": package_base, "package_base": package_base,

View File

@@ -22,7 +22,7 @@ import sys
from functools import cached_property from functools import cached_property
from requests.adapters import BaseAdapter, HTTPAdapter from requests.adapters import BaseAdapter, HTTPAdapter
from typing import Any, IO, Literal from typing import Any, ClassVar, IO, Literal
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from ahriman import __version__ from ahriman import __version__
@@ -39,14 +39,18 @@ class SyncHttpClient(LazyLogging):
wrapper around requests library to reduce boilerplate wrapper around requests library to reduce boilerplate
Attributes: Attributes:
DEFAULT_MAX_RETRIES(int): (class attribute) default maximum amount of retries
DEFAULT_RETRY_BACKOFF(float): (class attribute) default retry exponential backoff
DEFAULT_TIMEOUT(int | None): (class attribute) default HTTP request timeout in seconds
auth(tuple[str, str] | None): HTTP basic auth object if set auth(tuple[str, str] | None): HTTP basic auth object if set
retry(Retry): retry policy of the HTTP client. Disabled by default retry(Retry): retry policy of the HTTP client
suppress_errors(bool): suppress logging of request errors suppress_errors(bool): suppress logging of request errors
timeout(int | None): HTTP request timeout in seconds timeout(int | None): HTTP request timeout in seconds
""" """
retry: Retry = Retry() DEFAULT_MAX_RETRIES: ClassVar[int] = 0
timeout: int | None = None DEFAULT_RETRY_BACKOFF: ClassVar[float] = 0.0
DEFAULT_TIMEOUT: ClassVar[int | None] = 30
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *, def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
suppress_errors: bool = False) -> None: suppress_errors: bool = False) -> None:
@@ -65,10 +69,10 @@ class SyncHttpClient(LazyLogging):
self.suppress_errors = suppress_errors self.suppress_errors = suppress_errors
self.timeout = configuration.getint(section, "timeout", fallback=30) self.timeout = configuration.getint(section, "timeout", fallback=self.DEFAULT_TIMEOUT)
self.retry = SyncHttpClient.retry_policy( self.retry = SyncHttpClient.retry_policy(
max_retries=configuration.getint(section, "max_retries", fallback=0), max_retries=configuration.getint(section, "max_retries", fallback=self.DEFAULT_MAX_RETRIES),
retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=0.0), retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=self.DEFAULT_RETRY_BACKOFF),
) )
@cached_property @cached_property

View File

@@ -131,7 +131,6 @@ class ReportTrigger(Trigger):
"template_full": { "template_full": {
"type": "string", "type": "string",
"dependencies": ["templates"], "dependencies": ["templates"],
"required": True,
"empty": False, "empty": False,
}, },
"templates": { "templates": {

View File

@@ -88,11 +88,9 @@ class Repository(Executor, UpdateHandler):
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
AUR.timeout = configuration.getint("aur", "timeout", fallback=30) AUR.DEFAULT_MAX_RETRIES = configuration.getint("aur", "max_retries", fallback=0)
AUR.retry = AUR.retry_policy( AUR.DEFAULT_RETRY_BACKOFF = configuration.getfloat("aur", "retry_backoff", fallback=0.0)
max_retries=configuration.getint("aur", "max_retries", fallback=0), AUR.DEFAULT_TIMEOUT = configuration.getint("aur", "timeout", fallback=30)
retry_backoff=configuration.getfloat("aur", "retry_backoff", fallback=0.0),
)
def _set_context(self) -> None: def _set_context(self) -> None:
""" """

View File

@@ -375,7 +375,7 @@ class Package(LazyLogging):
Returns: Returns:
str: print-friendly string str: print-friendly string
""" """
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})""" details = "" if self.is_single_package else f" ({" ".join(sorted(self.packages.keys()))})"
return f"{self.base}{details}" return f"{self.base}{details}"
def vercmp(self, version: str) -> int: def vercmp(self, version: str) -> int:

View File

@@ -147,7 +147,7 @@ class PkgbuildPatch:
""" """
if "$" in source: if "$" in source:
# copy from library method with double quotes instead # copy from library method with double quotes instead
return f"""\"{source.replace("\"", "'\"'")}\"""" return f"\"{source.replace("\"", "'\"'")}\""
# otherwise just return normal call # otherwise just return normal call
return shlex.quote(source) return shlex.quote(source)
@@ -195,13 +195,13 @@ class PkgbuildPatch:
""" """
if isinstance(self.value, list): # list like if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value)) value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})""" return f"{self.key}=({value})"
if self.is_plain_diff: # no additional logic for plain diffs if self.is_plain_diff: # no additional logic for plain diffs
return self.value return self.value
# we suppose that function values are only supported in string-like values # we suppose that function values are only supported in string-like values
if self.is_function: if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here return f"{self.key} {self.value}" # no quoting enabled here
return f"""{self.key}={self.quote(self.value)}""" return f"{self.key}={self.quote(self.value)}"
def substitute(self, variables: dict[str, str]) -> str | list[str]: def substitute(self, variables: dict[str, str]) -> str | list[str]:
""" """

View File

@@ -154,7 +154,7 @@ def setup_auth(application: Application, configuration: Configuration, validator
cookie_name="AHRIMAN", cookie_name="AHRIMAN",
max_age=validator.max_age, max_age=validator.max_age,
httponly=True, httponly=True,
samesite="Strict", samesite="Lax",
) )
setup_session(application, storage) setup_session(application, storage)

View File

@@ -157,8 +157,7 @@ def test_package_update_get(database: SQLite, package_ahriman: Package) -> None:
database.package_update(package_ahriman) database.package_update(package_ahriman)
database.status_update(package_ahriman.base, status) database.status_update(package_ahriman.base, status)
assert next((db_package, db_status) assert next((db_package, db_status)
for db_package, db_status in database.packages_get() for db_package, db_status in database.packages_get()) == (package_ahriman, status)
if db_package.base == package_ahriman.base) == (package_ahriman, status)
def test_package_update_remove_get(database: SQLite, package_ahriman: Package) -> None: def test_package_update_remove_get(database: SQLite, package_ahriman: Package) -> None:
@@ -176,10 +175,10 @@ def test_package_update_update(database: SQLite, package_ahriman: Package) -> No
""" """
database.package_update(package_ahriman) database.package_update(package_ahriman)
package_ahriman.version = "1.0.0" package_ahriman.version = "1.0.0"
database.package_update(package_ahriman) database.package_update(package_ahriman)
assert next(db_package.version assert next(db_package.version
for db_package, _ in database.packages_get() for db_package, _ in database.packages_get()) == package_ahriman.version
if db_package.base == package_ahriman.base) == package_ahriman.version
def test_status_update(database: SQLite, package_ahriman: Package) -> None: def test_status_update(database: SQLite, package_ahriman: Package) -> None:
@@ -188,6 +187,19 @@ def test_status_update(database: SQLite, package_ahriman: Package) -> None:
""" """
status = BuildStatus() status = BuildStatus()
database.package_update(package_ahriman, database._repository_id) database.package_update(package_ahriman)
database.status_update(package_ahriman.base, status, database._repository_id) database.status_update(package_ahriman.base, status)
assert database.packages_get(database._repository_id) == [(package_ahriman, status)] assert database.packages_get() == [(package_ahriman, status)]
def test_status_update_skip_same_status(database: SQLite, package_ahriman: Package) -> None:
"""
must preserve original timestamp when status is unchanged
"""
status = BuildStatus(timestamp=42)
database.package_update(package_ahriman)
database.status_update(package_ahriman.base, status)
database.status_update(package_ahriman.base, BuildStatus())
assert next(db_status.timestamp
for _, db_status in database.packages_get()) == status.timestamp

View File

@@ -49,14 +49,18 @@ def test_retry_policy() -> None:
""" """
must set retry policy must set retry policy
""" """
SyncHttpClient.retry = SyncHttpClient.retry_policy(1, 2.0) SyncHttpClient.DEFAULT_MAX_RETRIES = 1
AUR.retry = AUR.retry_policy(3, 4.0) SyncHttpClient.DEFAULT_RETRY_BACKOFF = 2.0
AUR.DEFAULT_MAX_RETRIES = 3
AUR.DEFAULT_RETRY_BACKOFF = 4.0
assert SyncHttpClient.retry.connect == 1 client = SyncHttpClient()
assert SyncHttpClient.retry.backoff_factor == 2.0 assert client.retry.connect == 1
assert client.retry.backoff_factor == 2.0
assert AUR.retry.connect == 3 aur = AUR()
assert AUR.retry.backoff_factor == 4.0 assert aur.retry.connect == 3
assert aur.retry.backoff_factor == 4.0
def test_exception_response_text() -> None: def test_exception_response_text() -> None:

View File

@@ -31,8 +31,9 @@ def test_set_globals(configuration: Configuration) -> None:
configuration.set_option("aur", "max_retries", "10") configuration.set_option("aur", "max_retries", "10")
Repository._set_globals(configuration) Repository._set_globals(configuration)
assert AUR.timeout == 42 aur = AUR()
assert AUR.retry.connect == 10 assert aur.timeout == 42
assert aur.retry.connect == 10
def test_set_context(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None: def test_set_context(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None: