Syncing images
Every manifest engine keys its update off the payload's own image_name by
default. This can be changed with two ImageConfig settings, for when a deploy repo doesn't deploy the payload's image name verbatim — most commonly because it pulls
through a cloud-native registry mirror instead of the upstream registry:
deployed_as— the image name the manifests under this config actually deploy from, when it differs from the payload'simage_name. E.g. an ECR pull-through cache path such as123456789.dkr.ecr.us-east-1.amazonaws.com/docker-hub/gmri/sea-eagle-brown-3crsmirroring upstreamgmri/sea-eagle-brown-3crs. Unset falls back to the payload'simage_name.sync(defaultfalse) — whether odp-releaser should actually copy the payload's image todeployed_asbefore the bump is committed.
deployed_as is per-config only. Unlike other settings on
ImageConfig, there is no defaults.deployed_as.
The common case: declare-only, no sync
Leave sync unset (or don't set deployed_as at all) for registry-native
replication — an ECR pull-through cache being the common case here. A
pull-through cache populates itself the first time something pulls the mirrored
path, and there is no way to push into one directly, so there is nothing for
sync to do:
images:
gmri/sea-eagle-brown-3crs:
- events: [push]
deployed_as: 123456789.dkr.ecr.us-east-1.amazonaws.com/docker-hub/gmri/sea-eagle-brown-3crs
kustomize_manifests:
- ./kustomization.yaml
helm_charts:
- path: ./values.yaml
dagster_user_code: true
Active mirroring: sync: true
Set sync: true when the destination registry needs the image actively
pushed to it (ex: GHCR, Docker Hub, or a plain, non-pull-through ECR repository):
images:
ghcr.io/gulfofmaine/neracoos-mariners-dashboard-dev:
- events: [push]
deployed_as: gmri/neracoos-mariners-dashboard
sync: true
kustomize_manifests:
- path: apps/mariners-dev/kustomization.yaml
pin: digest
The bump_images action copies the image to
that registry, between writing the manifests and committing them, using
skopeo copy --all --preserve-digests, then checks the resulting digest and
skips a destination that already carries it.
A failed copy fails the bump
A merged manifest pointing at an image nobody pushed is an outage.
Set the sync input to false on the calling job
to disable the sync workflow-wide, as a kill switch independent of what any
individual image manifest declares.
A rejected bump PR still occupies the destination tag
In pull_request mode the copy happens before the PR opens, so an
abandoned or rejected bump leaves deployed_as:<tag> populated. On a
tag-immutable destination (ECR, or GHCR with immutability enabled), a
later dispatch of a different digest to that same tag can then never be
synced: the push is rejected, and the skip-if-already-there check does not
apply because the digests differ. Re-dispatching the same digest is
fine, which is the case the skip check covers. Deleting the orphaned tag
is currently manual.
Credentials
Provide destination credentials one of two ways:
sync_aws_role_arn(+sync_aws_region) for an ECR destination reached via OIDC — see below.sync_registry/sync_username/sync_passwordsecrets for a destination that uses plain username/password auth (GHCR, Docker Hub).
The copy also has to pull, and the source is the upstream registry the
payload names, which is often not the destination host. Set
sync_source_registry / sync_source_username / sync_source_password when
that source needs credentials the runner doesn't already have: a private
registry, or Docker Hub, where an anonymous pull draws on the shared runner IP's
rate limit. Because a failed copy fails the bump by design, an unauthenticated
source turns a rate limit into a blocked release.
If you compose the bump_images action
yourself rather than calling the workflow, the login is your job: skopeo reads
credentials from $HOME/.docker/config.json via the containers credential
search order, so a preceding docker/login-action (or
configure-aws-credentials + amazon-ecr-login) step is enough, with no
separate skopeo login. A caller that configures a Docker credential helper
instead is the exception — the credentials are then not in that file, and skopeo
won't resolve them.
Syncing to ECR via OIDC
Set sync_aws_role_arn (and sync_aws_region) to have
bump-images.yml assume an IAM role via OIDC and log in to
ECR before the bump runs — no long-lived AWS credentials stored as secrets. This
is why every caller needs id-token: write, whether or not it actually syncs.
id-token: write is a privilege grant
Granting it means every step in that job can mint an OIDC token scoped to
the calling repository, including this workflow's own steps and any action
they call. Anything that trusts your repo's OIDC subject is therefore
reachable from that job. permissions cannot be made conditional, so a
caller that never syncs still has to grant it to call this workflow at all.
If that trade isn't acceptable, compose the
bump_images action directly in your own
workflow instead, where you decide the job's permissions.
The role's trust policy keys on the calling repo, not on this reusable workflow:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-deploy-repo:*"
}
}
}
]
}
The OIDC subject claim (sub) describes the workflow run's repository (the
deploy repo that called bump-images.yml) even though the actual
aws-actions/configure-aws-credentials login runs inside the reusable workflow.
So each deploy repo gets its own tightly scoped role keyed on
repo:<that repo>:*, and nothing needs to trust every caller of this shared
workflow via job_workflow_ref.
Kustomize vs. Helm: where the mirror name lives
The two manifest engines carry a mirrored name in different places, so
deployed_as interacts with each differently — this is the one thing worth
understanding well before relying on either:
- Kustomize keeps matching
/images[name="<payload image_name>"]— theimages:entry itself always stays keyed on the upstream name. Kustomize's ownnewNamefield is the mirror, sodeployed_asmust equal that entry'snewName:
images:
- name: gmri/sea-eagle-brown-3crs # upstream name -- always the match key
newName: 123456789.dkr.ecr.us-east-1.amazonaws.com/docker-hub/gmri/sea-eagle-brown-3crs # must equal deployed_as
newTag: "ee1cadc"
odp-releaser validate image-manifest checks
this agreement: a missing or disagreeing newName is an error, and a
newName with no deployed_as declared is a warning (the mirror would then
be invisible to sync and to the Helm shorthand below).
- The Helm dagster user-deployments shorthand (
dagster_user_code: true) instead matches/deployments[image.repository="<deployed_as>"], because that chart's values layout has nonewNameequivalent —image.repositoryis the deployed name. Beforedeployed_asexisted, a values file whoseimage.repositoryalready named a mirror could not be bumped at all: nothing selected it.
A config with both a kustomize manifest and a dagster-shorthand Helm manifest
for the same mirrored image needs no special coordination: deployed_as is the
one setting both engines key off, each in the place that engine actually carries
a mirror.
The {deployed_image} placeholder
set values — on any of the three manifest types — can also reference
{deployed_image}, the same name just described (deployed_as when set,
otherwise the payload's own image_name). This lets a file_manifests entry
write the mirror registry directly instead of hand-typing it:
file_manifests:
- path: ../apps/config/deployment.json
set:
/spec/template/spec/containers[0]/image: "{deployed_image}@{digest}"
odp-releaser validate image-manifest warns
when a set value hard-codes the upstream image name literally on a config
that also sets deployed_as — almost always meant to be {deployed_image}
instead.
A shape limitation: no registry ports
deployed_as follows the same shape rule as an images: key: non-empty,
trimmed, lowercase, and free of @ and :. That last rule means a registry
host with an explicit port (registry.example.com:5000/foo) currently can't
be expressed as a deployed_as -- the same pre-existing limitation an
images: key and a payload image_name already share.