Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: deploy

# Plan on every pull request, apply on main. AWS access is GitHub OIDC, no
# stored keys: pull requests get a read-only plan role, and only main can
# assume the apply role (aws-oidc-bootstrap, role-voltcast.tf).

on:
pull_request:
paths:
- src/**
- infrastructure/**
- requirements-lambda.txt
- Makefile
- .github/workflows/deploy.yml
push:
branches: [main]
paths:
- src/**
- infrastructure/**
- requirements-lambda.txt
- Makefile
- .github/workflows/deploy.yml
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
terraform:
name: ${{ github.event_name == 'pull_request' && 'plan' || 'apply' }}
# Fork pull requests get no OIDC token; deploys only ever run from main.
if: >-
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|| github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ github.event_name == 'pull_request' && github.ref || 'main' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
IS_PR: ${{ github.event_name == 'pull_request' }}
AWS_REGION: ${{ vars.AWS_REGION }}
TF_INPUT: "0"
TF_IN_AUTOMATION: "1"
TF_VAR_alert_email: ${{ secrets.ALERT_EMAIL }}
# The plan role can't write the lock table; a plan never needs it.
TF_CLI_ARGS_plan: ${{ github.event_name == 'pull_request' && '-lock=false' || '' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
terraform_wrapper: false
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.event_name == 'pull_request' && vars.AWS_PLAN_ROLE_ARN || vars.AWS_APPLY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}

- name: Check alert email
# An empty alert_email would plan the alert subscription away.
run: |
if [ -z "$TF_VAR_alert_email" ]; then
echo "::error::The ALERT_EMAIL secret is not set; a deploy without it would delete the alert email subscription."
exit 1
fi

- name: Write backend config
env:
STATE_BUCKET: ${{ vars.TF_STATE_BUCKET }}
LOCK_TABLE: ${{ vars.TF_STATE_LOCK_TABLE }}
run: |
cat > infrastructure/backend.hcl <<EOF
bucket = "$STATE_BUCKET"
key = "voltcast/terraform.tfstate"
region = "$AWS_REGION"
dynamodb_table = "$LOCK_TABLE"
encrypt = true
EOF

- name: Plan
run: make plan PYTHON=python

- name: Summarise plan
run: |
terraform -chdir=infrastructure show -no-color tfplan > plan.txt
{
echo "### $(grep -E '^(Plan:|No changes)' plan.txt || echo 'Plan')"
echo
echo '<details><summary>Full plan</summary>'
echo
echo '```'
cat plan.txt
echo '```'
echo '</details>'
} >> "$GITHUB_STEP_SUMMARY"

- name: Apply
if: env.IS_PR != 'true'
run: make deploy
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ State goes to `./data` (gitignored), laid out exactly as in the S3 bucket.

## Deploy

Merging to `main` deploys. The [deploy workflow](.github/workflows/deploy.yml)
runs `make plan` on every pull request that touches the code or the
infrastructure, and puts the plan in the run summary. On `main` it plans
again and applies, stamping the Lambda with the merge commit's SHA. AWS
access is GitHub OIDC with no stored keys. Pull requests get a read-only
plan role, and only `main` can assume the apply role. Both roles live in
`aws-oidc-bootstrap`, scoped to voltcast's own resources.

The workflow reads the role ARNs, region, state bucket and lock table from
repository variables, and the alert address from the `ALERT_EMAIL` secret.
It refuses to run without that secret, because an empty address would
delete the alert subscription.

### By hand

Prerequisites: the shared Terraform state bucket and lock table from
`aws-oidc-bootstrap`, plus AWS credentials for the target account.

Expand All @@ -145,8 +160,8 @@ Berlin.
## Roadmap

1. **Live baseline service.** Live since 2026-10-14 in eu-central-1; the first live forecast is for 16 Oct 2026.
2. **Deploy from CI, public status page.** GitHub Actions deploys through OIDC (role
in `aws-oidc-bootstrap`). A public page shows the latest forecast against the
2. **Deploy from CI, public status page.** GitHub Actions deploys through OIDC
([Deploy](#deploy)). Still to come: a public page showing the latest forecast against the
cleared price, rolling error and model version, plus a monthly report with the real AWS bill.
3. **First learned model.** Features from ENTSO-E (load, wind and solar forecasts) and DWD
weather. Trained on SageMaker and registered in the Model Registry with
Expand Down
2 changes: 2 additions & 0 deletions infrastructure/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ variable "alert_email" {
description = "Where alarms go. Empty disables the email subscription."
type = string
default = ""
# The repo and its Actions logs are public; keep the address out of plans.
sensitive = true
}

# Promotion is a reviewed change to these two values - see docs/adr/0003.
Expand Down
Loading