images.yaml lists every image with the OpenStack releases it is built for (releases) and its upstream base image (base_image). One image is built per (image, release) pair, so adding a release means appending it to the relevant releases lists. custom-images/matrix.sh expands the file into build rows and is used by both the local script and CI.
Builds every listed release of every image and pushes the result:
bash custom-images/build-local.shThe openstack-tools version is part of its tag_template in images.yaml; bump it there.
(Optional) To disable timestamps use this
export USE_TIMESTAMP="false"IMAGES takes a comma-separated list of image names, OPENSTACK_RELEASE a single release; both are filters, empty means all. A release filter never matches images without releases (openvswitch, sbom-discovery, ceph); build those by name.
export IMAGES="nova,neutron"
export OPENSTACK_RELEASE="2026.1"
bash custom-images/build-local.shbuild-images.yml builds and pushes to ghcr.io/cloudification-io:
- on push to
main, the images whose files changed (every release of them; animages.yamlchange rebuilds everything) - weekly (Monday 03:00 UTC), the rows whose
base_imagedigest differs from the one recorded on the last build (labelorg.opencontainers.image.base.digest); the job summary lists the decision per row - on
workflow_dispatch, with the sameimagesandopenstack_releasefilters as the local script; leave both empty to force a full rebuild
mirror-upstream.yml copies the images in mirror-images.yaml from quay.io to ghcr.io/cloudification-io with mirror-to-ghcr.sh, on a push that changes the file and weekly (Monday 02:00 UTC). Kolla entries list their releases and use {release} in the tag. A tag is copied only when its upstream digest differs from the unsuffixed copy on GHCR, so unchanged images get no new dated tag; the force dispatch input (or FORCE=true) copies everything.
Mirror images from ghcr.io/cloudification-io to docker.io/cloudification using skopeo. The script mirrors the images listed in IMAGES, or discovers all packages via the GitHub API when IMAGES is unset.
In CI, the mirror-to-dockerhub job in build-images.yml runs the script after image builds, using the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets. Runs after main push and scheduled builds; a workflow_dispatch from any branch can preview the mirroring with the mirror_dry_run input.
gh auth login
skopeo login ghcr.io
skopeo login docker.iobash mirror-to-dockerhub.shPreview what would be mirrored without actually copying. The preview compares manifest digests, so it needs the same registry logins as a real run:
DRY_RUN=true bash mirror-to-dockerhub.shIMAGES=nova,horizon bash mirror-to-dockerhub.shBy default all tags are mirrored (MIRROR_MODE=all). To mirror only clean (non-timestamped) tags:
MIRROR_MODE=clean bash mirror-to-dockerhub.shOr only the latest timestamped tag per tag prefix:
MIRROR_MODE=latest-timestamped bash mirror-to-dockerhub.shCI uses MIRROR_MODE=recent, the clean tags plus the latest timestamped tag per prefix, so a run does not re-inspect every historical tag:
MIRROR_MODE=recent bash mirror-to-dockerhub.shAll discovered packages are mirrored by default. To exclude some:
EXCLUDE_IMAGES=coredns-k8s-gateway bash mirror-to-dockerhub.sh