Skip to content

Commit f6de284

Browse files
authored
Merge pull request #35 from EternisAI/ci/build-release-pipelines
ci: rework build/release pipelines + add Thailand GitOps deploy
2 parents 3b304c9 + 8a40e40 commit f6de284

8 files changed

Lines changed: 796 additions & 70 deletions

File tree

.dockerignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Keep the build context small and stable. This file is SHARED by both
2+
# Dockerfile and Dockerfile.thailand (same `context: .`), so it must NOT
3+
# exclude skills-thailand/ (the Thai overlay COPYs it) or the Dockerfiles
4+
# themselves (buildx reads Dockerfile.thailand from the context via `file:`).
5+
# Only exclude paths that NEITHER image COPYs.
6+
7+
# VCS / CI / repo metadata
8+
.git
9+
.github
10+
.gitignore
11+
.dockerignore
12+
13+
# Docs and scratch dirs (root *.md only — does NOT match skills/**/SKILL.md)
14+
*.md
15+
docs/
16+
plans/
17+
temp_scripts/
18+
19+
# Local env / OS cruft
20+
.env
21+
.env.*
22+
.DS_Store
23+
**/.DS_Store
Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,153 @@
1+
# Continuous build of the Thai-government overlay image
2+
# (agent-sandbox-thailand): the default image plus the Thailand-only skills in
3+
# skills-thailand/ (Dockerfile.thailand overlays them onto an ARG-templated
4+
# base). Like build.yml this has NO tag trigger — semver releases of the Thai
5+
# image are handled by release.yml.
6+
#
7+
# Three entry points, one job:
8+
# - workflow_run : after "Build and Push" rebuilt the base on this commit,
9+
# overlay that exact base (sha-<short>) and mirror its tags.
10+
# - push : Thai skills changed but the base did not — overlay the
11+
# current published base (`latest`). A guard skips this when
12+
# base-image paths also changed (the workflow_run path will
13+
# rebuild off the fresh base instead, avoiding a duplicate
14+
# build that would otherwise overlay a stale `latest`).
15+
# - workflow_dispatch : overlay an explicitly chosen base tag, tagging the
16+
# result with the dispatch branch's moving tag. Run it from a
17+
# branch, NEVER a tag — semver Thai images come from
18+
# release.yml (a tag-ref dispatch is rejected in resolve).
119
name: Build and Push (Thailand)
220

3-
# Builds the Thai-government sandbox image: the default agent-sandbox plus the
4-
# Thailand-only skills (Dockerfile.thailand overlays `skills-thailand/`). It runs
5-
# AFTER the default "Build and Push" workflow succeeds so the base image it
6-
# overlays already exists, and pins the base to that same commit's `sha-` tag.
7-
# This keeps the Thai skill out of the default image while shipping it in a
8-
# separate, optional image.
9-
1021
on:
1122
workflow_run:
1223
workflows: ["Build and Push"]
1324
types: [completed]
25+
# No `branches:` filter — gate on conclusion (and, implicitly, on the base
26+
# having actually built) inside the job. workflow_run.head_branch is
27+
# unreliable, so we don't filter on it.
28+
push:
1429
branches: [main]
30+
paths:
31+
- 'skills-thailand/**'
32+
- 'Dockerfile.thailand'
33+
- '.github/workflows/build-thailand.yml'
1534
workflow_dispatch:
1635
inputs:
1736
base_tag:
18-
description: "Default agent-sandbox tag to overlay (e.g. latest, 0.3.0, sha-abc1234)"
37+
description: "agent-sandbox base tag to overlay (e.g. latest, sha-abc1234). Run from a branch; semver images come from release.yml."
1938
default: latest
2039

40+
concurrency:
41+
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.ref }}
42+
cancel-in-progress: true
43+
2144
env:
2245
REGISTRY: ghcr.io
23-
# Separate image name: <owner>/agent-sandbox-thailand.
24-
IMAGE_NAME: ${{ github.repository }}-thailand
25-
BASE_IMAGE_NAME: eternisai/agent-sandbox
2646

2747
jobs:
2848
build:
29-
# workflow_run: only proceed if the base build succeeded. workflow_dispatch
30-
# always proceeds.
31-
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
49+
# workflow_run: only proceed if the base build succeeded. push/dispatch:
50+
# always enter; the resolve step decides whether to actually build.
51+
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
3252
runs-on: ubuntu-latest
3353
permissions:
3454
contents: read
3555
packages: write
3656

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

43-
- uses: docker/setup-buildx-action@v3
44-
45-
- uses: docker/login-action@v3
64+
# push only: did base-image paths also change in this push? If so, defer
65+
# to the chained workflow_run (which overlays the freshly built base).
66+
- name: Detect base-relevant changes
67+
if: ${{ github.event_name == 'push' }}
68+
id: basepaths
69+
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
4670
with:
47-
registry: ${{ env.REGISTRY }}
48-
username: ${{ github.actor }}
49-
password: ${{ secrets.GITHUB_TOKEN }}
71+
filters: |
72+
base:
73+
- 'Dockerfile'
74+
- 'skills/**'
75+
- 'plugins/**'
76+
- 'agent/**'
77+
- 'entrypoint.sh'
5078
51-
- name: Resolve base image tag
52-
id: base
79+
- name: Resolve build parameters
80+
id: resolve
81+
env:
82+
EVENT: ${{ github.event_name }}
83+
WR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
84+
WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
85+
BASE_PATHS_CHANGED: ${{ steps.basepaths.outputs.base }}
86+
INPUT_BASE_TAG: ${{ inputs.base_tag }}
5387
run: |
54-
if [ "${{ github.event_name }}" = "workflow_run" ]; then
55-
short="$(git rev-parse --short=7 '${{ github.event.workflow_run.head_sha }}')"
56-
echo "ref=sha-${short}" >> "$GITHUB_OUTPUT"
57-
else
58-
echo "ref=${{ inputs.base_tag }}" >> "$GITHUB_OUTPUT"
59-
fi
88+
set -euo pipefail
89+
image="${REGISTRY}/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
90+
thai="${image}-thailand"
91+
proceed=true
92+
slugify() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9._-]+#-#g; s#^[._-]+##; s#[._-]+$##' | cut -c1-128; }
93+
case "${EVENT}" in
94+
workflow_run)
95+
short="$(echo "${WR_HEAD_SHA}" | cut -c1-7)"
96+
base_ref="${image}:sha-${short}"
97+
if [ "${WR_HEAD_BRANCH}" = "main" ]; then moving="latest"; else moving="$(slugify "${WR_HEAD_BRANCH}")"; fi
98+
;;
99+
push)
100+
# Only build if base paths did NOT change (else defer to workflow_run).
101+
if [ "${BASE_PATHS_CHANGED}" = "true" ]; then proceed=false; fi
102+
short="${GITHUB_SHA::7}"
103+
base_ref="${image}:latest"
104+
moving="latest"
105+
;;
106+
workflow_dispatch)
107+
# Dispatch is for branch overlays only. On a tag ref the output
108+
# would be a v-prefixed slug (e.g. v0.3.0) overlaying the wrong
109+
# base and bypassing release.yml's promote — reject it.
110+
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
111+
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."
112+
exit 1
113+
fi
114+
short="${GITHUB_SHA::7}"
115+
base_ref="${image}:${INPUT_BASE_TAG}"
116+
if [ "${GITHUB_REF_NAME}" = "main" ]; then moving="latest"; else moving="$(slugify "${GITHUB_REF_NAME}")"; fi
117+
;;
118+
esac
119+
{
120+
echo "proceed=${proceed}"
121+
echo "thai=${thai}"
122+
echo "base_ref=${base_ref}"
123+
echo "short=${short}"
124+
echo "moving=${moving}"
125+
} >> "$GITHUB_OUTPUT"
126+
127+
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
128+
if: ${{ steps.resolve.outputs.proceed == 'true' }}
60129

61-
- uses: docker/metadata-action@v5
62-
id: meta
130+
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
131+
if: ${{ steps.resolve.outputs.proceed == 'true' }}
63132
with:
64-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
65-
tags: |
66-
type=raw,value=latest,enable={{is_default_branch}}
67-
type=sha
133+
registry: ${{ env.REGISTRY }}
134+
username: ${{ github.actor }}
135+
password: ${{ secrets.GITHUB_TOKEN }}
68136

69-
- uses: docker/build-push-action@v5
137+
- name: Build and push (overlay)
138+
if: ${{ steps.resolve.outputs.proceed == 'true' }}
139+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
70140
with:
71141
context: .
72142
file: Dockerfile.thailand
73-
push: true
74-
tags: ${{ steps.meta.outputs.tags }}
75-
labels: ${{ steps.meta.outputs.labels }}
76143
platforms: linux/amd64
144+
push: true
77145
build-args: |
78-
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:${{ steps.base.outputs.ref }}
146+
BASE_IMAGE=${{ steps.resolve.outputs.base_ref }}
147+
tags: |
148+
${{ steps.resolve.outputs.thai }}:sha-${{ steps.resolve.outputs.short }}
149+
${{ steps.resolve.outputs.thai }}:${{ steps.resolve.outputs.moving }}
150+
provenance: false
151+
sbom: false
79152
cache-from: type=gha
80153
cache-to: type=gha,mode=max

.github/workflows/build.yml

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
1+
# Continuous build of the default agent-sandbox image.
2+
#
3+
# Triggers on `main` pushes (path-scoped to files that actually go into the
4+
# image) and manual dispatch. Deliberately has NO tag trigger: semver releases
5+
# are handled by release.yml, which PROMOTES the already-built `sha-` image to
6+
# the version tag. Keeping tags out of this workflow lets `paths:` filtering
7+
# work correctly (a tag push usually introduces zero changed files, so a
8+
# `paths:`-filtered tag trigger would be silently skipped).
9+
#
10+
# Tags pushed:
11+
# - sha-<short> always — the immutable per-commit handle deploys pin to
12+
# - latest on main
13+
# - <branch-slug> on a manual dispatch from a feature branch
14+
#
15+
# After a successful run, build-thailand.yml (workflow_run) overlays the Thai
16+
# skills onto this exact commit's base image.
117
name: Build and Push
218

319
on:
420
push:
521
branches: [main]
22+
paths:
23+
- 'Dockerfile'
24+
- 'skills/**'
25+
- 'plugins/**'
26+
- 'agent/**'
27+
- 'entrypoint.sh'
28+
- '.github/workflows/build.yml'
629
workflow_dispatch:
730

31+
# Supersede an in-flight build when a newer commit lands on the same ref, so
32+
# the moving tag (latest / slug) always ends up pointing at the newest commit.
33+
concurrency:
34+
group: ${{ github.workflow }}-${{ github.ref }}
35+
cancel-in-progress: true
36+
837
env:
938
REGISTRY: ghcr.io
10-
IMAGE_NAME: ${{ github.repository }}
1139

1240
jobs:
1341
build:
@@ -17,30 +45,87 @@ jobs:
1745
packages: write
1846

1947
steps:
20-
- uses: actions/checkout@v4
48+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2149

22-
- uses: docker/setup-buildx-action@v3
50+
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
2351

24-
- uses: docker/login-action@v3
52+
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
2553
with:
2654
registry: ${{ env.REGISTRY }}
2755
username: ${{ github.actor }}
2856
password: ${{ secrets.GITHUB_TOKEN }}
2957

30-
- uses: docker/metadata-action@v5
58+
- name: Compute image name and tags
3159
id: meta
32-
with:
33-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
34-
tags: |
35-
type=raw,value=latest,enable={{is_default_branch}}
36-
type=sha
60+
run: |
61+
set -euo pipefail
62+
# Releases go through release.yml (push a vX.Y.Z tag -> promote the
63+
# sha-<short> image to <ver>). Dispatching this workflow on a tag ref
64+
# would instead build a v-prefixed slug tag and bypass that promote.
65+
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
66+
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."
67+
exit 1
68+
fi
69+
# GHCR requires a lowercase image path; ${{ github.repository }} may
70+
# contain uppercase (EternisAI/...), so lowercase it explicitly.
71+
image="${REGISTRY}/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
72+
short="${GITHUB_SHA::7}"
73+
if [ "${GITHUB_REF_NAME}" = "main" ]; then
74+
moving="latest"
75+
environment="staging"
76+
else
77+
# Manual dispatch from a feature branch: slugify the branch name to
78+
# a valid Docker tag. Not auto-deployed (environment=none).
79+
moving="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9._-]+#-#g; s#^[._-]+##; s#[._-]+$##' | cut -c1-128)"
80+
environment="none"
81+
fi
82+
{
83+
echo "image=${image}"
84+
echo "short=${short}"
85+
echo "moving=${moving}"
86+
echo "environment=${environment}"
87+
} >> "$GITHUB_OUTPUT"
3788
38-
- uses: docker/build-push-action@v5
89+
- name: Build and push
90+
id: build
91+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
3992
with:
4093
context: .
4194
platforms: linux/amd64
4295
push: true
43-
tags: ${{ steps.meta.outputs.tags }}
44-
labels: ${{ steps.meta.outputs.labels }}
96+
tags: |
97+
${{ steps.meta.outputs.image }}:sha-${{ steps.meta.outputs.short }}
98+
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.moving }}
99+
# Plain single-platform manifest with one stable digest (no
100+
# attestation index), which is what digest-pinned GitOps deploys want.
101+
provenance: false
102+
sbom: false
45103
cache-from: type=gha
46104
cache-to: type=gha,mode=max
105+
106+
- name: Write deploy-info
107+
run: |
108+
set -euo pipefail
109+
# Fast-path handoff to the (separate) staging deploy workflow. The
110+
# registry remains the source of truth — `digest` is always
111+
# re-resolvable from a tag via `oras resolve` — so this artifact is
112+
# short-lived (see retention-days) and the deploy must tolerate its
113+
# absence by reconstructing from the image.
114+
mkdir -p deploy-info
115+
cat > deploy-info/deploy-info.json <<EOF
116+
{
117+
"image": "${{ steps.meta.outputs.image }}",
118+
"tag": "sha-${{ steps.meta.outputs.short }}",
119+
"moving_tag": "${{ steps.meta.outputs.moving }}",
120+
"digest": "${{ steps.build.outputs.digest }}",
121+
"git_sha": "${GITHUB_SHA}",
122+
"ref": "${GITHUB_REF}",
123+
"environment": "${{ steps.meta.outputs.environment }}"
124+
}
125+
EOF
126+
127+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
128+
with:
129+
name: deploy-info
130+
path: deploy-info/deploy-info.json
131+
retention-days: 7

0 commit comments

Comments
 (0)