Repository navigation
feat(ci): build the scratchpad image on a GitHub-hosted runner with OIDC (ENG-2000) - #526
Draft
lucas-koontz wants to merge 1 commit into
Draft
lucas-koontz wants to merge 1 commit into
lucas-koontz wants to merge 1 commit into
Conversation
The scratchpad image build runs on ubuntu-latest and pushes with a short-lived AWS role it assumes through GitHub OIDC. GitHub recommends GitHub-hosted runners for public repositories. - Pull requests name no environment, assume gha-anton-ecr-dev and push to minds-anton-scratchpad-dev. - Staging builds name the staging environment, assume gha-anton-ecr-dev and push to minds-anton-scratchpad-dev. - Main builds name the prod environment, assume gha-anton-ecr-prod and push to minds-anton-scratchpad. The gate job still skips fork pull requests. Its new target step picks the environment, role and repository for each way in, and fails on any other caller input. build-push-ecr runs with builder: local, so the image builds on the runner itself. release.yml and publish-staging.yml grant id-token: write on the calling job only. Third-party actions in the build workflow are pinned by commit. The gate tests run the target step for each way in. They also pin the build job's wiring: the hosted runner, its own id-token grant, the role step before the push, and each caller's grant. A new sweep requires the gate on every job that can mint an OIDC token in a workflow an outside account can start, and the runner sweep now covers the same workflows. The actionlint config that declared the mdb-dev and mdb-prod labels is gone, so actionlint flags any new use of them. The README and the release docs page say where each build lands. Work by Lucas Koontz for "Take the public repos off the credentialed runner group and require devops to release a privileged run". Refs: ENG-2000
This was referenced Oct 4, 2026
Draft
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User story
As an anton maintainer who merges to
stagingandmainI want the scratchpad image to build on a GitHub-hosted runner with an AWS role for its own tier
So that pull requests, staging pushes and releases keep publishing the worker image without a self-hosted runner
Why this matters
When an engineer opens a pull request or merges to
stagingormain, anton builds the scratchpad image, the sandbox pod image that scratchpad-controller runs. Until now that build ran on a self-hosted runner, although GitHub recommends GitHub-hosted runners for public repositories like this one. Now it runs onubuntu-latestand pushes with a short-lived AWS role that the job assumes through GitHub's OpenID Connect (OIDC) tokens. Pull request and staging images land in a new dev-tier repository,minds-anton-scratchpad-dev, and only a build frommainpushes tominds-anton-scratchpad.Acceptance criteria
ubuntu-latestwith no GitHub environment, assumesgha-anton-ecr-dev, and pushesdevelopment-<sha>,development-head-<head sha>,developmentandlatesttominds-anton-scratchpad-dev.stagingbuilds in thestagingenvironment withgha-anton-ecr-devand movesstaginginminds-anton-scratchpad-dev. A push tomainbuilds in theprodenvironment withgha-anton-ecr-prodand movesproductioninminds-anton-scratchpad.id-token: writeand assumes its role beforebuild-push-ecrruns withbuilder: local.release.ymlandpublish-staging.ymlgrantid-token: writeonly on the job that calls the build.Build skipped: fork pull request.gha-anton-ecr-prodfails atAssume the ECR writer role, so nothing is pushed.stagingorproductionfails the gate'stargetstep, and the build never starts.tests/test_fork_build_gate.pyunless it needs the gate and carries its condition.How to test
Start as an anton maintainer with
uv,actionlintand read access to ECR in account168681354662, after the steps thatShips withlists before this PR.uv run --group dev pytest tests/test_fork_build_gate.py tests/test_pod_image_version.py -v. Expect31 passed.actionlint .github/workflows/scratchpad-dev-build.yml .github/workflows/publish-staging.yml .github/workflows/release.yml. Expect no output and exit code 0.Scratchpad image buildrun. Expect thebuildjob onubuntu-latestwith no environment, and expectAssume the ECR writer roleto assumegha-anton-ecr-dev. Thenaws ecr describe-images --region us-east-1 --repository-name minds-anton-scratchpad-dev --image-ids imageTag=development-head-<head sha>finds the image.mindsdb/anton. Expectgateto pass,buildto be skipped, and the run summary to sayBuild skipped: fork pull request. Close that pull request.""row'sroleinscratchpad-dev-build.ymltoarn:aws:iam::168681354662:role/gha-anton-ecr-prod. ExpectAssume the ECR writer roleto fail withNot authorized to perform sts:AssumeRoleWithWebIdentity, so nothing is pushed. Close that pull request.staging. In thepublish-staging.ymlrun, expectscratchpad-image / buildto hold thestagingenvironment and assumegha-anton-ecr-dev. Thenaws ecr describe-images --region us-east-1 --repository-name minds-anton-scratchpad-dev --image-ids imageTag=stagingshows animagePushedAtafter the merge.stagingtomain, expectrelease.yml'sscratchpad-image / buildto hold theprodenvironment and assumegha-anton-ecr-prod. Thenaws ecr describe-images --region us-east-1 --repository-name minds-anton-scratchpad --image-ids imageTag=productionshows animagePushedAtafter the release. The production path runs for the first time here, so this step is the one most likely to find a problem.Notes for the reviewer
Merge order: this PR merges into
stagingafter mindsdb/terraform#239 is applied and mindsdb/github-actions#71 is onmain. The build assumesgha-anton-ecr-devorgha-anton-ecr-prodand pushes tominds-anton-scratchpad-dev, and mindsdb/terraform#239 creates all three. It also passesbuilder: local, whichbuild-push-ecrgains in mindsdb/github-actions#71. Until both land, this PR's own image build fails atAssume the ECR writer role, so the PR stays a draft. The unit tests run as usual.The operator creates this repo's
stagingandprodenvironments before the merge. GitHub creates a missing environment the first time a job names it, and that environment has no branch rule. Each environment admits one branch, with no reviewers and no admin bypass. mindsdb/terraform#239 lists the same calls, so run them once.The loop prints
"can_admins_bypass": falseand["branch_policy"]for both environments, then["staging"]forstagingand["main"]forprod. Ifcan_admins_bypassreadstrue, the PUT ignored it, so stop before the merge.Rollback: revert this commit on
staging, and onmainif it was promoted, until mindsdb/terraform#240 applies. The revert restores the previous build, which pushes every image tominds-anton-scratchpad. Revert the mindsdb/scratchpad-controller and mindsdb/argocd-envs#25 changes with it, or thestagingtag they pull stops moving. Once mindsdb/terraform#240 lets onlygha-anton-ecr-prodpush tominds-anton-scratchpad, the reverted build cannot push there, so roll forward instead.After the merge, the moving
stagingtag lives in the dev tier. Each staging push movesminds-anton-scratchpad-dev:staging, andminds-anton-scratchpad:stagingkeeps its last image. mindsdb/scratchpad-controller and mindsdb/argocd-envs#25 switch their staging references once that tag exists.latestsplits the same way: inminds-anton-scratchpad-devit is the last pull request or staging build, and inminds-anton-scratchpadit moves only onmainbuilds.One build job serves all three ways in, so a pull request passes an empty environment.
environment: ${{ needs.gate.outputs.environment }}evaluates to''on a pull request. A job whose environment expression comes out empty runs with no environment, as actions/runner#2610 shows, so its token keeps thepull_requestsubject. The empty value cannot selectstagingorprod, so if GitHub ever handled it differently, the role step would fail rather than grant more. The alternative was a second copy of the build job.The gate's
targetstep holds the whole mapping. Onecasemaps each way in to its environment, role and repository, and fails on any other input. The tests execute that shell, so a wrong row failstest_each_way_in_builds_into_its_own_tier.Deliberate omissions.
mindsdb/github-actions/setup-envandbuild-push-ecrstay on@main, as in every anton workflow, so zizmor still reports those twounpinned-usesfindings. The third-party actions in the build workflow are pinned to the commits their floating major tags point at today, so nothing changes at run time..github/actionlint.yamlis deleted. It only declared themdb-devandmdb-prodlabels, which no anton workflow uses now, so actionlint flags any new use of them. Restore the file if you would rather keep it.staging, and the production path on the promotion tomain.build-push-ecrkeeps the cache in the GitHub Actions cache, keyed by the image repository name, so it warms after one build.release.ymlandpublish-staging.ymlstay as they are.Verified locally
uv run --group dev pytest tests/test_fork_build_gate.py tests/test_pod_image_version.py -vtest_verifier_eval_gate,test_dependency_pr_scope,test_fork_build_gate,test_pod_image_version,test_image_smoke)uv run --group dev pytest tests/ -q --ignore=tests/e2e.githuband the gate testspushand one onscheduleplusworkflow_dispatch, still passstaging, inevals.yml:145andverifier-eval.yml:163stagingdrop to 16. The build workflow goes from 7 to 2, the two@mainrefs above. The other 14 sit in untouched jobs and matchstagingworkflow_graph.py --default-permissions readfrom mindsdb/github-actionsstagingline for line, including its 3 notes about events that start two workflowsgh apitag lookups, 2026-10-03actions/checkoutv4.4.0 and v4,astral-sh/setup-uvv5.4.2 and v5, andaws-actions/configure-aws-credentialsv6.3.0 and v6 each resolve to the pinned commitbuild-push-ecrat the mindsdb/github-actions#71 head<env>-<sha>,<env>andlatest, plus<env>-head-<head sha>on pull requests, which matches the READMEorigin/staginggha-anton-ecr-dev,gha-anton-ecr-prodandminds-anton-scratchpad-devdo not exist yet. mindsdb/github-actionsmainhas nobuilderinput. anton has nostagingorprodenvironmentNot run: the docs site build (
npm run buildindocs/). The docs change adds one list item with no links.Ships with
Part of ENG-2000. This PR carries no
Deploys:lines and nodeploylabel.Merge order
stagingandprodenvironments.argocd-pr-env-deployto the github-actions merge commit from step 2. The operator also creates the deployer's GitHub App and sets its client ID and private key in thestagingandprodenvironments of cowork and cowork-server.staging. mindsdb/minds_python_sdk#90, mindsdb/engine#7, mindsdb/data-vault#4 and mindsdb/hashnode-starter-kit#39 merge intomain. These four depend on no other step.stagingtag, mindsdb/scratchpad-controller#80 and mindsdb/argocd-envs#25 merge.stagingtomainin their next release.mainbuilds push through the prod writer roles.mainandstaging.Sibling PRs, in merge order:
build-push-ecrgainsbuilder: localfor GitHub-hosted builds, andargocd-pr-env-deploytakes the pull request as inputs and checks its image in the dev tier.