polyc-runtime 2026.7.1

Shared Unix-coherence runtime for polychrome binaries: logging, health/metrics side-server, signals.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
//! Shared OCI-registry helpers: the published-component roster plus immutable
//! manifest-digest resolution, registry-agnostic.
//!
//! The cluster-upgrade path lives in two binaries — the operator CLI's
//! executor (`polychrome upgrade --cluster`) rolls the images, and the control
//! plane's mailbox originator captures the digests when it opens the approval
//! ask. Both must agree on *which* images are published, *where* they live, and
//! *how* a version's tag resolves to an immutable `sha256:…` content address, so
//! that logic lives here once rather than duplicated per binary.
//!
//! Where the images live is configuration, not a constant. The open-source
//! default is the public GHCR repositories; a private deployment overrides each
//! component's full image reference (registry host + repository) through the
//! `POLYCHROME_UPGRADE_IMAGE_*` environment variables — for example a Google
//! Artifact Registry path like
//! `us-east4-docker.pkg.dev/<project>/docker/polychrome-control-plane`. The
//! reference carries no tag; the tag is derived from the target version.
//!
//! Resolution follows the OCI distribution flow: obtain a bearer token for the
//! registry, `GET` the manifest, and read the registry-computed
//! `Docker-Content-Digest` header — the canonical content address pinned onto
//! the cluster. We never float a tag onto a running cluster. Which credential
//! the token comes from depends on the registry host: a public GHCR repository
//! uses the anonymous pull-token endpoint, while Google Artifact Registry / GCR
//! uses a GCP OAuth access token.

use std::time::Duration;

/// Registry host assumed when a configured image reference names no host (the
/// standard Docker heuristic: a leading path segment without a `.` or `:` is a
/// repository namespace, not a registry).
const DEFAULT_REGISTRY: &str = "ghcr.io";

/// Bounds the registry TCP connect so a dead host fails fast.
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);

/// Overall per-request budget for a registry round-trip.
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);

/// A digest-resolution failure.
///
/// A library error (`thiserror`) so callers on `anyhow` can add context and
/// callers on `thiserror` can wrap it.
#[derive(Debug, thiserror::Error)]
pub enum GhcrError {
    /// The target version did not parse as semver (so it can't map to a
    /// published image tag).
    #[error("target version `{0}` is not valid semver")]
    Version(String),
    /// The HTTP client could not be built or a request failed in transport.
    #[error("registry request failed: {0}")]
    Http(#[from] reqwest::Error),
    /// The registry accepted the request but returned no usable body/header
    /// (no token, or no digest header on the manifest response).
    #[error("registry response for {repo} had no {what}")]
    Malformed {
        /// The image repository the request was for.
        repo: String,
        /// What was missing (`pull token` / `digest header`).
        what: &'static str,
    },
    /// A GCP access token could not be obtained for a `*.pkg.dev` / `*.gcr.io`
    /// registry, so the digest cannot be resolved. Fail closed rather than fall
    /// back to an anonymous request that would 401.
    #[error("could not obtain a GCP access token for registry `{registry}`: {reason}")]
    GcpToken {
        /// The registry host the token was needed for.
        registry: String,
        /// Why the token could not be obtained.
        reason: String,
    },
}

/// One published component the cluster upgrade rolls.
///
/// Carries a short stable `name` (also the key an operator decision's signed
/// digest set is keyed on and the `--digest <name>=…` CLI arg uses), the
/// `Deployment` resource name, the `container` name inside the pod spec, the
/// environment variable that overrides its image reference, and the GHCR
/// default reference used when that variable is unset.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Component {
    /// Short stable identifier (`control-plane` / `slack` / `telegram`). The key
    /// in a signed decision's digest set and the `--digest` CLI arg.
    pub name: &'static str,
    /// `metadata.name` of the `Deployment`.
    pub deployment: &'static str,
    /// `spec.template.spec.containers[].name` the image lives on.
    pub container: &'static str,
    /// Environment variable that, when set, overrides this component's full
    /// image reference (registry host + repository, no tag).
    pub image_env: &'static str,
    /// Default image reference (public GHCR) used when [`Component::image_env`]
    /// is unset. May name a host or be a bare `owner/repo` (defaults to
    /// `ghcr.io`).
    pub default_image: &'static str,
}

impl Component {
    /// The bare GHCR image name this component actually publishes under —
    /// [`Component::default_image`]'s final path segment (e.g. `polychrome`,
    /// `polychrome-slack`, `polychrome-telegram`).
    ///
    /// This, not [`Component::name`], is the key
    /// `.github/workflows/publish.yml`'s `channel` job and
    /// `scripts/make_channel.sh` write `release-images.yaml` entries under
    /// (`crate::release_manifest::ManifestImage::name`) — the manifest is
    /// keyed by what was actually published, not by the CLI-facing
    /// identifier. [`Component::name`] remains the short stable id used for
    /// the `--digest <name>=…` CLI arg and a signed decision's digest set.
    #[must_use]
    pub fn ghcr_basename(&self) -> &str {
        self.default_image
            .rsplit('/')
            .next()
            .unwrap_or(self.default_image)
    }
}

/// The Deployments an upgrade rolls.
///
/// Names/containers verified against `manifests/base/*-deployment.yaml`,
/// `manifests/components/edges/trigger/deployment.yaml`, and
/// `manifests/components/scaffold/deployment.yaml`; default images against
/// `publish.yml`.
///
/// `scaffold`'s `default_image` names the GHCR basename it *would* publish
/// under if it ever did, purely so `Component::ghcr_basename` stays
/// well-formed — `.github/workflows/publish.yml` deliberately does NOT
/// publish it (`cloudbuild.yaml`: "scaffold is a GAR-only provisioning
/// connector"), so a hand-run upgrade with no `POLYCHROME_UPGRADE_IMAGE_SCAFFOLD`
/// override correctly fails loud (`ReleaseManifestError::MissingComponent`)
/// rather than silently resolving a public image that doesn't exist — the
/// same no-legacy, no-silent-fallback rule every other digest source already
/// follows. Every real deployment (including prod) sets the override.
///
/// `trigger` (#1325 investigation) — unlike scaffold, it IS published to GHCR
/// (`publish.yml`'s edge matrix) — was missing from this list entirely, so a
/// hand-run upgrade could never roll it past its `REPLACE_AT_BUILD_TIME`
/// placeholder image; a fresh `polychrome install` left that Deployment
/// permanently stuck at `InvalidImageName` with no sanctioned way to fix it.
/// Reproduced live bringing up a second GKE cluster with the gke overlay's
/// full edge set composed.
pub const COMPONENTS: [Component; 6] = [
    Component {
        name: "control-plane",
        deployment: "polychrome-control-plane",
        container: "polychrome",
        image_env: "POLYCHROME_UPGRADE_IMAGE_CONTROL_PLANE",
        default_image: "ghcr.io/officialunofficial/polychrome",
    },
    Component {
        name: "harness",
        deployment: "polychrome-harness",
        container: "polychrome-harness",
        image_env: "POLYCHROME_UPGRADE_IMAGE_HARNESS",
        default_image: "ghcr.io/officialunofficial/polychrome-harness",
    },
    Component {
        name: "slack",
        deployment: "polychrome-slack",
        container: "polychrome-slack",
        image_env: "POLYCHROME_UPGRADE_IMAGE_SLACK",
        default_image: "ghcr.io/officialunofficial/polychrome-slack",
    },
    Component {
        name: "telegram",
        deployment: "polychrome-telegram",
        container: "polychrome-telegram",
        image_env: "POLYCHROME_UPGRADE_IMAGE_TELEGRAM",
        default_image: "ghcr.io/officialunofficial/polychrome-telegram",
    },
    Component {
        name: "trigger",
        deployment: "polychrome-trigger",
        container: "polychrome-trigger",
        image_env: "POLYCHROME_UPGRADE_IMAGE_TRIGGER",
        default_image: "ghcr.io/officialunofficial/polychrome-trigger",
    },
    Component {
        name: "scaffold",
        deployment: "polychrome-scaffold",
        container: "scaffold",
        image_env: "POLYCHROME_UPGRADE_IMAGE_SCAFFOLD",
        default_image: "ghcr.io/officialunofficial/polychrome-scaffold",
    },
];

/// A resolved image reference — a registry host plus a repository, carrying no
/// tag. The tag is derived per upgrade from the target version.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageRef {
    /// Registry host (e.g. `ghcr.io` or `us-east4-docker.pkg.dev`).
    pub registry: String,
    /// Repository path within the registry (e.g.
    /// `officialunofficial/polychrome` or
    /// `official-unofficial/docker/polychrome-control-plane`).
    pub repository: String,
}

/// Split a full image reference string into its registry host and repository.
///
/// Applies the standard Docker heuristic: the segment before the first `/` is
/// the registry host **iff** it contains a `.` or a `:` (a hostname or
/// `host:port`); otherwise there is no host and the whole string is a
/// repository under the default registry (`ghcr.io`).
///
/// # Examples
/// - `us-east4-docker.pkg.dev/official-unofficial/docker/polychrome-control-plane`
///   → registry `us-east4-docker.pkg.dev`, repo
///   `official-unofficial/docker/polychrome-control-plane`.
/// - `officialunofficial/polychrome` → registry `ghcr.io`, repo
///   `officialunofficial/polychrome`.
#[must_use]
pub fn parse_image_ref(full: &str) -> ImageRef {
    match full.split_once('/') {
        Some((host, rest)) if host.contains('.') || host.contains(':') => ImageRef {
            registry: host.to_owned(),
            repository: rest.to_owned(),
        },
        _ => ImageRef {
            registry: DEFAULT_REGISTRY.to_owned(),
            repository: full.to_owned(),
        },
    }
}

/// Resolve a component's image reference from the environment, falling back to
/// its public GHCR default.
///
/// The environment lookup is injected (rather than read from the process) so
/// this stays a pure function — unit-testable without touching global state.
/// Production callers pass `|k| std::env::var(k).ok()`.
#[must_use]
pub fn image_ref<F>(component: &Component, env_lookup: F) -> ImageRef
where
    F: Fn(&str) -> Option<String>,
{
    let raw = env_lookup(component.image_env).unwrap_or_else(|| component.default_image.to_owned());
    parse_image_ref(&raw)
}

/// Normalize a `--version` / release tag into the image tag published.
///
/// `publish.yml` tags release manifests `{{version}}` (no leading `v`), so a
/// release `v0.4.0` (or `--version v0.4.0`) maps to the image tag `0.4.0`. The
/// tag is validated as semver so a typo can't pin the cluster to a
/// non-existent / floating tag.
///
/// # Errors
/// Returns [`GhcrError::Version`] if the stripped tag is not valid semver.
pub fn image_tag_for_version(version: &str) -> Result<String, GhcrError> {
    let tag = version.trim().trim_start_matches('v');
    semver::Version::parse(tag).map_err(|_| GhcrError::Version(version.to_owned()))?;
    Ok(tag.to_owned())
}

/// Build the digest-pinned image reference from a resolved [`ImageRef`] and a
/// content digest (`{registry}/{repository}@{digest}`).
#[must_use]
pub fn pinned_reference(image: &ImageRef, digest: &str) -> String {
    format!("{}/{}@{}", image.registry, image.repository, digest)
}

/// How to authenticate a digest-resolution request against a registry host.
///
/// Selected purely from the host by [`auth_for`]; the effectful token fetch is
/// isolated behind each variant in [`resolve_digest`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegistryAuth {
    /// The OCI anonymous pull-token flow (public GHCR): `GET /token?scope=…`.
    AnonymousToken,
    /// A GCP OAuth access token, for Google Artifact Registry (`*.pkg.dev`) and
    /// Container Registry (`*.gcr.io`).
    GcpMetadata,
}

/// Pick the registry-auth strategy for a host.
///
/// Google Artifact Registry (`*.pkg.dev`) and Container Registry (`gcr.io` /
/// `*.gcr.io`) require a GCP OAuth token; every other host (notably public
/// GHCR) uses the anonymous pull-token flow.
#[must_use]
pub fn auth_for(registry: &str) -> RegistryAuth {
    if registry.ends_with(".pkg.dev") || registry == "gcr.io" || registry.ends_with(".gcr.io") {
        RegistryAuth::GcpMetadata
    } else {
        RegistryAuth::AnonymousToken
    }
}

/// Build the shared HTTP client used for registry round-trips (bounded connect
/// and request timeouts, a `polychrome/<version>` user agent).
///
/// # Errors
/// Returns [`GhcrError::Http`] if the client can't be constructed.
pub fn http_client() -> Result<reqwest::Client, GhcrError> {
    Ok(reqwest::Client::builder()
        .timeout(REQUEST_TIMEOUT)
        .connect_timeout(CONNECT_TIMEOUT)
        .user_agent(concat!("polychrome/", env!("CARGO_PKG_VERSION")))
        .build()?)
}

/// Resolve the immutable manifest digest for `repository:tag` against `registry`.
///
/// The auth strategy is chosen from the host by [`auth_for`]: a public GHCR
/// repository authenticates with an anonymous bearer token from the registry
/// `token` endpoint, while a Google Artifact Registry / GCR host authenticates
/// with a GCP OAuth access token.
///
/// The `GET` reads the `Docker-Content-Digest` response header — the registry
/// computes it over the manifest bytes, so it is the canonical content address.
///
/// # Errors
/// Returns [`GhcrError`] if the bearer token can't be obtained, the manifest
/// request fails, or the response carries no digest header.
pub async fn resolve_digest(
    client: &reqwest::Client,
    registry: &str,
    repository: &str,
    tag: &str,
) -> Result<String, GhcrError> {
    let token = match auth_for(registry) {
        RegistryAuth::AnonymousToken => anonymous_pull_token(client, registry, repository).await?,
        RegistryAuth::GcpMetadata => gcp_access_token(client, registry).await?,
    };
    let url = format!("https://{registry}/v2/{repository}/manifests/{tag}");
    let resp = client
        .get(&url)
        .bearer_auth(&token)
        // Accept both the OCI image index and the Docker manifest-list media
        // types so multi-arch published images resolve to their index digest.
        .header(
            reqwest::header::ACCEPT,
            "application/vnd.oci.image.index.v1+json, \
             application/vnd.docker.distribution.manifest.list.v2+json, \
             application/vnd.oci.image.manifest.v1+json, \
             application/vnd.docker.distribution.manifest.v2+json",
        )
        .send()
        .await?
        .error_for_status()?;
    let digest = resp
        .headers()
        .get("docker-content-digest")
        .and_then(|v| v.to_str().ok())
        .ok_or_else(|| GhcrError::Malformed {
            repo: repository.to_owned(),
            what: "digest header",
        })?
        .to_owned();
    Ok(digest)
}

/// Fetch an anonymous pull token for a public repository (the OCI distribution
/// flow — GHCR).
async fn anonymous_pull_token(
    client: &reqwest::Client,
    registry: &str,
    repository: &str,
) -> Result<String, GhcrError> {
    let url =
        format!("https://{registry}/token?service={registry}&scope=repository:{repository}:pull");
    let body: serde_json::Value = client
        .get(&url)
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;
    // GHCR returns the bearer under `token`; some registries use `access_token`.
    body["token"]
        .as_str()
        .or_else(|| body["access_token"].as_str())
        .map(str::to_owned)
        .ok_or_else(|| GhcrError::Malformed {
            repo: repository.to_owned(),
            what: "pull token",
        })
}

/// Fetch a GCP OAuth access token for an Artifact Registry / GCR host.
///
/// In-cluster (with Workload Identity) this reads the token off the GCE
/// metadata server. For local use, point at Application Default Credentials
/// instead — export a token from `gcloud auth print-access-token` into the
/// environment ahead of the process, or run against a metadata proxy; the
/// resolver only needs a valid bearer for the registry host.
async fn gcp_access_token(client: &reqwest::Client, registry: &str) -> Result<String, GhcrError> {
    const METADATA_TOKEN_URL: &str = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
    let body: serde_json::Value = client
        .get(METADATA_TOKEN_URL)
        .header("Metadata-Flavor", "Google")
        .send()
        .await
        .map_err(|e| GhcrError::GcpToken {
            registry: registry.to_owned(),
            reason: format!("metadata request failed: {e}"),
        })?
        .error_for_status()
        .map_err(|e| GhcrError::GcpToken {
            registry: registry.to_owned(),
            reason: format!("metadata server returned an error: {e}"),
        })?
        .json()
        .await
        .map_err(|e| GhcrError::GcpToken {
            registry: registry.to_owned(),
            reason: format!("metadata response was not JSON: {e}"),
        })?;
    body["access_token"]
        .as_str()
        .map(str::to_owned)
        .ok_or_else(|| GhcrError::GcpToken {
            registry: registry.to_owned(),
            reason: "metadata response carried no `access_token`".to_owned(),
        })
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery)]

    use super::*;

    fn component(name: &str) -> &'static Component {
        COMPONENTS.iter().find(|c| c.name == name).unwrap()
    }

    #[test]
    fn image_tag_strips_v_and_validates() {
        assert_eq!(image_tag_for_version("v0.4.0").unwrap(), "0.4.0");
        assert_eq!(image_tag_for_version("0.4.0").unwrap(), "0.4.0");
        assert_eq!(image_tag_for_version("  v1.2.3 ").unwrap(), "1.2.3");
    }

    #[test]
    fn image_tag_rejects_non_semver() {
        assert!(image_tag_for_version("latest").is_err());
        assert!(image_tag_for_version("1.2").is_err());
        assert!(image_tag_for_version("v").is_err());
    }

    #[test]
    fn parse_splits_gar_host_from_repository() {
        let r = parse_image_ref(
            "us-east4-docker.pkg.dev/official-unofficial/docker/polychrome-control-plane",
        );
        assert_eq!(r.registry, "us-east4-docker.pkg.dev");
        assert_eq!(
            r.repository,
            "official-unofficial/docker/polychrome-control-plane"
        );
    }

    #[test]
    fn parse_defaults_hostless_ref_to_ghcr() {
        let r = parse_image_ref("officialunofficial/polychrome");
        assert_eq!(r.registry, "ghcr.io");
        assert_eq!(r.repository, "officialunofficial/polychrome");
    }

    #[test]
    fn parse_treats_host_port_as_registry() {
        let r = parse_image_ref("localhost:5000/team/app");
        assert_eq!(r.registry, "localhost:5000");
        assert_eq!(r.repository, "team/app");
    }

    #[test]
    fn image_ref_defaults_to_ghcr_with_empty_env() {
        let cp = image_ref(component("control-plane"), |_| None);
        assert_eq!(cp.registry, "ghcr.io");
        assert_eq!(cp.repository, "officialunofficial/polychrome");

        let slack = image_ref(component("slack"), |_| None);
        assert_eq!(
            pinned_reference(&slack, "sha256:x"),
            "ghcr.io/officialunofficial/polychrome-slack@sha256:x"
        );
    }

    #[test]
    fn image_ref_honors_gar_override() {
        let env = |k: &str| match k {
            "POLYCHROME_UPGRADE_IMAGE_CONTROL_PLANE" => Some(
                "us-east4-docker.pkg.dev/official-unofficial/docker/polychrome-control-plane"
                    .to_owned(),
            ),
            _ => None,
        };
        let cp = image_ref(component("control-plane"), env);
        assert_eq!(cp.registry, "us-east4-docker.pkg.dev");
        assert_eq!(
            cp.repository,
            "official-unofficial/docker/polychrome-control-plane"
        );
        // An unset component still falls back to its GHCR default.
        let slack = image_ref(component("slack"), env);
        assert_eq!(slack.registry, "ghcr.io");
    }

    #[test]
    fn pinned_reference_format_for_ghcr_and_gar() {
        let ghcr = ImageRef {
            registry: "ghcr.io".to_owned(),
            repository: "officialunofficial/polychrome".to_owned(),
        };
        assert_eq!(
            pinned_reference(&ghcr, "sha256:abc"),
            "ghcr.io/officialunofficial/polychrome@sha256:abc"
        );
        let gar = ImageRef {
            registry: "us-east4-docker.pkg.dev".to_owned(),
            repository: "official-unofficial/docker/polychrome-control-plane".to_owned(),
        };
        assert_eq!(
            pinned_reference(&gar, "sha256:def"),
            "us-east4-docker.pkg.dev/official-unofficial/docker/polychrome-control-plane@sha256:def"
        );
    }

    #[test]
    fn auth_for_selects_by_host() {
        assert_eq!(auth_for("ghcr.io"), RegistryAuth::AnonymousToken);
        assert_eq!(
            auth_for("us-east4-docker.pkg.dev"),
            RegistryAuth::GcpMetadata
        );
        assert_eq!(auth_for("us.gcr.io"), RegistryAuth::GcpMetadata);
        assert_eq!(auth_for("gcr.io"), RegistryAuth::GcpMetadata);
        assert_eq!(auth_for("docker.io"), RegistryAuth::AnonymousToken);
    }

    #[test]
    fn components_carry_stable_names() {
        let names: Vec<&str> = COMPONENTS.iter().map(|c| c.name).collect();
        assert_eq!(
            names,
            vec![
                "control-plane",
                "harness",
                "slack",
                "telegram",
                "trigger",
                "scaffold"
            ]
        );
    }

    /// The GHCR basename is deliberately NOT the same string as
    /// `Component::name` — it's the published image name
    /// (`.github/workflows/publish.yml`'s matrix), which is what
    /// `release_manifest::resolve_release_digests` must key its manifest
    /// lookup on. A regression test for the naming mismatch that let the
    /// release manifest reader and the publish workflow disagree on the key.
    #[test]
    fn ghcr_basename_matches_the_published_image_name_not_component_name() {
        let basenames: Vec<&str> = COMPONENTS.iter().map(Component::ghcr_basename).collect();
        assert_eq!(
            basenames,
            vec![
                "polychrome",
                "polychrome-harness",
                "polychrome-slack",
                "polychrome-telegram",
                "polychrome-trigger",
                "polychrome-scaffold",
            ]
        );
    }
}