Changed
- CI restructured to one workflow per image.
build-and-push.yml is replaced by
image-sf-ci.yml, image-sf-devcontainer.yml, image-sf-bulk.yml and release.yml.
Path filtering is now GitHub's own on.pull_request.paths rather than a changes job that
computed a matrix from the PR diff, so the image workflows are inherently parallel and adding
an image is copying one file. release.yml keeps a matrix because a release must push a
coherent set and aggregate every image's version report into one GitHub Release — which means
adding an image now also requires a release.yml matrix entry, or it is tested but never
published
Added
- All three images register a second account,
runner (UID 1001, GID 0), alongside
ci/vscode (1000). GitHub Actions container jobs mount /github/home and the runner
file-command dir owned by the runner's UID, so the job must be that UID to write them —
and sf crashes on a UID with no /etc/passwd entry, because oclif calls
os.userInfo(). Registering 1001 statically is what lets the shared Salesforce
workflows drop options: --user root and run unprivileged. Verified against a
simulated runner mount, not just a passwd lookup
- CI:
e2e job. On PRs touching sf-ci it retags the already-built image (no rebuild)
to a throwaway GHCR tag and runs the real weather2gp-release.yml in sf-develop-demo
against it as --user 1001, synchronously, failing the PR if that pipeline fails.
Quota-light: no scratch org, and --skip-validation draws on the 500/day pool.
Needs an E2E_DISPATCH_TOKEN secret with actions: write on the downstream repo
- sf-devcontainer:
openssh-client, so git clone/git push over ssh:// or
git@host:... remotes work, not just HTTPS. Previously failed with error: cannot run ssh: No such file or directory — the git package doesn't pull it in. Also ships a
/etc/ssh/ssh_config.d/99-devcontainer.conf setting StrictHostKeyChecking accept-new
repo-wide, so the first connection to a new host doesn't hang on an unanswerable
interactive "are you sure? (yes/no)" prompt
Changed — BREAKING
sf-ci and sf-bulk now run as non-root ci (UID 1000) at runtime, reverting the
USER root workaround added in 927c06d. Running CI pipelines as root was the wrong trade.
Consumers must run the container with UID 1000 — on ARC set the runner pod's
securityContext.runAsUser: 1000, or add options: --user 1000 to the container job.
Without that, /github/home (bind-mounted by the runner and owned by its UID) is unwritable
and the SF CLI fails with EACCES, exactly as before 927c06d. sf-devcontainer was already
non-root (vscode) and is unchanged. Requires a 3.0.0 release.
- Writable paths (
/home/ci, /opt/sf-data, /opt/sf-config) are now owned by GID 0 and
group-writable, so any UID that has a passwd entry can write them.
- Running under an arbitrary UID is explicitly not supported:
sf calls Node's
os.userInfo(), which throws ENOENT when the UID has no /etc/passwd entry. The usual
entrypoint workaround does not apply — GitHub Actions container jobs override ENTRYPOINT.
- The reference
.devcontainer/devcontainer.json now persists Salesforce org auth in named
Docker volumes (~/.sf, ~/.sfdx) instead of leaving it in the container's throwaway
layer, so a rebuild no longer costs you a re-login. Do not bind-mount your host's ~/.sf
or ~/.sfdx to share auth: those files are encrypted with the host OS keychain, which a
Linux container cannot read, and every org then reports AuthDecryptError. Verified
empirically on 2026-08-06. tests/test_sf_devcontainer.py::test_auth_dirs_not_host_mounted
guards this repo's own configs against the mistake
- sf-devcontainer's shell is now Starship with no framework, replacing Oh My Zsh +
Powerlevel10k. Plugins come from apt instead of git clones; zsh-completions is dropped
(not packaged for noble). The prompt shows the project's Salesforce target org, read from
.sf/config.json with jq — never by calling sf, which would add ~500 ms of Node
startup to every prompt. Anyone relying on OMZ aliases or p10k configure must move that
into ~/.zshrc.local
- CI: third-party actions are pinned to floating major tags (
@v7, @v4) instead of commit
SHAs. sigstore/cosign-installer (@v4.1.2) and aquasecurity/trivy-action (@v0.36.0)
stay exact — neither publishes a floating major tag.
Added
- CI: path-filtered builds. A
changes job reads the PR's changed files and builds only the
affected images — sf-<image>/** or tests/test_sf_<image>.py selects that image, while
.github/workflows/**, tests/requirements.txt, or any other tests/*.py rebuilds all of
them. Docs-only PRs now build nothing. Version tags are unaffected: they always build and
publish the full set so latest stays coherent across images. The image set is defined once,
in the IMAGES map of that job. A job summary lists which images were built and which were
skipped
Removed
- Superseded design docs:
docs/devcontainer-dx-design.md,
docs/reusable-workflow-migration-design.md, and
docs/superpowers/specs/2026-07-13-devcontainer-tools-docs-design.md. All three describe work
that shipped in 2.0.0 and is now documented in CLAUDE.md and the READMEs;
docs/README.md (the image decision guide) stays
Image tool versions
sf-bulk
| Component |
Version |
| Node.js |
v24.18.0 |
| npm |
11.16.0 |
| Salesforce CLI |
@salesforce/cli/2.146.3 linux-x64 node-v24.18.0 |
| Plugin |
Version |
| sfdx-git-delta |
6.45.1 |
sf-ci
| Component |
Version |
| Node.js |
v24.19.0 |
| npm |
11.17.0 |
| Salesforce CLI |
@salesforce/cli/2.146.3 linux-x64 node-v24.19.0 |
| Plugin |
Version |
| sfdx-git-delta |
6.45.1 |
sf-devcontainer
| Component |
Version |
| Node.js |
v24.19.0 |
| npm |
11.17.0 |
| Salesforce CLI |
@salesforce/cli/2.146.3 linux-x64 node-v24.19.0 |
| Plugin |
Version |
| sfdx-git-delta |
6.45.1 |
| @salesforce/plugin-code-analyzer |
5.15.0 |
| sfdx-browserforce-plugin |
6.3.3 |
What's Changed
- Add: own the per-image Docker build workflow instead of calling shared-github-actions by @gambe94 in #13
- Gate PRs on a real downstream Salesforce release, run unprivileged by @gambe94 in #14
- feat!: replace Oh My Zsh + Powerlevel10k with Starship, persist devcontainer org auth by @gambe94 in #15
Full Changelog: v2.0.0...v3.0.0