Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package apko for openSUSE:Factory checked in 
at 2025-12-12 21:42:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.1939 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Fri Dec 12 21:42:40 2025 rev:80 rq:1322609 version:0.30.30

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2025-12-09 
12:53:13.603190875 +0100
+++ /work/SRC/openSUSE:Factory/.apko.new.1939/apko.changes      2025-12-12 
21:44:01.396035169 +0100
@@ -1,0 +2,25 @@
+Fri Dec 12 12:16:14 UTC 2025 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.30.30:
+  * build(deps): bump github.com/chainguard-dev/clog from 1.7.0 to
+    1.8.0 (#1981)
+  * cache keys by name (#1968)
+  * build(deps): bump k8s.io/apimachinery from 0.34.2 to 0.34.3
+    (#1979)
+  * build(deps): bump go.opentelemetry.io/otel/trace from 1.38.0 to
+    1.39.0 (#1974)
+  * build(deps): bump golang.org/x/oauth2 from 0.33.0 to 0.34.0
+    (#1973)
+  * build(deps): bump golang.org/x/sys from 0.38.0 to 0.39.0
+    (#1972)
+  * build(deps): bump golang.org/x/sync from 0.18.0 to 0.19.0
+    (#1971)
+  * build(deps): bump github/codeql-action from 4.31.6 to 4.31.7
+    (#1970)
+  * Add the ability to specify additional certs to be added to the
+    image (#1977)
+  * build(deps): bump step-security/harden-runner from 2.13.3 to
+    2.14.0 (#1978)
+  * cli/dot: Ignore Duplicate Node errors (#1976)
+
+-------------------------------------------------------------------

Old:
----
  apko-0.30.29.obscpio

New:
----
  apko-0.30.30.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.OjmvDH/_old  2025-12-12 21:44:02.128066054 +0100
+++ /var/tmp/diff_new_pack.OjmvDH/_new  2025-12-12 21:44:02.132066223 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        0.30.29
+Version:        0.30.30
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.OjmvDH/_old  2025-12-12 21:44:02.184068417 +0100
+++ /var/tmp/diff_new_pack.OjmvDH/_new  2025-12-12 21:44:02.192068754 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/apko</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v0.30.29</param>
+    <param name="revision">v0.30.30</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.OjmvDH/_old  2025-12-12 21:44:02.228070273 +0100
+++ /var/tmp/diff_new_pack.OjmvDH/_new  2025-12-12 21:44:02.228070273 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/apko</param>
-              <param 
name="changesrevision">0b3827aa83414219539e3cfb82cd07f7972a1950</param></service></servicedata>
+              <param 
name="changesrevision">d2cb85c1f37e24120b415d4e4372a1b8f811f8e4</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-0.30.29.obscpio -> apko-0.30.30.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/examples/alpine-base.yaml 
new/apko-0.30.30/examples/alpine-base.yaml
--- old/apko-0.30.29/examples/alpine-base.yaml  2025-12-04 15:43:00.000000000 
+0100
+++ new/apko-0.30.30/examples/alpine-base.yaml  2025-12-12 12:19:29.000000000 
+0100
@@ -12,3 +12,4 @@
 
 archs:
   - amd64
+  - arm64
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/examples/certificates.yaml 
new/apko-0.30.30/examples/certificates.yaml
--- old/apko-0.30.29/examples/certificates.yaml 1970-01-01 01:00:00.000000000 
+0100
+++ new/apko-0.30.30/examples/certificates.yaml 2025-12-12 12:19:29.000000000 
+0100
@@ -0,0 +1,72 @@
+contents:
+  keyring:
+    - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
+  repositories:
+    - https://packages.wolfi.dev/os
+  packages:
+    - ca-certificates-bundle
+    - busybox
+    - apk-tools
+
+entrypoint:
+  command: /bin/sh -c
+
+archs:
+  - amd64
+
+certificates:
+  additional:
+    # From https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x1.pem
+    - name: pretend-pear-x1
+      content: |
+        -----BEGIN CERTIFICATE-----
+        MIIFmDCCA4CgAwIBAgIQU9C87nMpOIFKYpfvOHFHFDANBgkqhkiG9w0BAQsFADBm
+        MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy
+        aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ
+        ZWFyIFgxMB4XDTE1MDYwNDExMDQzOFoXDTM1MDYwNDExMDQzOFowZjELMAkGA1UE
+        BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
+        YXJjaCBHcm91cDEiMCAGA1UEAxMZKFNUQUdJTkcpIFByZXRlbmQgUGVhciBYMTCC
+        AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALbagEdDTa1QgGBWSYkyMhsc
+        ZXENOBaVRTMX1hceJENgsL0Ma49D3MilI4KS38mtkmdF6cPWnL++fgehT0FbRHZg
+        jOEr8UAN4jH6omjrbTD++VZneTsMVaGamQmDdFl5g1gYaigkkmx8OiCO68a4QXg4
+        wSyn6iDipKP8utsE+x1E28SA75HOYqpdrk4HGxuULvlr03wZGTIf/oRt2/c+dYmD
+        oaJhge+GOrLAEQByO7+8+vzOwpNAPEx6LW+crEEZ7eBXih6VP19sTGy3yfqK5tPt
+        TdXXCOQMKAp+gCj/VByhmIr+0iNDC540gtvV303WpcbwnkkLYC0Ft2cYUyHtkstO
+        fRcRO+K2cZozoSwVPyB8/J9RpcRK3jgnX9lujfwA/pAbP0J2UPQFxmWFRQnFjaq6
+        rkqbNEBgLy+kFL1NEsRbvFbKrRi5bYy2lNms2NJPZvdNQbT/2dBZKmJqxHkxCuOQ
+        FjhJQNeO+Njm1Z1iATS/3rts2yZlqXKsxQUzN6vNbD8KnXRMEeOXUYvbV4lqfCf8
+        mS14WEbSiMy87GB5S9ucSV1XUrlTG5UGcMSZOBcEUpisRPEmQWUOTWIoDQ5FOia/
+        GI+Ki523r2ruEmbmG37EBSBXdxIdndqrjy+QVAmCebyDx9eVEGOIpn26bW5LKeru
+        mJxa/CFBaKi4bRvmdJRLAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
+        Af8EBTADAQH/MB0GA1UdDgQWBBS182Xy/rAKkh/7PH3zRKCsYyXDFDANBgkqhkiG
+        9w0BAQsFAAOCAgEAncDZNytDbrrVe68UT6py1lfF2h6Tm2p8ro42i87WWyP2LK8Y
+        nLHC0hvNfWeWmjZQYBQfGC5c7aQRezak+tHLdmrNKHkn5kn+9E9LCjCaEsyIIn2j
+        qdHlAkepu/C3KnNtVx5tW07e5bvIjJScwkCDbP3akWQixPpRFAsnP+ULx7k0aO1x
+        qAeaAhQ2rgo1F58hcflgqKTXnpPM02intVfiVVkX5GXpJjK5EoQtLceyGOrkxlM/
+        sTPq4UrnypmsqSagWV3HcUlYtDinc+nukFk6eR4XkzXBbwKajl0YjztfrCIHOn5Q
+        CJL6TERVDbM/aAPly8kJ1sWGLuvvWYzMYgLzDul//rUF10gEMWaXVZV51KpS9DY/
+        5CunuvCXmEQJHo7kGcViT7sETn6Jz9KOhvYcXkJ7po6d93A/jy4GKPIPnsKKNEmR
+        xUuXY4xRdh45tMJnLTUDdC9FIU0flTeO9/vNpVA8OPU1i14vCz+MU8KX1bV3GXm/
+        fxlB7VBBjX9v5oUep0o/j68R/iDlCOM4VVfRa8gX6T2FU7fNdatvGro7uQzIvWof
+        gN9WUwCbEMBy/YhBSrXycKA8crgGg3x1mIsopn88JKwmMBa68oS7EHM9w7C4y71M
+        7DiA+/9Qdp9RBWJpTS9i/mDnJg1xvo8Xz49mrrgfmcAXTCJqXi24NatI3Oc=
+        -----END CERTIFICATE-----
+    # From https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x2.pem
+    - name: bogus-broccoli-x2
+      content: |
+        -----BEGIN CERTIFICATE-----
+        MIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL
+        MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0
+        eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj
+        b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE
+        BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
+        YXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy
+        MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo
+        FQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT
+        VqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+        VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI
+        KoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo
+        uc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9
+        vbtH7QiVzeKCOTQPINyRql6P
+        -----END CERTIFICATE-----
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/go.mod new/apko-0.30.30/go.mod
--- old/apko-0.30.29/go.mod     2025-12-04 15:43:00.000000000 +0100
+++ new/apko-0.30.30/go.mod     2025-12-12 12:19:29.000000000 +0100
@@ -4,7 +4,7 @@
 
 require (
        chainguard.dev/sdk v0.1.44
-       github.com/chainguard-dev/clog v1.7.0
+       github.com/chainguard-dev/clog v1.8.0
        github.com/charmbracelet/log v0.4.2
        github.com/go-git/go-git/v5 v5.16.4
        github.com/google/go-cmp v0.7.0
@@ -22,17 +22,17 @@
        github.com/tmc/dot v0.2.0
        github.com/u-root/u-root v0.15.0
        go.lsp.dev/uri v0.3.0
-       go.opentelemetry.io/otel v1.38.0
-       go.opentelemetry.io/otel/trace v1.38.0
+       go.opentelemetry.io/otel v1.39.0
+       go.opentelemetry.io/otel/trace v1.39.0
        go.step.sm/crypto v0.75.0
-       golang.org/x/oauth2 v0.33.0
-       golang.org/x/sync v0.18.0
-       golang.org/x/sys v0.38.0
+       golang.org/x/oauth2 v0.34.0
+       golang.org/x/sync v0.19.0
+       golang.org/x/sys v0.39.0
        golang.org/x/time v0.14.0
        google.golang.org/api v0.257.0
        gopkg.in/ini.v1 v1.67.0
        gopkg.in/yaml.v3 v3.0.1
-       k8s.io/apimachinery v0.34.2
+       k8s.io/apimachinery v0.34.3
        sigs.k8s.io/release-utils v0.12.2
 )
 
@@ -122,7 +122,7 @@
        go.opentelemetry.io/auto/sdk v1.2.1 // indirect
        
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc 
v0.63.0 // indirect
        go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 
// indirect
-       go.opentelemetry.io/otel/metric v1.38.0 // indirect
+       go.opentelemetry.io/otel/metric v1.39.0 // indirect
        go.yaml.in/yaml/v2 v2.4.2 // indirect
        golang.org/x/crypto v0.45.0 // indirect
        golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/go.sum new/apko-0.30.30/go.sum
--- old/apko-0.30.29/go.sum     2025-12-04 15:43:00.000000000 +0100
+++ new/apko-0.30.30/go.sum     2025-12-12 12:19:29.000000000 +0100
@@ -35,8 +35,8 @@
 github.com/cenkalti/backoff/v4 v4.1.3/go.mod 
h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/cespare/xxhash/v2 v2.3.0 
h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chainguard-dev/clog v1.7.0 
h1:guPznsK8vLHvzz1QJe2yU6MFeYaiSOFOQBYw4OXu+g8=
-github.com/chainguard-dev/clog v1.7.0/go.mod 
h1:4+WFhRMsGH79etYXY3plYdp+tCz/KCkU8fAr0HoaPvs=
+github.com/chainguard-dev/clog v1.8.0 
h1:frlTMEdg3XQR+ioQ6O9i92uigY8GTUcWKpuCFkhcCHA=
+github.com/chainguard-dev/clog v1.8.0/go.mod 
h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo=
 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc 
h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
 github.com/charmbracelet/colorprofile 
v0.2.3-0.20250311203215-f60798e515dc/go.mod 
h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
 github.com/charmbracelet/lipgloss v1.1.0 
h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
@@ -267,20 +267,20 @@
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc 
v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 
h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod 
h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
-go.opentelemetry.io/otel v1.38.0 
h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
-go.opentelemetry.io/otel v1.38.0/go.mod 
h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel v1.39.0 
h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod 
h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 
h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod 
h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 
h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod 
h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.38.0 
h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
-go.opentelemetry.io/otel/metric v1.38.0/go.mod 
h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/metric v1.39.0 
h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod 
h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
 go.opentelemetry.io/otel/sdk v1.38.0 
h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
 go.opentelemetry.io/otel/sdk v1.38.0/go.mod 
h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
 go.opentelemetry.io/otel/sdk/metric v1.38.0 
h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod 
h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
-go.opentelemetry.io/otel/trace v1.38.0 
h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
-go.opentelemetry.io/otel/trace v1.38.0/go.mod 
h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.opentelemetry.io/otel/trace v1.39.0 
h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod 
h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
 go.opentelemetry.io/proto/otlp v1.7.1 
h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
 go.opentelemetry.io/proto/otlp v1.7.1/go.mod 
h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw=
@@ -310,13 +310,13 @@
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
 golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
-golang.org/x/oauth2 v0.33.0/go.mod 
h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod 
h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod 
h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod 
h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -331,8 +331,8 @@
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -386,7 +386,7 @@
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
-k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
-k8s.io/apimachinery v0.34.2/go.mod 
h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
+k8s.io/apimachinery v0.34.3/go.mod 
h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
 sigs.k8s.io/release-utils v0.12.2 
h1:H06v3FuLElAkf7Ikkd9ll8hnhdtQ+OgktJAni3iIAl8=
 sigs.k8s.io/release-utils v0.12.2/go.mod 
h1:Ab9Lb/FpGUw4lUXj1QYbUcF2TRzll+GS7Md54W1G7sA=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/internal/cli/dot.go 
new/apko-0.30.30/internal/cli/dot.go
--- old/apko-0.30.29/internal/cli/dot.go        2025-12-04 15:43:00.000000000 
+0100
+++ new/apko-0.30.30/internal/cli/dot.go        2025-12-12 12:19:29.000000000 
+0100
@@ -213,7 +213,7 @@
                                        panic(err)
                                }
                        }
-                       if _, err := out.AddNode(n); err != nil {
+                       if _, err := out.AddNode(n); err != nil && 
!errors.Is(err, dot.ErrDuplicateNode) {
                                panic(err)
                        }
 
@@ -231,7 +231,7 @@
                                                }
                                        }
                                }
-                               if _, err := out.AddNode(d); err != nil {
+                               if _, err := out.AddNode(d); err != nil && 
!errors.Is(err, dot.ErrDuplicateNode) {
                                        panic(err)
                                }
                                if _, ok := edges[dep]; !ok || !span {
@@ -269,7 +269,7 @@
                        if err := n.Set("label", pkgver(pkg)); err != nil {
                                panic(err)
                        }
-                       if _, err := out.AddNode(n); err != nil {
+                       if _, err := out.AddNode(n); err != nil && 
!errors.Is(err, dot.ErrDuplicateNode) {
                                panic(err)
                        }
 
@@ -281,7 +281,7 @@
                                                        if err := 
p.Set("shape", "rect"); err != nil {
                                                                panic(err)
                                                        }
-                                                       if _, err := 
out.AddNode(p); err != nil {
+                                                       if _, err := 
out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) {
                                                                panic(err)
                                                        }
 
@@ -296,7 +296,7 @@
                                                        if err := 
p.Set("shape", "rect"); err != nil {
                                                                panic(err)
                                                        }
-                                                       if _, err := 
out.AddNode(p); err != nil {
+                                                       if _, err := 
out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) {
                                                                panic(err)
                                                        }
 
@@ -310,7 +310,7 @@
                                        }
                                }
                                p := dot.NewNode(prov)
-                               if _, err := out.AddNode(p); err != nil {
+                               if _, err := out.AddNode(p); err != nil && 
!errors.Is(err, dot.ErrDuplicateNode) {
                                        panic(err)
                                }
                                if _, ok := edges[pkg.Name]; !ok || !span {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/apk/apk/cache.go 
new/apko-0.30.30/pkg/apk/apk/cache.go
--- old/apko-0.30.29/pkg/apk/apk/cache.go       2025-12-04 15:43:00.000000000 
+0100
+++ new/apko-0.30.30/pkg/apk/apk/cache.go       2025-12-12 12:19:29.000000000 
+0100
@@ -368,6 +368,10 @@
                return filepath.Join(filepath.Dir(cacheFile), "APKINDEX")
        }
 
+       if strings.HasSuffix(cacheFile, ".rsa.pub") {
+               return filepath.Join(filepath.Dir(cacheFile), 
filepath.Base(cacheFile))
+       }
+
        return filepath.Dir(cacheFile)
 }
 
@@ -381,6 +385,11 @@
                ext = ".tar.gz"
        }
 
+       // Keep all the rsa.pub files under subdirectory named by full filename.
+       if strings.HasSuffix(cacheFile, ".rsa.pub") {
+               cacheDir = filepath.Join(cacheDir, filepath.Base(cacheFile))
+       }
+
        absPath, err := filepath.Abs(filepath.Join(cacheDir, etag+ext))
        if err != nil {
                return "", err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/build_implementation.go 
new/apko-0.30.30/pkg/build/build_implementation.go
--- old/apko-0.30.29/pkg/build/build_implementation.go  2025-12-04 
15:43:00.000000000 +0100
+++ new/apko-0.30.30/pkg/build/build_implementation.go  2025-12-12 
12:19:29.000000000 +0100
@@ -194,6 +194,10 @@
                return nil, fmt.Errorf("failed to mutate paths: %w", err)
        }
 
+       if err := bc.installCertificates(ctx); err != nil {
+               return nil, fmt.Errorf("failed to install certificates: %w", 
err)
+       }
+
        if err := bc.s6.WriteSupervisionTree(ctx, bc.ic.Entrypoint.Services); 
err != nil {
                return nil, fmt.Errorf("failed to write supervision tree: %w", 
err)
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/certificates.go 
new/apko-0.30.30/pkg/build/certificates.go
--- old/apko-0.30.29/pkg/build/certificates.go  1970-01-01 01:00:00.000000000 
+0100
+++ new/apko-0.30.30/pkg/build/certificates.go  2025-12-12 12:19:29.000000000 
+0100
@@ -0,0 +1,176 @@
+// Copyright 2025 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+       "bytes"
+       "context"
+       "crypto/sha256"
+       "crypto/x509"
+       "encoding/hex"
+       "encoding/pem"
+       "errors"
+       "fmt"
+       "io"
+       "io/fs"
+       "os"
+       "path/filepath"
+
+       "go.opentelemetry.io/otel"
+)
+
+const (
+       // Directory for individual certificate files (used by 
update-ca-certificates).
+       caCertsDir = "usr/local/share/ca-certificates"
+)
+
+var (
+       // Common paths for CA bundles.
+       caBundlePaths = []string{
+               "etc/ssl/certs/ca-certificates.crt",                        // 
Alpine default.
+               "var/lib/ecs/deps/execute-command/certs/tls-ca-bundle.pem", // 
AWS ECS-specific.
+       }
+)
+
+// parsedCertificate represents a parsed certificate with its metadata.
+type parsedCertificate struct {
+       pem         []byte
+       fingerprint string
+}
+
+// installCertificates installs inline certificates into the build context.
+func (bc *Context) installCertificates(ctx context.Context) error {
+       _, span := otel.Tracer("apko").Start(ctx, "installCertificates")
+       defer span.End()
+
+       if bc.ic.Certificates == nil || len(bc.ic.Certificates.Additional) == 0 
{
+               // No configuration, nothing to do.
+               return nil
+       }
+
+       builtTime, err := bc.GetBuildDateEpoch()
+       if err != nil {
+               return fmt.Errorf("failed to get build date epoch: %w", err)
+       }
+
+       // Create the ca-certificates directory if it doesn't exist
+       if err := bc.fs.MkdirAll(caCertsDir, 0o755); err != nil {
+               return fmt.Errorf("failed to create ca-certificates directory: 
%w", err)
+       }
+
+       // Open handles for all existing CA bundles to append to.
+       existingBundles := make([]io.WriteSeeker, 0, len(caBundlePaths))
+       for _, caBundlePath := range caBundlePaths {
+               file, err := bc.fs.OpenFile(caBundlePath, 
os.O_WRONLY|os.O_APPEND, 0o644)
+               if err != nil {
+                       if errors.Is(err, fs.ErrNotExist) {
+                               // If the bundle doesn't exist, nothing to do, 
we just ignore that.
+                               continue
+                       }
+                       return fmt.Errorf("failed to open CA bundle for 
appending: %w", err)
+               }
+               defer file.Close()
+
+               existingBundles = append(existingBundles, file)
+       }
+
+       for _, additional := range bc.ic.Certificates.Additional {
+               cert, err := parseCertificates(additional.Content)
+               if err != nil {
+                       return fmt.Errorf("failed to parse certificate %s: %w", 
additional.Name, err)
+               }
+
+               // Write individual certificate file for update-ca-certificates 
to pick up.
+               // Name is validated not to do any path shenanigans on 
configuration validation.
+               // The fingerprint is controlled to be a hash and so also 
doesn't allow shenanigans.
+               certPath := filepath.Join(caCertsDir, fmt.Sprintf("%s-%s.crt", 
additional.Name, cert.fingerprint))
+               if err := bc.fs.WriteFile(certPath, cert.pem, 0o644); err != 
nil {
+                       return fmt.Errorf("failed to write certificate file %s: 
%w", certPath, err)
+               }
+               if err := bc.fs.Chtimes(certPath, builtTime, builtTime); err != 
nil {
+                       return fmt.Errorf("failed to change times on 
certificate file %s: %w", certPath, err)
+               }
+
+               // Append to all existing bundles.
+               for _, bundle := range existingBundles {
+                       if _, err := bundle.Write(cert.pem); err != nil {
+                               return fmt.Errorf("failed to append certificate 
to bundle: %w", err)
+                       }
+                       // Put newlines in-between certificates to mimic 
update-ca-certificates behavior.
+                       if _, err := bundle.Write([]byte("\n")); err != nil {
+                               return fmt.Errorf("failed to append newline to 
bundle: %w", err)
+                       }
+               }
+       }
+
+       for _, caBundlePath := range caBundlePaths {
+               if err := bc.fs.Chtimes(caBundlePath, builtTime, builtTime); 
err != nil && !errors.Is(err, fs.ErrNotExist) {
+                       return fmt.Errorf("failed to change times on CA bundle 
%s: %w", caBundlePath, err)
+               }
+       }
+
+       return nil
+}
+
+// parseCertificates parses a string as a PEM-encoded certificate and returns
+// a parsedCertificate struct.
+func parseCertificates(pemData string) (*parsedCertificate, error) {
+       if pemData == "" {
+               return nil, fmt.Errorf("no certificate data provided")
+       }
+
+       var cert *parsedCertificate
+       rest := []byte(pemData)
+       for {
+               var block *pem.Block
+               block, rest = pem.Decode(rest)
+               if block == nil {
+                       break
+               } else if cert != nil {
+                       // More than one certificate found.
+                       return nil, fmt.Errorf("multiple certificates found; 
only one is allowed")
+               }
+
+               if block.Type != "CERTIFICATE" {
+                       return nil, fmt.Errorf("expected CERTIFICATE block, got 
%s", block.Type)
+               }
+
+               // Parse the certificate to validate it
+               _, err := x509.ParseCertificate(block.Bytes)
+               if err != nil {
+                       return nil, fmt.Errorf("failed to parse certificate: 
%w", err)
+               }
+
+               // Generate fingerprint (SHA256 hash of DER-encoded 
certificate).
+               hash := sha256.Sum256(block.Bytes)
+               fingerprint := hex.EncodeToString(hash[:])
+
+               // Re-encode to PEM. This drops any additional text from the 
original block.
+               var pemBuf bytes.Buffer
+               if err := pem.Encode(&pemBuf, block); err != nil {
+                       return nil, fmt.Errorf("failed to re-encode certificate 
to PEM: %w", err)
+               }
+
+               cert = &parsedCertificate{
+                       pem:         pemBuf.Bytes(),
+                       fingerprint: fingerprint,
+               }
+       }
+
+       if cert == nil {
+               return nil, fmt.Errorf("no certificates found in PEM data")
+       }
+       return cert, nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/certificates_test.go 
new/apko-0.30.30/pkg/build/certificates_test.go
--- old/apko-0.30.29/pkg/build/certificates_test.go     1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-0.30.30/pkg/build/certificates_test.go     2025-12-12 
12:19:29.000000000 +0100
@@ -0,0 +1,304 @@
+// Copyright 2025 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+       "context"
+       "fmt"
+       "io/fs"
+       "path/filepath"
+       "testing"
+       "time"
+
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
+       "chainguard.dev/apko/pkg/build/types"
+       "chainguard.dev/apko/pkg/options"
+
+       "github.com/google/go-cmp/cmp"
+)
+
+const (
+       // From 
https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x1.pem
+       testCertPEM = `-----BEGIN CERTIFICATE-----
+MIIFmDCCA4CgAwIBAgIQU9C87nMpOIFKYpfvOHFHFDANBgkqhkiG9w0BAQsFADBm
+MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy
+aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ
+ZWFyIFgxMB4XDTE1MDYwNDExMDQzOFoXDTM1MDYwNDExMDQzOFowZjELMAkGA1UE
+BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
+YXJjaCBHcm91cDEiMCAGA1UEAxMZKFNUQUdJTkcpIFByZXRlbmQgUGVhciBYMTCC
+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALbagEdDTa1QgGBWSYkyMhsc
+ZXENOBaVRTMX1hceJENgsL0Ma49D3MilI4KS38mtkmdF6cPWnL++fgehT0FbRHZg
+jOEr8UAN4jH6omjrbTD++VZneTsMVaGamQmDdFl5g1gYaigkkmx8OiCO68a4QXg4
+wSyn6iDipKP8utsE+x1E28SA75HOYqpdrk4HGxuULvlr03wZGTIf/oRt2/c+dYmD
+oaJhge+GOrLAEQByO7+8+vzOwpNAPEx6LW+crEEZ7eBXih6VP19sTGy3yfqK5tPt
+TdXXCOQMKAp+gCj/VByhmIr+0iNDC540gtvV303WpcbwnkkLYC0Ft2cYUyHtkstO
+fRcRO+K2cZozoSwVPyB8/J9RpcRK3jgnX9lujfwA/pAbP0J2UPQFxmWFRQnFjaq6
+rkqbNEBgLy+kFL1NEsRbvFbKrRi5bYy2lNms2NJPZvdNQbT/2dBZKmJqxHkxCuOQ
+FjhJQNeO+Njm1Z1iATS/3rts2yZlqXKsxQUzN6vNbD8KnXRMEeOXUYvbV4lqfCf8
+mS14WEbSiMy87GB5S9ucSV1XUrlTG5UGcMSZOBcEUpisRPEmQWUOTWIoDQ5FOia/
+GI+Ki523r2ruEmbmG37EBSBXdxIdndqrjy+QVAmCebyDx9eVEGOIpn26bW5LKeru
+mJxa/CFBaKi4bRvmdJRLAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
+Af8EBTADAQH/MB0GA1UdDgQWBBS182Xy/rAKkh/7PH3zRKCsYyXDFDANBgkqhkiG
+9w0BAQsFAAOCAgEAncDZNytDbrrVe68UT6py1lfF2h6Tm2p8ro42i87WWyP2LK8Y
+nLHC0hvNfWeWmjZQYBQfGC5c7aQRezak+tHLdmrNKHkn5kn+9E9LCjCaEsyIIn2j
+qdHlAkepu/C3KnNtVx5tW07e5bvIjJScwkCDbP3akWQixPpRFAsnP+ULx7k0aO1x
+qAeaAhQ2rgo1F58hcflgqKTXnpPM02intVfiVVkX5GXpJjK5EoQtLceyGOrkxlM/
+sTPq4UrnypmsqSagWV3HcUlYtDinc+nukFk6eR4XkzXBbwKajl0YjztfrCIHOn5Q
+CJL6TERVDbM/aAPly8kJ1sWGLuvvWYzMYgLzDul//rUF10gEMWaXVZV51KpS9DY/
+5CunuvCXmEQJHo7kGcViT7sETn6Jz9KOhvYcXkJ7po6d93A/jy4GKPIPnsKKNEmR
+xUuXY4xRdh45tMJnLTUDdC9FIU0flTeO9/vNpVA8OPU1i14vCz+MU8KX1bV3GXm/
+fxlB7VBBjX9v5oUep0o/j68R/iDlCOM4VVfRa8gX6T2FU7fNdatvGro7uQzIvWof
+gN9WUwCbEMBy/YhBSrXycKA8crgGg3x1mIsopn88JKwmMBa68oS7EHM9w7C4y71M
+7DiA+/9Qdp9RBWJpTS9i/mDnJg1xvo8Xz49mrrgfmcAXTCJqXi24NatI3Oc=
+-----END CERTIFICATE-----
+`
+       testCertPEMFingerprint = 
"e70570a989f8565aabdf7cae27abd1621872d6a3f811e3fef27e3dba02912198"
+
+       // From 
https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x2.pem
+       testCertPEM2 = `-----BEGIN CERTIFICATE-----
+MIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL
+MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0
+eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj
+b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE
+BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
+YXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo
+FQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT
+VqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI
+KoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo
+uc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9
+vbtH7QiVzeKCOTQPINyRql6P
+-----END CERTIFICATE-----
+`
+       testCertPEM2Fingerprint = 
"9b2a339fe6a3e85585c4cd75536cb8c1cf7cd603b9a64bec2521858ae48da85d"
+)
+
+func TestParseCertificates(t *testing.T) {
+       tests := []struct {
+               name            string
+               pemData         string
+               wantFingerprint string
+               wantPEM         []byte
+               wantErr         bool
+       }{{
+               name:            "single valid certificate",
+               pemData:         testCertPEM,
+               wantFingerprint: testCertPEMFingerprint,
+               wantPEM:         []byte(testCertPEM),
+               wantErr:         false,
+       }, {
+               name:            "single valid certificate with extra text",
+               pemData:         "extra text\n" + testCertPEM,
+               wantFingerprint: testCertPEMFingerprint,
+               wantPEM:         []byte(testCertPEM), // The extra text is 
stripped.
+               wantErr:         false,
+       }, {
+               name:    "multiple certificates",
+               pemData: testCertPEM + "\n" + testCertPEM2,
+               wantErr: true,
+       }, {
+               name:    "empty string",
+               pemData: "",
+               wantErr: true,
+       }, {
+               name:    "invalid PEM",
+               pemData: "not a certificate",
+               wantErr: true,
+       }}
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cert, err := parseCertificates(tt.pemData)
+                       if tt.wantErr {
+                               if err == nil {
+                                       t.Fatalf("expected error but got none")
+                               }
+                               return
+                       }
+
+                       if diff := cmp.Diff(tt.wantFingerprint, 
cert.fingerprint); diff != "" {
+                               t.Errorf("fingerprint mismatch (-want 
+got):\n%s", diff)
+                       }
+                       if diff := cmp.Diff(tt.wantPEM, cert.pem); diff != "" {
+                               t.Errorf("PEM data mismatch (-want +got):\n%s", 
diff)
+                       }
+               })
+       }
+}
+
+func TestInstallCertificates(t *testing.T) {
+       epoch := time.Unix(1337, 0)
+       t.Setenv("SOURCE_DATE_EPOCH", fmt.Sprintf("%d", epoch.Unix()))
+
+       tests := []struct {
+               name          string
+               cfg           *types.ImageCertificates
+               existingFiles map[string]string
+               wantFiles     map[string]string
+               wantErr       bool
+       }{{
+               name: "nil certificates config",
+               cfg:  nil,
+       }, {
+               name: "valid single certificate without existing bundle",
+               cfg: &types.ImageCertificates{
+                       Additional: []types.AdditionalCertificateEntry{
+                               {Name: "test-cert", Content: testCertPEM},
+                       },
+               },
+               existingFiles: map[string]string{},
+               wantFiles: map[string]string{
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-%s.crt", testCertPEMFingerprint)): testCertPEM,
+               },
+       }, {
+               name: "multiple certificate entries only one existing bundle",
+               cfg: &types.ImageCertificates{
+                       Additional: []types.AdditionalCertificateEntry{
+                               {Name: "test-cert-1", Content: testCertPEM},
+                               {Name: "test-cert-2", Content: testCertPEM2},
+                       },
+               },
+               existingFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n",
+               },
+               wantFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n" + 
testCertPEM + "\n" + testCertPEM2 + "\n",
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-1-%s.crt", testCertPEMFingerprint)):  testCertPEM,
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-2-%s.crt", testCertPEM2Fingerprint)): testCertPEM2,
+               },
+       }, {
+               name: "multiple certificate entries with multiple existing 
bundles",
+               cfg: &types.ImageCertificates{
+                       Additional: []types.AdditionalCertificateEntry{
+                               {Name: "test-cert-1", Content: testCertPEM},
+                               {Name: "test-cert-2", Content: testCertPEM2},
+                       },
+               },
+               existingFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n",
+                       caBundlePaths[1]: "# Another CA Bundle\n",
+               },
+               wantFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n" + 
testCertPEM + "\n" + testCertPEM2 + "\n",
+                       caBundlePaths[1]: "# Another CA Bundle\n" + testCertPEM 
+ "\n" + testCertPEM2 + "\n",
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-1-%s.crt", testCertPEMFingerprint)):  testCertPEM,
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-2-%s.crt", testCertPEM2Fingerprint)): testCertPEM2,
+               },
+       }, {
+               name: "multiple certificate entries with identical names",
+               cfg: &types.ImageCertificates{
+                       Additional: []types.AdditionalCertificateEntry{
+                               {Name: "test-cert", Content: testCertPEM},
+                               {Name: "test-cert", Content: testCertPEM2},
+                       },
+               },
+               existingFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n",
+               },
+               wantFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n" + 
testCertPEM + "\n" + testCertPEM2 + "\n",
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-%s.crt", testCertPEMFingerprint)):  testCertPEM,
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-%s.crt", testCertPEM2Fingerprint)): testCertPEM2,
+               },
+       }, {
+               name: "certificate with additional metadata",
+               cfg: &types.ImageCertificates{
+                       Additional: []types.AdditionalCertificateEntry{
+                               {Name: "test-cert", Content: "additional 
text\n" + testCertPEM},
+                       },
+               },
+               existingFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n",
+               },
+               wantFiles: map[string]string{
+                       caBundlePaths[0]: "# Existing CA Bundle\n" + 
testCertPEM + "\n",
+                       filepath.Join(caCertsDir, 
fmt.Sprintf("test-cert-%s.crt", testCertPEMFingerprint)): testCertPEM,
+               },
+       }}
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       fsys := apkfs.NewMemFS()
+                       bc := &Context{
+                               o: options.Options{
+                                       SourceDateEpoch: epoch,
+                               },
+                               ic: types.ImageConfiguration{
+                                       Certificates: tt.cfg,
+                               },
+                               fs: fsys,
+                       }
+
+                       for path, content := range tt.existingFiles {
+                               if err := fsys.MkdirAll(filepath.Dir(path), 
0o755); err != nil {
+                                       t.Fatalf("failed to create directory 
for existing file %s: %v", path, err)
+                               }
+                               if err := fsys.WriteFile(path, []byte(content), 
0o644); err != nil {
+                                       t.Fatalf("failed to write existing file 
%s: %v", path, err)
+                               }
+                       }
+
+                       err := bc.installCertificates(context.Background())
+                       if (err != nil) != tt.wantErr {
+                               t.Fatalf("installCertificates() error = %v, 
wantErr %v", err, tt.wantErr)
+                       }
+                       if tt.wantErr {
+                               // Expected error, nothing further to check
+                               return
+                       }
+                       if tt.cfg == nil || len(tt.cfg.Additional) == 0 {
+                               // Nothing further to check
+                               return
+                       }
+
+                       // Walk the entire filesystem to ensure we're checking 
contents for all
+                       // expected files.
+                       fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, 
err error) error {
+                               if err != nil {
+                                       t.Fatalf("error walking to %s: %v", 
path, err)
+                               }
+                               if d.IsDir() {
+                                       return nil
+                               }
+
+                               wantContent, ok := tt.wantFiles[path]
+                               if !ok {
+                                       t.Errorf("unexpected file created: %s", 
path)
+                                       return nil
+                               }
+
+                               data, err := fsys.ReadFile(path)
+                               if err != nil {
+                                       t.Fatalf("failed to read expected file 
%s: %v", path, err)
+                               }
+                               gotContent := string(data)
+                               if diff := cmp.Diff(wantContent, gotContent); 
diff != "" {
+                                       t.Errorf("file content mismatch for %s 
(-want +got):\n%s", path, diff)
+                               }
+
+                               stat, err := fsys.Stat(path)
+                               if err != nil {
+                                       t.Fatalf("failed to stat file %s: %v", 
path, err)
+                               }
+                               modTime := stat.ModTime()
+                               if !modTime.Equal(epoch) {
+                                       t.Errorf("file %s has mod time %v, want 
%v", path, modTime, epoch)
+                               }
+                               return nil
+                       })
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/types/image_configuration.go 
new/apko-0.30.30/pkg/build/types/image_configuration.go
--- old/apko-0.30.29/pkg/build/types/image_configuration.go     2025-12-04 
15:43:00.000000000 +0100
+++ new/apko-0.30.30/pkg/build/types/image_configuration.go     2025-12-12 
12:19:29.000000000 +0100
@@ -21,6 +21,7 @@
        "maps"
        "os"
        "reflect"
+       "regexp"
        "slices"
        "strings"
 
@@ -33,6 +34,10 @@
        "chainguard.dev/apko/pkg/vcs"
 )
 
+// Regex for valid certificate names. Since the name of the certificate is used
+// as a filename, we restrict it to a safe subset of characters.
+var certNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
+
 // Attempt to probe an upstream VCS URL if known.
 func (ic *ImageConfiguration) ProbeVCSUrl(ctx context.Context, imageConfigPath 
string) {
        log := clog.FromContext(ctx)
@@ -121,6 +126,9 @@
        if target.Layering == nil {
                target.Layering = ic.Layering
        }
+       if target.Certificates == nil {
+               target.Certificates = ic.Certificates
+       }
        if len(target.Archs) == 0 {
                target.Archs = ic.Archs
        }
@@ -223,6 +231,17 @@
                        return fmt.Errorf("configured group %v has no 
configured group name", g)
                }
        }
+
+       if ic.Certificates != nil {
+               for _, additional := range ic.Certificates.Additional {
+                       if additional.Name == "" {
+                               return fmt.Errorf("configured additional 
certificate has no name")
+                       }
+                       if !certNameRegex.MatchString(additional.Name) {
+                               return fmt.Errorf("configured additional 
certificate %q has an invalid name, it must match %s", additional.Name, 
certNameRegex.String())
+                       }
+               }
+       }
        return nil
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apko-0.30.29/pkg/build/types/image_configuration_test.go 
new/apko-0.30.30/pkg/build/types/image_configuration_test.go
--- old/apko-0.30.29/pkg/build/types/image_configuration_test.go        
2025-12-04 15:43:00.000000000 +0100
+++ new/apko-0.30.30/pkg/build/types/image_configuration_test.go        
2025-12-12 12:19:29.000000000 +0100
@@ -93,6 +93,12 @@
                        Volumes: []string{
                                "volume1",
                        },
+                       Certificates: &types.ImageCertificates{
+                               Additional: []types.AdditionalCertificateEntry{{
+                                       Name:    "foo",
+                                       Content: "bar",
+                               }},
+                       },
                },
                target: types.ImageConfiguration{
                        Contents: types.ImageContents{
@@ -120,6 +126,12 @@
                        Volumes: []string{
                                "volume1",
                        },
+                       Certificates: &types.ImageCertificates{
+                               Additional: []types.AdditionalCertificateEntry{{
+                                       Name:    "foo",
+                                       Content: "bar",
+                               }},
+                       },
                },
        }, {
                name: "simple blend of contents",
@@ -240,3 +252,51 @@
                })
        }
 }
+
+func TestValidate(t *testing.T) {
+       tests := []struct {
+               name          string
+               configuration types.ImageConfiguration
+               expectError   string
+       }{{
+               name: "no cert name",
+               configuration: types.ImageConfiguration{
+                       Certificates: &types.ImageCertificates{
+                               Additional: []types.AdditionalCertificateEntry{{
+                                       Name:    "",
+                                       Content: "test",
+                               }},
+                       },
+               },
+               expectError: "configured additional certificate has no name",
+       }, {
+               name: "path walking cert name",
+               configuration: types.ImageConfiguration{
+                       Certificates: &types.ImageCertificates{
+                               Additional: []types.AdditionalCertificateEntry{{
+                                       Name:    "trying/../../../to/break",
+                                       Content: "test",
+                               }},
+                       },
+               },
+               expectError: `configured additional certificate 
"trying/../../../to/break" has an invalid name, it must match ^[a-zA-Z0-9_-]+$`,
+       }, {
+               name: "weird characters cert name",
+               configuration: types.ImageConfiguration{
+                       Certificates: &types.ImageCertificates{
+                               Additional: []types.AdditionalCertificateEntry{{
+                                       Name:    "my-cert@123!",
+                                       Content: "test",
+                               }},
+                       },
+               },
+               expectError: `configured additional certificate "my-cert@123!" 
has an invalid name, it must match ^[a-zA-Z0-9_-]+$`,
+       }}
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := tt.configuration.Validate()
+                       require.EqualError(t, err, tt.expectError)
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/types/schema.json 
new/apko-0.30.30/pkg/build/types/schema.json
--- old/apko-0.30.29/pkg/build/types/schema.json        2025-12-04 
15:43:00.000000000 +0100
+++ new/apko-0.30.30/pkg/build/types/schema.json        2025-12-12 
12:19:29.000000000 +0100
@@ -3,6 +3,20 @@
   "$id": "https://chainguard.dev/apko/pkg/build/types/image-configuration";,
   "$ref": "#/$defs/ImageConfiguration",
   "$defs": {
+    "AdditionalCertificateEntry": {
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Required: Name of the certificate entry"
+        },
+        "content": {
+          "type": "string",
+          "description": "Required: PEM-encoded certificate content to install 
in the image.\nMust contain exactly one certificate.\nThe certificate will 
be:\n1. Appended to the default certificate bundles (e.g., 
/etc/ssl/certs/ca-certificates.crt)\n2. Installed as an individual file in the 
ca-certificates."
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
     "BaseImageDescriptor": {
       "properties": {
         "image": {
@@ -62,6 +76,19 @@
       "additionalProperties": false,
       "type": "object"
     },
+    "ImageCertificates": {
+      "properties": {
+        "additional": {
+          "items": {
+            "$ref": "#/$defs/AdditionalCertificateEntry"
+          },
+          "type": "array",
+          "description": "Additional certificates to install in the image"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
     "ImageConfiguration": {
       "properties": {
         "contents": {
@@ -134,6 +161,10 @@
         "layering": {
           "$ref": "#/$defs/Layering",
           "description": "Optional: Configuration to control layering of the 
OCI image."
+        },
+        "certificates": {
+          "$ref": "#/$defs/ImageCertificates",
+          "description": "Optional: Certificates to install in the container 
image"
         }
       },
       "additionalProperties": false,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-0.30.29/pkg/build/types/types.go 
new/apko-0.30.30/pkg/build/types/types.go
--- old/apko-0.30.29/pkg/build/types/types.go   2025-12-04 15:43:00.000000000 
+0100
+++ new/apko-0.30.30/pkg/build/types/types.go   2025-12-12 12:19:29.000000000 
+0100
@@ -219,6 +219,9 @@
 
        // Optional: Configuration to control layering of the OCI image.
        Layering *Layering `json:"layering,omitempty" yaml:"layering,omitempty"`
+
+       // Optional: Certificates to install in the container image
+       Certificates *ImageCertificates `json:"certificates,omitempty" 
yaml:"certificates,omitempty"`
 }
 
 // Architecture represents a CPU architecture for the container image.
@@ -436,3 +439,19 @@
        Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
        Budget   int    `json:"budget,omitempty" yaml:"budget,omitempty"`
 }
+
+type AdditionalCertificateEntry struct {
+       // Required: Name of the certificate entry
+       Name string `json:"name,omitempty" yaml:"name,omitempty"`
+       // Required: PEM-encoded certificate content to install in the image.
+       // Must contain exactly one certificate.
+       // The certificate will be:
+       // 1. Appended to the default certificate bundles (e.g., 
/etc/ssl/certs/ca-certificates.crt)
+       // 2. Installed as an individual file in the ca-certificates.
+       Content string `json:"content,omitempty" yaml:"content,omitempty"`
+}
+
+type ImageCertificates struct {
+       // Additional certificates to install in the image
+       Additional []AdditionalCertificateEntry `json:"additional,omitempty" 
yaml:"additional,omitempty"`
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.OjmvDH/_old  2025-12-12 21:44:03.164109765 +0100
+++ /var/tmp/diff_new_pack.OjmvDH/_new  2025-12-12 21:44:03.172110103 +0100
@@ -1,5 +1,5 @@
 name: apko
-version: 0.30.29
-mtime: 1764859380
-commit: 0b3827aa83414219539e3cfb82cd07f7972a1950
+version: 0.30.30
+mtime: 1765538369
+commit: d2cb85c1f37e24120b415d4e4372a1b8f811f8e4
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/apko/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.apko.new.1939/vendor.tar.gz differ: char 93, line 2

Reply via email to