1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
# The web bundle: built into an image here, served by Modal from that image.
#
# Separate from `build.yml` because it is a different size of thing. That one
# is three suites and a few minutes; this is a Flutter SDK inside a docker
# build, and it only has to happen for what actually gets served — so it runs
# on the default branch and nowhere else.
#
# The split is by what each side has. rickub has a registry, and docker is
# already logged into it before the first step runs, so this builds and
# pushes. Modal has somewhere to run it, so it pulls that exact tag and
# compiles nothing. What ends up on the internet is the commit that was
# built, by construction rather than by care.
#
# See .modal/web/README.md for the two credentials this needs, and
# https://rickub.com/docs/container-registry for why there is no login step.
name: web
on:
push:
branches: [main]
workflow_dispatch:
jobs:
image:
runs-on: ubuntu-latest
# Claims the image name on the first push. Without it the push is denied
# for a name that does not exist yet, which reads as an auth failure.
permissions:
packages: write
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- uses: actions/checkout@v4
# `$RICKUB_REGISTRY_HOST` and the token behind it are injected into the
# run; docker is authenticated before this step. A `docker login` here
# would be a second, worse copy of that.
#
# Tagged by the full commit sha and not `latest`: the deploy below
# names the same string, so the thing deployed cannot drift from the
# thing built. `latest` moves too, as a convenience for a human pulling
# it by hand.
- name: Build and push
id: build
run: |
set -euo pipefail
image="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:${{ github.sha }}"
moving="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:latest"
docker build -f .modal/web/Dockerfile -t "$image" -t "$moving" .
docker push "$image"
docker push "$moving"
echo "image=$image" >> "$GITHUB_OUTPUT"
# ...and Modal serves it.
#
# `modal deploy` and not `modal run`: a run is a job that ends, and this is
# a URL that should still be there on the next push. The app is named by
# `[container] name` in `.modal/web/container.toml`, so deploying again
# replaces the running one rather than standing a second one beside it.
deploy:
runs-on: ubuntu-latest
needs: [image]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install --quiet modal
- name: Deploy
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
# What `${FRQ_WEB_IMAGE}` in container.toml expands to. The job
# above computed it; passing it forward rather than recomputing it
# keeps one place saying what the tag is.
FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }}
run: modal deploy .modal/web/container.py
|