From a7363082dec2b32be8c277d0a947e4a9e0598fc5 Mon Sep 17 00:00:00 2001 From: Jinsoo Heo Date: Tue, 30 Jun 2026 22:08:04 +0900 Subject: [PATCH 1/3] ci(test): chart auto-pin smoke --- .../workflows/chronicle-image-pin-gate.yml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/.github/workflows/chronicle-image-pin-gate.yml b/.github/workflows/chronicle-image-pin-gate.yml index 98abd2f..270193d 100644 --- a/.github/workflows/chronicle-image-pin-gate.yml +++ b/.github/workflows/chronicle-image-pin-gate.yml @@ -10,12 +10,21 @@ name: chronicle-image-pin-gate # the malformed @sha256- (hyphen, the cosign sig-tag form), and short/over-long/invalid # digests. An embedded self-test asserts this on every run, so the detector cannot silently # regress. +# +# Auto-pin: for same-repo PRs the gate resolves each changed chart's .image (the standard +# chronicle chart shape: image.repository + image.tag) to its multi-arch index digest, writes +# the pin into values.yaml, and pushes ONE commit to the PR branch with the Chronicle bot App +# token (which re-triggers the gate -> green). The render-flag below stays the verdict, so any +# non-standard or unresolvable image is still flagged for a manual pin. Fork PRs get no token +# (secrets are withheld), so they keep the flag-only behaviour. on: pull_request: paths: - 'charts/**' +# contents:read for the default token; the auto-pin push authenticates with the bot App token +# (contents:write), not GITHUB_TOKEN. permissions: contents: read @@ -24,10 +33,38 @@ jobs: name: chronicle image pin gate runs-on: ubuntu-latest steps: + # Mint the bot App token FIRST (same-repo PRs only) so the checkout persists it as the git + # credential and the auto-pin push is write-capable (push via origin, no extraheader clash). + # continue-on-error so a transient mint failure can't block the required gate. + - name: Mint Chronicle bot token (same-repo PRs) + id: bot-token + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CHRONICLE_GITHUB_BOT_CLIENT_ID }} + private-key: ${{ secrets.CHRONICLE_GITHUB_BOT_PRIVATE_KEY }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + # PR HEAD branch so an auto-pin commit pushes cleanly; persisted credential is the bot + # App token for same-repo PRs (write), else the default GITHUB_TOKEN (read). + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ steps.bot-token.outputs.token || github.token }} - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 + # Best effort: public chronicle images resolve anonymously; private ones need this token. + # A login failure is fine -- the unresolved image just stays flagged by the render gate. + - name: Log in to GHCR for private image digests + continue-on-error: true + env: + GHCR_TOKEN: ${{ secrets.CHRONICLE_IMAGE_PULL_TOKEN }} + run: | + if [ -n "$GHCR_TOKEN" ]; then + echo "$GHCR_TOKEN" | docker login ghcr.io -u "${{ github.repository_owner }}" --password-stdin + else + echo "no CHRONICLE_IMAGE_PULL_TOKEN; resolving public images only" + fi - name: Self-test detector, then gate changed charts env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -91,3 +128,45 @@ jobs: done <<< "$changed" if [ "$fail" -eq 0 ]; then echo "OK: all changed charts pin their chronicle default images."; fi exit "$fail" + # Auto-pin the standard chronicle chart shape (image.repository + image.tag) for same-repo + # PRs and push ONE commit. Skipped without a bot token (fork / mint failure). The render + # gate above remains the verdict, so an unresolved or non-standard image stays flagged. + - name: Auto-pin chronicle image digests (one commit to the PR branch) + if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository && steps.bot-token.outputs.token != '' }} + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + set -uo pipefail + if ! changed=$(git diff --name-only "${BASE_SHA}...HEAD" -- 'charts/*/values.yaml'); then + echo "::warning::auto-pin: could not compute the PR diff; skipping (the render gate still gates)."; exit 0 + fi + [ -n "$changed" ] || { echo "No changed chart values.yaml."; exit 0; } + # Loop guard: never re-pin on top of an auto-pin (avoids a push -> re-run -> push cycle). + if git log -1 --pretty=%s | grep -q '^ci: auto-pin chronicle image digests'; then + echo "::warning::HEAD is already an auto-pin commit; skipping (loop guard)."; exit 0 + fi + pinned_files="" + while IFS= read -r f; do + [ -f "$f" ] || continue + repo=$(yq -r '.image.repository // ""' "$f") + tag=$(yq -r '.image.tag // ""' "$f") + case "$repo" in ghcr.io/chronicleprotocol/*) ;; *) continue ;; esac + case "$tag" in ""|*@sha256:*) continue ;; esac + digest=$(docker buildx imagetools inspect "$repo:$tag" --format '{{.Manifest.Digest}}' 2>/dev/null || true) + case "$digest" in + sha256:*) + NEWTAG="$tag@$digest" yq -i '.image.tag = strenv(NEWTAG)' "$f" + pinned_files="$pinned_files $f" + echo "auto-pinned $f: $repo:$tag -> $tag@$digest" + ;; + *) echo "::warning file=$f::could not resolve $repo:$tag to an index digest; left for the render gate" ;; + esac + done <<< "$changed" + [ -n "$pinned_files" ] || { echo "No auto-pins to push."; exit 0; } + git config user.name "chronicle-github-bot[bot]" + git config user.email "chronicle-github-bot[bot]@users.noreply.github.com" + git add -- $pinned_files + git commit -m "ci: auto-pin chronicle image digests" + git push origin "HEAD:${HEAD_REF}" + echo "::notice::pushed an auto-pin commit to ${HEAD_REF}; the re-run verifies it." From 916ab033b900217568450ab1d377458b6d25785f Mon Sep 17 00:00:00 2001 From: Jinsoo Heo Date: Tue, 30 Jun 2026 22:08:09 +0900 Subject: [PATCH 2/3] test: bare spire tag to trigger auto-pin --- charts/spire/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/spire/values.yaml b/charts/spire/values.yaml index b5b539f..fd32cce 100644 --- a/charts/spire/values.yaml +++ b/charts/spire/values.yaml @@ -8,7 +8,7 @@ image: repository: ghcr.io/chronicleprotocol/spire pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "0.68.3@sha256:79df4fb203224fa201623ae4d4f158faccfd410ec60f92841d161bb5b081d7ac" + tag: "0.68.3" imagePullSecrets: [] nameOverride: "" From 4173615a117104883df0a3cfac784ccef4646374 Mon Sep 17 00:00:00 2001 From: "chronicle-github-bot[bot]" Date: Tue, 30 Jun 2026 13:08:28 +0000 Subject: [PATCH 3/3] ci: auto-pin chronicle image digests --- charts/spire/values.yaml | 134 +++++++++++++++------------------------ 1 file changed, 51 insertions(+), 83 deletions(-) diff --git a/charts/spire/values.yaml b/charts/spire/values.yaml index fd32cce..6ac191d 100644 --- a/charts/spire/values.yaml +++ b/charts/spire/values.yaml @@ -3,21 +3,17 @@ # Declare variables to be passed into your templates. replicaCount: 1 - image: repository: ghcr.io/chronicleprotocol/spire pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "0.68.3" - + tag: "0.68.3@sha256:79df4fb203224fa201623ae4d4f158faccfd410ec60f92841d161bb5b081d7ac" imagePullSecrets: [] nameOverride: "" fullnameOverride: "spire" - entryPointOverride: [] argsOverride: [] extraArgs: [] - serviceAccount: # Specifies whether a service account should be created create: true @@ -26,21 +22,17 @@ serviceAccount: # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" - podAnnotations: {} +podSecurityContext: {} +# fsGroup: 2000 -podSecurityContext: - {} - # fsGroup: 2000 - -securityContext: - {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 +securityContext: {} +# capabilities: +# drop: +# - ALL +# readOnlyRootFilesystem: true +# runAsNonRoot: true +# runAsUser: 1000 service: type: ClusterIP @@ -54,17 +46,15 @@ service: libp2p: port: 8000 protocol: TCP - ### to add more listeners for webapi, add them here - # rpc: - # port: 9100 - # protocol: TCP - # webapi: - # port: 8080 - # protocol: TCP - annotations: - {} - # external-dns.alpha.kubernetes.io/hostname: DOMAIN_NAME - + ### to add more listeners for webapi, add them here + # rpc: + # port: 9100 + # protocol: TCP + # webapi: + # port: 8080 + # protocol: TCP + annotations: {} + # external-dns.alpha.kubernetes.io/hostname: DOMAIN_NAME # -- Optional ClusterIP metrics service for scraping metrics without exposing the metrics port on a public LoadBalancer. metricsService: enabled: false @@ -72,7 +62,6 @@ metricsService: port: 9090 targetPort: 9090 protocol: TCP - # -- Liveness probe liveness: enabled: true @@ -81,7 +70,6 @@ liveness: port: libp2p initialDelaySeconds: 10 periodSeconds: 10 - # -- Readiness probe readiness: enabled: true @@ -90,14 +78,12 @@ readiness: port: libp2p initialDelaySeconds: 10 periodSeconds: 10 - ingress: enabled: false className: "" - annotations: - {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: @@ -107,19 +93,17 @@ ingress: # - secretName: chart-example-tls # hosts: # - chart-example.local - -resources: - {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi +resources: {} +# We usually recommend not to specify default resources and to leave this as a conscious +# choice for the user. This also increases chances charts run on environments with little +# resources, such as Minikube. If you do want to specify resources, uncomment the following +# lines, adjust them as necessary, and remove the curly braces after 'resources:'. +# limits: +# cpu: 100m +# memory: 128Mi +# requests: +# cpu: 100m +# memory: 128Mi autoscaling: enabled: false @@ -127,57 +111,44 @@ autoscaling: maxReplicas: 100 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: 80 - nodeSelector: {} - tolerations: [] - affinity: {} - # Configurations for spire : default mode is to run as agent # if true, runs bootstrap mode bootstrap: false - # if true, runs stream mode stream: true - # if true, runs as a watch mode watch: false - # if true, runs spire in kafka made kafka: enabled: false kafkaEnpoint: kafka.default.svc.cluster.local:9092 kafkaTopics: {} - # - "--topic" - # - "data-point-v1" - # - "data_point/v1" - + # - "--topic" + # - "data-point-v1" + # - "data_point/v1" # custom Args that may be added to the agent customArgs: [] - ## logLevel is the log level for the spire server (debug,info,trace,etc) logLevel: null - ## logFormat is the log format for the spire server (json, or text) logFormat: null - # Environment variable listing env: # non sensitive variables # refer to https://github.com/chronicleprotocol/oracle-suite/tree/main/cmd/spire#environment-variables normal: {} - # CFG_ITEM_SEPARATOR: "\n" - # CFG_FEEDS: stage - # CFG_LIBP2P_PK_SEED: "1234567981234567981234567981234567981234123456798123456798123456" # 64 bytes libp2p for bootstrap only - # CFG_LIBP2P_BOOTSTRAP_ADDRS: |- - # /dns/bootstrap.some.domain/tcp/8000/p2p/ - + # CFG_ITEM_SEPARATOR: "\n" + # CFG_FEEDS: stage + # CFG_LIBP2P_PK_SEED: "1234567981234567981234567981234567981234123456798123456798123456" # 64 bytes libp2p for bootstrap only + # CFG_LIBP2P_BOOTSTRAP_ADDRS: |- + # /dns/bootstrap.some.domain/tcp/8000/p2p/ # This is a config file for spire. it can override the default config if supplied # It is in HCL format. # For more information on HCL, see https://github.com/chronicleprotocol/oracle-suite/blob/main/config.hcl configHcl: {} - serviceMonitor: # -- If true, a ServiceMonitor CRD is created for a prometheus operator # https://github.com/coreos/prometheus-operator @@ -202,17 +173,14 @@ serviceMonitor: scrapeTimeout: 30s # -- ServiceMonitor relabelings relabelings: [] - - # -- Extra K8s manifests to deploy -extraObjects: - [] - # - apiVersion: "kubernetes-client.io/v1" - # kind: ExternalSecret - # metadata: - # name: promtail-secrets - # spec: - # backendType: gcpSecretsManager - # data: - # - key: promtail-oauth2-creds - # name: client_secret \ No newline at end of file +extraObjects: [] +# - apiVersion: "kubernetes-client.io/v1" +# kind: ExternalSecret +# metadata: +# name: promtail-secrets +# spec: +# backendType: gcpSecretsManager +# data: +# - key: promtail-oauth2-creds +# name: client_secret