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
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Keep the build context small and stable. This file is SHARED by both
# Dockerfile and Dockerfile.thailand (same `context: .`), so it must NOT
# exclude skills-thailand/ (the Thai overlay COPYs it) or the Dockerfiles
# themselves (buildx reads Dockerfile.thailand from the context via `file:`).
# Only exclude paths that NEITHER image COPYs.

# VCS / CI / repo metadata
.git
.github
.gitignore
.dockerignore

# Docs and scratch dirs (root *.md only — does NOT match skills/**/SKILL.md)
*.md
docs/
plans/
temp_scripts/

# Local env / OS cruft
.env
.env.*
.DS_Store
**/.DS_Store
155 changes: 114 additions & 41 deletions .github/workflows/build-thailand.yml
Original file line number Diff line number Diff line change
@@ -1,80 +1,153 @@
# Continuous build of the Thai-government overlay image
# (agent-sandbox-thailand): the default image plus the Thailand-only skills in
# skills-thailand/ (Dockerfile.thailand overlays them onto an ARG-templated
# base). Like build.yml this has NO tag trigger — semver releases of the Thai
# image are handled by release.yml.
#
# Three entry points, one job:
# - workflow_run : after "Build and Push" rebuilt the base on this commit,
# overlay that exact base (sha-<short>) and mirror its tags.
# - push : Thai skills changed but the base did not — overlay the
# current published base (`latest`). A guard skips this when
# base-image paths also changed (the workflow_run path will
# rebuild off the fresh base instead, avoiding a duplicate
# build that would otherwise overlay a stale `latest`).
# - workflow_dispatch : overlay an explicitly chosen base tag, tagging the
# result with the dispatch branch's moving tag. Run it from a
# branch, NEVER a tag — semver Thai images come from
# release.yml (a tag-ref dispatch is rejected in resolve).
name: Build and Push (Thailand)

# Builds the Thai-government sandbox image: the default agent-sandbox plus the
# Thailand-only skills (Dockerfile.thailand overlays `skills-thailand/`). It runs
# AFTER the default "Build and Push" workflow succeeds so the base image it
# overlays already exists, and pins the base to that same commit's `sha-` tag.
# This keeps the Thai skill out of the default image while shipping it in a
# separate, optional image.

on:
workflow_run:
workflows: ["Build and Push"]
types: [completed]
# No `branches:` filter — gate on conclusion (and, implicitly, on the base
# having actually built) inside the job. workflow_run.head_branch is
# unreliable, so we don't filter on it.
push:
branches: [main]
paths:
- 'skills-thailand/**'
- 'Dockerfile.thailand'
- '.github/workflows/build-thailand.yml'
workflow_dispatch:
inputs:
base_tag:
description: "Default agent-sandbox tag to overlay (e.g. latest, 0.3.0, sha-abc1234)"
description: "agent-sandbox base tag to overlay (e.g. latest, sha-abc1234). Run from a branch; semver images come from release.yml."
default: latest

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
# Separate image name: <owner>/agent-sandbox-thailand.
IMAGE_NAME: ${{ github.repository }}-thailand
BASE_IMAGE_NAME: eternisai/agent-sandbox

jobs:
build:
# workflow_run: only proceed if the base build succeeded. workflow_dispatch
# always proceeds.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
# workflow_run: only proceed if the base build succeeded. push/dispatch:
# always enter; the resolve step decides whether to actually build.
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Build from the same commit the base image was built from.
# Build from the same commit the base image was built from on the
# workflow_run path; otherwise the pushed / dispatched ref.
ref: ${{ github.event.workflow_run.head_sha || github.ref }}

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
# push only: did base-image paths also change in this push? If so, defer
# to the chained workflow_run (which overlays the freshly built base).
- name: Detect base-relevant changes
if: ${{ github.event_name == 'push' }}
id: basepaths
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
filters: |
base:
- 'Dockerfile'
- 'skills/**'
- 'plugins/**'
- 'agent/**'
- 'entrypoint.sh'

- name: Resolve base image tag
id: base
- name: Resolve build parameters
id: resolve
env:
EVENT: ${{ github.event_name }}
WR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
BASE_PATHS_CHANGED: ${{ steps.basepaths.outputs.base }}
INPUT_BASE_TAG: ${{ inputs.base_tag }}
run: |
if [ "${{ github.event_name }}" = "workflow_run" ]; then
short="$(git rev-parse --short=7 '${{ github.event.workflow_run.head_sha }}')"
echo "ref=sha-${short}" >> "$GITHUB_OUTPUT"
else
echo "ref=${{ inputs.base_tag }}" >> "$GITHUB_OUTPUT"
fi
set -euo pipefail
image="${REGISTRY}/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
thai="${image}-thailand"
proceed=true
slugify() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9._-]+#-#g; s#^[._-]+##; s#[._-]+$##' | cut -c1-128; }
case "${EVENT}" in
workflow_run)
short="$(echo "${WR_HEAD_SHA}" | cut -c1-7)"
base_ref="${image}:sha-${short}"
if [ "${WR_HEAD_BRANCH}" = "main" ]; then moving="latest"; else moving="$(slugify "${WR_HEAD_BRANCH}")"; fi
;;
push)
# Only build if base paths did NOT change (else defer to workflow_run).
if [ "${BASE_PATHS_CHANGED}" = "true" ]; then proceed=false; fi
short="${GITHUB_SHA::7}"
base_ref="${image}:latest"
moving="latest"
;;
workflow_dispatch)
# Dispatch is for branch overlays only. On a tag ref the output
# would be a v-prefixed slug (e.g. v0.3.0) overlaying the wrong
# base and bypassing release.yml's promote — reject it.
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
echo "::error::Do not dispatch this workflow for a tag (${GITHUB_REF_NAME}). Semver Thai images are produced by release.yml when you push a vX.Y.Z git tag (it promotes agent-sandbox-thailand:sha-<short> -> <ver>). Re-run this dispatch from a branch."
exit 1
fi
short="${GITHUB_SHA::7}"
base_ref="${image}:${INPUT_BASE_TAG}"
if [ "${GITHUB_REF_NAME}" = "main" ]; then moving="latest"; else moving="$(slugify "${GITHUB_REF_NAME}")"; fi
;;
esac
{
echo "proceed=${proceed}"
echo "thai=${thai}"
echo "base_ref=${base_ref}"
echo "short=${short}"
echo "moving=${moving}"
} >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
if: ${{ steps.resolve.outputs.proceed == 'true' }}

- uses: docker/metadata-action@v5
id: meta
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
if: ${{ steps.resolve.outputs.proceed == 'true' }}
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v5
- name: Build and push (overlay)
if: ${{ steps.resolve.outputs.proceed == 'true' }}
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: Dockerfile.thailand
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
push: true
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ steps.base.outputs.ref }}
BASE_IMAGE=${{ steps.resolve.outputs.base_ref }}
tags: |
${{ steps.resolve.outputs.thai }}:sha-${{ steps.resolve.outputs.short }}
${{ steps.resolve.outputs.thai }}:${{ steps.resolve.outputs.moving }}
provenance: false
sbom: false
cache-from: type=gha
cache-to: type=gha,mode=max
111 changes: 98 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
# Continuous build of the default agent-sandbox image.
#
# Triggers on `main` pushes (path-scoped to files that actually go into the
# image) and manual dispatch. Deliberately has NO tag trigger: semver releases
# are handled by release.yml, which PROMOTES the already-built `sha-` image to
# the version tag. Keeping tags out of this workflow lets `paths:` filtering
# work correctly (a tag push usually introduces zero changed files, so a
# `paths:`-filtered tag trigger would be silently skipped).
#
# Tags pushed:
# - sha-<short> always — the immutable per-commit handle deploys pin to
# - latest on main
# - <branch-slug> on a manual dispatch from a feature branch
#
# After a successful run, build-thailand.yml (workflow_run) overlays the Thai
# skills onto this exact commit's base image.
name: Build and Push

on:
push:
branches: [main]
paths:
- 'Dockerfile'
- 'skills/**'
- 'plugins/**'
- 'agent/**'
- 'entrypoint.sh'
- '.github/workflows/build.yml'
workflow_dispatch:

# Supersede an in-flight build when a newer commit lands on the same ref, so
# the moving tag (latest / slug) always ends up pointing at the newest commit.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
Expand All @@ -17,30 +45,87 @@ jobs:
packages: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: docker/setup-buildx-action@v3
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

- uses: docker/login-action@v3
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/metadata-action@v5
- name: Compute image name and tags
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha
run: |
set -euo pipefail
# Releases go through release.yml (push a vX.Y.Z tag -> promote the
# sha-<short> image to <ver>). Dispatching this workflow on a tag ref
# would instead build a v-prefixed slug tag and bypass that promote.
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
echo "::error::Do not dispatch this workflow for a tag (${GITHUB_REF_NAME}). Push a vX.Y.Z git tag to trigger release.yml, or dispatch from a branch."
exit 1
fi
# GHCR requires a lowercase image path; ${{ github.repository }} may
# contain uppercase (EternisAI/...), so lowercase it explicitly.
image="${REGISTRY}/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
short="${GITHUB_SHA::7}"
if [ "${GITHUB_REF_NAME}" = "main" ]; then
moving="latest"
environment="staging"
else
# Manual dispatch from a feature branch: slugify the branch name to
# a valid Docker tag. Not auto-deployed (environment=none).
moving="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9._-]+#-#g; s#^[._-]+##; s#[._-]+$##' | cut -c1-128)"
environment="none"
fi
{
echo "image=${image}"
echo "short=${short}"
echo "moving=${moving}"
echo "environment=${environment}"
} >> "$GITHUB_OUTPUT"

- uses: docker/build-push-action@v5
- name: Build and push
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: |
${{ steps.meta.outputs.image }}:sha-${{ steps.meta.outputs.short }}
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.moving }}
# Plain single-platform manifest with one stable digest (no
# attestation index), which is what digest-pinned GitOps deploys want.
provenance: false
sbom: false
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Write deploy-info
run: |
set -euo pipefail
# Fast-path handoff to the (separate) staging deploy workflow. The
# registry remains the source of truth — `digest` is always
# re-resolvable from a tag via `oras resolve` — so this artifact is
# short-lived (see retention-days) and the deploy must tolerate its
# absence by reconstructing from the image.
mkdir -p deploy-info
cat > deploy-info/deploy-info.json <<EOF
{
"image": "${{ steps.meta.outputs.image }}",
"tag": "sha-${{ steps.meta.outputs.short }}",
"moving_tag": "${{ steps.meta.outputs.moving }}",
"digest": "${{ steps.build.outputs.digest }}",
"git_sha": "${GITHUB_SHA}",
"ref": "${GITHUB_REF}",
"environment": "${{ steps.meta.outputs.environment }}"
}
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: deploy-info
path: deploy-info/deploy-info.json
retention-days: 7
Loading