mourningdove/config/update-workflows.py

214 lines
6.8 KiB
Python
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
#!/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!")