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
70 changes: 70 additions & 0 deletions .github/scripts/agenthits-ghcr-retention.sh
Original file line number Diff line number Diff line change
@@ -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:-<untagged>}"
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
112 changes: 112 additions & 0 deletions .github/scripts/agenthits-next-release.sh
Original file line number Diff line number Diff line change
@@ -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})"
50 changes: 50 additions & 0 deletions .github/workflows/agenthits-ghcr-retention.yml
Original file line number Diff line number Diff line change
@@ -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
74 changes: 54 additions & 20 deletions .github/workflows/agenthits-image.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand All @@ -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<<EOF"
echo "${IMAGE}:${STABLE_TAG}"
echo "${IMAGE}:${STABLE_TAG}-${SHORT_SHA}"
echo "${IMAGE}:sha-${GITHUB_SHA}"
if [ "$REBUILD" != true ]; then
echo "${IMAGE}:${STABLE_TAG}"
echo "${IMAGE}:${STABLE_TAG}-${SHORT_SHA}"
fi
if [ -n "$RELEASE_TAG" ]; then
echo "${IMAGE}:${RELEASE_TAG}"
fi
echo "EOF"
echo "cache=${IMAGE}:buildcache"
echo "official-version=${OFFICIAL_VERSION}"
echo "fork-version=${FORK_VERSION}"
} >> "$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
Expand Down
Loading