diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 75d614557d7..00000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "bun"
- directory: "/"
- target-branch: "AgentHits-Dev"
- schedule:
- interval: "weekly"
- day: "monday"
- cooldown:
- default-days: 3
- open-pull-requests-limit: 5
- groups:
- minor-and-patch:
- update-types: ["minor", "patch"]
- ignore:
- # Major bumps are done by hand, one PR each, after a check on dev.
- - dependency-name: "*"
- update-types: ["version-update:semver-major"]
- # Held back on purpose: these break typecheck, change API behavior or need migrations.
- - dependency-name: "better-auth"
- update-types: ["version-update:semver-minor"]
- - dependency-name: "@better-auth/*"
- update-types: ["version-update:semver-minor"]
- # Pinned in the root overrides so better-auth and better-call share one copy;
- # a bump of the direct dependency alone does nothing.
- - dependency-name: "@better-auth/utils"
- - dependency-name: "stripe"
- - dependency-name: "@stepperize/react"
- - dependency-name: "@dokploy/trpc-openapi"
- - dependency-name: "ssh2"
diff --git a/.github/workflows/deps-auto-update.yml b/.github/workflows/deps-auto-update.yml
new file mode 100644
index 00000000000..c6641096d13
--- /dev/null
+++ b/.github/workflows/deps-auto-update.yml
@@ -0,0 +1,296 @@
+# Автообновление зависимостей Bun, как deps-auto-update.yml в репозиториях hit-proxy (решение владельца
+# 2026-10-27; заменило Dependabot для Bun: тот двигает только прямые зависимости, а security-обновлений для
+# Bun у него нет).
+# Каждый день: bun update всего lockfile в пределах диапазонов package.json - minor и patch, прямые и
+# транзитивные пакеты, версии не моложе суток; затем bun audit. Итог - ветка deps/auto-update и один PR в
+# AgentHits-Dev. Пока PR не влит, ветка каждый день пересобирается от свежей AgentHits-Dev с самыми новыми
+# версиями. Чужой commit в ветке открытого PR останавливает пересборку до merge или закрытия PR. Workflow не
+# мержит и не деплоит.
+# Отличия от hit-proxy: база AgentHits-Dev, проверки ветки - pull-request.yml, bun workspaces (общий
+# bun.lock в корне) обновляются через bun update --recursive. Файл одинаковый в dokploy и dokploy-mcp.
+name: Deps auto-update
+
+on:
+ schedule:
+ - cron: "0 3 * * *" # каждый день, 03:00 UTC
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: deps-auto-update
+ cancel-in-progress: false
+
+env:
+ BRANCH: deps/auto-update
+ BASE: AgentHits-Dev
+ # Workflow проверок, который запускается на ветке через workflow_dispatch.
+ CI_WORKFLOW: pull-request.yml
+ # Каталоги Bun-проектов через пробел, в каждом package.json и bun.lock.
+ DEP_DIRS: "."
+ # Минимальный возраст версии в секундах: сутки, как minimumReleaseAge в bunfig.toml.
+ MIN_RELEASE_AGE: "86400"
+ BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
+
+jobs:
+ # Новые пакеты скачиваются здесь, поэтому у job только чтение: токен read, credentials checkout не
+ # сохраняются. Пишет job publish, в котором Bun и пакеты не запускаются.
+ update:
+ if: github.ref == 'refs/heads/AgentHits-Dev'
+ runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-24.04' }}
+ timeout-minutes: 20
+ outputs:
+ changed: ${{ steps.update.outputs.changed }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.4.4
+ - name: bun update, проверка scope и отчёт
+ id: update
+ env:
+ NO_COLOR: "1"
+ run: |
+ set -euo pipefail
+ out="$RUNNER_TEMP/deps"
+ rm -rf "$out"
+ mkdir -p "$out/files"
+ allowed=()
+ declare -A recursive
+ for dir in $DEP_DIRS; do
+ prefix=""
+ [ "$dir" = "." ] || prefix="$dir/"
+ allowed+=("${prefix}package.json" "${prefix}bun.lock")
+ # Workspaces делят bun.lock корня: обновляются все сразу, их package.json тоже разрешены.
+ recursive[$dir]=""
+ while IFS= read -r pattern; do
+ recursive[$dir]=1
+ while IFS= read -r ws; do
+ allowed+=("${prefix}${ws}")
+ done < <(cd "$dir" && compgen -G "$pattern/package.json" || true)
+ done < <(cd "$dir" && jq -r '(.workspaces // []) | if type == "object" then (.packages // []) else . end | .[]' package.json)
+ (cd "$dir" && bun install --frozen-lockfile && bun update ${recursive[$dir]:+--recursive} --minimum-release-age="$MIN_RELEASE_AGE")
+ done
+
+ changed_files=()
+ while IFS= read -r line; do
+ path="${line:3}"
+ ok=false
+ for a in "${allowed[@]}"; do
+ if [ "$path" = "$a" ]; then ok=true; fi
+ done
+ if ! $ok; then
+ echo "::error::bun update изменил путь вне package.json и bun.lock: $path"
+ exit 1
+ fi
+ changed_files+=("$path")
+ done < <(git status --porcelain --untracked-files=all)
+
+ if [ "${#changed_files[@]}" -eq 0 ]; then
+ echo "Обновлений нет: всё актуально в пределах диапазонов и порога возраста."
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ printf '%s\n' "${changed_files[@]}" > "$out/files.txt"
+ for f in "${changed_files[@]}"; do
+ mkdir -p "$out/files/$(dirname "$f")"
+ cp "$f" "$out/files/$f"
+ done
+
+ today="$(date -u +%F)"
+ : > "$out/changes.txt"
+ : > "$out/audit.txt"
+ {
+ echo "Автообновление зависимостей от $today (workflow \`deps-auto-update\`): \`bun update\` в пределах диапазонов \`package.json\`, только minor и patch, прямые и транзитивные пакеты, версии не моложе суток. Ветка пересобирается каждый день в 03:00 UTC, пока PR открыт."
+ echo
+ for dir in $DEP_DIRS; do
+ prefix=""
+ [ "$dir" = "." ] || prefix="$dir/"
+ # "-/+ \"ключ\": [\"имя@версия\"" из bun.lock -> ключ, было, стало.
+ git diff -U0 -- "${prefix}bun.lock" | perl -ne '
+ if (/^([-+]) "([^"]+)": \["(?:@[^\/"]+\/)?[^@"]+@([^"]+)"/) { $v{$2}{$1} = $3 }
+ END {
+ for (sort keys %v) {
+ my ($o, $n) = ($v{$_}{"-"} // "", $v{$_}{"+"} // "");
+ print join("\t", $_, $o, $n), "\n" if $o ne $n;
+ }
+ }' > "$out/changes.tsv"
+ audit="$(cd "$dir" && bun audit 2>&1 || true)"
+ audit_line="$(printf '%s\n' "$audit" | grep -E 'vulnerabilit' | tail -n 1 || true)"
+ echo "${prefix:-./}: ${audit_line:-bun audit без итога}" >> "$out/audit.txt"
+ awk -F'\t' -v p="$prefix" '{ printf "%s%s %s -> %s\n", p, $1, ($2 == "" ? "(новый)" : $2), ($3 == "" ? "(удалён)" : $3) }' "$out/changes.tsv" >> "$out/changes.txt"
+
+ echo "### \`$dir\` — пакетов: $(wc -l < "$out/changes.tsv" | tr -d ' ')"
+ echo
+ echo "| Пакет | Было | Стало |"
+ echo "|---|---|---|"
+ awk -F'\t' '{ printf "| `%s` | %s | %s |\n", $1, ($2 == "" ? "—" : $2), ($3 == "" ? "удалён" : $3) }' "$out/changes.tsv"
+ echo
+ echo "**bun audit:** ${audit_line:-нет итоговой строки, см. ниже}"
+ echo
+ echo "Вывод bun audit
"
+ echo
+ echo '```'
+ printf '%s\n' "$audit" | tail -n 60
+ echo '```'
+ echo " "
+ echo
+ echo "bun outdated: колонка Latest выше Update - это major, их workflow не берёт
"
+ echo
+ echo '```'
+ (cd "$dir" && bun outdated ${recursive[$dir]:+--recursive} 2>&1 || true) | tail -n 120
+ echo '```'
+ echo " "
+ echo
+ done
+ echo "---"
+ echo "Перед merge: зелёный прогон \`$CI_WORKFLOW\` с событием \`workflow_dispatch\` на голове PR (workflow запускает его сам: push от GITHUB_TOKEN событие pull_request не создаёт). Commit в эту ветку останавливает ежедневную пересборку. Major - отдельными PR."
+ } > "$out/report.md"
+
+ total="$(wc -l < "$out/changes.txt" | tr -d ' ')"
+ {
+ echo "зависимости: автообновление библиотек ($today)"
+ echo
+ echo "Что сделано: bun update в пределах диапазонов package.json (workflow"
+ echo "deps-auto-update), версии не моложе суток. Изменено пакетов: $total."
+ echo
+ head -n 80 "$out/changes.txt"
+ if [ "$total" -gt 80 ]; then echo "... и ещё $((total - 80)), полный список в PR."; fi
+ echo
+ echo "Зачем: ежедневное автообновление зависимостей (решение владельца"
+ echo "2026-10-27), включая транзитивные пакеты, которые Dependabot не двигает."
+ echo
+ echo "Проверки: bun install --frozen-lockfile до обновления; bun audit:"
+ cat "$out/audit.txt"
+ echo "Проверки ветки запускаются отдельно ($CI_WORKFLOW, workflow_dispatch)."
+ } > "$out/commit.txt"
+
+ cat "$out/report.md" >> "$GITHUB_STEP_SUMMARY"
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ - uses: actions/upload-artifact@v4
+ if: steps.update.outputs.changed == 'true'
+ with:
+ name: deps-auto-update
+ path: ${{ runner.temp }}/deps
+ retention-days: 3
+
+ publish:
+ needs: update
+ runs-on: ${{ vars.CI_RUNS_ON || 'ubuntu-24.04' }}
+ timeout-minutes: 10
+ permissions:
+ contents: write
+ pull-requests: write
+ actions: write
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 0
+ - uses: actions/download-artifact@v4
+ if: needs.update.outputs.changed == 'true'
+ with:
+ name: deps-auto-update
+ path: ${{ runner.temp }}/deps
+ - name: Открытый PR ветки
+ id: pr
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const { data } = await github.rest.pulls.list({
+ ...context.repo,
+ state: 'open',
+ head: `${context.repo.owner}:${process.env.BRANCH}`,
+ base: process.env.BASE,
+ });
+ core.setOutput('number', data.length ? String(data[0].number) : '');
+ - name: Commit и push ветки
+ id: branch
+ env:
+ CHANGED: ${{ needs.update.outputs.changed }}
+ PR: ${{ steps.pr.outputs.number }}
+ run: |
+ set -euo pipefail
+ remote=false
+ if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null; then
+ git fetch -q origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH"
+ remote=true
+ fi
+ if [ -n "$PR" ] && $remote; then
+ foreign="$(git log --format='%h %ae' "origin/$BASE..origin/$BRANCH" | grep -v -F " $BOT_EMAIL" || true)"
+ if [ -n "$foreign" ]; then
+ echo "::notice::В ветке $BRANCH открытого PR #$PR есть чужие commit, пересборка остановлена до merge или закрытия PR: $(echo "$foreign" | tr '\n' ' ')"
+ echo "action=skip" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ fi
+ if [ "$CHANGED" != "true" ]; then
+ echo "action=close" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ git config user.name "github-actions[bot]"
+ git config user.email "$BOT_EMAIL"
+ git checkout -q -B "$BRANCH"
+ cp -R "$RUNNER_TEMP/deps/files/." .
+ mapfile -t files < "$RUNNER_TEMP/deps/files.txt"
+ git add -- "${files[@]}"
+ git commit -q -F "$RUNNER_TEMP/deps/commit.txt"
+ if $remote && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse "origin/$BRANCH^{tree}")" ]; then
+ echo "Ветка $BRANCH уже содержит то же дерево: push не нужен."
+ echo "action=same" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ git push -q --force origin "HEAD:refs/heads/$BRANCH"
+ echo "action=pushed" >> "$GITHUB_OUTPUT"
+ - name: PR и проверки ветки
+ uses: actions/github-script@v8
+ env:
+ ACTION: ${{ steps.branch.outputs.action }}
+ PR: ${{ steps.pr.outputs.number }}
+ with:
+ script: |
+ const fs = require('node:fs');
+ const { ACTION: action, BRANCH: branch, BASE: base, CI_WORKFLOW: ciWorkflow, RUNNER_TEMP: tmp } = process.env;
+ const repo = context.repo;
+ let pr = process.env.PR ? Number(process.env.PR) : null;
+ const summary = (text) => core.summary.addRaw(`${text}\n`).write();
+
+ if (action === 'skip') {
+ await summary(`PR #${pr}: в ветке чужие commit, пересборка пропущена.`);
+ return;
+ }
+ if (action === 'close') {
+ if (pr) {
+ await github.rest.issues.createComment({ ...repo, issue_number: pr, body: `Обновлений нет: ${base} уже содержит актуальные версии в пределах диапазонов. PR закрыт, ветка удалена.` });
+ await github.rest.pulls.update({ ...repo, pull_number: pr, state: 'closed' });
+ }
+ try {
+ await github.rest.git.deleteRef({ ...repo, ref: `heads/${branch}` });
+ } catch (error) {
+ if (error.status !== 404 && error.status !== 422) throw error;
+ }
+ await summary(pr ? `Обновлений нет, PR #${pr} закрыт.` : 'Обновлений нет.');
+ return;
+ }
+
+ const title = 'зависимости: автообновление библиотек';
+ let body = fs.readFileSync(`${tmp}/deps/report.md`, 'utf8');
+ if (body.length > 60000) body = `${body.slice(0, 60000)}\n\n… отчёт обрезан, полный - в summary прогона.`;
+ if (pr) {
+ await github.rest.pulls.update({ ...repo, pull_number: pr, title, body });
+ } else {
+ try {
+ ({ data: { number: pr } } = await github.rest.pulls.create({ ...repo, head: branch, base, title, body }));
+ } catch (error) {
+ core.setFailed(`Ветка ${branch} обновлена, но PR не создан (${error.status}: ${error.message}). Разрешите Actions создавать PR: Settings репозитория (и организации, если она есть) → Actions → General → Workflow permissions → «Allow GitHub Actions to create and approve pull requests».`);
+ }
+ }
+ if (action === 'pushed') {
+ await github.rest.actions.createWorkflowDispatch({ ...repo, workflow_id: ciWorkflow, ref: branch });
+ }
+ await summary(pr ? `PR #${pr}: ${action === 'pushed' ? `ветка обновлена, ${ciWorkflow} запущен` : 'ветка без изменений, описание обновлено'}.` : `Ветка ${branch} обновлена, PR нет.`);
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 080abd085b4..5bb2ed0bb15 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -3,12 +3,15 @@ name: Pull Request
on:
pull_request:
branches: [main, canary, AgentHits-Dev]
+ # deps-auto-update.yml starts the checks of its branch this way: a push with
+ # GITHUB_TOKEN creates no pull_request event.
+ workflow_dispatch:
permissions:
contents: read
concurrency:
- group: pull-request-${{ github.event.pull_request.number }}
+ group: pull-request-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
diff --git a/CLAUDE.md b/CLAUDE.md
index 010c38e40af..e9f4bfa0094 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -16,3 +16,13 @@ Every pull request targets `AgentHits-Dev`, including a PR that builds on anothe
## Releases
Every push to `AgentHits-Dev` (every merged PR) gets a GitHub release `agenthits-vX.Y.Z` from `.github/workflows/agenthits-release.yml`. The `agenthits-` prefix keeps fork tags apart from upstream Dokploy `v0.x` tags. The version goes up by patch, by minor when a merged PR title starts with `feat`, and by major only through a manual run with `bump: major`. The release notes are the merged PR's title and body, so write the PR description for someone reading the release. The image of a release is `ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z` and panels show that tag as the fork version; `agenthits-dev` always points at the newest build. `.github/workflows/agenthits-ghcr-retention.yml` deletes old GHCR versions weekly but never a release tag, `agenthits-dev` or `buildcache`.
+
+## Dependencies
+
+`.github/workflows/deps-auto-update.yml` runs every day at 03:00 UTC (manually: `gh workflow run deps-auto-update.yml --repo TaskFlow-Systems/dokploy --ref AgentHits-Dev`). It runs `bun update --recursive` within the `package.json` ranges (minor and patch, direct and transitive packages, versions at least one day old), then `bun audit`, and keeps one PR `зависимости: автообновление библиотек` from `deps/auto-update` into `AgentHits-Dev`, rebuilt daily until it is merged. It starts `pull-request.yml` on the branch through `workflow_dispatch`; judge the PR by that run. It never merges. The same workflow runs in the owner's other projects; it replaced Dependabot for Bun, which moves only direct dependencies. It needs "Allow GitHub Actions to create and approve pull requests" in the repo's Actions settings.
+
+- Held packages stay pinned to exact versions (`better-auth`, `@better-auth/*`, `stripe`, `@stepperize/react`, `@dokploy/trpc-openapi`) or `~` (`ssh2`), and the root `overrides` are exact, so the workflow does not move them. Keep new holds pinned the same way.
+- Major updates are done by hand, one PR each (the `dokploy-upgrader` agent), with `bun add @ --minimum-release-age=86400`.
+- The one-day age is `minimumReleaseAge` in `bunfig.toml` and `MIN_RELEASE_AGE` in the workflow; change both together.
+- A commit pushed to the branch of the open PR stops its daily rebuild until the PR is merged or closed.
+- `.github/workflows/dependency-audit.yml` runs `bun audit` every Monday for advisories published between updates.
diff --git a/bunfig.toml b/bunfig.toml
index b5d11bc61ef..5821272f2e6 100644
--- a/bunfig.toml
+++ b/bunfig.toml
@@ -1,3 +1,4 @@
[install]
-# Freshly published versions are where hijacked releases live; most get pulled within days.
-minimumReleaseAge = 259200
+# Freshly published versions are where hijacked releases live; most get pulled within a day.
+# One day, as in the owner's other projects and in deps-auto-update.yml (owner's decision 2026-10-27).
+minimumReleaseAge = 86400