Skip to content

feat(actions): add a hosted build mode, PR-env inputs and commit pins (ENG-2000) - #71

Open
lucas-koontz wants to merge 1 commit into
mainfrom
feat/eng-2000-hosted-build-mode
Open

lucas-koontz wants to merge 1 commit into
mainfrom
feat/eng-2000-hosted-build-mode

Conversation

@lucas-koontz

@lucas-koontz lucas-koontz commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

User story

As a maintainer of a public repository that builds and deploys with these actions
I want build-push-ecr to build on a GitHub-hosted runner, and argocd-pr-env-deploy to deploy a PR that another workflow names
So that my repository's builds run on GitHub-hosted runners and its PR environments keep deploying

Why this matters

Today a maintainer who builds a public repository's image with build-push-ecr has to run that job on mdb-dev, because the action builds on the in-cluster BuildKit service with the runner pod's own AWS role. GitHub recommends GitHub-hosted runners for public repositories, because anyone can open a pull request there. With builder: local, the same action builds on a GitHub-hosted runner with a role the job assumes through GitHub OIDC, and private callers keep today's commands. argocd-pr-env-deploy used to work only inside a pull_request run, so it now takes the PR as inputs, and a scheduled workflow in another repository can deploy PR environments.

Acceptance criteria

  • A caller that passes no builder runs the same aws and docker commands as before, argument for argument, and gets the same image output.
  • With builder: local, the action makes no aws call and creates no remote builder. Its build adds only --cache-from type=gha,scope=<module-name> --cache-to type=gha,scope=<module-name>,mode=max, on a BuildKit container started from moby/buildkit:v0.33.1 by its index digest.
  • Negative: any other builder value, the empty string included, fails the build step before any aws or docker call.
  • Every third-party action in build-push-ecr, snyk-docker-scan, setup-env, helm-deploy and argocd-pr-env-deploy is pinned to a full commit SHA with a # vX.Y.Z comment, and snyk-docker-scan installs Snyk CLI v1.1307.4.
  • A pull_request run of argocd-pr-env-deploy that passes none of the new inputs deploys development-<merge SHA>, as before. A schedule or workflow_dispatch run that passes repository, pr-number and head-sha deploys development-head-<head-sha> for that PR.
  • When mindsdb-<repo>-dev exists, the action checks that repo's PR image tags there and only there. Otherwise it checks mindsdb-<repo>. Negative: a tag that exists only in the other repository fails the step.
  • Negative: a repository that is not owner/name, a pr-number that is not digits, or a head-sha that is not 40 lowercase hex characters stops the step before any GitHub API, ECR or ArgoCD call. The log shows the rule, never the value.
  • The prune job in .github/workflows/stale-deploy-label.yml runs on ubuntu-latest.

How to test

Start on a Linux machine with bash 5, Python 3.12, uv and jq, in a checkout of this branch.

  1. Run uv run --with pyyaml --with pytest --python 3.12 -m pytest tests/ -q, the command CI runs. Expect 276 passed. On macOS, whose bash 3.2 cannot parse the argocd-pr-env-deploy script, expect 258 passed, 18 skipped.
  2. Run actionlint -color at the repository root. Expect no output and exit code 0.
  3. Check that the tests depend on the change. Run git checkout origin/main -- build-push-ecr/action.yml, then the pytest command from step 1 on tests/test_build_push_ecr.py. Expect 8 failures. Restore the file with git checkout HEAD -- build-push-ecr/action.yml.
  4. After the merge, open the next build of a private repository that calls build-push-ecr without builder. Expect a green run whose build step log still shows the aws ecr create-repository attempt and the remote-buildkit-agent builder.
  5. After mindsdb/deployer pins this PR's merge commit, dispatch its pr-envs.yml while a cowork PR labelled deploy has its head image in mindsdb-cowork-dev. Expect the argocd-pr-env-deploy step to log argocd app set pr-cowork-<PR number> with tags.cowork=development-head-<head SHA>.
  6. After mindsdb/terraform PR A and this PR are in, open the build job of the mindsdb/cowork sibling PR. Expect it to run on ubuntu-latest, run the Expose the GitHub Actions cache to docker buildx step, and push development-head-<head SHA> to mindsdb-cowork-dev. Re-run the job, and expect the build log to mark unchanged layers CACHED. Local mode has not run on a GitHub-hosted runner before, so this is the step most likely to find a problem.

Notes for the reviewer

Merge order: mindsdb/terraform PR A, then this PR, then the mindsdb/deployer re-pin. mindsdb/anton, mindsdb/cowork and mindsdb/cowork-server call build-push-ecr@main with builder: local. Their builds need this PR on main, plus the roles and dev-tier repositories that PR A creates. mindsdb/deployer pins argocd-pr-env-deploy to 331c0be, which has none of the new inputs, so it re-pins to this PR's merge commit before it merges.

Once this PR is on main and a -dev repository exists, PR environments look for that repo's images there only. A cowork or cowork-server PR whose image was built into mindsdb-cowork or mindsdb-cowork-server fails the check until it builds again into the dev tier. The action never falls back to the other repository.

Operator steps this PR needs.

  • Right after the merge, read the commit the deployer pins: gh api repos/mindsdb/github-actions/commits/main --jq .sha.
  • To bump BuildKit, follow the README's "Building an image into ECR" section. Take the version from gh api repos/moby/buildkit/releases/latest --jq .tag_name and its digest from docker buildx imagetools inspect moby/buildkit:<version>, then change both together in build-push-ecr/action.yml. Nothing bumps that pin automatically, and the same holds for snyk-version in snyk-docker-scan.

Rollback: revert the merge commit on main. Private callers see no difference, because they run the default remote path either way. Once anton, cowork or cowork-server build with builder: local, revert their build changes first. The reverted action has no builder input, so it would run the remote path on a GitHub-hosted runner, which cannot reach the in-cluster BuildKit service. The deployer keeps working, because it pins a commit.

The pins change nothing that runs today. Each pinned SHA is the commit that both the floating tag and the version tag point at today. snyk/actions/setup was on master, which has no tag, so it moves to v1.0.0, whose setup/ tree is identical to master's. Snyk CLI v1.1307.4 is the current release. BuildKit v0.33.1 has the index digest that buildx's default buildx-stable-1 tag points at today, and remote mode keeps that default.

Local mode exports the cache service's token to the rest of the caller job. docker buildx reads the cache URL and token from ACTIONS_* variables, which the runner gives to actions but not to run: steps. crazy-max/ghaction-github-runtime exports them. The token prints as ***, because the runner's Worker.cs masks every endpoint credential. In a job that grants id-token: write, ScriptHandler.cs already gives every run: step that same token as ACTIONS_ID_TOKEN_REQUEST_TOKEN. Local mode requires that grant, so it exposes nothing new.

stale-deploy-label.yml moves every caller's prune job to GitHub-hosted runners. The job needs only gh and the caller's token. Private callers run it there too.

Deliberate omissions.

  • zizmor still reports 12 template-injection findings in these composites: 7 in build-push-ecr, 3 in snyk-docker-scan and 2 in setup-env. Each is on a ${{ }} expression that was already in its run: block. Moving extra-build-args into an environment variable changes how bash parses quotes in it, and remote mode had to stay identical for every private caller.
  • The cache flags have no ignore-error=true, so a cache-service error fails a local build instead of skipping the cache write.
  • Pins cover the composites that the build, scan and PR-environment jobs call. The other composites keep their current refs.
  • The argocd-pr-env-deploy tests hand GitHub API responses to the curl stub as raw dicts, because the stub serves them verbatim as JSON. A typed model would only be serialized back.
  • tests/test_build_push_ecr.py repeats the step() helper from tests/test_notify_pipeline_status.py. A shared helper can wait for a third copy.

Verified locally

Check Result
pytest tests/ -q on Ubuntu 24.04 x86_64 (bash 5.2.21, jq 1.7, Python 3.12.3) 276 passed
Same suite on Debian bookworm arm64 (bash 5.2.15, jq 1.6, Python 3.12.12) 276 passed
Same suite on macOS (bash 3.2.57) 258 passed, 18 skipped. The skipped tests run the argocd-pr-env-deploy script, which needs bash 4
Each changed action replaced by its main version, then its tests run build-push-ecr: 8 of 10 fail. argocd-pr-env-deploy: 21 of 22 fail. The pin tests fail for snyk-docker-scan (2) and setup-env (1). Dropping --max-items 1 alone fails 5 tests
41 deliberate breakages of the new behavior, from earlier runs on the same code All 41 caught
Remote mode, main against this branch, for all 29 build-push-ecr calls in 23 workflow files across 15 repositories that code search found, for development, staging and production, with and without a PR head SHA 174 runs, 0 differences in any aws or docker argv or in GITHUB_OUTPUT. The one added step runs only when builder == 'local'
actionlint 1.7.12 Clean
scripts/workflow_graph.py --allow-external-reusables All 14 workflows compose
zizmor 1.28.0 offline on the changed composites and .github/workflows, main against this branch 44 findings drop to 38. The 6 unpinned-uses findings are gone, and nothing is new. Both sides splice the same 37 ${{ }} expressions into run: blocks
zizmor 1.28.0 online audits, from an earlier run on these pins No impostor-commit, ref-version-mismatch or known-vulnerable-actions findings
gh api on each pinned action, 2026-10-03 Every pinned SHA equals both its floating tag and its version tag. snyk/actions has the same setup/ tree at master and v1.0.0
docker buildx imagetools inspect, 2026-10-03 moby/buildkit:v0.33.1 has the pinned index digest, and buildx-stable-1 points at the same digest. v0.33.1 is the latest BuildKit release
Snyk CLI release v1.1307.4 is the latest release (2026-09-23). An earlier check found its sha256 equal to the latest download on both Snyk hosts
aws ecr describe-images on an existing repository, read-only, from an earlier run With --max-items 1: one image and exit 0. Without it, one page holds 100 images, and the CLI fetches every page
actions/runner source, main NodeScriptActionHandler.cs gives actions ACTIONS_RUNTIME_TOKEN, ScriptHandler.cs gives run: steps the same token when the job can mint OIDC tokens, and Worker.cs masks it
Pre-PR sweep against origin/main No whitespace errors, ticket ids in added comments or debug leftovers

Ships with

Part of ENG-2000. This PR changes shared CI actions, so it carries no Deploys: lines and no deploy label.

Merge order

  1. mindsdb/terraform#239 merges, and the operator applies it: the GitHub OIDC providers, the image build and installer upload roles, the dev-tier image repositories and the deployment environment settings.
  2. This PR merges.
  3. The operator creates mindsdb/deployer, a new private repository, and the deployer PR opens then. That PR merges once it re-pins argocd-pr-env-deploy to this PR's merge commit. The operator also creates the deployer's GitHub App and sets its client ID and private key in the staging and prod environments of cowork and cowork-server.
  4. mindsdb/anton#526, mindsdb/cowork#1117 and mindsdb/cowork-server#621 merge into staging. mindsdb/minds_python_sdk#90, mindsdb/engine#7, mindsdb/data-vault#4 and mindsdb/hashnode-starter-kit#39 merge into main. These four depend on no other step.
  5. Once each dev-tier image repository holds its staging tag, mindsdb/scratchpad-controller#80 and mindsdb/argocd-envs#25 merge.
  6. anton, cowork and cowork-server each promote staging to main in their next release.
  7. mindsdb/terraform#240 applies once the main builds push through the prod writer roles.
  8. mindsdb/Kubernetes-Foundational-Services#166 merges, and the operator upgrades it on both clusters, once cowork-server's change is on main and staging.
  9. The operator finishes with one step outside these repositories.

Sibling PRs, in merge order:

  • mindsdb/terraform#239: creates the GitHub OIDC providers, the image build and installer upload roles and the dev-tier image repositories, and sets up the deployment environments.
  • mindsdb/deployer, a new private repository whose PR opens in step 3: rolls cowork and cowork-server out to staging and prod, and syncs their PR environments.
  • mindsdb/anton#526: builds the scratchpad image on GitHub-hosted runners, and pushes pull request and staging builds to the dev tier.
  • mindsdb/cowork#1117: runs every cowork job on GitHub-hosted runners, and rolls staging and prod out through mindsdb/deployer.
  • mindsdb/cowork-server#621: runs every job on GitHub-hosted runners, and deploys staging and prod through mindsdb/deployer.
  • mindsdb/minds_python_sdk#90: runs the release tests and the PyPI publish on GitHub-hosted runners, and publishes only after the release tests pass.
  • mindsdb/engine#7: deletes the jobs that ran on self-hosted runners, and keeps the pull request unit tests on GitHub-hosted runners.
  • mindsdb/data-vault#4: deletes the jobs that ran on self-hosted runners, and keeps the pull request unit tests on GitHub-hosted runners.
  • mindsdb/hashnode-starter-kit#39: deletes the two workflows that build and deploy the blog.
  • mindsdb/scratchpad-controller#80: points the dev and staging scratchpad workers at the dev-tier image.
  • mindsdb/argocd-envs#25: points PR environments at the dev-tier images of cowork, cowork-server and the scratchpad worker.
  • mindsdb/terraform#240, the second terraform change: sets the prod-tier image repository policies.
  • mindsdb/Kubernetes-Foundational-Services#166: updates the self-hosted runner chart on both clusters.

build-push-ecr gains a `builder` input. The default, `remote`, runs the
same create-repository, set-repository-policy, buildx create and build
calls as before. `local` builds on the docker-container builder that
setup-buildx-action starts on the runner. It caches layers in the GitHub
Actions cache under module-name, and it never creates an ECR repository
or changes a repository policy. So a caller job can push with a role it
assumes through GitHub OIDC, and that role needs push rights only.

In local mode, crazy-max/ghaction-github-runtime first exports the
cache service's ACTIONS_* variables, which buildx needs and run steps do
not get. The BuildKit container runs moby/buildkit v0.33.1, pinned to
its multi-arch index digest, because it holds the ECR registry
credentials while it pushes.

argocd-pr-env-deploy gains `repository`, `pr-number` and `head-sha`
inputs. They default to the pull_request event's values, so today's
callers pass nothing new. A scheduled or dispatched workflow passes them
to deploy a named PR. Before any API, ECR or ArgoCD call, the action
refuses a value that is not owner/name, a number or a 40-character SHA,
and it never prints the refused value. Outside a pull_request run it
deploys the PR head's development-head-<sha> tag, because that run
built no image of its own.

The action's ECR existence check now uses one repository per repo. When
mindsdb-<repo>-dev exists, it checks PR image tags there and only there.
Otherwise it checks mindsdb-<repo>. It asks describe-images whether the
-dev repository exists, so ecr:DescribeImages stays the only ECR
permission the action needs.

Every third-party action in build-push-ecr, snyk-docker-scan and
setup-env is pinned to the commit its tag points at today, with the
version in a comment. snyk-docker-scan also pins the Snyk CLI to
v1.1307.4, the current release. stale-deploy-label's prune job runs on
ubuntu-latest instead of mdb-dev.

The README documents the local builder, the new inputs, the
one-repository check and how to bump the BuildKit pin. Its secret-source
table describes what an environment's deployment branch policy can do.

New pytest suites run the build and deploy step scripts under bash with
aws, docker, curl and argocd stubbed, and assert on the argv of each
aws, docker and argocd call. Another suite fails if a pin loses its
commit, digest or version.

Part of Lucas Koontz's ticket "Take the public repos off the
credentialed runner group and require devops to release a privileged
run".

Refs: ENG-2000
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant