{"id":51226762,"url":"https://github.com/cartyc/image-syncer","last_synced_at":"2026-06-28T12:30:48.587Z","repository":{"id":365599093,"uuid":"1272439834","full_name":"cartyc/image-syncer","owner":"cartyc","description":"Sync images from cgr.dev to you favorite registry","archived":false,"fork":false,"pushed_at":"2026-06-18T02:47:14.000Z","size":41,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-18T04:24:10.725Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cartyc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-17T15:59:04.000Z","updated_at":"2026-06-18T02:47:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cartyc/image-syncer","commit_stats":null,"previous_names":["cartyc/image-syncer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cartyc/image-syncer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cartyc%2Fimage-syncer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cartyc%2Fimage-syncer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cartyc%2Fimage-syncer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cartyc%2Fimage-syncer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cartyc","download_url":"https://codeload.github.com/cartyc/image-syncer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cartyc%2Fimage-syncer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34889047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-28T12:30:44.248Z","updated_at":"2026-06-28T12:30:48.571Z","avatar_url":"https://github.com/cartyc.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cgr-sync\n\nA one-shot CLI that mirrors [Chainguard](https://www.chainguard.dev/) images\nfrom `cgr.dev` into a **private OCI registry** — Google Artifact Registry,\nJFrog Artifactory, Cloudsmith, or any registry that speaks the OCI distribution\nAPI. Built to run as a CI/CD step: read a config, copy what's missing, exit with\na status code.\n\n- **Diff-based \u0026 idempotent** — lists source tags, compares digests against the\n  destination, and copies only what's missing or changed. Re-running is cheap.\n- **Multi-arch aware** — copies image indexes whole (all platforms) via\n  [`go-containerregistry`](https://github.com/google/go-containerregistry).\n- **Supply-chain preserving** — also mirrors cosign signatures, attestations,\n  and SBOMs (the `sha256-\u003cdigest\u003e.sig`/`.att`/`.sbom` tag scheme), so the mirror\n  stays verifiable.\n- **Verify before mirror** — optionally gate copying on a cosign policy\n  (certificate identity + OIDC issuer), so only verified images are mirrored.\n- **Any OCI registry** — auth comes from the standard Docker keychain\n  (`~/.docker/config.json`), ambient cloud credentials, and cred helpers; no\n  per-vendor code required.\n\n## How it works\n\nAt a high level, cgr-sync reads a config, pulls from the Chainguard source, and\npushes only what's missing to your private registry:\n\n```mermaid\nflowchart LR\n  cfg[/\"cgr-sync.yaml\u003cbr/\u003erepos + tag selectors\"/]\n  auth[/\"docker config +\u003cbr/\u003ecloud credentials\"/]\n  src[(\"cgr.dev/\u0026lt;org\u0026gt;\u003cbr/\u003esource\")]\n  tool[\"cgr-sync\u003cbr/\u003ego-containerregistry\"]\n  dst[(\"private OCI registry\u003cbr/\u003eGAR / Artifactory / Cloudsmith\")]\n\n  cfg -.-\u003e tool\n  auth -.-\u003e tool\n  src --\u003e|\"list tags · pull\"| tool\n  tool --\u003e|\"push missing / changed\"| dst\n```\n\nFor each repository it lists the source tags, selects the ones you asked for,\nand diffs each by digest — copying only what the destination is missing or has\nstale, then mirroring that image's cosign artifacts:\n\n```mermaid\nflowchart TD\n  start([run]) --\u003e list[\"list source tags\"]\n  list --\u003e select[\"select tags\u003cbr/\u003e(explicit list, or all + include/exclude regex)\"]\n  select --\u003e tag{\"for each\u003cbr/\u003eselected tag\"}\n  tag --\u003e sd[\"get source digest\"]\n  sd --\u003e dd[\"get destination digest\"]\n  dd --\u003e cmp{\"dest exists and\u003cbr/\u003edigest matches?\"}\n  cmp --\u003e|\"yes\"| skip[\"skip — in sync\"]\n  cmp --\u003e|\"no / 404\"| dry{\"dry-run?\"}\n  dry --\u003e|\"yes\"| plan[\"log: would copy\"]\n  dry --\u003e|\"no\"| copy[\"crane.Copy\u003cbr/\u003e(manifest + all platforms)\"]\n  copy --\u003e sigs{\"mirror\u003cbr/\u003esignatures?\"}\n  sigs --\u003e|\"yes\"| cosign[\"copy sha256-\u0026lt;digest\u0026gt;\u003cbr/\u003e.sig / .att / .sbom\"]\n  skip --\u003e next([next tag])\n  plan --\u003e next\n  sigs --\u003e|\"no\"| next\n  cosign --\u003e next\n```\n\n## Install / build\n\n```sh\ngo build -o cgr-sync ./cmd/cgr-sync\n```\n\n### Container\n\nA `Containerfile` builds cgr-sync on free, public Chainguard images — the Go\ntoolchain (`cgr.dev/chainguard/go`) for the build, a distroless nonroot runtime\n(`cgr.dev/chainguard/static`), and `cgr.dev/chainguard/cosign` for the\nverify-before-mirror feature:\n\n```sh\ndocker build -f Containerfile -t cgr-sync --build-arg VERSION=\"$(git describe --tags --always)\" .\n\n# one-shot sync\ndocker run --rm -v \"$PWD/cgr-sync.yaml:/cgr-sync.yaml:ro\" cgr-sync sync -config /cgr-sync.yaml\n\n# webhook listener\ndocker run --rm -p 8080:8080 -v \"$PWD/cgr-sync.yaml:/cgr-sync.yaml:ro\" \\\n  cgr-sync serve -config /cgr-sync.yaml -audience https://mirror.example.com/events\n```\n\nRegistry credentials come from the standard keychain at runtime — mount a Docker\nconfig (`-v ~/.docker/config.json:/tmp/.docker/config.json:ro` with `DOCKER_CONFIG=/tmp/.docker`)\nor use the platform's ambient cloud credentials. If you don't use `verify`, drop\nthe `cosign` stage + COPY from the `Containerfile` for a smaller image.\n\n## Usage\n\nTwo modes — a one-shot `sync` (run on a schedule / in CI) and a `serve`\nlistener that mirrors in near-real-time on Chainguard push events. They're\ncomplementary: `serve` reacts immediately to new images; the scheduled `sync`\nreconciles anything a missed event would have left behind.\n\n```sh\ncgr-sync sync  -config cgr-sync.yaml             # one-shot mirror (default subcommand)\ncgr-sync sync  -config cgr-sync.yaml -dry-run    # plan only, copy nothing\ncgr-sync serve -config cgr-sync.yaml -audience https://mirror.example.com/events\n```\n\n`sync` flags:\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `-config` | `cgr-sync.yaml` | Path to the config file. |\n| `-dry-run` | `false` | Plan and print the work; copy nothing. |\n| `-continue-on-error` | `false` | Keep going after a failure instead of exiting on the first. |\n| `-no-signatures` | `false` | Mirror images only; skip cosign artifacts. |\n| `-timeout` | `2m` | Per-operation registry timeout (0 = none). |\n\nRegistry calls retry on transient failures (network errors, 429, 5xx) with\nbackoff, and re-runs are write-free when everything is already in sync (images\n**and** signatures are diffed by digest).\n\nExit code is `0` on success, `1` if any image failed, `2` on a config error.\n(`cgr-sync` with no subcommand defaults to `sync`; `cgr-sync -version` prints the version.)\n\n## Event-driven mode (Chainguard CloudEvents)\n\n`cgr-sync serve` runs an HTTP webhook that listens for Chainguard\n[registry events](https://edu.chainguard.dev/chainguard/administration/cloudevents/events-example/).\nOn a `push` event (`dev.chainguard.registry.push.v1`) it validates the event's\nOIDC token, then mirrors that single image — but only if the repository is in\nyour config and the tag passes its selector. Other event types and\nunconfigured repos are acknowledged and ignored.\n\n```mermaid\nflowchart LR\n  reg[(\"cgr.dev push\")] --\u003e|\"CloudEvent\"| wh[\"cgr-sync serve\u003cbr/\u003e/events webhook\"]\n  wh --\u003e|\"validate OIDC token\"| wh\n  wh --\u003e|\"matches config?\"| sync[\"mirror that image\u003cbr/\u003e+ signatures\"]\n  sync --\u003e dst[(\"private OCI registry\")]\n```\n\nRun the listener (it needs a public URL, e.g. behind an Ingress / Cloud Run):\n\n```sh\ncgr-sync serve \\\n  -config cgr-sync.yaml \\\n  -addr :8080 \\\n  -audience https://mirror.example.com/events \\   # your public webhook URL (token audience)\n  -identity webhook:\u003cyour-UIDP\u003e                    # optional: pin the event subject\n```\n\nThen subscribe it to your organization's events:\n\n```sh\nchainctl events subscriptions create https://mirror.example.com/events\n```\n\n`serve` flags:\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `-config` | `cgr-sync.yaml` | Path to the config file. |\n| `-addr` | `:8080` | Listen address. |\n| `-path` | `/events` | Webhook path. |\n| `-audience` | | Expected token audience = your public webhook URL. Required unless `-insecure-skip-verify`. |\n| `-identity` | | Expected event subject, e.g. `webhook:\u003cUIDP\u003e` (optional extra check). |\n| `-no-signatures` | `false` | Mirror images only; skip cosign artifacts. |\n| `-timeout` | `2m` | Per-operation registry timeout (0 = none). |\n| `-insecure-skip-verify` | `false` | **Local testing only** — skip token validation. Never expose publicly. |\n\nTokens are verified against the Chainguard issuer `https://issuer.enforce.dev`\n(signature, expiry, issuer, audience, and — if set — subject). `GET /healthz`\nreturns `ok` for liveness probes.\n\n## Configuration\n\nSee [`examples/cgr-sync.yaml`](examples/cgr-sync.yaml). Per-repo settings inherit\nfrom `defaults`:\n\n```yaml\ndefaults:\n  source: cgr.dev/example.com\n  destination: us-docker.pkg.dev/my-project/cgr-mirror\n  tags:\n    list: [\"latest\"]\n\nrepositories:\n  - name: python\n    tags:\n      list: [\"latest\", \"3.12\", \"3.13\"]\n  - name: node\n    tags:\n      all: true\n      include: '^[0-9]+$'    # major-version tags only\n      exclude: '-dev$'\n  - name: nginx\n    destination: cloudsmith.io/my-org/cgr-mirror/nginx   # per-repo override\n    tags: { list: [\"latest\"] }\n```\n\nA repository's `destination` may be a registry+namespace prefix (the repo name\nis appended) or a full repository path (used as-is). `${VAR}` anywhere in the\nconfig is expanded from the environment (a bare `$`, e.g. in a regex like\n`-dev$`, is left alone), so registries/secrets can come from CI variables.\n\n## Verification (cosign)\n\nSet a `verify` policy to gate mirroring on signature verification — an image is\ncopied only if its cosign signature satisfies the policy, so you never mirror an\nunverified or tampered image. It maps onto `cosign verify` and requires the\n`cosign` binary on `PATH`.\n\n```yaml\ndefaults:\n  verify:\n    enabled: true\n    certificate_identity: https://github.com/chainguard-images/images-private/.github/workflows/release.yaml@refs/heads/main\n    certificate_oidc_issuer: https://token.actions.githubusercontent.com\n    # ...or the regex forms: certificate_identity_regexp / certificate_oidc_issuer_regexp\n```\n\nVerification runs against the exact **digest** about to be mirrored (not the tag,\navoiding a tag-vs-content race). Like `tags`, a `verify` block is inherited\nwhole from `defaults` unless a repository specifies its own. `enabled: true`\nrequires an identity (exact or regex) and an issuer (exact or regex).\n\n## Authentication\n\ncgr-sync uses your existing registry credentials — log in with each registry's\nnormal tooling before running:\n\n- **Source (`cgr.dev`):** `chainctl auth login` (interactive) or, for CI, a\n  non-interactive identity/pull token written into the Docker config. Avoid the\n  interactive browser flow in pipelines.\n- **Google Artifact Registry:** ambient credentials (workload identity / service\n  account) are picked up automatically, or `gcloud auth configure-docker`.\n- **Artifactory / Cloudsmith / other:** `docker login \u003cregistry\u003e` (token/API\n  key), which cgr-sync reads from `~/.docker/config.json`.\n\n\u003e **Mirroring only public images?** If your Docker config has an *interactive*\n\u003e credential helper for the source (e.g. a `cgr.dev` helper that opens a browser),\n\u003e it can be invoked even for anonymous pulls and block. Point at an empty config\n\u003e to force anonymous: `DOCKER_CONFIG=$(mktemp -d) cgr-sync sync …`.\n\n## CI/CD\n\nRun it as a job step. Sketch (GitLab CI):\n\n```yaml\nmirror-images:\n  image: golang:1.26\n  script:\n    - go build -o cgr-sync ./cmd/cgr-sync\n    - ./cgr-sync -config cgr-sync.yaml -continue-on-error\n```\n\nProvide non-interactive credentials via the job environment / Docker config so\nno browser login is triggered.\n\n## Status / roadmap\n\nMVP — generic OCI mirroring with signature/attestation copy, cosign\nverify-before-mirror, and event-driven mirroring via Chainguard CloudEvents.\nPlanned next: referrers-API artifact mirroring, semver tag selection,\nconcurrency, and per-vendor auth conveniences.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcartyc%2Fimage-syncer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcartyc%2Fimage-syncer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcartyc%2Fimage-syncer/lists"}