feat: implement publish stage smart sync from security mirror to canonical
## Problem / Goal
`goreleaser` publishes the container image, OCI Helm chart, and packages exclusively to the security mirror's registries. For non-security releases, these artifacts need to be available on the canonical project's registries to be publicly accessible. The `publish` stage in the `release-platform` component currently holds only a placeholder job ([delivery#21857](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21857)) with no real implementation.
## Background / Context
The Release Platform uses a three-mirror architecture (canonical → security → build). With mirror-aware CI jobs ([delivery#21789](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21789), [common-ci-tasks!1398](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1398)), goreleaser is gated to the security mirror only. The publish stage must sync artifacts back to canonical after deployment, but only when the commit exists on canonical (meaning the code is public).
The ["smart sync"](https://gitlab.com/groups/gitlab-com/gl-infra/software-delivery/-/work_items/51#development-flow) concept: check whether the deployed commit is already present on canonical, then sync automatically if it is, or expose a manual job if it exists only on the security mirror.
## Proposed Solution
Add a smart sync implementation in the `publish` stage of the `release-platform` component in `common-ci-tasks` (`src/ci/components/release-platform.jsonnet`), replacing the existing placeholder job.
**Step 1 -- Commit-on-canonical check:** call the GitLab API to test whether `$CI_COMMIT_SHA` is present on the canonical project. The check uses `JOB-TOKEN` from the security mirror's CI job, which requires canonical's CI/CD job token allowlist to include the security mirror project.
**Step 2 -- Sync artifacts** from the security mirror to the canonical project:
- Container image: `skopeo copy` from security registry to canonical registry (exact semver tag)
- OCI Helm chart: `skopeo copy` from security registry to canonical registry (bare-semver tag, `v` stripped)
- Packages: download from security package registry, re-upload to canonical package registry via GitLab API (bare-semver version, `v` stripped)
**Step 3 -- Conditional execution:**
- Commit is public: sync runs automatically
- Commit exists only on the security mirror: sync is a manual job
**Implementation:** Since GitLab CI rules evaluate at pipeline creation time, `when:` cannot be flipped based on a job artifact at runtime. A dynamically generated child pipeline is the correct approach: the generator job runs the check and emits a child pipeline YAML with either `when: on_success` or `when: manual` for the sync jobs.
The sync jobs run on the **security mirror only** (`noCanonical.noBuild` guard + explicit `release_platform_enabled: true` check).
**Step 4 -- Automate the cross-project deploy token + CI/CD variables** via the release-platform Terraform module so every release-platform project gets the publish stage's auth set up automatically (deploy token on canonical with `read_registry`/`write_registry`/`read_package_registry`/`write_package_registry` scopes, two CI/CD variables on the security mirror), instead of requiring manual setup per project.
**Step 5 -- Automate the cross-project CI/CD job token allowlist** via the release-platform Terraform module, adding the security mirror to the canonical project's job token scope so the commit-on-canonical check (`JOB-TOKEN`-authenticated GET against canonical's API) doesn't get rejected with HTTP 403 by default.
## Out of Scope
- Publishing to `charts.gitlab.io` (tracked in [delivery#21973](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21973))
- TUBE / UBI image build rework (tracked in [software-delivery#59](https://gitlab.com/groups/gitlab-com/gl-infra/software-delivery/-/work_items/59))
- Cross-instance sync from dev.gitlab.org (build mirror) -- not required for this path
## Affected Systems
- [`gitlab-com/gl-infra/common-ci-tasks`](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks) -- `src/ci/components/release-platform.jsonnet`
- [`gitlab-com/gl-infra/terraform-modules/gitlab/release-platform`](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform) -- deploy token + CI/CD variable + job token scope automation
- [`gitlab-com/gl-infra/infra-mgmt`](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt) -- consumes the new release-platform module versions (Renovate-managed)
- [`gitlab-org/software-delivery/release-platform-canary`](https://gitlab.com/gitlab-org/software-delivery/release-platform-canary) -- validation target
- [`gitlab-org/security/release-platform-canary`](https://gitlab.com/gitlab-org/security/release-platform-canary) -- source of artifacts (security mirror)
## Dependencies
- ~~Container registry enabled on security mirror: [`terraform-modules/gitlab/release-platform!33`](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/33)~~ merged
- ~~Runway provisioner updated to security mirror project ID: [`runway/provisioner!1460`](https://gitlab.com/gitlab-com/gl-infra/platform/runway/provisioner/-/merge_requests/1460)~~ merged
- ~~Vault provisioning for the security mirror (unblocks `goreleaser`)~~ resolved via [`terraform-modules/gitlab/release-platform!38`](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/38) + [`infra-mgmt!2660`](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt/-/merge_requests/2660)
- `release_platform_enabled: true` already set in `release-platform-canary` main branch
## Resources
- Parent epic: [software-delivery#51](https://gitlab.com/groups/gitlab-com/gl-infra/software-delivery/-/work_items/51)
- Mirror-aware CI jobs: [delivery#21789](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21789)
- Placeholder issue: [delivery#21857](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21857)
- Helm chart publishing discussion: [delivery#21973](https://gitlab.com/gitlab-com/gl-infra/delivery/-/work_items/21973)
- Goreleaser mirror-aware rules: [common-ci-tasks!1398](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1398)
- Canonical canary repo: https://gitlab.com/gitlab-org/software-delivery/release-platform-canary
- Security mirror canary repo: https://gitlab.com/gitlab-org/security/release-platform-canary
## Tasks
- [x] Implement commit-on-canonical check (GitLab API call for `$CI_COMMIT_SHA`)
- [x] Implement dynamic child pipeline generator job
- [x] Apply `noCanonical.noBuild` mirror guard + `release_platform_enabled` check to all publish jobs
- [x] Remove existing `release-platform-publish` placeholder
- [x] Implement container image sync (`skopeo copy` security to canonical)
- [x] Implement OCI Helm chart sync (`skopeo copy` security to canonical, bare-semver tag)
- [x] Implement package sync (download from security, re-upload to canonical via API, bare-semver version)
- [x] Automate canonical-registry deploy token (with read+write registry/package_registry scopes) + CI/CD variable provisioning via the `release-platform` Terraform module
- [x] Automate canonical's CI/CD job token allowlist via [release-platform!42](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/42)
- [x] Fix latent shell syntax bug in publish stage heredoc ([common-ci-tasks!1448](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1448))
- [x] Fix skopeo image entrypoint override in `sync-registry-artifacts` ([common-ci-tasks!1449](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1449))
- [x] Strip `v` prefix from helm chart tag in publish stage ([common-ci-tasks!1453](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1453))
- [x] Validate auto-sync path end-to-end on release-platform-canary v1.5.3 -- container image, OCI Helm chart, and generic package all confirmed on canonical
- [x] Fix manual-sync path: replace broken auto/manual trigger split (v3.24.2) with single `trigger-publish-pipeline` + `sync-start` no-op auto-job + `strategy: depend` ([common-ci-tasks!1476](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1476))
- [x] Validate manual-sync path end-to-end on release-platform-canary v1.6.6 -- parent pipeline green, bridge resolves with downstream success, downstream has `sync-start` succeeded + `sync-registry-artifacts`/`sync-packages` awaiting manual play (mechanics same as auto path validated at v1.5.3)
## Status / Progress
| MR | Status |
|----|--------|
| [common-ci-tasks!1435](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1435) | merged -- dynamic child pipeline mechanism |
| [common-ci-tasks!1437](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1437) | merged -- container image + OCI Helm chart sync via skopeo |
| [common-ci-tasks!1443](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1443) | merged -- generic package sync via Package Registry API |
| [terraform-modules/gitlab/release-platform!41](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/41) | merged + applied -- automate deploy token + CI/CD variable provisioning, tagged `v2.5.0` |
| [infra-mgmt!2725](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt/-/merge_requests/2725) | merged + applied -- bumped release-platform to `v2.5.0`; canary migration completed |
| [release-platform-canary!48](https://gitlab.com/gitlab-org/software-delivery/release-platform-canary/-/merge_requests/48) | merged -- consumer wiring on release-platform-canary |
| [common-ci-tasks!1448](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1448) | merged -- fix shell syntax bug from backticks in publish stage heredoc, tagged `v3.23.1` |
| [terraform-modules/gitlab/release-platform!42](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/42) | merged + applied -- automate canonical's job token allowlist, tagged `v2.6.0` |
| [infra-mgmt!2727](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt/-/merge_requests/2727) | merged + applied -- bumped release-platform to `v2.6.0` |
| [common-ci-tasks!1449](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1449) | merged + tagged `v3.23.2` -- fix skopeo image entrypoint override |
| [infra-mgmt!2735](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt/-/merge_requests/2735) | merged + applied -- canary deploy token rotated via `terraform -replace` |
| [terraform-modules/gitlab/release-platform!44](https://gitlab.com/gitlab-com/gl-infra/terraform-modules/gitlab/release-platform/-/merge_requests/44) | merged + tagged `v3.1.0` -- add `read_registry` and `read_package_registry` scopes |
| [infra-mgmt!2737](https://gitlab.com/gitlab-com/gl-infra/infra-mgmt/-/merge_requests/2737) | merged + applied -- bumped release-platform to `v3.1.0` |
| [common-ci-tasks!1453](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1453) | merged + tagged `v3.23.3` -- strip `v` prefix from helm chart tag |
| [release-platform-canary!56](https://gitlab.com/gitlab-org/software-delivery/release-platform-canary/-/merge_requests/56) | merged -- triggered v1.5.3 release that validated the auto-sync path |
| [common-ci-tasks!1471](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1471) | merged + tagged `v3.24.2` -- (broken) split `trigger-publish-pipeline` into `-auto`/`-manual` to fix manual-case parent failure; the SYNC_TYPE rules never matched at pipeline creation (gitlab-org/gitlab#352326) so no bridge was created |
| [common-ci-tasks!1476](https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1476) | merged + tagged `v4.0.0` -- replace broken split with single `trigger-publish-pipeline` + `sync-start` no-op auto-job in child YAML + `strategy: depend` on parent trigger |
| [release-platform-canary!60](https://gitlab.com/gitlab-org/software-delivery/release-platform-canary/-/merge_requests/60) | merged -- bumped common-ci-tasks pin to `v4.0` |
### Validation pipelines
| Pipeline | Result |
|----------|--------|
| <https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/merge_requests/1471> v3.24.2 release on canary v1.6.0 | parent failed; downstream all-manual marked `skipped`, propagated as parent failure (motivated MR 1476) |
| canary v1.6.4 (post-v3.24.2 retest): <https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502106761> | parent green but no trigger bridge created (proved the rules-evaluation gap) |
| canary v1.6.5 (spike of MR 1476 design): <https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502317110> | parent green, downstream resolved with `sync-start` success + manual sync jobs awaiting play |
| canary v1.6.6 (post-v4.0.0 final validation): <https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502426115> -> downstream <https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502435432> | parent green, bridge resolves with downstream success, downstream `sync-start: success` + `sync-registry-artifacts: manual` + `sync-packages: manual` |
## Acceptance Criteria
- [x] Commit-on-canonical check implemented
- [x] Dynamic child pipeline generates auto / manual sync job based on check result
- [x] Container image synced correctly (security registry to canonical registry)
- [x] OCI Helm chart synced correctly (security registry to canonical registry)
- [x] Packages synced correctly (security package registry to canonical package registry)
- [x] Sync job runs only on security mirror
- [x] Deploy token on canonical (`read_registry`+`write_registry`+`read_package_registry`+`write_package_registry`) and `CANONICAL_REGISTRY_TOKEN_USERNAME` / `CANONICAL_REGISTRY_TOKEN` masked CI/CD variables on the security mirror are provisioned automatically by the `release-platform` Terraform module for every release-platform project
- [x] Canonical's CI/CD job token allowlist auto-includes the security mirror so the commit-on-canonical check is not rejected with HTTP 403
- [x] Auto-sync path validated end-to-end on release-platform-canary (v1.5.3): container image at `:v1.5.3`, OCI Helm chart at `:1.5.3`, and generic package at `1.5.3` all confirmed on canonical
- [x] Manual-sync path validated end-to-end on release-platform-canary v1.6.6: parent pipeline green, bridge resolves with downstream success, downstream pipeline contains `sync-start: success` + `sync-registry-artifacts: manual` + `sync-packages: manual` ([parent](https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502426115), [downstream](https://gitlab.com/gitlab-org/security/release-platform-canary/-/pipelines/2502435432)). Sync job mechanics (`skopeo copy`, package download/upload) are shared with the auto path validated at v1.5.3.
issue