Skip to content

Commit 2deee82

Browse files
committed
Merge branch 'main' of github.com:emagedoc/CodeClash
2 parents a4c15d8 + d41b188 commit 2deee82

37 files changed

Lines changed: 1186 additions & 71 deletions

.cursor/rules/style.mdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ alwaysApply: true
2121
13. Write concise code and thing of elegant solution.
2222
14. When logging exceptions, always include `exc_info=True` to capture the full traceback.
2323
15. Command line arguments should be parsed with `argparse`.
24+
16. Generally prefer to use keyword arguments over positional arguments. If there are somewhat specific optional arguments, mark them as keyword-only.
25+
17. Use logger instead of print statements.
2426

2527
## Test style
2628

aws/README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# AWS
22

3-
## Overview
3+
## Usage
44

5-
* `setup/`: Everything needed to set up AWS to run codeclash jobs
5+
```bash
6+
# Hello world example
7+
aws/run_job.py --show-logs -- echo "hello world"
8+
9+
# Run main.py with a config file
10+
# Remember to use the docker_and_sync.sh wrapper script!
11+
# ❌ aws/run_job.py --show-logs -- python main.py configs/test/battlesnake_pvp_test.yaml
12+
aws/run_job.py --show-logs -- aws/docker_and_sync.sh python main.py configs/test/battlesnake_pvp_test.yaml
13+
```
14+
15+
> [!WARNING]
16+
> Everything needs to be committed & pushed to the repo! You can use a different branch than main for this.
17+
18+
## Setup
19+
20+
* `setup/`: Everything needed to set up AWS to run codeclash jobs. Advanced user stuff.

aws/docker_and_sync.sh

100644100755
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
#!/bin/bash
22

3+
# This wrapper is used together with a script like main.py to run a command in a container
4+
# while ensuring that docker is alive and guaranteeing to sync logs to S3 on exit.
5+
6+
echo "═══════════════════════════════════════════════════════════════════════════════"
7+
echo "🚀 Wrapper script docker_and_sync.sh"
8+
echo "═══════════════════════════════════════════════════════════════════════════════"
9+
310
set -x
411
set -euo pipefail
512

613
echo "📅 Container built at: $BUILD_TIMESTAMP"
714

8-
echo "Git pull"
9-
git pull
1015

1116
# Function to sync logs on exit
1217
cleanup() {
@@ -56,13 +61,10 @@ done
5661
docker run hello-world
5762

5863
# Pull images from ECR so we don't have to build them
59-
DOCKER_REGISTRY="039984708918.dkr.ecr.us-east-1.amazonaws.com"
60-
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $DOCKER_REGISTRY
61-
62-
for game in battlesnake; do
63-
docker pull $DOCKER_REGISTRY/codeclash/$game
64-
docker tag $DOCKER_REGISTRY/codeclash/$game codeclash/$game
65-
done
64+
export AWS_DOCKER_REGISTRY="039984708918.dkr.ecr.us-east-1.amazonaws.com"
65+
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_DOCKER_REGISTRY
66+
export AWS_S3_BUCKET="codeclash"
67+
export AWS_S3_PREFIX="logs"
6668

6769
# Create logs directory
6870
mkdir -p logs
@@ -73,3 +75,7 @@ ulimit -n 65536
7375

7476
# Execute the command passed to container
7577
"$@"
78+
79+
echo "═══════════════════════════════════════════════════════════════════════════════"
80+
echo "✅ Wrapper script docker_and_sync.sh finished"
81+
echo "═══════════════════════════════════════════════════════════════════════════════"

aws/get_job_log.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ def main():
6363
parser = argparse.ArgumentParser(
6464
description="Retrieve logs for AWS Batch jobs",
6565
formatter_class=argparse.RawDescriptionHelpFormatter,
66-
epilog="""
67-
Examples:
68-
%(prog)s abc123-def456-ghi789
69-
""",
7066
)
7167

7268
parser.add_argument("job_id", help="Job ID to retrieve logs for")

aws/run_job.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,28 @@
1414
import argparse
1515
import json
1616
import shlex
17-
import subprocess
1817
import sys
1918
import time
2019
from typing import Any
2120

2221
import boto3
2322

23+
from codeclash.utils.git_utils import get_current_git_branch, has_unpushed_commits, is_git_repo_dirty
2424
from codeclash.utils.log import get_logger
2525

2626
logger = get_logger("launch", emoji="🚀")
2727

2828

29-
def get_current_git_branch() -> str:
30-
"""Get the current git branch name."""
31-
try:
32-
result = subprocess.run(
33-
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True
34-
)
35-
return result.stdout.strip()
36-
except subprocess.CalledProcessError as e:
37-
logger.error(f"Failed to get git branch: {e}")
38-
raise
29+
def check_git_status_and_confirm() -> None:
30+
if not is_git_repo_dirty() and not has_unpushed_commits():
31+
return
32+
33+
if input("Git repository dirty/unpushed, continue anyway? (y/N): ").strip().lower() not in ("y", "yes"):
34+
sys.exit(1)
3935

4036

4137
class AWSBatchJobLauncher:
42-
def __init__(self, job_definition_name: str = "codeclash-yolo-test", job_queue: str = "codeclash-test-queue"):
38+
def __init__(self, job_definition_name: str = "codeclash-default-job", job_queue: str = "codeclash-queue"):
4339
self.batch_client = boto3.client("batch")
4440
self.logs_client = boto3.client("logs")
4541
self.job_definition_name = job_definition_name
@@ -154,11 +150,9 @@ def main():
154150
)
155151
parser.add_argument("--job-name", help="Custom job name (auto-generated if not specified)")
156152
parser.add_argument(
157-
"--job-definition", default="codeclash-yolo-test", help="Job definition name (default: codeclash-yolo-test)"
158-
)
159-
parser.add_argument(
160-
"--job-queue", default="codeclash-test-queue", help="Job queue name (default: codeclash-test-queue)"
153+
"--job-definition", default="codeclash-default-job", help="Job definition name (default: codeclash-default-job)"
161154
)
155+
parser.add_argument("--job-queue", default="codeclash-queue", help="Job queue name (default: codeclash-queue)")
162156
parser.add_argument("--wait", action="store_true", help="Wait for the job to complete before exiting")
163157
parser.add_argument("--show-logs", action="store_true", help="Show job logs after completion (implies --wait)")
164158

@@ -171,6 +165,8 @@ def main():
171165

172166
args = parser.parse_args(aws_args)
173167

168+
check_git_status_and_confirm()
169+
174170
launcher = AWSBatchJobLauncher(job_definition_name=args.job_definition, job_queue=args.job_queue)
175171

176172
# Submit the job
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
import logging
6+
from pathlib import Path
7+
8+
import boto3
9+
10+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def clean_old_job_definitions(job_name: str, batch_client) -> int:
15+
response = batch_client.describe_job_definitions(jobDefinitionName=job_name, status="ACTIVE")
16+
17+
job_definitions = response["jobDefinitions"]
18+
19+
if not job_definitions:
20+
logger.info(f"No active job definitions found for: {job_name}")
21+
return 0
22+
23+
job_definitions.sort(key=lambda x: x["revision"])
24+
old_revisions = job_definitions[:-1]
25+
26+
logger.info(f"Found {len(job_definitions)} active revisions for {job_name}")
27+
logger.info(f"Latest revision: {job_definitions[-1]['revision']}")
28+
29+
if not old_revisions:
30+
logger.info("No old revisions to clean up")
31+
return 0
32+
33+
for job_def in old_revisions:
34+
logger.info(f"Deregistering old revision: {job_name}:{job_def['revision']}")
35+
batch_client.deregister_job_definition(jobDefinition=job_def["jobDefinitionArn"])
36+
37+
logger.info(f"✅ Successfully cleaned up {len(old_revisions)} old revisions for {job_name}")
38+
return len(old_revisions)
39+
40+
41+
def main():
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument("job_definition_json", nargs="?", default="job_definition.json")
44+
parser.add_argument("--region", default="us-east-1")
45+
46+
args = parser.parse_args()
47+
48+
json_path = Path(args.job_definition_json)
49+
data = json.loads(json_path.read_text())
50+
job_name = data["jobDefinitionName"]
51+
52+
batch_client = boto3.client("batch", region_name=args.region)
53+
clean_old_job_definitions(job_name, batch_client)
54+
55+
56+
if __name__ == "__main__":
57+
main()

aws/setup/batch/environment.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"computeEnvironmentName": "codeclash-batch",
3+
"computeEnvironmentArn": "arn:aws:batch:us-east-1:039984708918:compute-environment/codeclash-batch",
4+
"ecsClusterArn": "arn:aws:ecs:us-east-1:039984708918:cluster/AWSBatch-codeclash-batch-1561352c-9234-3de1-999a-dcd1c9989b32",
5+
"tags": {
6+
"project": "codeclash",
7+
"author": "kilian"
8+
},
9+
"type": "MANAGED",
10+
"state": "ENABLED",
11+
"status": "VALID",
12+
"statusReason": "ComputeEnvironment Healthy",
13+
"computeResources": {
14+
"type": "EC2",
15+
"allocationStrategy": "BEST_FIT_PROGRESSIVE",
16+
"minvCpus": 2,
17+
"maxvCpus": 16,
18+
"desiredvCpus": 2,
19+
"instanceTypes": [
20+
"optimal"
21+
],
22+
"subnets": [
23+
"subnet-02f03254ff2613e05",
24+
"subnet-0e2125855c3ef70da",
25+
"subnet-08945e7d77f277c44",
26+
"subnet-030c8969a2f8b25eb",
27+
"subnet-06521a7e32f9320a8",
28+
"subnet-0440a980c38055bfa"
29+
],
30+
"securityGroupIds": [
31+
"sg-014723b4566a8214d"
32+
],
33+
"instanceRole": "arn:aws:iam::039984708918:instance-profile/kilian-codeclash-ecsInstanceRole",
34+
"launchTemplate": {
35+
"launchTemplateName": "kilian-codeclash-launch-template",
36+
"version": "$Latest"
37+
},
38+
"ec2Configuration": [
39+
{
40+
"imageType": "ECS_AL2"
41+
}
42+
]
43+
},
44+
"serviceRole": "arn:aws:iam::039984708918:role/aws-service-role/batch.amazonaws.com/AWSServiceRoleForBatch",
45+
"updatePolicy": {
46+
"terminateJobsOnUpdate": false,
47+
"jobExecutionTimeoutMinutes": 30
48+
},
49+
"containerOrchestrationType": "ECS",
50+
"uuid": "f413880a-eecc-3dfd-b139-0e9143381b4d"
51+
}

aws/setup/batch/iam-ebs.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Role": {
3+
"Path": "/",
4+
"RoleName": "kilian-codeclash-ecs-role-for-ebs-volumes",
5+
"AssumeRolePolicyDocument": {
6+
"Version": "2012-10-17",
7+
"Statement": [
8+
{
9+
"Effect": "Allow",
10+
"Principal": {
11+
"Service": "ecs.amazonaws.com"
12+
},
13+
"Action": "sts:AssumeRole"
14+
}
15+
]
16+
},
17+
"Description": "ECS Infrastructure role for managing EBS volumes",
18+
"MaxSessionDuration": 3600,
19+
"Tags": [
20+
{
21+
"Key": "project",
22+
"Value": "codeclash"
23+
},
24+
{
25+
"Key": "author",
26+
"Value": "kilian"
27+
}
28+
]
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Role": {
3+
"Path": "/",
4+
"RoleName": "kilian-codeclash-ecsInstanceRole",
5+
"RoleId": "AROAQST2GHU3K4I5VM26G",
6+
"Arn": "arn:aws:iam::039984708918:role/kilian-codeclash-ecsInstanceRole",
7+
"CreateDate": "2025-09-24 02:40:57+00:00",
8+
"AssumeRolePolicyDocument": {
9+
"Version": "2012-10-17",
10+
"Statement": [
11+
{
12+
"Effect": "Allow",
13+
"Principal": {
14+
"Service": "ec2.amazonaws.com"
15+
},
16+
"Action": "sts:AssumeRole"
17+
}
18+
]
19+
},
20+
"Description": "Allows EC2 instances to call AWS services on your behalf.",
21+
"MaxSessionDuration": 3600,
22+
"Tags": [
23+
{
24+
"Key": "project",
25+
"Value": "codeclash"
26+
},
27+
{
28+
"Key": "author",
29+
"Value": "kilian"
30+
}
31+
],
32+
"RoleLastUsed": {
33+
"LastUsedDate": "2025-09-25 19:28:45+00:00",
34+
"Region": "us-east-1"
35+
}
36+
}
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"Role": {
3+
"Path": "/",
4+
"RoleName": "kilian-codeclash-execution-role",
5+
"RoleId": "AROAQST2GHU3FIDU7YRBJ",
6+
"Arn": "arn:aws:iam::039984708918:role/kilian-codeclash-execution-role",
7+
"CreateDate": "2025-09-24 17:49:33+00:00",
8+
"AssumeRolePolicyDocument": {
9+
"Version": "2012-10-17",
10+
"Statement": [
11+
{
12+
"Sid": "",
13+
"Effect": "Allow",
14+
"Principal": {
15+
"Service": "ecs-tasks.amazonaws.com"
16+
},
17+
"Action": "sts:AssumeRole"
18+
}
19+
]
20+
},
21+
"Description": "Allows ECS tasks to call AWS services on your behalf.",
22+
"MaxSessionDuration": 3600,
23+
"Tags": [
24+
{
25+
"Key": "project",
26+
"Value": "codeclash"
27+
},
28+
{
29+
"Key": "author",
30+
"Value": "kilian"
31+
}
32+
],
33+
"RoleLastUsed": {
34+
"LastUsedDate": "2025-09-25 19:02:55+00:00",
35+
"Region": "us-east-1"
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)