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"