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
90 changes: 90 additions & 0 deletions .github/scripts/update_debian_snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Advance the agent-server Debian snapshot after a seven-day observation period."""

from __future__ import annotations

import argparse
import re
import urllib.request
from datetime import UTC, datetime, timedelta
from pathlib import Path


MINIMUM_AGE = timedelta(days=7)
SNAPSHOT_RE = re.compile(r"(?m)^ARG DEBIAN_SNAPSHOT=(\d{8}T\d{6}Z)$")
ARCHIVES = ("debian", "debian-security")


def eligible_snapshot(now: datetime) -> datetime:
if now.tzinfo is None:
raise ValueError("now must be timezone-aware")
cutoff = now.astimezone(UTC) - MINIMUM_AGE
return cutoff.replace(hour=0, minute=0, second=0, microsecond=0)


def format_snapshot(value: datetime) -> str:
return value.astimezone(UTC).strftime("%Y%m%dT%H%M%SZ")


def validate_snapshot_age(snapshot: datetime, now: datetime) -> None:
age = now.astimezone(UTC) - snapshot.astimezone(UTC)
if age < MINIMUM_AGE:
raise ValueError(f"snapshot is only {age} old; minimum age is {MINIMUM_AGE}")


def verify_snapshot(snapshot: str) -> None:
for archive in ARCHIVES:
url = f"https://snapshot.debian.org/archive/{archive}/{snapshot}/"
request = urllib.request.Request(url, method="HEAD")
try:
with urllib.request.urlopen(request, timeout=30) as response:
if response.status != 200:
raise ValueError(f"{url} returned HTTP {response.status}")
except OSError as exc:
raise ValueError(f"unable to verify {url}: {exc}") from exc


def current_snapshot(path: Path) -> str:
matches = SNAPSHOT_RE.findall(path.read_text(encoding="utf-8"))
if len(matches) != 1:
raise ValueError(f"expected exactly one DEBIAN_SNAPSHOT in {path}")
return matches[0]


def update_dockerfile(path: Path, snapshot: str) -> bool:
current = current_snapshot(path)
if snapshot <= current:
return False
text = path.read_text(encoding="utf-8")
updated = SNAPSHOT_RE.sub(f"ARG DEBIAN_SNAPSHOT={snapshot}", text)
path.write_text(updated, encoding="utf-8")
return True


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dockerfile", type=Path, required=True)
parser.add_argument(
"--now",
type=lambda value: datetime.fromisoformat(value.replace("Z", "+00:00")),
default=datetime.now(UTC),
help="UTC reference time for deterministic testing",
)
parser.add_argument("--skip-network-check", action="store_true")
return parser.parse_args()


def main() -> int:
args = parse_args()
snapshot_time = eligible_snapshot(args.now)
validate_snapshot_age(snapshot_time, args.now)
snapshot = format_snapshot(snapshot_time)
if not args.skip_network_check:
verify_snapshot(snapshot)
changed = update_dockerfile(args.dockerfile, snapshot)
print(f"Debian snapshot: {snapshot} ({'updated' if changed else 'unchanged'})")
return 0


if __name__ == "__main__":
raise SystemExit(main())
10 changes: 5 additions & 5 deletions .github/workflows/server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
base_image:
description: Base runtime image
type: string
default: nikolaik/python-nodejs:python3.13-nodejs22-slim
default: python-node-runtime
image:
description: GHCR image name
type: string
Expand Down Expand Up @@ -285,7 +285,7 @@ jobs:
image_flavor: default
acp_provider_flavor: default
arch: amd64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04
platform: linux/amd64

Expand All @@ -294,7 +294,7 @@ jobs:
image_flavor: default
acp_provider_flavor: default
arch: arm64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04-arm
platform: linux/arm64

Expand All @@ -304,7 +304,7 @@ jobs:
image_flavor: slim
acp_provider_flavor: none
arch: amd64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04
platform: linux/amd64

Expand All @@ -313,7 +313,7 @@ jobs:
image_flavor: slim
acp_provider_flavor: none
arch: arm64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04-arm
platform: linux/arm64

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/typescript-client-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
uv sync --frozen
uv run ./openhands-agent-server/openhands/agent_server/docker/build.py \
--build-ctx-only --arch amd64 \
--base-image nikolaik/python-nodejs:python3.13-nodejs22-slim
--base-image python-node-runtime

- name: Build agent-server image from this branch
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
Expand All @@ -87,7 +87,7 @@ jobs:
# Scope server.yml populates with mode=max on push to main.
cache-from: type=gha,scope=agent-server-python-amd64
build-args: |
BASE_IMAGE=nikolaik/python-nodejs:python3.13-nodejs22-slim
BASE_IMAGE=python-node-runtime
OPENHANDS_BUILD_GIT_SHA=${{ github.event.pull_request.head.sha || github.sha }}
OPENHANDS_BUILD_GIT_REF=${{ github.head_ref != '' && format('refs/heads/{0}', github.head_ref) || github.ref }}
INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli
Expand Down
101 changes: 101 additions & 0 deletions .github/workflows/update-debian-snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: Update Agent Server Debian Snapshot

on:
schedule:
- cron: 17 6 * * 1
workflow_dispatch:

permissions:
contents: write
pull-requests: write

concurrency:
group: update-agent-server-debian-snapshot
cancel-in-progress: true

jobs:
update-snapshot:
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
GH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
BRANCH: chore/update-agent-server-debian-snapshot
DOCKERFILE: openhands-agent-server/openhands/agent_server/docker/Dockerfile
IMAGE: openhands/agent-server-binary-minimal:snapshot-update
steps:
- name: Checkout
uses: actions/checkout@v7
with:
token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}

- name: Select the newest snapshot older than seven days
run: python .github/scripts/update_debian_snapshot.py --dockerfile "$DOCKERFILE"

- name: Build binary-minimal
run: |
docker build \
--target binary-minimal \
--build-arg INSTALL_ACP_PROVIDERS= \
--tag "$IMAGE" \
--file "$DOCKERFILE" .

- name: Scan binary-minimal
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.IMAGE }}
format: json
output: trivy.json
scanners: vuln

- name: Open or refresh update PR
env:
REPO: ${{ github.repository }}
run: |
set -euo pipefail
if git diff --quiet; then
echo "Debian snapshot is already current."
exit 0
fi

SNAPSHOT=$(sed -n 's/^ARG DEBIAN_SNAPSHOT=//p' "$DOCKERFILE")
TRIVY_COUNTS=$(jq -r '
[.Results[]?.Vulnerabilities[]?]
| group_by(.Severity)
| map("\(.[0].Severity): \(length)")
| join(", ")
' trivy.json)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin "$BRANCH" || true
git checkout -B "$BRANCH"
git add "$DOCKERFILE"
git commit -m "chore(agent-server): update Debian snapshot to $SNAPSHOT" \
-m "Use the newest UTC snapshot that has completed the seven-day observation period." \
-m "Co-authored-by: openhands <openhands@all-hands.dev>"
git push --force-with-lease -u origin "$BRANCH"

BODY=$(cat <<EOF
## Summary

Advance the agent-server runtime to Debian snapshot \`$SNAPSHOT\`, the newest UTC snapshot that has completed the seven-day observation period.

The workflow successfully built and scanned \`binary-minimal\` before opening this PR.

**Trivy findings:** $TRIVY_COUNTS

Normal pull-request CI builds and tests the complete image matrix before merge.

_This pull request was created by an automated workflow._
EOF
)
EXISTING=$(gh pr list --repo "$REPO" --head "$BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$EXISTING" ]; then
gh pr edit "$EXISTING" --repo "$REPO" \
--title "chore(agent-server): update Debian snapshot to $SNAPSHOT" \
--body "$BODY"
else
gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "chore(agent-server): update Debian snapshot to $SNAPSHOT" \
--body "$BODY"
fi
4 changes: 2 additions & 2 deletions clients/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clients/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openhands/typescript-client",
"version": "1.47.0",
"version": "1.48.0",
"description": "TypeScript client for OpenHands Agent Server",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
Loading
Loading