diff --git a/.github/dependency_updates.py b/.github/dependency_updates.py
index fd42bfc..ab0512d 100644
--- a/.github/dependency_updates.py
+++ b/.github/dependency_updates.py
@@ -5,8 +5,8 @@
change Dependabot can't size), plus "security" for a security fix and "for admins" when that fix is in
something Requestarr ships (the backend, the base images, or a web app package the built app contains).
update the oldest open "automatic update": bring it up to date with dev, give it the next version and its
- changelog section, run build on that commit and let GitHub merge it once build passes. One at a time, so
- two updates never take the same version; the next one goes when this one has merged.
+ changelog section, run build on that commit and merge it once build passes (hold it if build fails). One
+ at a time, so two updates never take the same version; the next one goes when this one has merged.
--self-test checks the parts that decide, against built-in samples.
Needs git, the GitHub CLI (GH_TOKEN) and the repository checked out with full history.
@@ -228,14 +228,22 @@ def build_runs(repo, sha):
def update():
repo = os.environ["REPO"]
ensure_labels(repo)
- waiting = json.loads(gh("pr", "list", "--repo", repo, "--base", "dev", "--state", "open", "--label", AUTOMATIC,
+ # An update that merged or was held makes way for the next one in the same run.
+ for _ in range(10):
+ if not update_next(repo):
+ return
+
+
+def update_next(repo):
+ """Moves the oldest automatic update on; True when it merged or was held, so the next one can go."""
+ waiting =json.loads(gh("pr", "list", "--repo", repo, "--base", "dev", "--state", "open", "--label", AUTOMATIC,
"--json", "number,headRefName,labels,author,createdAt,isDraft"))
waiting = [p for p in waiting if p["author"]["login"] in ("app/dependabot", "dependabot[bot]", "dependabot")
and REVIEW not in {l["name"] for l in p["labels"]}]
if not waiting:
print("No automatic update is waiting.")
- return
- pr = sorted(waiting, key=lambda p: p["createdAt"])[0]
+ return False
+ pr =sorted(waiting, key=lambda p: p["createdAt"])[0]
number, branch = pr["number"], pr["headRefName"]
labels = {l["name"] for l in pr["labels"]}
print("Next automatic update: #%d (%s); %d waiting" % (number, branch, len(waiting)))
@@ -257,7 +265,7 @@ def update():
if others:
git("merge", "--abort", check=False)
hold(repo, number, "it conflicts with dev in %s." % ", ".join(others))
- return
+ return True
# Dependabot never changes these two: start from dev's and add this update's version on top.
git("checkout", "origin/dev", "--", "CHANGELOG.md", "src/Directory.Build.props")
version = next_version(*tops.values())
@@ -276,7 +284,7 @@ def update():
if pushed.returncode != 0:
sys.stderr.write(pushed.stderr)
hold(repo, number, "its version and changelog section could not be added (the push was refused).")
- return
+ return True
print("#%d: version %d.%d.%d added" % ((number,) + version))
sha = git("rev-parse", "HEAD")
@@ -286,13 +294,20 @@ def update():
if any(r["status"] == "completed" and r["conclusion"] in FAILED for r in runs) \
and not any(r["conclusion"] == "success" for r in runs):
hold(repo, number, "build failed on it.")
- return
- if not any(r["status"] != "completed" or r["conclusion"] == "success" for r in runs):
+ return True
+ if any(r["conclusion"] == "success" for r in runs):
+ # Build passed on this commit (this run was started when it finished): merge now rather than wait for
+ # auto-merge, whose merge into dev wouldn't start this workflow again for the next update.
+ if subprocess.run(["gh", "pr", "merge", str(number), "--repo", repo, "--merge"]).returncode == 0:
+ print("#%d: build passed, merged into dev" % number)
+ return True
+ elif not any(r["status"] != "completed" for r in runs):
# Commits pushed by this workflow don't start workflows, so build is started here on this commit.
gh("workflow", "run", "build.yml", "--repo", repo, "--ref", branch, "-f", "base=dev")
print("#%d: build started on %s" % (number, sha[:7]))
gh("pr", "merge", str(number), "--repo", repo, "--auto", "--merge")
print("#%d: merges on its own once build passes" % number)
+ return False
# ---- self-test --------------------------------------------------------------------------------------------------
diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml
index 1426347..875fb45 100644
--- a/.github/workflows/dependency-updates.yml
+++ b/.github/workflows/dependency-updates.yml
@@ -2,15 +2,19 @@ name: Dependency updates
# Dependabot's pull requests into dev (docs/development.md#dependency-updates; .github/dependency_updates.py does the
# work). A new update is labelled: patch and minor "automatic update", major "needs review" (held, with a comment).
-# Automatic updates go one at a time: the next version and its changelog section are added, build runs on that commit,
-# and GitHub merges it once build passes. This runs again when dev moves and every hour, because a merge this workflow
-# enabled doesn't start workflows itself, so a waiting update always carries a version above dev's, qa's and staging's.
+# Automatic updates go one at a time: the next version and its changelog section are added and build is started on that
+# commit. When that build finishes this runs again and merges the update (or holds it if build failed), then starts the
+# next one. It also runs when dev moves and every hour, so a waiting update always carries a version above dev's, qa's
+# and staging's.
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [dev]
push:
branches: [dev]
+ workflow_run:
+ workflows: [Build]
+ types: [completed]
schedule:
- cron: "23 * * * *"
workflow_dispatch:
@@ -50,7 +54,8 @@ jobs:
needs: sort
if: >
always() && needs.sort.result != 'failure' &&
- (github.event_name != 'pull_request' || github.event.pull_request.user.login == 'dependabot[bot]')
+ (github.event_name != 'pull_request' || github.event.pull_request.user.login == 'dependabot[bot]') &&
+ (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7327eae..6e5f69e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ with nothing in it is left out, and a version with nothing for either has only i
in Discord: both sections in the admin channel, and **For users** in the announcements channel, word for word. Each
version heading carries the day it was made, which its card in Discord shows.
+## 0.4.6 (2026-10-25)
+
## 0.4.5 (2026-10-25)
## 0.4.4 (2026-10-25)
diff --git a/docs/development.md b/docs/development.md
index ecf4c9f..2c401f4 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -170,10 +170,11 @@ So each kind sends at most one pull request a week for its patch and minor updat
**What happens to them** (`.github/workflows/dependency-updates.yml`, `.github/dependency_updates.py`):
- A new update is sorted. Patch and minor get the **automatic update** label. Major updates, and updates Dependabot can't size, get **needs review** and a comment, and wait. A security fix follows the same rule by size, and also gets **security**. It gets **for admins** too when the fix is in something Requestarr ships: the backend, the base images, or a web app package the built app contains (followed through `yarn.lock` from `dependencies`, not `devDependencies`).
-- Automatic updates go one at a time, oldest first. The workflow brings the update up to date with `dev`, adds the next version above `dev`'s, `qa`'s and `staging`'s to `src/Directory.Build.props` and `CHANGELOG.md`, starts `build` on that commit, and turns on GitHub's auto-merge. GitHub merges it once `build` passes (the full build: backend tests on Linux and Windows, the web app, the image, the secret scan and the Changelog check).
+- Automatic updates go one at a time, oldest first. The workflow brings the update up to date with `dev`, adds the next version above `dev`'s, `qa`'s and `staging`'s to `src/Directory.Build.props` and `CHANGELOG.md`, starts `build` on that commit, and turns on GitHub's auto-merge. When that `build` finishes the workflow runs again: it merges the update if `build` passed (the full build: backend tests on Linux and Windows, the web app, the image, the secret scan and the Changelog check) and starts on the next one.
- The changelog section is the version heading only, which the changelog rule allows for a change nobody would notice. A security fix labelled **for admins** gets one line under For admins: "**Security update:** parts Requestarr is built from were updated to fix known security problems, so update when you can."
- If an automatic update conflicts with `dev` (other than the version and changelog, which it rewrites), can't be pushed, or fails `build`, it is moved to **needs review** with a comment.
-- The workflow runs when an update opens or changes, when `dev` moves, and every hour. The hourly run matters: a merge made through auto-merge that this workflow turned on doesn't start other workflows, so the next update waits for it.
+- The workflow runs when an update opens or changes, when `dev` moves, when a `build` it started finishes, and every hour as a fallback (GitHub can run scheduled workflows late or skip them).
+- A package used in more than one project must have its version set in one place, or an update moves only some of them and `build` fails. NUnit, for example, comes only from `Requestarr.Test.Common`.
**Major updates and held updates** are Claude Code's: review the changes, fix what breaks, and merge in a normal task (a pull request into `dev` with its own version and changelog, or the held one finished and merged by hand).
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index eed68be..6377229 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -74,7 +74,7 @@
Copyright 2026-$([System.DateTime]::Now.ToString('yyyy')) Runemark Labs. Portions copyright 2014-2026 prowlarr.com and the Servarr team (GNU General Public v3)
- 0.4.5.*
+ 0.4.6.*
$(Configuration)-dev
en
@@ -121,7 +121,7 @@
-
+
diff --git a/yarn.lock b/yarn.lock
index 66a558e..012ba7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3418,9 +3418,9 @@ fast-levenshtein@^2.0.6:
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-uri@^3.0.1:
- version "3.1.8"
- resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.8.tgz#f7db8d942e20eead3cfe93b0beeb4eb0169e938b"
- integrity sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==
+ version "3.1.9"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.9.tgz#f1deba822b1d6ac79da2f860e872a63470cff203"
+ integrity sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==
fastest-levenshtein@^1.0.16:
version "1.0.16"