diff --git a/.github/scripts/agenthits-ghcr-retention.sh b/.github/scripts/agenthits-ghcr-retention.sh new file mode 100755 index 00000000000..81865c339a2 --- /dev/null +++ b/.github/scripts/agenthits-ghcr-retention.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Deletes old GHCR versions of the AgentHits image. Kept: every version tagged +# agenthits-vX.Y.Z, agenthits-dev or buildcache, the newest KEEP_RECENT other +# versions, and every manifest a kept image index points at (GHCR lists the +# platform manifests of an index as separate untagged versions, and deleting +# them breaks the tagged image). +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN must be a token of the package owner with read:packages and delete:packages}" +owner="${PACKAGE_OWNER:-agentHits}" +package="${PACKAGE_NAME:-dokploy}" +image="${IMAGE:-ghcr.io/agenthits/dokploy}" +keep_recent="${KEEP_RECENT:-10}" +dry_run="${DRY_RUN:-true}" +api="users/${owner}/packages/container/${package}/versions" + +versions="$(gh api --paginate "${api}?per_page=100" | jq -s 'add // []')" + +plan="$(jq --argjson keep "$keep_recent" ' + def protected: + (.metadata.container.tags // []) + | any(. == "agenthits-dev" or . == "buildcache" or test("^agenthits-v[0-9]+\\.[0-9]+\\.[0-9]+$")); + sort_by(.created_at) | reverse + | map(select(protected | not)) as $rest + | {keep: (map(select(protected)) + $rest[:$keep]), delete: $rest[$keep:]} +' <<<"$versions")" + +if ! jq -e 'any(.keep[]; (.metadata.container.tags // []) | index("agenthits-dev"))' <<<"$plan" >/dev/null; then + echo "::error::No version tagged agenthits-dev in the listing of ${owner}/${package}; refusing to delete anything." + exit 1 +fi + +work_dir="$(mktemp -d)" +: >"${work_dir}/children" +while read -r digest; do + docker buildx imagetools inspect --raw "${image}@${digest}" >"${work_dir}/manifest.json" + jq -r '.manifests[]?.digest' "${work_dir}/manifest.json" >>"${work_dir}/children" +done < <(jq -r '.keep[].name' <<<"$plan") +children="$(jq -R . "${work_dir}/children" | jq -s 'unique')" +delete="$(jq --argjson children "$children" '.delete | map(select(.name as $d | $children | index($d) | not))' <<<"$plan")" + +echo "Versions: $(jq length <<<"$versions"), keeping $(($(jq length <<<"$versions") - $(jq length <<<"$delete"))), deleting $(jq length <<<"$delete") (dry run: ${dry_run})." + +failed=0 +while IFS=$'\t' read -r id digest created tags; do + label="${id} ${digest} ${created} ${tags:-}" + if [ "$dry_run" = true ]; then + echo "would delete ${label}" + elif gh api -X DELETE "${api}/${id}" >/dev/null; then + echo "deleted ${label}" + else + echo "::warning::could not delete ${label}" + failed=$((failed + 1)) + fi +done < <(jq -r '.[] | [.id, .name, .created_at, ((.metadata.container.tags // []) | join(","))] | @tsv' <<<"$delete") + +if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + echo "## GHCR retention for ${owner}/${package}" + echo + echo "Dry run: ${dry_run}. Versions listed: $(jq length <<<"$versions"), to delete: $(jq length <<<"$delete"), failed: ${failed}." + echo + echo "Kept tags: $(jq -r '[.keep[].metadata.container.tags // [] | .[]] | join(", ")' <<<"$plan")" + } >>"$GITHUB_STEP_SUMMARY" +fi + +if [ "$failed" != 0 ]; then + echo "::error::${failed} version(s) could not be deleted" + exit 1 +fi diff --git a/.github/scripts/agenthits-next-release.sh b/.github/scripts/agenthits-next-release.sh new file mode 100755 index 00000000000..240d277861f --- /dev/null +++ b/.github/scripts/agenthits-next-release.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Picks the next agenthits-vX.Y.Z tag for HEAD and writes release notes from +# every pull request merged since the previous tag. Collecting the whole range, +# not only the PR of this push, keeps a change in the next release when a run +# was dropped from the concurrency queue or failed before publishing. +# +# Outputs (GITHUB_OUTPUT): build, tag, previous, title, notes. +set -euo pipefail + +: "${REPO:?REPO (owner/name) is required}" +: "${GITHUB_OUTPUT:?}" +bump="${BUMP:-auto}" +base_branch="${BASE_BRANCH:-AgentHits-Dev}" +image="${IMAGE:-ghcr.io/agenthits/dokploy}" +work_dir="$(mktemp -d)" + +previous="$(git tag -l 'agenthits-v*' --sort=-v:refname | + grep -E '^agenthits-v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" + +if [ -z "$previous" ]; then + echo "::warning::No agenthits-vX.Y.Z tag exists yet, so this push gets an image but no release. Publish agenthits-v1.0.0 to start the release line." + echo "build=true" >>"$GITHUB_OUTPUT" + exit 0 +fi + +if [ "$(git rev-list --count "${previous}..HEAD")" = 0 ]; then + echo "Nothing to release: HEAD has no commits after ${previous}." + echo "build=false" >>"$GITHUB_OUTPUT" + exit 0 +fi + +: >"${work_dir}/prs.jsonl" +: >"${work_dir}/direct.jsonl" +while read -r sha; do + pr="$(gh api "repos/${REPO}/commits/${sha}/pulls" | + jq -c --arg base "$base_branch" \ + '[.[] | select(.merged_at != null and .base.ref == $base)][0] // empty + | {number, title, body: (.body // ""), url: .html_url}')" + if [ -n "$pr" ]; then + printf '%s\n' "$pr" >>"${work_dir}/prs.jsonl" + else + jq -nc --arg sha "$sha" --arg title "$(git log -1 --format=%s "$sha")" \ + '{sha: $sha, title: $title}' >>"${work_dir}/direct.jsonl" + fi +done < <(git rev-list --first-parent --reverse "${previous}..HEAD") + +prs="$(jq -s 'reduce .[] as $pr ([]; if any(.[]; .number == $pr.number) then . else . + [$pr] end)' "${work_dir}/prs.jsonl")" +direct="$(jq -s '.' "${work_dir}/direct.jsonl")" + +if [ "$bump" = auto ]; then + if jq -e -n --argjson prs "$prs" --argjson direct "$direct" \ + 'any($prs[], $direct[]; .title | startswith("feat"))' >/dev/null; then + bump="minor" + else + bump="patch" + fi +fi + +IFS=. read -r major minor patch <<<"${previous#agenthits-v}" +case "$bump" in +major) major=$((major + 1)) minor=0 patch=0 ;; +minor) minor=$((minor + 1)) patch=0 ;; +patch) patch=$((patch + 1)) ;; +*) + echo "::error::Unknown bump '${bump}', expected auto, patch, minor or major" + exit 1 + ;; +esac +tag="agenthits-v${major}.${minor}.${patch}" + +title="AgentHits Dokploy ${tag#agenthits-}" +if [ "$(jq -n --argjson prs "$prs" --argjson direct "$direct" '($prs | length) + ($direct | length)')" = 1 ]; then + title="${title} — $(jq -rn --argjson prs "$prs" --argjson direct "$direct" '($prs + $direct)[0].title')" +fi + +notes="${work_dir}/notes.md" +{ + jq -r '.[] | "## \(.title) ([#\(.number)](\(.url)))\n\n\(.body | sub("\\s+$"; ""))\n"' <<<"$prs" + if [ "$(jq length <<<"$direct")" != 0 ]; then + echo "## Коммиты без pull request" + echo + jq -r --arg repo "$REPO" '.[] | "- \(.title) ([`\(.sha[:12])`](https://github.com/\($repo)/commit/\(.sha)))"' <<<"$direct" + echo + fi + echo "---" + echo + echo "Образ: \`${image}:${tag}\`" + echo + echo "Все изменения с ${previous}: https://github.com/${REPO}/compare/${previous}...${tag}" +} >"$notes" + +delimiter="NOTES_$(openssl rand -hex 16)" +{ + echo "build=true" + echo "tag=${tag}" + echo "previous=${previous}" + echo "title=${title}" + echo "notes<<${delimiter}" + cat "$notes" + echo "${delimiter}" +} >>"$GITHUB_OUTPUT" + +if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + echo "## ${title}" + echo + echo "${previous} → ${tag} (${bump})" + echo + cat "$notes" + } >>"$GITHUB_STEP_SUMMARY" +fi +echo "Next release: ${tag} (${bump} after ${previous})" diff --git a/.github/workflows/agenthits-ghcr-retention.yml b/.github/workflows/agenthits-ghcr-retention.yml new file mode 100644 index 00000000000..f4361743da0 --- /dev/null +++ b/.github/workflows/agenthits-ghcr-retention.yml @@ -0,0 +1,50 @@ +name: AgentHits GHCR Retention + +# Scheduled runs delete; manual runs only list what would go unless dry-run is +# switched off. Release tags, agenthits-dev and buildcache are never deleted. +on: + schedule: + - cron: "17 3 * * 1" + workflow_dispatch: + inputs: + dry-run: + description: Only list the versions that would be deleted + type: boolean + default: true + keep-recent: + description: Newest versions to keep besides release, agenthits-dev and buildcache tags + type: number + default: 10 + +permissions: + contents: read + +concurrency: + group: agenthits-ghcr-retention + cancel-in-progress: false + +jobs: + prune: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + # The package belongs to the agentHits account, so only its token can + # list and delete versions; the job token has no access to it. + username: agentHits + password: ${{ secrets.GHCR_PUSH_TOKEN }} + + - name: Delete old image versions + env: + GH_TOKEN: ${{ secrets.GHCR_PUSH_TOKEN }} + PACKAGE_OWNER: agentHits + PACKAGE_NAME: dokploy + IMAGE: ghcr.io/agenthits/dokploy + KEEP_RECENT: ${{ inputs.keep-recent || 10 }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry-run || 'false' }} + run: bash .github/scripts/agenthits-ghcr-retention.sh diff --git a/.github/workflows/agenthits-image.yml b/.github/workflows/agenthits-image.yml index 720a7b3f420..e59456ad76b 100644 --- a/.github/workflows/agenthits-image.yml +++ b/.github/workflows/agenthits-image.yml @@ -1,17 +1,30 @@ name: AgentHits Dokploy Image +# Pushes to AgentHits-Dev build through agenthits-release.yml, which calls this +# workflow with the release tag. A manual run builds the selected branch, or, +# with release-tag set to a published release, rebuilds that release's image. on: - push: - branches: [AgentHits-Dev] + workflow_call: + inputs: + release-tag: + description: agenthits-vX.Y.Z tag the calling workflow publishes for this commit + type: string + default: "" workflow_dispatch: + inputs: + release-tag: + description: "Existing agenthits-vX.Y.Z tag to rebuild (empty: dev build of the selected branch)" + type: string + default: "" permissions: contents: read packages: write -# Cancel superseded dev builds on rapid pushes. +# Cancel superseded manual builds. The workflow name keeps release builds, which +# agenthits-release.yml already runs one at a time, out of this group. concurrency: - group: agenthits-image-${{ github.ref }} + group: agenthits-image-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: @@ -28,6 +41,26 @@ jobs: with: fetch-depth: 0 + - name: Check out published release + id: release + env: + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + if [ -z "$RELEASE_TAG" ]; then + exit 0 + fi + if ! printf '%s' "$RELEASE_TAG" | grep -Eq '^agenthits-v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::release-tag must look like agenthits-vX.Y.Z, got '${RELEASE_TAG}'" + exit 1 + fi + # The release workflow creates its tag only after this image is + # pushed, so an existing tag means a rebuild of an older release: + # build that commit and leave agenthits-dev on the branch tip. + if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then + git checkout -q --detach "refs/tags/${RELEASE_TAG}" + echo "rebuild=true" >> "$GITHUB_OUTPUT" + fi + - name: Prepare production env file run: | cp apps/dokploy/.env.production.example .env.production @@ -37,31 +70,32 @@ jobs: - name: Set image tags id: tags + env: + RELEASE_TAG: ${{ inputs.release-tag }} + REBUILD: ${{ steps.release.outputs.rebuild }} run: | - SHORT_SHA="${GITHUB_SHA::12}" + SHORT_SHA="$(git rev-parse --short=12 HEAD)" IMAGE="${REGISTRY}/${IMAGE_NAME}" OFFICIAL_VERSION="$(node -p "require('./apps/dokploy/package.json').version")" - OFFICIAL_REF="${OFFICIAL_VERSION}" - if git ls-remote --exit-code --tags https://github.com/Dokploy/dokploy.git "refs/tags/${OFFICIAL_VERSION}" >/dev/null 2>&1; then - git fetch --no-tags https://github.com/Dokploy/dokploy.git "refs/tags/${OFFICIAL_VERSION}:refs/tags/${OFFICIAL_VERSION}" + if [ -n "$RELEASE_TAG" ]; then + FORK_VERSION="$RELEASE_TAG" else - OFFICIAL_REF="refs/remotes/upstream/canary" - if ! git rev-parse --verify --quiet "${OFFICIAL_REF}^{commit}" >/dev/null; then - git fetch --no-tags https://github.com/Dokploy/dokploy.git "refs/heads/canary:${OFFICIAL_REF}" - fi + FORK_VERSION="$(git describe --tags --match 'agenthits-v*' --abbrev=12 2>/dev/null || echo "${STABLE_TAG}-${SHORT_SHA}")" fi - FORK_DISTANCE="$(git rev-list --count "${OFFICIAL_REF}..HEAD")" - FORK_VERSION="off_${OFFICIAL_VERSION}/Fork_${FORK_DISTANCE}+${SHORT_SHA}" { echo "tags<> "$GITHUB_OUTPUT" - echo "cache=${IMAGE}:buildcache" >> "$GITHUB_OUTPUT" - echo "official-version=${OFFICIAL_VERSION}" >> "$GITHUB_OUTPUT" - echo "fork-version=${FORK_VERSION}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 diff --git a/.github/workflows/agenthits-release.yml b/.github/workflows/agenthits-release.yml new file mode 100644 index 00000000000..52ecdbd5636 --- /dev/null +++ b/.github/workflows/agenthits-release.yml @@ -0,0 +1,79 @@ +name: AgentHits Release + +on: + push: + branches: [AgentHits-Dev] + workflow_dispatch: + inputs: + bump: + description: "Version part to raise (auto: minor if a merged PR title starts with feat, else patch)" + type: choice + options: [auto, patch, minor, major] + default: auto + +permissions: + contents: read + +# The next version is counted from the newest tag, so runs must not overlap. +# A queued run that GitHub replaces loses nothing: the next run collects every +# PR merged since the newest tag. +concurrency: + group: agenthits-release + cancel-in-progress: false + +jobs: + version: + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + outputs: + build: ${{ steps.next.outputs.build }} + tag: ${{ steps.next.outputs.tag }} + title: ${{ steps.next.outputs.title }} + notes: ${{ steps.next.outputs.notes }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Compute next version and notes + id: next + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + BUMP: ${{ inputs.bump || 'auto' }} + run: bash .github/scripts/agenthits-next-release.sh + + image: + needs: version + if: needs.version.outputs.build == 'true' + uses: ./.github/workflows/agenthits-image.yml + permissions: + contents: read + packages: write + with: + release-tag: ${{ needs.version.outputs.tag }} + secrets: inherit + + release: + needs: [version, image] + if: needs.version.outputs.tag != '' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Publish GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.version.outputs.tag }} + TITLE: ${{ needs.version.outputs.title }} + NOTES: ${{ needs.version.outputs.notes }} + run: | + printf '%s\n' "$NOTES" > release-notes.md + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "$TITLE" \ + --notes-file release-notes.md diff --git a/CLAUDE.md b/CLAUDE.md index 5e9e409f354..010c38e40af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,3 +12,7 @@ ## Pull requests Every pull request targets `AgentHits-Dev`, including a PR that builds on another open PR. Checks run only for that base, so a PR stacked on another branch gets none. When one PR depends on another, say so in the description; do not change the base. + +## 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`. diff --git a/README.md b/README.md index 4181f34716e..3db816e6c29 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Dashboard показывает две версии: ```text Official: v0.29.8 -Fork: off_v0.29.8/Fork_+ +Fork: agenthits-vX.Y.Z ``` `Official` - официальная база Dokploy, а `Fork` - сборка AgentHits поверх этой @@ -79,7 +79,7 @@ docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Imag curl -fsSL https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.sh -o install-agenthits.sh chmod +x install-agenthits.sh -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev-7bc40dd7fad5 \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh ``` @@ -108,7 +108,7 @@ bash install-agenthits.sh update curl -fsSL https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.sh -o install-agenthits.sh chmod +x install-agenthits.sh -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev- \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh update ``` @@ -141,7 +141,7 @@ The dashboard shows two versions: ```text Official: v0.29.8 -Fork: off_v0.29.8/Fork_+ +Fork: agenthits-vX.Y.Z ``` `Official` is the official Dokploy base, and `Fork` is the AgentHits build on @@ -195,7 +195,7 @@ Install a specific tested build: curl -fsSL https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.sh -o install-agenthits.sh chmod +x install-agenthits.sh -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev-7bc40dd7fad5 \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh ``` @@ -226,7 +226,7 @@ Update to a specific build: curl -fsSL https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.sh -o install-agenthits.sh chmod +x install-agenthits.sh -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev- \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh update ``` diff --git a/docs/agenthits-fork.md b/docs/agenthits-fork.md index 3948d75256e..e919431417c 100644 --- a/docs/agenthits-fork.md +++ b/docs/agenthits-fork.md @@ -13,11 +13,12 @@ ghcr.io/agenthits/dokploy:agenthits-dev ```text Official: v0.29.8 -Fork: off_v0.29.8/Fork_+ +Fork: agenthits-vX.Y.Z ``` -Пример: если образ собран поверх `v0.29.8` и содержит 157 коммитов форка, -версия будет выглядеть как `off_v0.29.8/Fork_157+abc123def456`. +`agenthits-vX.Y.Z` — тег GitHub release, в который вошла сборка: каждый +влитый в `AgentHits-Dev` PR выпускает новый релиз. Сборка, собранная вручную +между релизами, показывается как `agenthits-vX.Y.Z-<коммитов после релиза>-g`. ### Что это дает @@ -71,11 +72,12 @@ The official base version and fork version are shown separately: ```text Official: v0.29.8 -Fork: off_v0.29.8/Fork_+ +Fork: agenthits-vX.Y.Z ``` -Example: if the image is built on top of `v0.29.8` and includes 157 fork -commits, the fork version looks like `off_v0.29.8/Fork_157+abc123def456`. +`agenthits-vX.Y.Z` is the GitHub release the build belongs to: every PR merged +into `AgentHits-Dev` publishes a new release. A manual build between releases +shows as `agenthits-vX.Y.Z--g`. ### Why this helps diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md index ae2adb7d297..113e2f906e7 100644 --- a/docs/agenthits-install.md +++ b/docs/agenthits-install.md @@ -10,14 +10,14 @@ ghcr.io/agenthits/dokploy:agenthits-dev ``` Тег `agenthits-dev` всегда указывает на последнюю сборку ветки -`AgentHits-Dev`. Для воспроизводимой установки можно использовать immutable -тег вида `agenthits-dev-`. +`AgentHits-Dev`. Для воспроизводимой установки используйте тег релиза +`agenthits-vX.Y.Z` (список — в GitHub Releases); эти теги не удаляются. В dashboard отображаются две версии: ```text Official: v0.29.8 -Fork: off_v0.29.8/Fork_+ +Fork: agenthits-vX.Y.Z ``` `Official` показывает официальный Dokploy base, а `Fork` показывает сборку @@ -162,7 +162,7 @@ Hyper-V VM (Windows Pro/Enterprise/Education): curl -fsSL https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.sh -o install-agenthits.sh chmod +x install-agenthits.sh -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev-7bc40dd7fad5 \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh ``` @@ -314,7 +314,7 @@ Docker service image digest с `ghcr.io/agenthits/dokploy:agenthits-dev`. Обновление на конкретный immutable tag: ```bash -DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-dev- \ +DOKPLOY_IMAGE=ghcr.io/agenthits/dokploy:agenthits-vX.Y.Z \ DOKPLOY_RELEASE_TAG=agenthits-dev \ bash install-agenthits.sh update ``` @@ -356,5 +356,6 @@ echo "YOUR_GITHUB_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --passwo `DOKPLOY_FORK_VERSION` вручную только для custom/local images. - Dashboard update button в AgentHits fork обновляет из `ghcr.io/agenthits/dokploy:agenthits-dev`. -- Для rollback храните конкретный tag `agenthits-dev-` или - `sha-` после успешной проверки. +- Для rollback используйте тег релиза `agenthits-vX.Y.Z`. Теги + `agenthits-dev-` живут только для последних ~10 сборок: старые + версии образа удаляет еженедельная очистка GHCR.