The registry it pushes to is the one it has
The image build went to GitLab CI, which was the wrong half of the arrangement. GitLab reads source and builds nothing -- that is the point of that file -- and it has no registry here that Modal can pull from without a credential invented for the occasion. rickub is `origin`, it runs the suites, and `registry.rickub.com` is already authenticated to docker before a workflow's first step. So the jobs move to `.rickub/workflows/web.yml`, and .gitlab-ci.yml goes back to what it was. A separate workflow from `build.yml` rather than two more jobs in it: that one is three suites and a few minutes on every push, this is a Flutter SDK inside a docker build, and it only has to happen for what actually gets served. One credential still cannot come from the run. Modal pulls the private image on every cold start, and a workflow's registry token is short-lived by design, so `[container] registry_secret` names a Modal Secret holding a rickub deploy token -- pull-only by construction -- rather than anything this repo can mint. Making the image public removes it entirely, which .modal/web/README.md says out loud. A .dockerignore came with the move. `COPY . .` over a working checkout would hand the daemon 3.3GB of .toolchain that the image fetches for itself by sha256 a few lines later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
627b785 parent: 5693bd5 added
.dockerignore +24 -0 | new file mode 100644 | ||
| @@ -0,0 +1,24 @@ | ||
| 1 | +# What `docker build` should not carry to the daemon, for the one Dockerfile | |
| 2 | +# in this tree (.modal/web/Dockerfile). All of it is either generated by the | |
| 3 | +# build that is about to happen or state belonging to the machine running it, | |
| 4 | +# and .toolchain alone is 3.3GB of SDK that the image fetches for itself by | |
| 5 | +# sha256 anyway. | |
| 6 | +# | |
| 7 | +# The same list, in the same spirit, as `[build] ignore` in | |
| 8 | +# .modal/dev/container.toml -- one mechanism uploads to Modal and the other | |
| 9 | +# to a docker daemon, and neither wants a laptop's build tree. | |
| 10 | +.git | |
| 11 | +.toolchain/ | |
| 12 | +build/ | |
| 13 | +flutter/build/ | |
| 14 | +flutter/.home/ | |
| 15 | +flutter/.dart_tool/ | |
| 16 | +dart/*/.dart_tool/ | |
| 17 | +nim/nimcache/ | |
| 18 | +.cpcache/ | |
| 19 | +.clj-kondo/ | |
| 20 | +.lsp/ | |
| 21 | +result | |
| 22 | +result-* | |
| 23 | +__pycache__/ | |
| 24 | +.web-local/ | |
| new file mode 100644 | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | +# What `docker build` should not carry to the daemon, for the one Dockerfile | ||
| 2 | +# in this tree (.modal/web/Dockerfile). All of it is either generated by the | ||
| 3 | +# build that is about to happen or state belonging to the machine running it, | ||
| 4 | +# and .toolchain alone is 3.3GB of SDK that the image fetches for itself by | ||
| 5 | +# sha256 anyway. | ||
| 6 | +# | ||
| 7 | +# The same list, in the same spirit, as `[build] ignore` in | ||
| 8 | +# .modal/dev/container.toml -- one mechanism uploads to Modal and the other | ||
| 9 | +# to a docker daemon, and neither wants a laptop's build tree. | ||
| 10 | +.git | ||
| 11 | +.toolchain/ | ||
| 12 | +build/ | ||
| 13 | +flutter/build/ | ||
| 14 | +flutter/.home/ | ||
| 15 | +flutter/.dart_tool/ | ||
| 16 | +dart/*/.dart_tool/ | ||
| 17 | +nim/nimcache/ | ||
| 18 | +.cpcache/ | ||
| 19 | +.clj-kondo/ | ||
| 20 | +.lsp/ | ||
| 21 | +result | ||
| 22 | +result-* | ||
| 23 | +__pycache__/ | ||
| 24 | +.web-local/ | ||
modified
.gitlab-ci.yml +1 -61 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | # push. There used to be a second, scheduled job that re-resolved the |
| 6 | 6 | # jolt-native flake input; there is no jolt half any more and no input to |
| 7 | 7 | # follow, so there is nothing for a schedule to do. |
| 8 | -stages: [check, image, deploy] | |
| 8 | +stages: [check] | |
| 9 | 9 | |
| 10 | 10 | # Two toolchains and one artifact between them. |
| 11 | 11 | # |
| @@ -61,63 +61,3 @@ dart-test: | ||
| 61 | 61 | - apt-get update -qq && apt-get install -y -qq libssl3 |
| 62 | 62 | script: |
| 63 | 63 | - cd dart/frq_core && dart pub get && dart test -r expanded |
| 64 | - | |
| 65 | -# The web bundle, as an image, built by CI. | |
| 66 | -# | |
| 67 | -# Kaniko rather than docker: a GitLab runner has no docker daemon to lend and | |
| 68 | -# this needs no privileged mode. The context is the repo and the Dockerfile | |
| 69 | -# is `.modal/web/Dockerfile`, whose first stage is the same pinned Flutter and | |
| 70 | -# Nim that `just build web` uses -- so this is not a second way to build the | |
| 71 | -# web target, it is the first one inside an image. | |
| 72 | -# | |
| 73 | -# `--cache=true` makes the toolchain layer a pull rather than a fetch on every | |
| 74 | -# push: the apt line and the COPY above it change rarely, and the gigabyte | |
| 75 | -# behind them is the slow half. | |
| 76 | -build-web-image: | |
| 77 | - stage: image | |
| 78 | - needs: [nim-test] | |
| 79 | - image: | |
| 80 | - name: gcr.io/kaniko-project/executor:v1.23.2-debug | |
| 81 | - entrypoint: [""] | |
| 82 | - script: | |
| 83 | - - mkdir -p /kaniko/.docker | |
| 84 | - - | | |
| 85 | - cat > /kaniko/.docker/config.json <<EOF | |
| 86 | - {"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}} | |
| 87 | - EOF | |
| 88 | - - /kaniko/executor | |
| 89 | - --context "$CI_PROJECT_DIR" | |
| 90 | - --dockerfile "$CI_PROJECT_DIR/.modal/web/Dockerfile" | |
| 91 | - --destination "$CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA" | |
| 92 | - --destination "$CI_REGISTRY_IMAGE/web:latest" | |
| 93 | - --cache=true | |
| 94 | - --cache-repo "$CI_REGISTRY_IMAGE/web-cache" | |
| 95 | - rules: | |
| 96 | - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | |
| 97 | - | |
| 98 | -# ...and Modal deploys that image, without rebuilding it. | |
| 99 | -# | |
| 100 | -# `modal deploy` and not `modal run`: a run is a job that ends, and this is a | |
| 101 | -# URL that should still be there on the next push. The app is named by | |
| 102 | -# `[container] name` in `.modal/web/container.toml`, so deploying again | |
| 103 | -# replaces the running one rather than starting a second. | |
| 104 | -# | |
| 105 | -# Two things have to exist outside this file. MODAL_TOKEN_ID and | |
| 106 | -# MODAL_TOKEN_SECRET are CI variables (masked, protected); and a Modal Secret | |
| 107 | -# named `gitlab-registry` holds REGISTRY_USERNAME / REGISTRY_PASSWORD for a | |
| 108 | -# GitLab deploy token with `read_registry`, because the image above is private | |
| 109 | -# and Modal pulls it on every cold start rather than once here. | |
| 110 | -deploy-web: | |
| 111 | - stage: deploy | |
| 112 | - needs: [build-web-image] | |
| 113 | - image: python:3.13-slim | |
| 114 | - variables: | |
| 115 | - # The tag, not `latest`: `${FRQ_WEB_IMAGE}` in container.toml expands to | |
| 116 | - # this, so what is deployed is the commit that was built and no other. | |
| 117 | - FRQ_WEB_IMAGE: "$CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA" | |
| 118 | - before_script: | |
| 119 | - - pip install --quiet --no-cache-dir modal | |
| 120 | - script: | |
| 121 | - - modal deploy .modal/web/container.py | |
| 122 | - rules: | |
| 123 | - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | |
| @@ -5,7 +5,7 @@ | |||
| 5 | # push. There used to be a second, scheduled job that re-resolved the | 5 | # push. There used to be a second, scheduled job that re-resolved the |
| 6 | # jolt-native flake input; there is no jolt half any more and no input to | 6 | # jolt-native flake input; there is no jolt half any more and no input to |
| 7 | # follow, so there is nothing for a schedule to do. | 7 | # follow, so there is nothing for a schedule to do. |
| 8 | -stages: [check, image, deploy] | 8 | +stages: [check] |
| 9 | 9 | ||
| 10 | # Two toolchains and one artifact between them. | 10 | # Two toolchains and one artifact between them. |
| 11 | # | 11 | # |
| @@ -61,63 +61,3 @@ dart-test: | |||
| 61 | - apt-get update -qq && apt-get install -y -qq libssl3 | 61 | - apt-get update -qq && apt-get install -y -qq libssl3 |
| 62 | script: | 62 | script: |
| 63 | - cd dart/frq_core && dart pub get && dart test -r expanded | 63 | - cd dart/frq_core && dart pub get && dart test -r expanded |
| 64 | - | ||
| 65 | -# The web bundle, as an image, built by CI. | ||
| 66 | -# | ||
| 67 | -# Kaniko rather than docker: a GitLab runner has no docker daemon to lend and | ||
| 68 | -# this needs no privileged mode. The context is the repo and the Dockerfile | ||
| 69 | -# is `.modal/web/Dockerfile`, whose first stage is the same pinned Flutter and | ||
| 70 | -# Nim that `just build web` uses -- so this is not a second way to build the | ||
| 71 | -# web target, it is the first one inside an image. | ||
| 72 | -# | ||
| 73 | -# `--cache=true` makes the toolchain layer a pull rather than a fetch on every | ||
| 74 | -# push: the apt line and the COPY above it change rarely, and the gigabyte | ||
| 75 | -# behind them is the slow half. | ||
| 76 | -build-web-image: | ||
| 77 | - stage: image | ||
| 78 | - needs: [nim-test] | ||
| 79 | - image: | ||
| 80 | - name: gcr.io/kaniko-project/executor:v1.23.2-debug | ||
| 81 | - entrypoint: [""] | ||
| 82 | - script: | ||
| 83 | - - mkdir -p /kaniko/.docker | ||
| 84 | - - | | ||
| 85 | - cat > /kaniko/.docker/config.json <<EOF | ||
| 86 | - {"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}} | ||
| 87 | - EOF | ||
| 88 | - - /kaniko/executor | ||
| 89 | - --context "$CI_PROJECT_DIR" | ||
| 90 | - --dockerfile "$CI_PROJECT_DIR/.modal/web/Dockerfile" | ||
| 91 | - --destination "$CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA" | ||
| 92 | - --destination "$CI_REGISTRY_IMAGE/web:latest" | ||
| 93 | - --cache=true | ||
| 94 | - --cache-repo "$CI_REGISTRY_IMAGE/web-cache" | ||
| 95 | - rules: | ||
| 96 | - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | ||
| 97 | - | ||
| 98 | -# ...and Modal deploys that image, without rebuilding it. | ||
| 99 | -# | ||
| 100 | -# `modal deploy` and not `modal run`: a run is a job that ends, and this is a | ||
| 101 | -# URL that should still be there on the next push. The app is named by | ||
| 102 | -# `[container] name` in `.modal/web/container.toml`, so deploying again | ||
| 103 | -# replaces the running one rather than starting a second. | ||
| 104 | -# | ||
| 105 | -# Two things have to exist outside this file. MODAL_TOKEN_ID and | ||
| 106 | -# MODAL_TOKEN_SECRET are CI variables (masked, protected); and a Modal Secret | ||
| 107 | -# named `gitlab-registry` holds REGISTRY_USERNAME / REGISTRY_PASSWORD for a | ||
| 108 | -# GitLab deploy token with `read_registry`, because the image above is private | ||
| 109 | -# and Modal pulls it on every cold start rather than once here. | ||
| 110 | -deploy-web: | ||
| 111 | - stage: deploy | ||
| 112 | - needs: [build-web-image] | ||
| 113 | - image: python:3.13-slim | ||
| 114 | - variables: | ||
| 115 | - # The tag, not `latest`: `${FRQ_WEB_IMAGE}` in container.toml expands to | ||
| 116 | - # this, so what is deployed is the commit that was built and no other. | ||
| 117 | - FRQ_WEB_IMAGE: "$CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA" | ||
| 118 | - before_script: | ||
| 119 | - - pip install --quiet --no-cache-dir modal | ||
| 120 | - script: | ||
| 121 | - - modal deploy .modal/web/container.py | ||
| 122 | - rules: | ||
| 123 | - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | ||
modified
.modal/web/Dockerfile +3 -3 | @@ -7,9 +7,9 @@ | ||
| 7 | 7 | # on every cold start, is the small half. |
| 8 | 8 | # |
| 9 | 9 | # This is the one place in the tree where a build happens inside a Dockerfile |
| 10 | -# rather than in `just` or a Modal sandbox. It is deliberate: CI is what has | |
| 11 | -# a registry to push to, and an image is what Modal can deploy without | |
| 12 | -# rebuilding anything. The build itself is still `tools/toolchain.sh` -- the | |
| 10 | +# rather than in `just` or a Modal sandbox. It is deliberate: rickub is what | |
| 11 | +# has a registry to push to -- and docker already authenticated to it -- while | |
| 12 | +# an image is what Modal can deploy without rebuilding anything. The build itself is still `tools/toolchain.sh` -- the | |
| 13 | 13 | # same pinned Flutter and Nim as `just build web` -- so nothing about the |
| 14 | 14 | # output depends on being in a container. |
| 15 | 15 | |
| @@ -7,9 +7,9 @@ | |||
| 7 | # on every cold start, is the small half. | 7 | # on every cold start, is the small half. |
| 8 | # | 8 | # |
| 9 | # This is the one place in the tree where a build happens inside a Dockerfile | 9 | # This is the one place in the tree where a build happens inside a Dockerfile |
| 10 | -# rather than in `just` or a Modal sandbox. It is deliberate: CI is what has | 10 | +# rather than in `just` or a Modal sandbox. It is deliberate: rickub is what |
| 11 | -# a registry to push to, and an image is what Modal can deploy without | 11 | +# has a registry to push to -- and docker already authenticated to it -- while |
| 12 | -# rebuilding anything. The build itself is still `tools/toolchain.sh` -- the | 12 | +# an image is what Modal can deploy without rebuilding anything. The build itself is still `tools/toolchain.sh` -- the |
| 13 | # same pinned Flutter and Nim as `just build web` -- so nothing about the | 13 | # same pinned Flutter and Nim as `just build web` -- so nothing about the |
| 14 | # output depends on being in a container. | 14 | # output depends on being in a container. |
| 15 | 15 | ||
modified
.modal/web/README.md +20 -11 | @@ -1,26 +1,35 @@ | ||
| 1 | 1 | # `web` |
| 2 | 2 | |
| 3 | - FRQ_WEB_IMAGE=registry.gitlab.com/<ns>/frq/web:<sha> \ | |
| 3 | + FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ | |
| 4 | 4 | modal deploy .modal/web/container.py |
| 5 | 5 | |
| 6 | 6 | Defined by `container.toml`; `../_loader.py` is what reads it, and |
| 7 | 7 | its comments are the spec. |
| 8 | 8 | |
| 9 | -Unlike `dev`, this container builds nothing. CI builds the image -- | |
| 10 | -`Dockerfile` here, two stages, the second one just the bundle and a | |
| 11 | -python -- and pushes it to the GitLab registry; this deploys that | |
| 9 | +Unlike `dev`, this container builds nothing. rickub builds the image | |
| 10 | +-- `Dockerfile` here, two stages, the second one just the bundle and | |
| 11 | +a python -- and pushes it to `registry.rickub.com`; this deploys that | |
| 12 | 12 | exact tag. So the thing served is the thing that was built and |
| 13 | -tested, and a deploy is a pull rather than a compile. | |
| 13 | +tested, and a deploy is a pull rather than a compile. The workflow is | |
| 14 | +`.rickub/workflows/web.yml`. | |
| 14 | 15 | |
| 15 | 16 | `runtime = "web"`: a Function whose [run] command listens on the one |
| 16 | 17 | [network] port, fronted by a stable https URL. `modal deploy` leaves |
| 17 | 18 | it up, and deploying again replaces it in place because the app is |
| 18 | 19 | named by `[container] name`. |
| 19 | 20 | |
| 20 | -Two credentials live outside the repo: | |
| 21 | +Two credentials live outside the repo, both one-time setup: | |
| 21 | 22 | |
| 22 | -* `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`, as protected CI variables. | |
| 23 | -* A Modal Secret named `gitlab-registry`, holding `REGISTRY_USERNAME` | |
| 24 | - and `REGISTRY_PASSWORD` -- a GitLab deploy token with | |
| 25 | - `read_registry`. Modal pulls the private image on every cold start, | |
| 26 | - not once at deploy time, so this has to be Modal's to keep. | |
| 23 | +* `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`, as rickub repository | |
| 24 | + secrets. The registry needs no secret of its own -- rickub | |
| 25 | + authenticates docker before a workflow's first step. | |
| 26 | +* A Modal Secret named `rickub-registry`, holding `REGISTRY_USERNAME` | |
| 27 | + and `REGISTRY_PASSWORD` for a rickub deploy token (Settings → | |
| 28 | + Packages; pull-only, and the username is any label). Modal pulls the | |
| 29 | + private image on every cold start rather than once at deploy time, | |
| 30 | + so this has to be Modal's to keep and cannot be the run's own | |
| 31 | + short-lived registry token. | |
| 32 | + | |
| 33 | +Making the image public instead -- its detail page, Manage, | |
| 34 | +visibility -- removes the need for that second one entirely, at the | |
| 35 | +price of anyone being able to pull the bundle. | |
| @@ -1,26 +1,35 @@ | |||
| 1 | # `web` | 1 | # `web` |
| 2 | 2 | ||
| 3 | - FRQ_WEB_IMAGE=registry.gitlab.com/<ns>/frq/web:<sha> \ | 3 | + FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> \ |
| 4 | modal deploy .modal/web/container.py | 4 | modal deploy .modal/web/container.py |
| 5 | 5 | ||
| 6 | Defined by `container.toml`; `../_loader.py` is what reads it, and | 6 | Defined by `container.toml`; `../_loader.py` is what reads it, and |
| 7 | its comments are the spec. | 7 | its comments are the spec. |
| 8 | 8 | ||
| 9 | -Unlike `dev`, this container builds nothing. CI builds the image -- | 9 | +Unlike `dev`, this container builds nothing. rickub builds the image |
| 10 | -`Dockerfile` here, two stages, the second one just the bundle and a | 10 | +-- `Dockerfile` here, two stages, the second one just the bundle and |
| 11 | -python -- and pushes it to the GitLab registry; this deploys that | 11 | +a python -- and pushes it to `registry.rickub.com`; this deploys that |
| 12 | exact tag. So the thing served is the thing that was built and | 12 | exact tag. So the thing served is the thing that was built and |
| 13 | -tested, and a deploy is a pull rather than a compile. | 13 | +tested, and a deploy is a pull rather than a compile. The workflow is |
| 14 | +`.rickub/workflows/web.yml`. | ||
| 14 | 15 | ||
| 15 | `runtime = "web"`: a Function whose [run] command listens on the one | 16 | `runtime = "web"`: a Function whose [run] command listens on the one |
| 16 | [network] port, fronted by a stable https URL. `modal deploy` leaves | 17 | [network] port, fronted by a stable https URL. `modal deploy` leaves |
| 17 | it up, and deploying again replaces it in place because the app is | 18 | it up, and deploying again replaces it in place because the app is |
| 18 | named by `[container] name`. | 19 | named by `[container] name`. |
| 19 | 20 | ||
| 20 | -Two credentials live outside the repo: | 21 | +Two credentials live outside the repo, both one-time setup: |
| 21 | 22 | ||
| 22 | -* `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`, as protected CI variables. | 23 | +* `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`, as rickub repository |
| 23 | -* A Modal Secret named `gitlab-registry`, holding `REGISTRY_USERNAME` | 24 | + secrets. The registry needs no secret of its own -- rickub |
| 24 | - and `REGISTRY_PASSWORD` -- a GitLab deploy token with | 25 | + authenticates docker before a workflow's first step. |
| 25 | - `read_registry`. Modal pulls the private image on every cold start, | 26 | +* A Modal Secret named `rickub-registry`, holding `REGISTRY_USERNAME` |
| 26 | - not once at deploy time, so this has to be Modal's to keep. | 27 | + and `REGISTRY_PASSWORD` for a rickub deploy token (Settings → |
| 28 | + Packages; pull-only, and the username is any label). Modal pulls the | ||
| 29 | + private image on every cold start rather than once at deploy time, | ||
| 30 | + so this has to be Modal's to keep and cannot be the run's own | ||
| 31 | + short-lived registry token. | ||
| 32 | + | ||
| 33 | +Making the image public instead -- its detail page, Manage, | ||
| 34 | +visibility -- removes the need for that second one entirely, at the | ||
| 35 | +price of anyone being able to pull the bundle. | ||
modified
.modal/web/container.toml +8 -4 | @@ -6,10 +6,14 @@ description = "the web bundle CI built, served at a URL" | ||
| 6 | 6 | # spec is read, so the tag is whatever commit the deploy job is deploying. |
| 7 | 7 | # Unset, it fails loudly rather than deploying something stale. |
| 8 | 8 | registry = "${FRQ_WEB_IMAGE}" |
| 9 | -# The GitLab container registry is private, so Modal needs credentials to | |
| 10 | -# pull. A Modal Secret of this name holding REGISTRY_USERNAME and | |
| 11 | -# REGISTRY_PASSWORD -- a GitLab deploy token with `read_registry` is enough. | |
| 12 | -registry_secret = "gitlab-registry" | |
| 9 | +# Images on registry.rickub.com are private by default, so Modal needs | |
| 10 | +# credentials to pull -- and it pulls on every cold start, not once at deploy | |
| 11 | +# time, so the CI job's own registry token is no use: it is short-lived by | |
| 12 | +# design. A Modal Secret of this name holds REGISTRY_USERNAME and | |
| 13 | +# REGISTRY_PASSWORD for a rickub *deploy* token, which is pull-only by | |
| 14 | +# construction. The username is a label of your choosing; only the token is | |
| 15 | +# checked. | |
| 16 | +registry_secret = "rickub-registry" | |
| 13 | 17 | # Not a Sandbox: a Sandbox is a run that ends, and this is meant to stay up |
| 14 | 18 | # between pushes. `web` is a Function that Modal fronts with a stable https |
| 15 | 19 | # URL and `modal deploy` leaves running. |
| @@ -6,10 +6,14 @@ description = "the web bundle CI built, served at a URL" | |||
| 6 | # spec is read, so the tag is whatever commit the deploy job is deploying. | 6 | # spec is read, so the tag is whatever commit the deploy job is deploying. |
| 7 | # Unset, it fails loudly rather than deploying something stale. | 7 | # Unset, it fails loudly rather than deploying something stale. |
| 8 | registry = "${FRQ_WEB_IMAGE}" | 8 | registry = "${FRQ_WEB_IMAGE}" |
| 9 | -# The GitLab container registry is private, so Modal needs credentials to | 9 | +# Images on registry.rickub.com are private by default, so Modal needs |
| 10 | -# pull. A Modal Secret of this name holding REGISTRY_USERNAME and | 10 | +# credentials to pull -- and it pulls on every cold start, not once at deploy |
| 11 | -# REGISTRY_PASSWORD -- a GitLab deploy token with `read_registry` is enough. | 11 | +# time, so the CI job's own registry token is no use: it is short-lived by |
| 12 | -registry_secret = "gitlab-registry" | 12 | +# design. A Modal Secret of this name holds REGISTRY_USERNAME and |
| 13 | +# REGISTRY_PASSWORD for a rickub *deploy* token, which is pull-only by | ||
| 14 | +# construction. The username is a label of your choosing; only the token is | ||
| 15 | +# checked. | ||
| 16 | +registry_secret = "rickub-registry" | ||
| 13 | # Not a Sandbox: a Sandbox is a run that ends, and this is meant to stay up | 17 | # Not a Sandbox: a Sandbox is a run that ends, and this is meant to stay up |
| 14 | # between pushes. `web` is a Function that Modal fronts with a stable https | 18 | # between pushes. `web` is a Function that Modal fronts with a stable https |
| 15 | # URL and `modal deploy` leaves running. | 19 | # URL and `modal deploy` leaves running. |
modified
.rickub/workflows/build.yml +6 -4 | @@ -2,12 +2,14 @@ | ||
| 2 | 2 | # deliberately builds nothing. This is the other half: three suites, each one |
| 3 | 3 | # checking a different seam. |
| 4 | 4 | # |
| 5 | -# Nothing here is handed to Modal. The header used to say this job passed the | |
| 6 | -# work to a Sandbox against the `devshell` volume — that was the web bundle, | |
| 7 | -# and both the web target and the container that built it are gone. What runs | |
| 8 | -# now is small enough for a runner: Nim in about a second, Dart in less, and | |
| 5 | +# Nothing here is handed to Modal, and nothing here is built for keeps. What | |
| 6 | +# runs is small enough for a runner: Nim in about a second, Dart in less, and | |
| 9 | 7 | # Flutter for as long as a Flutter toolchain takes to arrive. |
| 10 | 8 | # |
| 9 | +# The web bundle is next door in `web.yml`, because it is the other size of | |
| 10 | +# thing — a Flutter SDK inside a docker build, pushed to the registry and | |
| 11 | +# deployed to Modal, and only for what actually gets served. | |
| 12 | +# | |
| 11 | 13 | # Every toolchain is a sha256-pinned tarball rather than an apt package or a |
| 12 | 14 | # third-party setup action, which is this repo's habit — see |
| 13 | 15 | # `tools/toolchain.sh`, which fetches the same two by the same hashes. |
| @@ -2,12 +2,14 @@ | |||
| 2 | # deliberately builds nothing. This is the other half: three suites, each one | 2 | # deliberately builds nothing. This is the other half: three suites, each one |
| 3 | # checking a different seam. | 3 | # checking a different seam. |
| 4 | # | 4 | # |
| 5 | -# Nothing here is handed to Modal. The header used to say this job passed the | 5 | +# Nothing here is handed to Modal, and nothing here is built for keeps. What |
| 6 | -# work to a Sandbox against the `devshell` volume — that was the web bundle, | 6 | +# runs is small enough for a runner: Nim in about a second, Dart in less, and |
| 7 | -# and both the web target and the container that built it are gone. What runs | ||
| 8 | -# now is small enough for a runner: Nim in about a second, Dart in less, and | ||
| 9 | # Flutter for as long as a Flutter toolchain takes to arrive. | 7 | # Flutter for as long as a Flutter toolchain takes to arrive. |
| 10 | # | 8 | # |
| 9 | +# The web bundle is next door in `web.yml`, because it is the other size of | ||
| 10 | +# thing — a Flutter SDK inside a docker build, pushed to the registry and | ||
| 11 | +# deployed to Modal, and only for what actually gets served. | ||
| 12 | +# | ||
| 11 | # Every toolchain is a sha256-pinned tarball rather than an apt package or a | 13 | # Every toolchain is a sha256-pinned tarball rather than an apt package or a |
| 12 | # third-party setup action, which is this repo's habit — see | 14 | # third-party setup action, which is this repo's habit — see |
| 13 | # `tools/toolchain.sh`, which fetches the same two by the same hashes. | 15 | # `tools/toolchain.sh`, which fetches the same two by the same hashes. |
added
.rickub/workflows/web.yml +78 -0 | new file mode 100644 | ||
| @@ -0,0 +1,78 @@ | ||
| 1 | +# The web bundle: built into an image here, served by Modal from that image. | |
| 2 | +# | |
| 3 | +# Separate from `build.yml` because it is a different size of thing. That one | |
| 4 | +# is three suites and a few minutes; this is a Flutter SDK inside a docker | |
| 5 | +# build, and it only has to happen for what actually gets served — so it runs | |
| 6 | +# on the default branch and nowhere else. | |
| 7 | +# | |
| 8 | +# The split is by what each side has. rickub has a registry, and docker is | |
| 9 | +# already logged into it before the first step runs, so this builds and | |
| 10 | +# pushes. Modal has somewhere to run it, so it pulls that exact tag and | |
| 11 | +# compiles nothing. What ends up on the internet is the commit that was | |
| 12 | +# built, by construction rather than by care. | |
| 13 | +# | |
| 14 | +# See .modal/web/README.md for the two credentials this needs, and | |
| 15 | +# https://rickub.com/docs/container-registry for why there is no login step. | |
| 16 | + | |
| 17 | +name: web | |
| 18 | + | |
| 19 | +on: | |
| 20 | + push: | |
| 21 | + branches: [main] | |
| 22 | + workflow_dispatch: | |
| 23 | + | |
| 24 | +jobs: | |
| 25 | + image: | |
| 26 | + runs-on: ubuntu-latest | |
| 27 | + # Claims the image name on the first push. Without it the push is denied | |
| 28 | + # for a name that does not exist yet, which reads as an auth failure. | |
| 29 | + permissions: | |
| 30 | + packages: write | |
| 31 | + outputs: | |
| 32 | + image: ${{ steps.build.outputs.image }} | |
| 33 | + steps: | |
| 34 | + - uses: actions/checkout@v4 | |
| 35 | + | |
| 36 | + # `$RICKUB_REGISTRY_HOST` and the token behind it are injected into the | |
| 37 | + # run; docker is authenticated before this step. A `docker login` here | |
| 38 | + # would be a second, worse copy of that. | |
| 39 | + # | |
| 40 | + # Tagged by the full commit sha and not `latest`: the deploy below | |
| 41 | + # names the same string, so the thing deployed cannot drift from the | |
| 42 | + # thing built. `latest` moves too, as a convenience for a human pulling | |
| 43 | + # it by hand. | |
| 44 | + - name: Build and push | |
| 45 | + id: build | |
| 46 | + run: | | |
| 47 | + set -euo pipefail | |
| 48 | + image="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:${{ github.sha }}" | |
| 49 | + moving="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:latest" | |
| 50 | + docker build -f .modal/web/Dockerfile -t "$image" -t "$moving" . | |
| 51 | + docker push "$image" | |
| 52 | + docker push "$moving" | |
| 53 | + echo "image=$image" >> "$GITHUB_OUTPUT" | |
| 54 | + | |
| 55 | + # ...and Modal serves it. | |
| 56 | + # | |
| 57 | + # `modal deploy` and not `modal run`: a run is a job that ends, and this is | |
| 58 | + # a URL that should still be there on the next push. The app is named by | |
| 59 | + # `[container] name` in `.modal/web/container.toml`, so deploying again | |
| 60 | + # replaces the running one rather than standing a second one beside it. | |
| 61 | + deploy: | |
| 62 | + runs-on: ubuntu-latest | |
| 63 | + needs: [image] | |
| 64 | + steps: | |
| 65 | + - uses: actions/checkout@v4 | |
| 66 | + - uses: actions/setup-python@v5 | |
| 67 | + with: | |
| 68 | + python-version: "3.13" | |
| 69 | + - run: pip install --quiet modal | |
| 70 | + - name: Deploy | |
| 71 | + env: | |
| 72 | + MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} | |
| 73 | + MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} | |
| 74 | + # What `${FRQ_WEB_IMAGE}` in container.toml expands to. The job | |
| 75 | + # above computed it; passing it forward rather than recomputing it | |
| 76 | + # keeps one place saying what the tag is. | |
| 77 | + FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }} | |
| 78 | + run: modal deploy .modal/web/container.py | |
| new file mode 100644 | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | +# The web bundle: built into an image here, served by Modal from that image. | ||
| 2 | +# | ||
| 3 | +# Separate from `build.yml` because it is a different size of thing. That one | ||
| 4 | +# is three suites and a few minutes; this is a Flutter SDK inside a docker | ||
| 5 | +# build, and it only has to happen for what actually gets served — so it runs | ||
| 6 | +# on the default branch and nowhere else. | ||
| 7 | +# | ||
| 8 | +# The split is by what each side has. rickub has a registry, and docker is | ||
| 9 | +# already logged into it before the first step runs, so this builds and | ||
| 10 | +# pushes. Modal has somewhere to run it, so it pulls that exact tag and | ||
| 11 | +# compiles nothing. What ends up on the internet is the commit that was | ||
| 12 | +# built, by construction rather than by care. | ||
| 13 | +# | ||
| 14 | +# See .modal/web/README.md for the two credentials this needs, and | ||
| 15 | +# https://rickub.com/docs/container-registry for why there is no login step. | ||
| 16 | + | ||
| 17 | +name: web | ||
| 18 | + | ||
| 19 | +on: | ||
| 20 | + push: | ||
| 21 | + branches: [main] | ||
| 22 | + workflow_dispatch: | ||
| 23 | + | ||
| 24 | +jobs: | ||
| 25 | + image: | ||
| 26 | + runs-on: ubuntu-latest | ||
| 27 | + # Claims the image name on the first push. Without it the push is denied | ||
| 28 | + # for a name that does not exist yet, which reads as an auth failure. | ||
| 29 | + permissions: | ||
| 30 | + packages: write | ||
| 31 | + outputs: | ||
| 32 | + image: ${{ steps.build.outputs.image }} | ||
| 33 | + steps: | ||
| 34 | + - uses: actions/checkout@v4 | ||
| 35 | + | ||
| 36 | + # `$RICKUB_REGISTRY_HOST` and the token behind it are injected into the | ||
| 37 | + # run; docker is authenticated before this step. A `docker login` here | ||
| 38 | + # would be a second, worse copy of that. | ||
| 39 | + # | ||
| 40 | + # Tagged by the full commit sha and not `latest`: the deploy below | ||
| 41 | + # names the same string, so the thing deployed cannot drift from the | ||
| 42 | + # thing built. `latest` moves too, as a convenience for a human pulling | ||
| 43 | + # it by hand. | ||
| 44 | + - name: Build and push | ||
| 45 | + id: build | ||
| 46 | + run: | | ||
| 47 | + set -euo pipefail | ||
| 48 | + image="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:${{ github.sha }}" | ||
| 49 | + moving="$RICKUB_REGISTRY_HOST/${{ github.repository }}-web:latest" | ||
| 50 | + docker build -f .modal/web/Dockerfile -t "$image" -t "$moving" . | ||
| 51 | + docker push "$image" | ||
| 52 | + docker push "$moving" | ||
| 53 | + echo "image=$image" >> "$GITHUB_OUTPUT" | ||
| 54 | + | ||
| 55 | + # ...and Modal serves it. | ||
| 56 | + # | ||
| 57 | + # `modal deploy` and not `modal run`: a run is a job that ends, and this is | ||
| 58 | + # a URL that should still be there on the next push. The app is named by | ||
| 59 | + # `[container] name` in `.modal/web/container.toml`, so deploying again | ||
| 60 | + # replaces the running one rather than standing a second one beside it. | ||
| 61 | + deploy: | ||
| 62 | + runs-on: ubuntu-latest | ||
| 63 | + needs: [image] | ||
| 64 | + steps: | ||
| 65 | + - uses: actions/checkout@v4 | ||
| 66 | + - uses: actions/setup-python@v5 | ||
| 67 | + with: | ||
| 68 | + python-version: "3.13" | ||
| 69 | + - run: pip install --quiet modal | ||
| 70 | + - name: Deploy | ||
| 71 | + env: | ||
| 72 | + MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} | ||
| 73 | + MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} | ||
| 74 | + # What `${FRQ_WEB_IMAGE}` in container.toml expands to. The job | ||
| 75 | + # above computed it; passing it forward rather than recomputing it | ||
| 76 | + # keeps one place saying what the tag is. | ||
| 77 | + FRQ_WEB_IMAGE: ${{ needs.image.outputs.image }} | ||
| 78 | + run: modal deploy .modal/web/container.py | ||
modified
CLAUDE.md +4 -3 | @@ -29,9 +29,10 @@ nothing until the Sandbox starts — the image build streams to the client and | ||
| 29 | 29 | nowhere else. |
| 30 | 30 | |
| 31 | 31 | Two containers, and they are not the same kind of thing. `dev` is a build |
| 32 | -that ends. `web` is a deploy: CI builds `.modal/web/Dockerfile` into the | |
| 33 | -GitLab registry, and `modal deploy .modal/web/container.py` serves that exact | |
| 34 | -tag at a URL, building nothing. So `just build web` on a laptop and the thing | |
| 32 | +that ends. `web` is a deploy: rickub builds `.modal/web/Dockerfile` into | |
| 33 | +`registry.rickub.com` (`.rickub/workflows/web.yml`), and | |
| 34 | +`modal deploy .modal/web/container.py` serves that exact tag at a URL, | |
| 35 | +building nothing. So `just build web` on a laptop and the thing | |
| 35 | 36 | on the internet come from the same two commands, run in different places — |
| 36 | 37 | and a deploy is a pull rather than a compile. |
| 37 | 38 | |
| @@ -29,9 +29,10 @@ nothing until the Sandbox starts — the image build streams to the client and | |||
| 29 | nowhere else. | 29 | nowhere else. |
| 30 | 30 | ||
| 31 | Two containers, and they are not the same kind of thing. `dev` is a build | 31 | Two containers, and they are not the same kind of thing. `dev` is a build |
| 32 | -that ends. `web` is a deploy: CI builds `.modal/web/Dockerfile` into the | 32 | +that ends. `web` is a deploy: rickub builds `.modal/web/Dockerfile` into |
| 33 | -GitLab registry, and `modal deploy .modal/web/container.py` serves that exact | 33 | +`registry.rickub.com` (`.rickub/workflows/web.yml`), and |
| 34 | -tag at a URL, building nothing. So `just build web` on a laptop and the thing | 34 | +`modal deploy .modal/web/container.py` serves that exact tag at a URL, |
| 35 | +building nothing. So `just build web` on a laptop and the thing | ||
| 35 | on the internet come from the same two commands, run in different places — | 36 | on the internet come from the same two commands, run in different places — |
| 36 | and a deploy is a pull rather than a compile. | 37 | and a deploy is a pull rather than a compile. |
| 37 | 38 | ||
modified
justfile +5 -5 | @@ -140,13 +140,13 @@ modal container="dev" *args: | ||
| 140 | 140 | |
| 141 | 141 | # Deploy, rather than run: a URL that stays up between pushes. |
| 142 | 142 | # |
| 143 | -# Nothing is built here. `.modal/web/` points at an image CI already made and | |
| 144 | -# pushed, and FRQ_WEB_IMAGE is which tag of it — so this is the same command | |
| 145 | -# the `deploy-web` job runs, with the tag named by hand instead of by the | |
| 146 | -# commit. Normally you want the job; this is for deploying an older tag, or a | |
| 143 | +# Nothing is built here. `.modal/web/` points at an image rickub already made | |
| 144 | +# and pushed, and FRQ_WEB_IMAGE is which tag of it — so this is the same | |
| 145 | +# command the `web` workflow's deploy job runs, with the tag named by hand | |
| 146 | +# instead of by the commit. Normally you want the job; this is for deploying an older tag, or a | |
| 147 | 147 | # first deploy before CI has one. |
| 148 | 148 | # |
| 149 | -# FRQ_WEB_IMAGE=registry.gitlab.com/<ns>/frq/web:<sha> just deploy web | |
| 149 | +# FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> just deploy web | |
| 150 | 150 | [doc('deploy a .modal/ container as a URL (needs FRQ_WEB_IMAGE)')] |
| 151 | 151 | deploy container="web": |
| 152 | 152 | #!/usr/bin/env bash |
| @@ -140,13 +140,13 @@ modal container="dev" *args: | |||
| 140 | 140 | ||
| 141 | # Deploy, rather than run: a URL that stays up between pushes. | 141 | # Deploy, rather than run: a URL that stays up between pushes. |
| 142 | # | 142 | # |
| 143 | -# Nothing is built here. `.modal/web/` points at an image CI already made and | 143 | +# Nothing is built here. `.modal/web/` points at an image rickub already made |
| 144 | -# pushed, and FRQ_WEB_IMAGE is which tag of it — so this is the same command | 144 | +# and pushed, and FRQ_WEB_IMAGE is which tag of it — so this is the same |
| 145 | -# the `deploy-web` job runs, with the tag named by hand instead of by the | 145 | +# command the `web` workflow's deploy job runs, with the tag named by hand |
| 146 | -# commit. Normally you want the job; this is for deploying an older tag, or a | 146 | +# instead of by the commit. Normally you want the job; this is for deploying an older tag, or a |
| 147 | # first deploy before CI has one. | 147 | # first deploy before CI has one. |
| 148 | # | 148 | # |
| 149 | -# FRQ_WEB_IMAGE=registry.gitlab.com/<ns>/frq/web:<sha> just deploy web | 149 | +# FRQ_WEB_IMAGE=registry.rickub.com/nandi/frq-web:<sha> just deploy web |
| 150 | [doc('deploy a .modal/ container as a URL (needs FRQ_WEB_IMAGE)')] | 150 | [doc('deploy a .modal/ container as a URL (needs FRQ_WEB_IMAGE)')] |
| 151 | deploy container="web": | 151 | deploy container="web": |
| 152 | #!/usr/bin/env bash | 152 | #!/usr/bin/env bash |