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
10 changes: 8 additions & 2 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ on:
# Allows manual triggering of the workflow
workflow_dispatch:

# Run on workflow file changes (without pushing)
push:
paths:
- '.github/workflows/containers.yml'
- 'docker/build-container.sh'

jobs:
build-and-push:
runs-on: ubuntu-latest
strategy:
matrix:
platform: [intel, cuda, vulkan, cpu, musa]
platform: [intel, cuda, vulkan, cpu, musa, rocm]
fail-fast: false
steps:
- name: Checkout code
Expand All @@ -31,7 +37,7 @@ jobs:
- name: Run build-container
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./docker/build-container.sh ${{ matrix.platform }} true
run: ./docker/build-container.sh ${{ matrix.platform }} ${{ github.event_name != 'push' }}

# note make sure mostlygeek/llama-swap has admin rights to the llama-swap package
# see: https://github.com/actions/delete-package-versions/issues/74
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ llama-swap is a light weight, transparent proxy server that provides automatic m
- when summarizing changes only include details that require further action
- just say "Done." when there is no further action
- use `gh` to create PRs and load issues
- do not mention "created by claude" in commit messages
- do include Co-Authored-By or created by when committing changes or creating PRs
- keep PR descriptions short and focused on changes.
- never include a test plan

Expand Down
93 changes: 79 additions & 14 deletions docker/build-container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,37 @@

cd $(dirname "$0")

# use this to test locally, example:
# GITHUB_TOKEN=$(gh auth token) LOG_DEBUG=1 DEBUG_ABORT_BUILD=1 ./docker/build-container.sh rocm
# you need read:package scope on the token. Generate a personal access token with
# the scopes: gist, read:org, repo, write:packages
# then: gh auth login (and copy/paste the new token)

log_debug() {
if [ "$LOG_DEBUG" = "1" ]; then
echo "[DEBUG] $*"
fi
}

log_info() {
echo "[INFO] $*"
}

ARCH=$1
PUSH_IMAGES=${2:-false}

# List of allowed architectures
ALLOWED_ARCHS=("intel" "vulkan" "musa" "cuda" "cpu")
ALLOWED_ARCHS=("intel" "vulkan" "musa" "cuda" "cpu" "rocm")

# Check if ARCH is in the allowed list
if [[ ! " ${ALLOWED_ARCHS[@]} " =~ " ${ARCH} " ]]; then
echo "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
Comment on lines 28 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address shellcheck warnings for array handling.

Static analysis flags several issues:

  1. SC2199/SC2076: Array pattern matching in [[ ]] with =~ should use * or a loop.
  2. SC2145: Mixing string and array in the log message concatenates elements without a separator.
🔧 Proposed fix using a loop
-if [[ ! " ${ALLOWED_ARCHS[@]} " =~ " ${ARCH} " ]]; then
-  log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
+is_valid_arch=false
+for valid_arch in "${ALLOWED_ARCHS[@]}"; do
+  if [[ "$ARCH" == "$valid_arch" ]]; then
+    is_valid_arch=true
+    break
+  fi
+done
+if [[ "$is_valid_arch" == "false" ]]; then
+  log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[*]}"

Alternatively, if you prefer the concise pattern-matching approach, use * instead of @ and unquote the regex:

-if [[ ! " ${ALLOWED_ARCHS[@]} " =~ " ${ARCH} " ]]; then
-  log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
+if [[ ! " ${ALLOWED_ARCHS[*]} " =~ " ${ARCH} " ]]; then
+  log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[*]}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ ! " ${ALLOWED_ARCHS[@]} " =~ " ${ARCH} " ]]; then
echo "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[@]}"
if [[ ! " ${ALLOWED_ARCHS[*]} " =~ " ${ARCH} " ]]; then
log_info "Error: ARCH must be one of the following: ${ALLOWED_ARCHS[*]}"
🧰 Tools
🪛 Shellcheck (0.11.0)

[error] 26-26: Arrays implicitly concatenate in [[ ]]. Use a loop (or explicit * instead of @).

(SC2199)


[warning] 26-26: Remove quotes from right-hand side of =~ to match as a regex rather than literally.

(SC2076)


[error] 27-27: Argument mixes string and array. Use * or separate argument.

(SC2145)

🤖 Prompt for AI Agents
In @docker/build-container.sh around lines 26 - 27, The current ARCH membership
check uses unsafe array pattern matching and concatenates array elements wrongly
in the log; replace the [[ ! " ${ALLOWED_ARCHS[@]} " =~ " ${ARCH} " ]] test with
a proper loop that iterates over ALLOWED_ARCHS and breaks on match (or use [[ !
" ${ALLOWED_ARCHS[*]} " == *" ${ARCH} "* ]] with careful quoting), and update
the log_info call to print a joined, space-separated list using
"${ALLOWED_ARCHS[*]}" (not mixing array and string) so membership testing and
the error message are shellcheck-compliant; adjust code around the conditional
that references ALLOWED_ARCHS, ARCH and log_info accordingly.

exit 1
fi

# Check if GITHUB_TOKEN is set and not empty
if [[ -z "$GITHUB_TOKEN" ]]; then
echo "Error: GITHUB_TOKEN is not set or is empty."
log_info "Error: GITHUB_TOKEN is not set or is empty."
exit 1
fi

Expand All @@ -32,25 +48,74 @@ LS_REPO=${GITHUB_REPOSITORY:-mostlygeek/llama-swap}
# have to strip out the 'v' due to .tar.gz file naming
LS_VER=$(curl -s https://api.github.com/repos/${LS_REPO}/releases/latest | jq -r .tag_name | sed 's/v//')

# Fetches the most recent llama.cpp tag matching the given prefix
# Handles pagination to search beyond the first 100 results
# $1 - tag_prefix (e.g., "server" or "server-vulkan")
# Returns: the version number extracted from the tag
fetch_llama_tag() {
local tag_prefix=$1
local page=1
local per_page=100

while true; do
log_debug "Fetching page $page for tag prefix: $tag_prefix"

local response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/ggml-org/packages/container/llama.cpp/versions?per_page=${per_page}&page=${page}")

# Check for API errors
if echo "$response" | jq -e '.message' > /dev/null 2>&1; then
local error_msg=$(echo "$response" | jq -r '.message')
log_info "GitHub API error: $error_msg"
return 1
fi

# Check if response is empty array (no more pages)
if [ "$(echo "$response" | jq 'length')" -eq 0 ]; then
log_debug "No more pages (empty response)"
return 1
fi

# Extract matching tag from this page
local found_tag=$(echo "$response" | jq -r \
".[] | select(.metadata.container.tags[]? | startswith(\"$tag_prefix\")) | .metadata.container.tags[] | select(startswith(\"$tag_prefix\"))" \
| sort -r | head -n1)

if [ -n "$found_tag" ]; then
log_debug "Found tag: $found_tag on page $page"
echo "$found_tag" | awk -F '-' '{print $NF}'
return 0
fi

page=$((page + 1))

# Safety limit to prevent infinite loops
if [ $page -gt 50 ]; then
log_info "Reached pagination safety limit (50 pages)"
return 1
fi
done
}

if [ "$ARCH" == "cpu" ]; then
# cpu only containers just use the server tag
LCPP_TAG=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/ggml-org/packages/container/llama.cpp/versions" \
| jq -r '.[] | select(.metadata.container.tags[] | startswith("server")) | .metadata.container.tags[]' \
| sort -r | head -n1 | awk -F '-' '{print $3}')
LCPP_TAG=$(fetch_llama_tag "server")
BASE_TAG=server-${LCPP_TAG}
else
LCPP_TAG=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/ggml-org/packages/container/llama.cpp/versions" \
| jq -r --arg arch "$ARCH" '.[] | select(.metadata.container.tags[] | startswith("server-\($arch)")) | .metadata.container.tags[]' \
| sort -r | head -n1 | awk -F '-' '{print $3}')
LCPP_TAG=$(fetch_llama_tag "server-${ARCH}")
BASE_TAG=server-${ARCH}-${LCPP_TAG}
fi

# Abort if LCPP_TAG is empty.
if [[ -z "$LCPP_TAG" ]]; then
echo "Abort: Could not find llama-server container for arch: $ARCH"
log_info "Abort: Could not find llama-server container for arch: $ARCH"
exit 1
else
log_info "LCPP_TAG: $LCPP_TAG"
fi

if [[ ! -z "$DEBUG_ABORT_BUILD" ]]; then
log_info "Abort: DEBUG_ABORT_BUILD set"
exit 0
fi

for CONTAINER_TYPE in non-root root; do
Expand All @@ -68,7 +133,7 @@ for CONTAINER_TYPE in non-root root; do
USER_HOME=/app
fi

echo "Building $CONTAINER_TYPE $CONTAINER_TAG $LS_VER"
log_info "Building $CONTAINER_TYPE $CONTAINER_TAG $LS_VER"
docker build -f llama-swap.Containerfile --build-arg BASE_TAG=${BASE_TAG} --build-arg LS_VER=${LS_VER} --build-arg UID=${USER_UID} \
--build-arg LS_REPO=${LS_REPO} --build-arg GID=${USER_GID} --build-arg USER_HOME=${USER_HOME} -t ${CONTAINER_TAG} -t ${CONTAINER_LATEST} \
--build-arg BASE_IMAGE=${BASE_IMAGE} .
Expand Down
Loading