Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions .github/dependency_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)))
Expand All @@ -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())
Expand All @@ -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")
Expand All @@ -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 --------------------------------------------------------------------------------------------------
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/dependency-updates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
4 changes: 2 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<Copyright>Copyright 2026-$([System.DateTime]::Now.ToString('yyyy')) Runemark Labs. Portions copyright 2014-2026 prowlarr.com and the Servarr team (GNU General Public v3)</Copyright>

<!-- Should be replaced by CI -->
<AssemblyVersion>0.4.5.*</AssemblyVersion>
<AssemblyVersion>0.4.6.*</AssemblyVersion>
<AssemblyConfiguration>$(Configuration)-dev</AssemblyConfiguration>
<NeutralLanguage>en</NeutralLanguage>

Expand Down Expand Up @@ -121,7 +121,7 @@
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<!-- NUnit comes from Requestarr.Test.Common only, so a dependency update moves every test project at once -->
<PackageReference Include="NUnit3TestAdapter" Version="6.3.0" />
<PackageReference Include="NunitXml.TestLogger" Version="8.0.0" />
</ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading