Skip to content

Commit fb6e822

Browse files
committed
ci: manage Kite releases and deployments from GitHub
1 parent 8617993 commit fb6e822

13 files changed

Lines changed: 1027 additions & 68 deletions

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.github/workflows/ @CopilotKit/admin
2+
/deployment/aws/ @CopilotKit/admin
3+
/scripts/release/ @CopilotKit/admin

.github/workflows/delivery.yml

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
name: Delivery / main and releases
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: write
9+
id-token: write
10+
packages: write
11+
pull-requests: read
12+
13+
jobs:
14+
verify:
15+
permissions:
16+
contents: read
17+
uses: ./.github/workflows/verify.yml
18+
with:
19+
ref: ${{ github.sha }}
20+
21+
metadata:
22+
needs: verify
23+
runs-on: ubuntu-latest
24+
outputs:
25+
is_release: ${{ steps.release.outputs.is_release }}
26+
release_version: ${{ steps.release.outputs.release_version }}
27+
version_label: ${{ steps.release.outputs.version_label }}
28+
steps:
29+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30+
with:
31+
ref: ${{ github.sha }}
32+
- id: pull-request
33+
name: Require an associated merged pull request
34+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
35+
with:
36+
script: |
37+
const { owner, repo } = context.repo;
38+
const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
39+
owner,
40+
repo,
41+
commit_sha: context.sha,
42+
});
43+
const pull = pulls.find((candidate) =>
44+
candidate.base.ref === "main" &&
45+
candidate.merged_at !== null &&
46+
candidate.merge_commit_sha === context.sha
47+
);
48+
if (!pull) {
49+
core.setFailed(`${context.sha} is not the merge commit of a reviewed PR into main`);
50+
return;
51+
}
52+
core.setOutput("head_ref", pull.head.ref);
53+
- id: release
54+
name: Classify merge
55+
env:
56+
HEAD_REF: ${{ steps.pull-request.outputs.head_ref }}
57+
run: |
58+
set -euo pipefail
59+
{
60+
echo "is_release=false"
61+
echo "version_label=main"
62+
} >> "$GITHUB_OUTPUT"
63+
if [[ "$HEAD_REF" =~ ^release/publish/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
64+
version="${BASH_REMATCH[1]}"
65+
package_version=$(node -p "require('./package.json').version")
66+
test "$version" = "$package_version" || {
67+
echo "Release branch v$version does not match package.json $package_version"
68+
exit 1
69+
}
70+
{
71+
echo "is_release=true"
72+
echo "release_version=v$version"
73+
echo "version_label=v$version"
74+
} >> "$GITHUB_OUTPUT"
75+
fi
76+
77+
build-agent:
78+
needs: metadata
79+
runs-on: ubuntu-latest
80+
outputs:
81+
digest: ${{ steps.build.outputs.digest }}
82+
steps:
83+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
84+
with:
85+
ref: ${{ github.sha }}
86+
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
87+
with:
88+
registry: ghcr.io
89+
username: ${{ github.actor }}
90+
password: ${{ secrets.GITHUB_TOKEN }}
91+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
92+
- id: tags
93+
env:
94+
IS_RELEASE: ${{ needs.metadata.outputs.is_release }}
95+
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
96+
run: |
97+
{
98+
echo 'value<<EOF'
99+
echo 'ghcr.io/copilotkit/opentag-agent:main'
100+
echo "ghcr.io/copilotkit/opentag-agent:sha-${{ github.sha }}"
101+
if [ "$IS_RELEASE" = true ]; then
102+
echo "ghcr.io/copilotkit/opentag-agent:$RELEASE_VERSION"
103+
echo "ghcr.io/copilotkit/opentag-agent:${RELEASE_VERSION%.*}"
104+
fi
105+
echo EOF
106+
} >> "$GITHUB_OUTPUT"
107+
- id: build
108+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
109+
with:
110+
context: .
111+
file: deployment/docker/agent.Dockerfile
112+
platforms: linux/amd64
113+
push: true
114+
tags: ${{ steps.tags.outputs.value }}
115+
labels: |
116+
org.opencontainers.image.revision=${{ github.sha }}
117+
org.opencontainers.image.version=${{ needs.metadata.outputs.version_label }}
118+
cache-from: type=gha,scope=agent
119+
cache-to: type=gha,mode=max,scope=agent
120+
121+
build-runtime:
122+
needs: metadata
123+
runs-on: ubuntu-latest
124+
outputs:
125+
digest: ${{ steps.build.outputs.digest }}
126+
steps:
127+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
128+
with:
129+
ref: ${{ github.sha }}
130+
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
131+
with:
132+
registry: ghcr.io
133+
username: ${{ github.actor }}
134+
password: ${{ secrets.GITHUB_TOKEN }}
135+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
136+
- id: tags
137+
env:
138+
IS_RELEASE: ${{ needs.metadata.outputs.is_release }}
139+
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
140+
run: |
141+
{
142+
echo 'value<<EOF'
143+
echo 'ghcr.io/copilotkit/opentag-runtime:main'
144+
echo "ghcr.io/copilotkit/opentag-runtime:sha-${{ github.sha }}"
145+
if [ "$IS_RELEASE" = true ]; then
146+
echo "ghcr.io/copilotkit/opentag-runtime:$RELEASE_VERSION"
147+
echo "ghcr.io/copilotkit/opentag-runtime:${RELEASE_VERSION%.*}"
148+
fi
149+
echo EOF
150+
} >> "$GITHUB_OUTPUT"
151+
- id: build
152+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
153+
with:
154+
context: .
155+
file: deployment/docker/runtime.Dockerfile
156+
platforms: linux/amd64
157+
push: true
158+
tags: ${{ steps.tags.outputs.value }}
159+
labels: |
160+
org.opencontainers.image.revision=${{ github.sha }}
161+
org.opencontainers.image.version=${{ needs.metadata.outputs.version_label }}
162+
cache-from: type=gha,scope=runtime
163+
cache-to: type=gha,mode=max,scope=runtime
164+
165+
staging:
166+
needs: [metadata, build-agent, build-runtime]
167+
permissions:
168+
contents: read
169+
id-token: write
170+
uses: ./.github/workflows/deploy-aws.yml
171+
with:
172+
agent_image: ghcr.io/copilotkit/opentag-agent@${{ needs.build-agent.outputs.digest }}
173+
environment: staging
174+
github_environment: kite-staging
175+
ref: ${{ github.sha }}
176+
runtime_image: ghcr.io/copilotkit/opentag-runtime@${{ needs.build-runtime.outputs.digest }}
177+
178+
release:
179+
if: needs.metadata.outputs.is_release == 'true'
180+
needs: [metadata, build-agent, build-runtime, staging]
181+
runs-on: ubuntu-latest
182+
steps:
183+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
184+
with:
185+
fetch-depth: 0
186+
ref: ${{ github.sha }}
187+
- name: Create release manifest
188+
env:
189+
AGENT_DIGEST: ${{ needs.build-agent.outputs.digest }}
190+
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
191+
RUNTIME_DIGEST: ${{ needs.build-runtime.outputs.digest }}
192+
SOURCE_COMMIT: ${{ github.sha }}
193+
run: |
194+
jq -n \
195+
--arg version "$RELEASE_VERSION" \
196+
--arg commit "$SOURCE_COMMIT" \
197+
--arg agent_digest "$AGENT_DIGEST" \
198+
--arg runtime_digest "$RUNTIME_DIGEST" \
199+
'{version:$version,commit:$commit,images:{agent:{repository:"ghcr.io/copilotkit/opentag-agent",digest:$agent_digest,reference:("ghcr.io/copilotkit/opentag-agent@"+$agent_digest)},runtime:{repository:"ghcr.io/copilotkit/opentag-runtime",digest:$runtime_digest,reference:("ghcr.io/copilotkit/opentag-runtime@"+$runtime_digest)}}}' \
200+
> container-images.json
201+
- name: Create annotated tag idempotently
202+
env:
203+
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
204+
SOURCE_COMMIT: ${{ github.sha }}
205+
run: |
206+
set -euo pipefail
207+
if git rev-parse "$RELEASE_VERSION" >/dev/null 2>&1; then
208+
test "$(git rev-list -n 1 "$RELEASE_VERSION")" = "$SOURCE_COMMIT"
209+
else
210+
git config user.name copilotkit-devops
211+
git config user.email devops@copilotkit.ai
212+
git tag -a "$RELEASE_VERSION" "$SOURCE_COMMIT" -m "Release $RELEASE_VERSION"
213+
git push origin "$RELEASE_VERSION"
214+
fi
215+
- name: Create GitHub Release idempotently
216+
env:
217+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
218+
RELEASE_VERSION: ${{ needs.metadata.outputs.release_version }}
219+
run: |
220+
set -euo pipefail
221+
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
222+
gh release edit "$RELEASE_VERSION" --title "$RELEASE_VERSION" --notes-file release-notes.md
223+
gh release upload "$RELEASE_VERSION" container-images.json --clobber
224+
else
225+
gh release create "$RELEASE_VERSION" container-images.json \
226+
--title "$RELEASE_VERSION" --notes-file release-notes.md --verify-tag
227+
fi
228+
229+
prod:
230+
if: needs.metadata.outputs.is_release == 'true' && needs.release.result == 'success'
231+
needs: [metadata, build-agent, build-runtime, release]
232+
permissions:
233+
contents: read
234+
id-token: write
235+
uses: ./.github/workflows/deploy-aws.yml
236+
with:
237+
agent_image: ghcr.io/copilotkit/opentag-agent@${{ needs.build-agent.outputs.digest }}
238+
environment: prod
239+
github_environment: kite-prod
240+
ref: ${{ github.sha }}
241+
runtime_image: ghcr.io/copilotkit/opentag-runtime@${{ needs.build-runtime.outputs.digest }}
242+
243+
community:
244+
if: needs.metadata.outputs.is_release == 'true' && needs.release.result == 'success'
245+
needs: [metadata, build-agent, build-runtime, release]
246+
permissions:
247+
contents: read
248+
id-token: write
249+
uses: ./.github/workflows/deploy-aws.yml
250+
with:
251+
agent_image: ghcr.io/copilotkit/opentag-agent@${{ needs.build-agent.outputs.digest }}
252+
environment: community
253+
github_environment: kite-community
254+
ref: ${{ github.sha }}
255+
runtime_image: ghcr.io/copilotkit/opentag-runtime@${{ needs.build-runtime.outputs.digest }}

.github/workflows/deploy-aws.yml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: Deploy / AWS environment
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
agent_image:
7+
required: true
8+
type: string
9+
environment:
10+
required: true
11+
type: string
12+
github_environment:
13+
required: true
14+
type: string
15+
ref:
16+
required: true
17+
type: string
18+
runtime_image:
19+
required: true
20+
type: string
21+
22+
permissions:
23+
contents: read
24+
id-token: write
25+
26+
concurrency:
27+
group: kite-deploy-${{ inputs.environment }}
28+
cancel-in-progress: ${{ inputs.environment == 'staging' }}
29+
30+
jobs:
31+
deploy:
32+
runs-on: ubuntu-latest
33+
timeout-minutes: 30
34+
environment: ${{ inputs.github_environment }}
35+
steps:
36+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37+
with:
38+
ref: ${{ inputs.ref }}
39+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
40+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
41+
with:
42+
node-version: 22.x
43+
cache: pnpm
44+
cache-dependency-path: deployment/aws/pnpm-lock.yaml
45+
- run: pnpm --dir deployment/aws install --frozen-lockfile
46+
- name: Configure AWS credentials
47+
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
48+
with:
49+
aws-region: ${{ vars.AWS_REGION }}
50+
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
51+
role-session-name: opentag-${{ inputs.environment }}-${{ github.run_id }}
52+
- name: Validate deployment configuration
53+
env:
54+
AWS_REGION: ${{ vars.AWS_REGION }}
55+
CHANNEL_NAME: ${{ vars.INTELLIGENCE_CHANNEL_NAME }}
56+
DATADOG_SECRET_ARN: ${{ vars.DATADOG_API_KEY_SECRET_ARN }}
57+
OPENTAG_SECRET_ARN: ${{ vars.OPENTAG_SECRET_ARN }}
58+
VPC_ID: ${{ vars.VPC_ID }}
59+
run: |
60+
for name in AWS_REGION CHANNEL_NAME DATADOG_SECRET_ARN OPENTAG_SECRET_ARN VPC_ID; do
61+
test -n "${!name}" || {
62+
echo "Missing required GitHub Environment variable: $name"
63+
exit 1
64+
}
65+
done
66+
- name: Deploy exact images
67+
working-directory: deployment/aws
68+
env:
69+
AGENT_IMAGE: ${{ inputs.agent_image }}
70+
CHANNEL_NAME: ${{ vars.INTELLIGENCE_CHANNEL_NAME }}
71+
DATADOG_SECRET_ARN: ${{ vars.DATADOG_API_KEY_SECRET_ARN }}
72+
DATADOG_SITE: ${{ vars.DATADOG_SITE }}
73+
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
74+
INTELLIGENCE_API_URL: ${{ vars.INTELLIGENCE_API_URL }}
75+
INTELLIGENCE_GATEWAY_WS_URL: ${{ vars.INTELLIGENCE_GATEWAY_WS_URL }}
76+
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
77+
OPENTAG_SECRET_ARN: ${{ vars.OPENTAG_SECRET_ARN }}
78+
RUNTIME_IMAGE: ${{ inputs.runtime_image }}
79+
VPC_ID: ${{ vars.VPC_ID }}
80+
run: |
81+
set -euo pipefail
82+
context=(
83+
-c appName=kite
84+
-c sharedCluster=true
85+
-c "environment=$DEPLOY_ENVIRONMENT"
86+
-c "serviceName=kite-$DEPLOY_ENVIRONMENT"
87+
-c "vpcId=$VPC_ID"
88+
-c "channelName=$CHANNEL_NAME"
89+
-c "agentImage=$AGENT_IMAGE"
90+
-c "runtimeImage=$RUNTIME_IMAGE"
91+
-c "datadogSite=${DATADOG_SITE:-datadoghq.com}"
92+
)
93+
test -z "$INTELLIGENCE_API_URL" || context+=( -c "intelligenceApiUrl=$INTELLIGENCE_API_URL" )
94+
test -z "$INTELLIGENCE_GATEWAY_WS_URL" || context+=( -c "intelligenceGatewayWsUrl=$INTELLIGENCE_GATEWAY_WS_URL" )
95+
test -z "$LOG_LEVEL" || context+=( -c "logLevel=$LOG_LEVEL" )
96+
pnpm exec cdk deploy "kite-$DEPLOY_ENVIRONMENT" --exclusively \
97+
"${context[@]}" \
98+
--parameters "kite-$DEPLOY_ENVIRONMENT:OpenTagSecretArn=$OPENTAG_SECRET_ARN" \
99+
--parameters "kite-$DEPLOY_ENVIRONMENT:DatadogApiKeySecretArn=$DATADOG_SECRET_ARN" \
100+
--require-approval never
101+
- name: Verify ECS rollout and image digests
102+
env:
103+
AGENT_IMAGE: ${{ inputs.agent_image }}
104+
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
105+
RUNTIME_IMAGE: ${{ inputs.runtime_image }}
106+
run: |
107+
set -euo pipefail
108+
service="kite-$DEPLOY_ENVIRONMENT"
109+
aws ecs wait services-stable --cluster kite --services "$service"
110+
task_definition=$(aws ecs describe-services \
111+
--cluster kite --services "$service" \
112+
--query 'services[0].taskDefinition' --output text)
113+
agent=$(aws ecs describe-task-definition --task-definition "$task_definition" \
114+
--query "taskDefinition.containerDefinitions[?name=='agent'].image | [0]" --output text)
115+
runtime=$(aws ecs describe-task-definition --task-definition "$task_definition" \
116+
--query "taskDefinition.containerDefinitions[?name=='runtime'].image | [0]" --output text)
117+
test "$agent" = "$AGENT_IMAGE"
118+
test "$runtime" = "$RUNTIME_IMAGE"
119+
running=$(aws ecs describe-services --cluster kite --services "$service" \
120+
--query 'services[0].runningCount' --output text)
121+
test "$running" = "1"

0 commit comments

Comments
 (0)