214 lines
6.8 KiB
Python
214 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
update-workflows.py
|
||
|
|
|
||
|
|
Generate GitHub Actions workflow files for worker deployment.
|
||
|
|
Worker definitions are loaded from config/workers.json (single source of truth).
|
||
|
|
|
||
|
|
Authors:
|
||
|
|
Mark Smith <mark@dreamwidth.org>
|
||
|
|
|
||
|
|
Copyright (c) 2022-2025 by Dreamwidth Studios, LLC.
|
||
|
|
|
||
|
|
This program is free software; you may redistribute it and/or modify it under
|
||
|
|
the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# Paths
|
||
|
|
script_dir = Path(__file__).parent
|
||
|
|
config_file = script_dir / "workers.json"
|
||
|
|
workflows_dir = script_dir.parent / ".github" / "workflows"
|
||
|
|
tasks_dir = workflows_dir / "tasks"
|
||
|
|
|
||
|
|
# Load worker definitions from JSON
|
||
|
|
with open(config_file) as f:
|
||
|
|
config = json.load(f)
|
||
|
|
|
||
|
|
workers = config["workers"]
|
||
|
|
worker_names = sorted(workers.keys())
|
||
|
|
|
||
|
|
print(f"Loaded {len(workers)} workers from {config_file}")
|
||
|
|
|
||
|
|
|
||
|
|
def generate_workflow(image_name, workflow_name):
|
||
|
|
"""Generate a worker deploy workflow file."""
|
||
|
|
|
||
|
|
# Build options list
|
||
|
|
options = ["- ALL WORKERS (*)"]
|
||
|
|
for name in worker_names:
|
||
|
|
options.append(f"- {name}")
|
||
|
|
options_str = "\n ".join(options)
|
||
|
|
|
||
|
|
# Build render steps
|
||
|
|
render_steps = []
|
||
|
|
for name in worker_names:
|
||
|
|
render_steps.append(f"""
|
||
|
|
- name: ({name}) Render Amazon ECS task definition
|
||
|
|
if: github.event.inputs.service == 'ALL WORKERS (*)' || github.event.inputs.service == '{name}'
|
||
|
|
id: render-worker-container-{name}
|
||
|
|
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||
|
|
with:
|
||
|
|
task-definition: ".github/workflows/tasks/worker-{name}-service.json"
|
||
|
|
container-name: ${{{{ env.CONTAINER_NAME }}}}
|
||
|
|
image: "${{{{ env.IMAGE_BASE }}}}@${{{{ github.event.inputs.tag }}}}"
|
||
|
|
""")
|
||
|
|
|
||
|
|
# Build deploy steps
|
||
|
|
deploy_steps = []
|
||
|
|
for name in worker_names:
|
||
|
|
deploy_steps.append(f"""
|
||
|
|
- name: ({name}) Deploy to Amazon ECS service
|
||
|
|
if: github.event.inputs.service == 'ALL WORKERS (*)' || github.event.inputs.service == '{name}'
|
||
|
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||
|
|
with:
|
||
|
|
task-definition: ${{{{ steps.render-worker-container-{name}.outputs.task-definition }}}}
|
||
|
|
cluster: ${{{{ env.ECS_CLUSTER }}}}
|
||
|
|
service: "worker-{name}-service"
|
||
|
|
""")
|
||
|
|
|
||
|
|
workflow = f"""#
|
||
|
|
# AUTO-GENERATED by config/update-workflows.py. DO NOT EDIT.
|
||
|
|
#
|
||
|
|
|
||
|
|
name: (deploy) {workflow_name}
|
||
|
|
|
||
|
|
on:
|
||
|
|
workflow_dispatch:
|
||
|
|
inputs:
|
||
|
|
service:
|
||
|
|
type: choice
|
||
|
|
description: Which service to deploy
|
||
|
|
options:
|
||
|
|
{options_str}
|
||
|
|
tag:
|
||
|
|
type: string
|
||
|
|
description: SHA256 to deploy (include "sha256:" prefix)
|
||
|
|
required: true
|
||
|
|
|
||
|
|
env:
|
||
|
|
REGION: us-east-1
|
||
|
|
ECS_CLUSTER: dreamwidth
|
||
|
|
CONTAINER_NAME: worker
|
||
|
|
IMAGE_BASE: ghcr.io/dreamwidth/{image_name}
|
||
|
|
|
||
|
|
jobs:
|
||
|
|
deploy:
|
||
|
|
if: github.repository == 'dreamwidth/dreamwidth'
|
||
|
|
|
||
|
|
runs-on: ubuntu-latest
|
||
|
|
|
||
|
|
steps:
|
||
|
|
- name: Checkout Code
|
||
|
|
uses: actions/checkout@v3
|
||
|
|
|
||
|
|
- name: Configure AWS Credentials
|
||
|
|
uses: aws-actions/configure-aws-credentials@v1
|
||
|
|
with:
|
||
|
|
aws-access-key-id: ${{{{ secrets.AWS_ACCESS_KEY_ID }}}}
|
||
|
|
aws-secret-access-key: ${{{{ secrets.AWS_SECRET_ACCESS_KEY }}}}
|
||
|
|
aws-region: ${{{{ env.REGION }}}}
|
||
|
|
{"".join(render_steps).rstrip()}
|
||
|
|
{"".join(deploy_steps).rstrip()}
|
||
|
|
|
||
|
|
- name: Notify Discord
|
||
|
|
uses: sarisia/actions-status-discord@v1
|
||
|
|
if: always()
|
||
|
|
with:
|
||
|
|
title: "${{{{ github.event.inputs.service }}}} DEPLOY STARTED"
|
||
|
|
description: "Deploying `${{{{ github.event.inputs.tag }}}}` to `${{{{ github.event.inputs.service }}}}`\\n\\nClick the header above to watch the deployment progress."
|
||
|
|
url: "https://${{{{ env.REGION }}}}.console.aws.amazon.com/ecs/v2/clusters/dreamwidth/services?region=${{{{ env.REGION }}}}"
|
||
|
|
webhook: ${{{{ secrets.DISCORD_WEBHOOK }}}}
|
||
|
|
nocontext: true
|
||
|
|
"""
|
||
|
|
return workflow
|
||
|
|
|
||
|
|
|
||
|
|
def generate_task_definition(name, cpu, memory):
|
||
|
|
"""Generate a task definition JSON file for a worker."""
|
||
|
|
|
||
|
|
task = {
|
||
|
|
"containerDefinitions": [
|
||
|
|
{
|
||
|
|
"name": "worker",
|
||
|
|
"image": "ghcr.io/dreamwidth/worker:latest",
|
||
|
|
"cpu": 0,
|
||
|
|
"portMappings": [],
|
||
|
|
"essential": True,
|
||
|
|
"command": [
|
||
|
|
"bash",
|
||
|
|
"/opt/startup-prod.sh",
|
||
|
|
f"bin/worker/{name}",
|
||
|
|
"-v"
|
||
|
|
],
|
||
|
|
"environment": [],
|
||
|
|
"mountPoints": [
|
||
|
|
{
|
||
|
|
"sourceVolume": "dw-config",
|
||
|
|
"containerPath": "/dw/etc",
|
||
|
|
"readOnly": True
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"volumesFrom": [],
|
||
|
|
"linuxParameters": {
|
||
|
|
"initProcessEnabled": True
|
||
|
|
},
|
||
|
|
"logConfiguration": {
|
||
|
|
"logDriver": "awslogs",
|
||
|
|
"options": {
|
||
|
|
"awslogs-create-group": "true",
|
||
|
|
"awslogs-group": f"/dreamwidth/worker/{name}",
|
||
|
|
"awslogs-region": "us-east-1",
|
||
|
|
"awslogs-stream-prefix": "worker"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"family": f"worker-{name}",
|
||
|
|
"taskRoleArn": "arn:aws:iam::194396987458:role/dreamwidth-ecsTaskRole",
|
||
|
|
"executionRoleArn": "arn:aws:iam::194396987458:role/dreamwidth-ecsTaskExecutionRole",
|
||
|
|
"networkMode": "awsvpc",
|
||
|
|
"volumes": [
|
||
|
|
{
|
||
|
|
"name": "dw-config",
|
||
|
|
"efsVolumeConfiguration": {
|
||
|
|
"fileSystemId": "fs-f9f3e04d",
|
||
|
|
"rootDirectory": "/etc-workers",
|
||
|
|
"transitEncryption": "DISABLED"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"requiresCompatibilities": ["FARGATE"],
|
||
|
|
"cpu": str(cpu),
|
||
|
|
"memory": str(memory)
|
||
|
|
}
|
||
|
|
|
||
|
|
return json.dumps(task, indent=4)
|
||
|
|
|
||
|
|
|
||
|
|
# Generate deployment workflows
|
||
|
|
for image_name, workflow_name, filename in [
|
||
|
|
("worker22", "workers (22.04)", "worker22-deploy.yml"),
|
||
|
|
]:
|
||
|
|
print(f"Generating {workflows_dir / filename}...")
|
||
|
|
workflow_content = generate_workflow(image_name, workflow_name)
|
||
|
|
with open(workflows_dir / filename, "w") as f:
|
||
|
|
f.write(workflow_content)
|
||
|
|
print(f" Written {len(worker_names)} worker entries")
|
||
|
|
|
||
|
|
# Generate task definitions
|
||
|
|
print(f"Generating task definitions in {tasks_dir}...")
|
||
|
|
tasks_dir.mkdir(exist_ok=True)
|
||
|
|
for name in worker_names:
|
||
|
|
w = workers[name]
|
||
|
|
task_content = generate_task_definition(name, w["cpu"], w["memory"])
|
||
|
|
with open(tasks_dir / f"worker-{name}-service.json", "w") as f:
|
||
|
|
f.write(task_content)
|
||
|
|
f.write("\n")
|
||
|
|
print(f" Written {len(worker_names)} task definition files")
|
||
|
|
|
||
|
|
print("\nDone!")
|