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
4 changes: 2 additions & 2 deletions .github/scripts/agenthits-ghcr-retention.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/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
# agenthits-vX.Y.Z, agenthits-dev, agenthits-stable 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).
Expand All @@ -19,7 +19,7 @@ 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]+$"));
| any(. == "agenthits-dev" or . == "agenthits-stable" 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:]}
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/agenthits-ghcr-retention.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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.
# switched off. Release tags, agenthits-dev, agenthits-stable and buildcache are
# never deleted.
on:
schedule:
- cron: "17 3 * * 1"
Expand All @@ -12,7 +13,7 @@ on:
type: boolean
default: true
keep-recent:
description: Newest versions to keep besides release, agenthits-dev and buildcache tags
description: Newest versions to keep besides release, agenthits-dev, agenthits-stable and buildcache tags
type: number
default: 10

Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/agenthits-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,14 @@ jobs:
--target "$GITHUB_SHA" \
--title "$TITLE" \
--notes-file release-notes.md

stable:
needs: [version, release]
if: needs.version.outputs.tag != ''
uses: ./.github/workflows/agenthits-stable-tag.yml
permissions:
contents: read
packages: write
with:
release-tag: ${{ needs.version.outputs.tag }}
secrets: inherit
80 changes: 80 additions & 0 deletions .github/workflows/agenthits-stable-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: AgentHits Stable Tag

# Panels update from agenthits-stable, so it must name a published release and
# nothing else. The tag is a carbon copy of the release's manifest: the digest
# stays the same and nothing is rebuilt. A manual run with an older release
# moves the update channel back to it.
on:
workflow_call:
inputs:
release-tag:
description: Published agenthits-vX.Y.Z release to point agenthits-stable at
type: string
required: true
workflow_dispatch:
inputs:
release-tag:
description: "Published agenthits-vX.Y.Z release to point agenthits-stable at (empty: the latest release)"
type: string
default: ""

permissions:
contents: read
packages: write

concurrency:
group: agenthits-stable-tag
cancel-in-progress: false

env:
IMAGE: ghcr.io/agenthits/dokploy
STABLE_TAG: agenthits-stable

jobs:
move-tag:
runs-on: ubuntu-24.04
steps:
- name: Resolve the release
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
if [ -z "$RELEASE_TAG" ]; then
RELEASE_TAG="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
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
if ! gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json tagName >/dev/null; then
echo "::error::${RELEASE_TAG} is not a published GitHub release"
exit 1
fi
echo "tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
cleanup: false

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ secrets.GHCR_PUSH_TOKEN && 'agentHits' || github.actor }}
password: ${{ secrets.GHCR_PUSH_TOKEN || secrets.GITHUB_TOKEN }}

- name: Point agenthits-stable at the release
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
docker buildx imagetools create --prefer-index=false \
--tag "${IMAGE}:${STABLE_TAG}" "${IMAGE}:${RELEASE_TAG}"
docker buildx imagetools inspect --raw "${IMAGE}:${RELEASE_TAG}" > release.json
docker buildx imagetools inspect --raw "${IMAGE}:${STABLE_TAG}" > stable.json
if ! cmp -s release.json stable.json; then
echo "::error::${STABLE_TAG} does not carry the manifest of ${RELEASE_TAG}"
exit 1
fi
echo "${STABLE_TAG} now points at ${RELEASE_TAG} (sha256:$(sha256sum < release.json | cut -d' ' -f1))"
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Every pull request targets `AgentHits-Dev`, including a PR that builds on anothe

## Releases

One GitHub release `agenthits-vX.Y.Z` covers a batch of work, not a single PR (owner's decision 2026-10-27). Every push to `AgentHits-Dev` builds the image through `.github/workflows/agenthits-release.yml` (`agenthits-dev`, `agenthits-dev-<sha12>`), and panels show such a build as `agenthits-vX.Y.Z-N-g<sha12>`: the previous release plus N commits. A push cuts the release once 20 PRs (`RELEASE_EVERY`, first-parent commits) have been merged since the previous release; to release a finished batch sooner, run `gh workflow run agenthits-release.yml --repo TaskFlow-Systems/dokploy --ref AgentHits-Dev`. Several PRs that belong together can also be merged as one combined PR. The `agenthits-` prefix keeps fork tags apart from upstream Dokploy `v0.x` tags. The version goes up by patch, by minor when a PR title in the batch starts with `feat`, and by major only through a manual run with `bump: major`. The release notes are the title and body of every PR merged since the previous release, so write each 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`.
One GitHub release `agenthits-vX.Y.Z` covers a batch of work, not a single PR (owner's decision 2026-10-27). Every push to `AgentHits-Dev` builds the image through `.github/workflows/agenthits-release.yml` (`agenthits-dev`, `agenthits-dev-<sha12>`), and panels show such a build as `agenthits-vX.Y.Z-N-g<sha12>`: the previous release plus N commits. A push cuts the release once 20 PRs (`RELEASE_EVERY`, first-parent commits) have been merged since the previous release; to release a finished batch sooner, run `gh workflow run agenthits-release.yml --repo TaskFlow-Systems/dokploy --ref AgentHits-Dev`. Several PRs that belong together can also be merged as one combined PR. The `agenthits-` prefix keeps fork tags apart from upstream Dokploy `v0.x` tags. The version goes up by patch, by minor when a PR title in the batch starts with `feat`, and by major only through a manual run with `bump: major`. The release notes are the title and body of every PR merged since the previous release, so write each 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. After the GitHub release is published, `.github/workflows/agenthits-stable-tag.yml` points `agenthits-stable` at the release's manifest (same digest, no rebuild); the panel's Update follows `agenthits-stable` and never offers a release older than the running build. A manual run of that workflow with an older release moves the channel back; `DOKPLOY_AGENTHITS_UPDATE_TAG=agenthits-dev` on a panel's service makes it follow every build. `install-agenthits.sh` and `update.sh` still default to `agenthits-dev`. `.github/workflows/agenthits-ghcr-retention.yml` deletes old GHCR versions weekly but never a release tag, `agenthits-dev`, `agenthits-stable` or `buildcache`.

## Dependencies

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Imag
```

Dashboard update button в AgentHits fork проверяет и обновляет image из
`ghcr.io/agenthits/dokploy:agenthits-dev`.
`ghcr.io/agenthits/dokploy:agenthits-stable`: это последний выпущенный релиз
`agenthits-vX.Y.Z`. Сборку новее этого релиза кнопка не откатывает назад.

Подробная инструкция: [docs/agenthits-install.md](docs/agenthits-install.md).

Expand Down Expand Up @@ -241,7 +242,9 @@ docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Imag
```

The dashboard update button in the AgentHits fork checks and updates the image
from `ghcr.io/agenthits/dokploy:agenthits-dev`.
from `ghcr.io/agenthits/dokploy:agenthits-stable`, the latest published
`agenthits-vX.Y.Z` release. It never moves a build newer than that release back
to it.

Full guide: [docs/agenthits-install.md](docs/agenthits-install.md).

Expand Down
8 changes: 4 additions & 4 deletions apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ describe("Dokploy update commands", () => {
it("pulls the AgentHits image while the old container still runs", () => {
expectPullBeforeUpdate(
getAgentHitsUpdateCommand("v0.30.6"),
"docker pull ghcr.io/agenthits/dokploy\\:agenthits-dev",
"docker pull ghcr.io/agenthits/dokploy\\:agenthits-stable",
);
});

Expand Down Expand Up @@ -470,7 +470,7 @@ esac
});

describe("Dokploy update command", () => {
const AGENTHITS_IMAGE = "ghcr.io/agenthits/dokploy:agenthits-dev";
const AGENTHITS_IMAGE = "ghcr.io/agenthits/dokploy:agenthits-stable";
const PINNED = `sha256:${"c".repeat(64)}`;
const FORK_VERSION = "agenthits-v1.0.9-3-g0123456789ab";

Expand Down Expand Up @@ -557,7 +557,7 @@ esac
]);
expect(stdout).toContain(UPDATE_IMAGE_PULLED_MARKER);
expect(update).toBe(
`service update --force --no-resolve-image --image ${AGENTHITS_IMAGE}@${PINNED} --env-add RELEASE_TAG=agenthits-dev --env-add DOKPLOY_OFFICIAL_VERSION=v0.30.7 --env-add DOKPLOY_FORK_VERSION=${FORK_VERSION} --env-add DOKPLOY_KEEP_OLD_IMAGES=2 --env-add DOKPLOY_PREVIOUS_IMAGE=${PREVIOUS} --update-order stop-first --update-failure-action rollback dokploy`,
`service update --force --no-resolve-image --image ${AGENTHITS_IMAGE}@${PINNED} --env-add RELEASE_TAG=agenthits-stable --env-add DOKPLOY_OFFICIAL_VERSION=v0.30.7 --env-add DOKPLOY_FORK_VERSION=${FORK_VERSION} --env-add DOKPLOY_KEEP_OLD_IMAGES=2 --env-add DOKPLOY_PREVIOUS_IMAGE=${PREVIOUS} --update-order stop-first --update-failure-action rollback dokploy`,
);
expect(calls.at(-1)).toBe(update);
},
Expand Down Expand Up @@ -631,7 +631,7 @@ esac
if (url.includes("/token")) {
return Response.json({ token: "token" });
}
if (url.endsWith("/manifests/agenthits-dev")) {
if (url.endsWith("/manifests/agenthits-stable")) {
return Response.json(
{ config: { digest: "sha256:config" } },
{ headers: { "docker-content-digest": latestDigest } },
Expand Down
Loading