Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package apko for openSUSE:Factory checked in 
at 2026-10-01 16:46:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.1253 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Thu Oct  1 16:46:38 2026 rev:139 rq:1381771 version:1.4.6

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-09-28 
10:48:47.958851662 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.1253/apko.changes      2026-10-01 
16:47:29.053145110 +0200
@@ -1,0 +2,15 @@
+Thu Oct 01 04:59:24 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.4.6:
+  * build(deps): bump google.golang.org/api from 0.298.0 to 0.299.0
+    (#2524)
+  * fix: pin libssl3 in old-glibc example (#2527)
+  * build(deps): bump k8s.io/apimachinery from 0.37.0 to 0.37.1
+    (#2530)
+  * build(deps): bump github.com/klauspost/compress from 1.20.0 to
+    1.20.1 (#2531)
+  * build(deps): bump chainguard.dev/sdk from 0.1.278 to 0.1.293
+    (#2537)
+  * build(deps): bump the codeql group with 2 updates (#2533)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.4.5.obscpio

New:
----
  apko-1.4.6.obscpio

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

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.Jc31Qi/_old  2026-10-01 16:47:30.740215820 +0200
+++ /var/tmp/diff_new_pack.Jc31Qi/_new  2026-10-01 16:47:30.742215903 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.4.5
+Version:        1.4.6
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.Jc31Qi/_old  2026-10-01 16:47:30.788217831 +0200
+++ /var/tmp/diff_new_pack.Jc31Qi/_new  2026-10-01 16:47:30.792217999 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/apko.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v1.4.5</param>
+    <param name="revision">refs/tags/v1.4.6</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.Jc31Qi/_old  2026-10-01 16:47:30.829219550 +0200
+++ /var/tmp/diff_new_pack.Jc31Qi/_new  2026-10-01 16:47:30.834219759 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/chainguard-dev/apko</param>
               <param 
name="changesrevision">861f83f69e6fa9114405a2f7bb5cf6585ad00421</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/apko.git</param>
-              <param 
name="changesrevision">739e7ce3f675ffb232e16849b51ff93657f1570c</param></service></servicedata>
+              <param 
name="changesrevision">51c298aa5249f43193e3cb8684a2a60facfcb69d</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.4.5.obscpio -> apko-1.4.6.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/examples/old-glibc.yaml 
new/apko-1.4.6/examples/old-glibc.yaml
--- old/apko-1.4.5/examples/old-glibc.yaml      2026-09-22 00:49:38.000000000 
+0200
+++ new/apko-1.4.6/examples/old-glibc.yaml      2026-09-30 14:19:13.000000000 
+0200
@@ -11,6 +11,7 @@
     - openssl=3.0.7-r0
     - sysstat=12.6.2-r0
     - libcrypto3=3.0.8-r0
+    - libssl3=3.6.4-r7
 
 archs:
   - x86_64
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/go.mod new/apko-1.4.6/go.mod
--- old/apko-1.4.5/go.mod       2026-09-22 00:49:38.000000000 +0200
+++ new/apko-1.4.6/go.mod       2026-09-30 14:19:13.000000000 +0200
@@ -3,10 +3,11 @@
 go 1.27.0
 
 require (
-       chainguard.dev/sdk v0.1.278
+       chainguard.dev/sdk v0.1.293
        github.com/chainguard-dev/clog v1.8.1
        github.com/charmbracelet/log v1.0.0
        github.com/erofs/go-erofs v0.3.2-0.20260804074615-52cc42c5291c
+       github.com/github/go-spdx/v2 v2.7.0
        github.com/go-git/go-git/v5 v5.19.2
        github.com/google/go-cmp v0.7.0
        github.com/google/go-containerregistry v0.22.1
@@ -15,7 +16,7 @@
        github.com/hashicorp/go-retryablehttp v0.7.8
        github.com/hashicorp/golang-lru/v2 v2.0.7
        github.com/invopop/jsonschema v0.14.0
-       github.com/klauspost/compress v1.20.0
+       github.com/klauspost/compress v1.20.1
        github.com/klauspost/pgzip v1.2.6
        github.com/package-url/packageurl-go v0.1.7
        github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
@@ -34,18 +35,18 @@
        golang.org/x/sys v0.48.0
        golang.org/x/term v0.46.0
        golang.org/x/time v0.16.0
-       google.golang.org/api v0.298.0
+       google.golang.org/api v0.299.0
        gopkg.in/ini.v1 v1.67.3
        gopkg.in/yaml.v3 v3.0.1
-       k8s.io/apimachinery v0.37.0
+       k8s.io/apimachinery v0.37.1
        sigs.k8s.io/release-utils v0.12.4
 )
 
 require (
        chainguard.dev/go-grpc-kit v0.20.0 // indirect
-       cloud.google.com/go/auth v0.23.2 // indirect
+       cloud.google.com/go/auth v0.23.3 // indirect
        cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
-       cloud.google.com/go/compute/metadata v0.9.0 // indirect
+       cloud.google.com/go/compute/metadata v0.9.1 // indirect
        dario.cat/mergo v1.0.2 // indirect
        filippo.io/edwards25519 v1.2.0 // indirect
        github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -83,9 +84,9 @@
        github.com/go-logr/logr v1.4.4 // indirect
        github.com/go-logr/stdr v1.2.2 // indirect
        github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 
indirect
-       github.com/google/s2a-go v0.1.9 // indirect
+       github.com/google/s2a-go v0.1.10 // indirect
        github.com/google/uuid v1.6.0 // indirect
-       github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
+       github.com/googleapis/enterprise-certificate-proxy v0.3.22 // indirect
        github.com/googleapis/gax-go/v2 v2.24.1 // indirect
        github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus 
v1.1.0 // indirect
        github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 // indirect
@@ -135,8 +136,8 @@
        golang.org/x/net v0.59.0 // indirect
        golang.org/x/text v0.42.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260803160001-6ac0973c030d // indirect
-       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260825221802-da73d73af1c5 // indirect
-       google.golang.org/grpc v1.83.2 // indirect
+       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260921155816-b14227669459 // indirect
+       google.golang.org/grpc v1.84.0 // indirect
        google.golang.org/protobuf v1.36.12 // indirect
        gopkg.in/warnings.v0 v0.1.2 // indirect
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/go.sum new/apko-1.4.6/go.sum
--- old/apko-1.4.5/go.sum       2026-09-22 00:49:38.000000000 +0200
+++ new/apko-1.4.6/go.sum       2026-09-30 14:19:13.000000000 +0200
@@ -1,13 +1,13 @@
 chainguard.dev/go-grpc-kit v0.20.0 
h1:MDwZtTlUlMSMEPcoi9m8tnu4g+M88T1toUZ3Th/+0Gc=
 chainguard.dev/go-grpc-kit v0.20.0/go.mod 
h1:ocuwyRX9tqRRvJkS3/ZkDYGMnl+6FLC4bvF0cm6DGsk=
-chainguard.dev/sdk v0.1.278 h1:vM5AkOAlrhMXpEQumITidz+w0nQxNIYAXbyPN5nqD6o=
-chainguard.dev/sdk v0.1.278/go.mod 
h1:qdToj7HJ0neJdut3yLmYC1TIfF6sp5RPmlFXgZ4r1kA=
-cloud.google.com/go/auth v0.23.2 
h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
-cloud.google.com/go/auth v0.23.2/go.mod 
h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
+chainguard.dev/sdk v0.1.293 h1:g2cSwqFulEqLYN6jB9pRkVdXoLttq1CHx+LgmMRRGcU=
+chainguard.dev/sdk v0.1.293/go.mod 
h1:fDIxqEbOn4iyGhS1ggkdcbrePUTBCJljMzH2XlTWwS4=
+cloud.google.com/go/auth v0.23.3 
h1:UMK+oBtuNGMCR/6i6mmySUItqjOazpJrbmZyhGbGBWo=
+cloud.google.com/go/auth v0.23.3/go.mod 
h1:fClbry28fo7XkxhSeT6AQtAVAp6Jy0fW9N99PoPNPFM=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 
h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod 
h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.9.0 
h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
-cloud.google.com/go/compute/metadata v0.9.0/go.mod 
h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+cloud.google.com/go/compute/metadata v0.9.1 
h1:CTE1OWBQ0vnF5uHwdFAQJvMQ0Fi/KRcqqKTo9V0F8Ik=
+cloud.google.com/go/compute/metadata v0.9.1/go.mod 
h1:NtnlvB6X3t4R6xSWyVX/ZWk493PCxGQlhI/iqxh4M8I=
 dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
@@ -86,6 +86,8 @@
 github.com/fatih/color v1.19.0/go.mod 
h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
 github.com/felixge/httpsnoop v1.1.0 
h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
 github.com/felixge/httpsnoop v1.1.0/go.mod 
h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
+github.com/github/go-spdx/v2 v2.7.0 
h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
+github.com/github/go-spdx/v2 v2.7.0/go.mod 
h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
 github.com/gliderlabs/ssh v0.3.8 
h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod 
h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 
h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -115,14 +117,14 @@
 github.com/google/go-cmp v0.7.0/go.mod 
h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-containerregistry v0.22.1 
h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
 github.com/google/go-containerregistry v0.22.1/go.mod 
h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
-github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
-github.com/google/s2a-go v0.1.9/go.mod 
h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/s2a-go v0.1.10 
h1:EMp+aOuXN6l8cE/gjF5Bt+vyZxsUuyCWe9chDWR/+uU=
+github.com/google/s2a-go v0.1.10/go.mod 
h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 
h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod 
h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.20 
h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
-github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod 
h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
+github.com/googleapis/enterprise-certificate-proxy v0.3.22 
h1:NU4XpII6jD+Dxcot94fqjE+AfJoE/lQP9q3faYGzC/c=
+github.com/googleapis/enterprise-certificate-proxy v0.3.22/go.mod 
h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
 github.com/googleapis/gax-go/v2 v2.24.1 
h1:AtqTN21IXMMWo99LiEVAiBfNNQmO40d8xUfZI640mc0=
 github.com/googleapis/gax-go/v2 v2.24.1/go.mod 
h1:bWeBei0NVwaNZKb2y1HUBS7gLXIF3/Tu3pq7j8D2Tb0=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 
h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
@@ -149,8 +151,8 @@
 github.com/kelseyhightower/envconfig v1.4.0/go.mod 
h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kevinburke/ssh_config v1.6.0 
h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
 github.com/kevinburke/ssh_config v1.6.0/go.mod 
h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
-github.com/klauspost/compress v1.20.0 
h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
-github.com/klauspost/compress v1.20.0/go.mod 
h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
+github.com/klauspost/compress v1.20.1 
h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
+github.com/klauspost/compress v1.20.1/go.mod 
h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
 github.com/klauspost/cpuid/v2 v2.4.0 
h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
 github.com/klauspost/cpuid/v2 v2.4.0/go.mod 
h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
 github.com/klauspost/pgzip v1.2.6 
h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -356,16 +358,16 @@
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod 
h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/api v0.298.0 h1:YW18RkHBMZBA1ergX0m4biagzgbiPTb2uTsRsDPWNRY=
-google.golang.org/api v0.298.0/go.mod 
h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
+google.golang.org/api v0.299.0 h1:b3K+ydSMd0kh6TQI6bJyApRQfqQX2MfSOaVkpM59mJw=
+google.golang.org/api v0.299.0/go.mod 
h1:zlR3GVA8b2R5nv5Ij9UWe37StVB3cxDD7DBFi4ZFsHw=
 google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d 
h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
 google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod 
h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
 google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d 
h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
 google.golang.org/genproto/googleapis/api 
v0.0.0-20260803160001-6ac0973c030d/go.mod 
h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 
h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
-google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260825221802-da73d73af1c5/go.mod 
h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
-google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
-google.golang.org/grpc v1.83.2/go.mod 
h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 
h1:b0xCahf3FK2m2Cv0p4vTozGPWncCvLfwV86UNg8xWU8=
+google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260921155816-b14227669459/go.mod 
h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
+google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
+google.golang.org/grpc v1.84.0/go.mod 
h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
 google.golang.org/protobuf v1.36.12 
h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
 google.golang.org/protobuf v1.36.12/go.mod 
h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -383,8 +385,8 @@
 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.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
-k8s.io/apimachinery v0.37.0/go.mod 
h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
+k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE=
+k8s.io/apimachinery v0.37.1/go.mod 
h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw=
 pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
 pgregory.net/rapid v1.2.0/go.mod 
h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
 sigs.k8s.io/release-utils v0.12.4 
h1:kuG6WTWGCKx5uUrJwl2uFErOKOw+4Ba8WrPmOQh5J3g=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/pkg/apk/expandapk/expandapk.go 
new/apko-1.4.6/pkg/apk/expandapk/expandapk.go
--- old/apko-1.4.5/pkg/apk/expandapk/expandapk.go       2026-09-22 
00:49:38.000000000 +0200
+++ new/apko-1.4.6/pkg/apk/expandapk/expandapk.go       2026-09-30 
14:19:13.000000000 +0200
@@ -31,7 +31,7 @@
 )
 
 var readerPool = sync.Pool{
-       New: func() interface{} {
+       New: func() any {
                return bufio.NewReaderSize(nil, 1<<20)
        },
 }
@@ -43,7 +43,7 @@
 }
 
 var writerPool = sync.Pool{
-       New: func() interface{} {
+       New: func() any {
                return bufio.NewWriterSize(nil, 1<<20)
        },
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apko-1.4.5/pkg/sbom/generator/spdx/internal_sbom_test.go 
new/apko-1.4.6/pkg/sbom/generator/spdx/internal_sbom_test.go
--- old/apko-1.4.5/pkg/sbom/generator/spdx/internal_sbom_test.go        
1970-01-01 01:00:00.000000000 +0100
+++ new/apko-1.4.6/pkg/sbom/generator/spdx/internal_sbom_test.go        
2026-09-30 14:19:13.000000000 +0200
@@ -0,0 +1,358 @@
+// Copyright 2026 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 spdx
+
+import (
+       "archive/tar"
+       "encoding/json"
+       "io/fs"
+       "os"
+       "path/filepath"
+       "slices"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+
+       "chainguard.dev/apko/pkg/apk/apk"
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
+)
+
+// installed returns an installed package whose database entry lists owns.
+func installed(name, version string, owns ...string) *apk.InstalledPackage {
+       ipkg := &apk.InstalledPackage{Name: name, Version: version, Arch: 
"x86_64", License: "MIT"}
+       for _, p := range owns {
+               ipkg.Files = append(ipkg.Files, tar.Header{Name: p})
+       }
+       return ipkg
+}
+
+func sbomAt(nameVersion string) string {
+       return "var/lib/db/sbom/" + nameVersion + ".spdx.json"
+}
+
+func apkRef(name, version string) ExternalRef {
+       return ExternalRef{
+               Category: "PACKAGE_MANAGER",
+               Type:     "purl",
+               Locator:  "pkg:apk/wolfi/" + name + "@" + version + 
"?arch=x86_64",
+       }
+}
+
+func record(name, version string, refs ...ExternalRef) Package {
+       return Package{
+               ID:           stringToIdentifier("SPDXRef-Package-" + name + 
"-" + version),
+               Name:         name,
+               Version:      version,
+               ExternalRefs: refs,
+       }
+}
+
+// fixture reads a real apk SBOM from testdata.
+func fixture(t *testing.T, name string) []byte {
+       t.Helper()
+       b, err := os.ReadFile(filepath.Join("testdata", "apk_sboms", name))
+       require.NoError(t, err)
+       return b
+}
+
+// internalSBOM encodes a document that describes primary and reaches each of 
reachable from it.
+func internalSBOM(t *testing.T, primary Package, reachable ...Package) []byte {
+       t.Helper()
+       doc := Document{
+               ID:                "SPDXRef-DOCUMENT",
+               DocumentDescribes: []string{primary.ID},
+               Packages:          append([]Package{primary}, reachable...),
+       }
+       for _, r := range reachable {
+               doc.Relationships = append(doc.Relationships, Relationship{
+                       Element: primary.ID, Type: "GENERATED_FROM", Related: 
r.ID,
+               })
+       }
+       b, err := json.Marshal(doc)
+       require.NoError(t, err)
+       return b
+}
+
+func TestInternalSBOMIdentity(t *testing.T) {
+       source := Package{
+               ID:      "SPDXRef-Package-github.com-example-foo-v1.0.0",
+               Name:    "foo-src",
+               Version: "v1.0.0",
+               ExternalRefs: []ExternalRef{{
+                       Category: "PACKAGE_MANAGER", Type: "purl", Locator: 
"pkg:github/example/[email protected]",
+               }, {
+                       Category: "SECURITY", Type: "cpe23Type", Locator: 
"cpe:2.3:a:example:foo:1.0.0:*:*:*:*:*:*:*",
+               }},
+               Checksums: []Checksum{{Algorithm: "SHA1", Value: "a1"}, 
{Algorithm: "SHA256", Value: "b2"}},
+       }
+       // Builds of one upstream source can disagree on its license or 
reference order.
+       relicensed := source
+       relicensed.LicenseDeclared = "BSD-3-Clause"
+       reordered := source
+       reordered.ExternalRefs = slices.Clone(source.ExternalRefs)
+       slices.Reverse(reordered.ExternalRefs)
+       reordered.Checksums = slices.Clone(source.Checksums)
+       slices.Reverse(reordered.Checksums)
+       repurled := source
+       repurled.ExternalRefs = []ExternalRef{{
+               Category: "PACKAGE_MANAGER", Type: "purl", Locator: 
"pkg:github/example/[email protected]",
+       }}
+       rehashed := source
+       rehashed.Checksums = []Checksum{{Algorithm: "SHA256", Value: "c3"}}
+       foo := record("foo", "1.0.0-r0", apkRef("foo", "1.0.0-r0"))
+       openssl := record("openssl", "3.0.1-r0", apkRef("openssl", "3.0.1-r0"))
+
+       for _, tt := range []struct {
+               name    string
+               pkgs    []*apk.InstalledPackage
+               files   map[string][]byte
+               present []string // name@version entries the image SBOM must 
hold
+               absent  []string // name@version entries it must not hold
+               purls   []string // PURLs it must hold
+               wantErr string
+       }{{
+               name:    "matching SBOM is copied with its reachable packages",
+               pkgs:    []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files:   map[string][]byte{sbomAt("foo-1.0.0-r0"): 
internalSBOM(t, foo, source)},
+               present: []string{"[email protected]", "[email protected]"},
+       }, {
+               name:    "SBOM version without the epoch is accepted",
+               pkgs:    []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files:   map[string][]byte{sbomAt("foo-1.0.0-r0"): 
internalSBOM(t, record("foo", "1.0.0"))},
+               present: []string{"[email protected]"},
+       }, {
+               name: "SBOM cataloging the package's own apk entry is accepted",
+               pkgs: []*apk.InstalledPackage{
+                       installed("wolfi-baselayout", "20230201-r30", 
sbomAt("wolfi-baselayout-20230201-r30")),
+               },
+               files: map[string][]byte{
+                       sbomAt("wolfi-baselayout-20230201-r30"): fixture(t, 
"wolfi-baselayout-20230201-r30.spdx.json"),
+               },
+               present: []string{"wolfi-baselayout@20230201-r30"},
+               purls: []string{
+                       
"pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64&origin=wolfi-baselayout",
+               },
+       }, {
+               name:    "package without an SBOM is described from the 
installed database",
+               pkgs:    []*apk.InstalledPackage{installed("no-sbom", 
"1.0.0-r0", "usr/bin/no-sbom")},
+               present: []string{"[email protected]"},
+               purls:   
[]string{"pkg:apk/unknown/[email protected]?arch=x86_64"},
+       }, {
+               name: "SBOM owned by another package is ignored",
+               pkgs: []*apk.InstalledPackage{
+                       installed("busybox", "1.36.1-r0", "bin/busybox"),
+                       installed("planter", "1.0.0-r0", 
sbomAt("planter-1.0.0-r0"), sbomAt("busybox-1.36.1-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("planter-1.0.0-r0"):  internalSBOM(t, 
record("planter", "1.0.0-r0")),
+                       sbomAt("busybox-1.36.1-r0"): internalSBOM(t, 
record("busybox", "9.9.9-r0")),
+               },
+               present: []string{"[email protected]", "[email protected]"},
+               absent:  []string{"[email protected]"},
+       }, {
+               name: "SBOM describing another package fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("evil-helper", "1.0.0-r0", 
sbomAt("evil-helper-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("evil-helper-1.0.0-r0"): internalSBOM(t,
+                               record("openssl", "9.9.9-r0", apkRef("openssl", 
"9.9.9-r0"))),
+               },
+               wantErr: "evil-helper",
+       }, {
+               name:    "SBOM describing another version fails",
+               pkgs:    []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files:   map[string][]byte{sbomAt("foo-1.0.0-r0"): 
internalSBOM(t, record("foo", "2.0.0-r0"))},
+               wantErr: "foo",
+       }, {
+               name: "described apk PURL naming another package fails",
+               pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t,
+                               record("foo", "1.0.0-r0", apkRef("openssl", 
"3.0.1-r0"))),
+               },
+               wantErr: "foo",
+       }, {
+               name: "reachable package with another package's apk PURL fails",
+               pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo,
+                               record("openssl", "9.9.9-r0", apkRef("openssl", 
"9.9.9-r0"))),
+               },
+               wantErr: "foo",
+       }, {
+               name: "reachable package named for the package with another apk 
PURL fails",
+               pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, Package{
+                               ID: "SPDXRef-Package-apk-foo-0123", Name: 
"foo", Version: "1.0.0-r0",
+                               ExternalRefs: []ExternalRef{apkRef("openssl", 
"3.0.1-r0")},
+                       }),
+               },
+               wantErr: "foo",
+       }, {
+               name: "reachable package using an installed package's SPDX ID 
fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("aaa", "1.0.0-r0", sbomAt("aaa-1.0.0-r0")),
+                       installed("openssl", "3.0.1-r0", 
sbomAt("openssl-3.0.1-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("aaa-1.0.0-r0"): internalSBOM(t, record("aaa", 
"1.0.0-r0"),
+                               Package{ID: openssl.ID, Name: "openssl-src", 
Version: "3.5.0"}),
+                       sbomAt("openssl-3.0.1-r0"): internalSBOM(t, openssl),
+               },
+               wantErr: "aaa",
+       }, {
+               name: "described package using another installed package's SPDX 
ID fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("aaa", "1.0.0-r0", sbomAt("aaa-1.0.0-r0")),
+                       installed("openssl", "3.0.1-r0", 
sbomAt("openssl-3.0.1-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("aaa-1.0.0-r0"): internalSBOM(t,
+                               Package{ID: openssl.ID, Name: "aaa", Version: 
"1.0.0-r0"}),
+                       sbomAt("openssl-3.0.1-r0"): internalSBOM(t, openssl),
+               },
+               wantErr: "aaa",
+       }, {
+               name: "shared SPDX ID with a different license is kept once",
+               pkgs: []*apk.InstalledPackage{
+                       installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")),
+                       installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source),
+                       sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", 
"1.0.0-r0"), relicensed),
+               },
+               present: []string{"[email protected]", "[email protected]"},
+       }, {
+               name: "shared SPDX ID with reordered references and checksums 
is kept once",
+               pkgs: []*apk.InstalledPackage{
+                       installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")),
+                       installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source),
+                       sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", 
"1.0.0-r0"), reordered),
+               },
+               present: []string{"[email protected]", "[email protected]"},
+       }, {
+               name: "shared SPDX ID with a different version fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")),
+                       installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source),
+                       sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", 
"1.0.0-r0"),
+                               Package{ID: source.ID, Name: "foo-src", 
Version: "v9.9.9"}),
+               },
+               wantErr: source.ID,
+       }, {
+               name: "shared SPDX ID with a different PURL fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")),
+                       installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source),
+                       sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", 
"1.0.0-r0"), repurled),
+               },
+               wantErr: source.ID,
+       }, {
+               name: "shared SPDX ID with a different checksum fails",
+               pkgs: []*apk.InstalledPackage{
+                       installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")),
+                       installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")),
+               },
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source),
+                       sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", 
"1.0.0-r0"), rehashed),
+               },
+               wantErr: source.ID,
+       }, {
+               name:    "malformed SBOM fails",
+               pkgs:    []*apk.InstalledPackage{installed("broken", 
"1.0.0-r0", sbomAt("broken-1.0.0-r0"))},
+               files:   map[string][]byte{sbomAt("broken-1.0.0-r0"): 
[]byte("{not json")},
+               wantErr: "broken",
+       }, {
+               name: "SBOM that describes nothing fails",
+               pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): 
[]byte(`{"SPDXID":"SPDXRef-DOCUMENT","packages":[` +
+                               
`{"SPDXID":"SPDXRef-Package-foo-1.0.0-r0","name":"foo","versionInfo":"1.0.0-r0"}]}`),
+               },
+               wantErr: "foo",
+       }, {
+               name: "SBOM describing a missing element fails",
+               pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", 
sbomAt("foo-1.0.0-r0"))},
+               files: map[string][]byte{
+                       sbomAt("foo-1.0.0-r0"): 
[]byte(`{"SPDXID":"SPDXRef-DOCUMENT",` +
+                               
`"documentDescribes":["SPDXRef-Package-ghost"],"packages":[]}`),
+               },
+               wantErr: "foo",
+       }} {
+               t.Run(tt.name, func(t *testing.T) {
+                       fsys := apkfs.NewMemFS()
+                       require.NoError(t, fsys.MkdirAll("var/lib/db/sbom", 
0o755))
+                       for p, b := range tt.files {
+                               require.NoError(t, fsys.WriteFile(p, b, 0o644))
+                       }
+                       opts := testOpts(fsys)
+                       opts.Packages = tt.pkgs
+
+                       out := filepath.Join(t.TempDir(), "sbom.spdx.json")
+                       err := New().Generate(t.Context(), opts, out)
+                       if tt.wantErr != "" {
+                               require.ErrorContains(t, err, tt.wantErr)
+                               return
+                       }
+                       require.NoError(t, err)
+
+                       b, err := os.ReadFile(out)
+                       require.NoError(t, err)
+                       var doc Document
+                       require.NoError(t, json.Unmarshal(b, &doc))
+                       entries := map[string]struct{}{}
+                       purls := map[string]struct{}{}
+                       for _, p := range doc.Packages {
+                               entries[p.Name+"@"+p.Version] = struct{}{}
+                               for _, ref := range p.ExternalRefs {
+                                       purls[ref.Locator] = struct{}{}
+                               }
+                       }
+                       for _, want := range tt.present {
+                               require.Contains(t, entries, want)
+                       }
+                       for _, notWant := range tt.absent {
+                               require.NotContains(t, entries, notWant)
+                       }
+                       for _, want := range tt.purls {
+                               require.Contains(t, purls, want)
+                       }
+               })
+       }
+}
+
+// statErrFS fails every Stat, as a filesystem that denies access would.
+type statErrFS struct{ apkfs.FullFS }
+
+func (statErrFS) Stat(string) (fs.FileInfo, error) { return nil, 
fs.ErrPermission }
+
+func TestLocateApkSBOMStatError(t *testing.T) {
+       ipkg := installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))
+       _, err := locateApkSBOM(statErrFS{apkfs.NewMemFS()}, ipkg)
+       require.ErrorIs(t, err, fs.ErrPermission)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/license.go 
new/apko-1.4.6/pkg/sbom/generator/spdx/license.go
--- old/apko-1.4.5/pkg/sbom/generator/spdx/license.go   1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.4.6/pkg/sbom/generator/spdx/license.go   2026-09-30 
14:19:13.000000000 +0200
@@ -0,0 +1,123 @@
+// Copyright 2026 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 spdx
+
+import (
+       "crypto/sha256"
+       "encoding/hex"
+       "slices"
+       "strings"
+
+       "github.com/github/go-spdx/v2/spdxexp"
+)
+
+// licenseRefPrefix marks references built from apk database license fields,
+// so they cannot collide with a reference an embedded SBOM defines.
+const licenseRefPrefix = "LicenseRef-apk-"
+
+// licenseExpression converts an apk license field into a valid SPDX license
+// expression. Most fields already are one. Others list licenses separated by
+// spaces, use lowercase operators, or name licenses SPDX does not list; those
+// names become references whose extracted text is the original name.
+func licenseExpression(raw string) (string, []LicensingInfo) {
+       raw = strings.TrimSpace(raw)
+       switch {
+       case raw == "":
+               return NOASSERTION, nil
+       case isLicenseExpression(raw):
+               return raw, nil
+       }
+
+       tokens := strings.Fields(strings.NewReplacer("(", " ( ", ")", " ) 
").Replace(raw))
+       bare := true
+       for i, tok := range tokens {
+               if op := strings.ToUpper(tok); op == "AND" || op == "OR" || op 
== "WITH" {
+                       tokens[i], bare = op, false
+               }
+       }
+       if bare {
+               // A list naming every license that applies. One that names no 
listed
+               // license is a single name, such as "Public Domain".
+               if !slices.ContainsFunc(tokens, isLicenseExpression) {
+                       return licenseRef(raw)
+               }
+               tokens = joinAnd(tokens)
+       }
+
+       var refs []LicensingInfo
+       for i, tok := range tokens {
+               if isExpressionSyntax(tok) || (i > 0 && tokens[i-1] == "WITH") 
|| isLicenseExpression(tok) {
+                       continue
+               }
+               id := licenseRefID(tok)
+               tokens[i] = id
+               if !slices.ContainsFunc(refs, func(r LicensingInfo) bool { 
return r.LicenseID == id }) {
+                       refs = append(refs, LicensingInfo{LicenseID: id, 
ExtractedText: tok})
+               }
+       }
+
+       expr := strings.NewReplacer("( ", "(", " )", 
")").Replace(strings.Join(tokens, " "))
+       if !isLicenseExpression(expr) {
+               return licenseRef(raw)
+       }
+       return expr, refs
+}
+
+func isLicenseExpression(s string) bool {
+       ok, _ := spdxexp.ValidateLicenses([]string{s})
+       return ok
+}
+
+func isExpressionSyntax(tok string) bool {
+       switch tok {
+       case "AND", "OR", "WITH", "(", ")":
+               return true
+       }
+       return false
+}
+
+func joinAnd(licenses []string) []string {
+       out := make([]string, 0, 2*len(licenses)-1)
+       for i, l := range licenses {
+               if i > 0 {
+                       out = append(out, "AND")
+               }
+               out = append(out, l)
+       }
+       return out
+}
+
+// licenseRef describes all of raw as one unlisted license.
+func licenseRef(raw string) (string, []LicensingInfo) {
+       id := licenseRefID(raw)
+       return id, []LicensingInfo{{LicenseID: id, ExtractedText: raw}}
+}
+
+// licenseRefID derives a reference ID from name, keeping the characters SPDX
+// allows in an ID and hashing a name that has none.
+func licenseRefID(name string) string {
+       id := strings.Trim(strings.Map(func(r rune) rune {
+               switch {
+               case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r 
<= '9', r == '.', r == '-':
+                       return r
+               }
+               return '-'
+       }, name), "-")
+       if id == "" {
+               h := sha256.Sum256([]byte(name))
+               id = hex.EncodeToString(h[:8])
+       }
+       return licenseRefPrefix + id
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/license_test.go 
new/apko-1.4.6/pkg/sbom/generator/spdx/license_test.go
--- old/apko-1.4.5/pkg/sbom/generator/spdx/license_test.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.4.6/pkg/sbom/generator/spdx/license_test.go      2026-09-30 
14:19:13.000000000 +0200
@@ -0,0 +1,153 @@
+// Copyright 2026 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 spdx
+
+import (
+       "encoding/json"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       "github.com/github/go-spdx/v2/spdxexp"
+       "github.com/google/go-cmp/cmp"
+       "github.com/stretchr/testify/require"
+
+       "chainguard.dev/apko/pkg/apk/apk"
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
+)
+
+func TestLicenseExpression(t *testing.T) {
+       ref := func(id, text string) []LicensingInfo {
+               return []LicensingInfo{{LicenseID: id, ExtractedText: text}}
+       }
+       for _, tt := range []struct {
+               name string
+               raw  string
+               want string
+               refs []LicensingInfo
+       }{{
+               name: "empty field is no assertion",
+               raw:  "",
+               want: NOASSERTION,
+       }, {
+               name: "listed license is kept",
+               raw:  "MIT",
+               want: "MIT",
+       }, {
+               name: "valid expression is kept",
+               raw:  "MIT AND BSD-2-Clause",
+               want: "MIT AND BSD-2-Clause",
+       }, {
+               name: "listed exception is kept",
+               raw:  "GPL-2.0-or-later WITH Autoconf-exception-2.0",
+               want: "GPL-2.0-or-later WITH Autoconf-exception-2.0",
+       }, {
+               name: "nested expression is kept",
+               raw:  "(MIT OR Apache-2.0) AND Zlib",
+               want: "(MIT OR Apache-2.0) AND Zlib",
+       }, {
+               name: "lowercase operator is uppercased",
+               raw:  "BSD-2-Clause AND CC-BY-SA-4.0 and CC0-1.0",
+               want: "BSD-2-Clause AND CC-BY-SA-4.0 AND CC0-1.0",
+       }, {
+               name: "space-separated list is joined with AND",
+               raw:  "Apache-2.0 MIT",
+               want: "Apache-2.0 AND MIT",
+       }, {
+               name: "unlisted license in a list becomes a reference",
+               raw:  "BSD MIT",
+               want: "LicenseRef-apk-BSD AND MIT",
+               refs: ref("LicenseRef-apk-BSD", "BSD"),
+       }, {
+               name: "unlisted license in an expression becomes a reference",
+               raw:  "(BSD-2-Clause OR custom) AND MIT",
+               want: "(BSD-2-Clause OR LicenseRef-apk-custom) AND MIT",
+               refs: ref("LicenseRef-apk-custom", "custom"),
+       }, {
+               name: "repeated unlisted license is referenced once",
+               raw:  "custom AND GPL-2.0-only AND custom",
+               want: "LicenseRef-apk-custom AND GPL-2.0-only AND 
LicenseRef-apk-custom",
+               refs: ref("LicenseRef-apk-custom", "custom"),
+       }, {
+               name: "unlisted license alone becomes a reference",
+               raw:  "custom:chromiumos",
+               want: "LicenseRef-apk-custom-chromiumos",
+               refs: ref("LicenseRef-apk-custom-chromiumos", 
"custom:chromiumos"),
+       }, {
+               name: "list naming no listed license is one name",
+               raw:  "Public Domain",
+               want: "LicenseRef-apk-Public-Domain",
+               refs: ref("LicenseRef-apk-Public-Domain", "Public Domain"),
+       }, {
+               name: "malformed expression becomes one reference",
+               raw:  "MIT AND",
+               want: "LicenseRef-apk-MIT-AND",
+               refs: ref("LicenseRef-apk-MIT-AND", "MIT AND"),
+       }, {
+               name: "unlisted exception becomes one reference",
+               raw:  "GPL-2.0-or-later WITH custom-exception",
+               want: "LicenseRef-apk-GPL-2.0-or-later-WITH-custom-exception",
+               refs: 
ref("LicenseRef-apk-GPL-2.0-or-later-WITH-custom-exception", "GPL-2.0-or-later 
WITH custom-exception"),
+       }} {
+               t.Run(tt.name, func(t *testing.T) {
+                       got, refs := licenseExpression(tt.raw)
+                       if got != tt.want {
+                               t.Errorf("expression: got = %q, want = %q", 
got, tt.want)
+                       }
+                       if diff := cmp.Diff(tt.refs, refs); diff != "" {
+                               t.Errorf("references (-want, +got):\n%s", diff)
+                       }
+                       if got != NOASSERTION {
+                               if ok, bad := 
spdxexp.ValidateLicenses([]string{got}); !ok {
+                                       t.Errorf("%q is not a valid SPDX 
license expression: %q", got, bad)
+                               }
+                       }
+               })
+       }
+}
+
+func TestLicenseExpressionUnnamed(t *testing.T) {
+       got, refs := licenseExpression("???")
+       require.True(t, strings.HasPrefix(got, "LicenseRef-apk-"), got)
+       require.Equal(t, []LicensingInfo{{LicenseID: got, ExtractedText: 
"???"}}, refs)
+       ok, bad := spdxexp.ValidateLicenses([]string{got})
+       require.True(t, ok, bad)
+}
+
+func TestInstalledPackageLicense(t *testing.T) {
+       fsys := apkfs.NewMemFS()
+       opts := testOpts(fsys)
+       gpl := installed("gpl-tool", "1.0.0-r0")
+       gpl.License = "GPL-3.0-or-later custom"
+       vendor := installed("vendor-tool", "2.0.0-r0")
+       vendor.License = "custom"
+       opts.Packages = []*apk.InstalledPackage{gpl, vendor}
+
+       out := filepath.Join(t.TempDir(), "sbom.spdx.json")
+       require.NoError(t, New().Generate(t.Context(), opts, out))
+       b, err := os.ReadFile(out)
+       require.NoError(t, err)
+       var doc Document
+       require.NoError(t, json.Unmarshal(b, &doc))
+
+       declared := map[string]string{}
+       for _, p := range doc.Packages {
+               declared[p.Name] = p.LicenseDeclared
+       }
+       require.Equal(t, "GPL-3.0-or-later AND LicenseRef-apk-custom", 
declared["gpl-tool"])
+       require.Equal(t, "LicenseRef-apk-custom", declared["vendor-tool"])
+       require.Equal(t, []LicensingInfo{{LicenseID: "LicenseRef-apk-custom", 
ExtractedText: "custom"}}, doc.LicensingInfos)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/spdx.go 
new/apko-1.4.6/pkg/sbom/generator/spdx/spdx.go
--- old/apko-1.4.5/pkg/sbom/generator/spdx/spdx.go      2026-09-22 
00:49:38.000000000 +0200
+++ new/apko-1.4.6/pkg/sbom/generator/spdx/spdx.go      2026-09-30 
14:19:13.000000000 +0200
@@ -15,13 +15,17 @@
 package spdx
 
 import (
+       "cmp"
        "context"
        "encoding/json"
        "errors"
        "fmt"
+       "io/fs"
        "os"
+       "path"
+       "reflect"
        "regexp"
-       "sort"
+       "slices"
        "strings"
        "time"
        "unicode/utf8"
@@ -29,6 +33,7 @@
        "github.com/chainguard-dev/clog"
        v1 "github.com/google/go-containerregistry/pkg/v1"
        purl "github.com/package-url/packageurl-go"
+       "k8s.io/apimachinery/pkg/util/sets"
        "sigs.k8s.io/release-utils/version"
 
        "chainguard.dev/apko/pkg/apk/apk"
@@ -154,21 +159,38 @@
                }
        }
 
+       reserved := reservedIDs(opts.Packages)
        for _, pkg := range opts.Packages {
-               // Check to see if the apk contains an sbom describing itself
-               if err := sx.ProcessInternalApkSBOM(ctx, opts, doc, pkg); err 
!= nil {
-                       return fmt.Errorf("parsing internal apk SBOM: %w", err)
+               if err := sx.processInternalApkSBOM(ctx, opts, doc, pkg, 
reserved); err != nil {
+                       return fmt.Errorf("describing package %q: %w", 
pkg.Name+"-"+pkg.Version, err)
                }
        }
 
+       // Packages built from the same origin or upstream source share 
records, so
+       // keep one copy. Two records under one ID that identify different 
components
+       // would let one package's SBOM displace another's, so refuse them. 
Builds of
+       // one source can still disagree on metadata such as its license; keep 
the
+       // first record then.
        dedupedPackages := make([]Package, 0, len(doc.Packages))
-       seenIDs := make(map[string]struct{})
-       for i := range doc.Packages {
-               if _, ok := seenIDs[doc.Packages[i].ID]; !ok {
-                       seenIDs[doc.Packages[i].ID] = struct{}{}
-                       dedupedPackages = append(dedupedPackages, 
doc.Packages[i])
-               } else {
-                       clog.FromContext(ctx).Debug("duplicate package ID found 
in SBOM, deduplicating package...", "ID", doc.Packages[i].ID)
+       seen := make(map[string]int, len(doc.Packages))
+       for _, p := range doc.Packages {
+               j, ok := seen[p.ID]
+               if !ok {
+                       seen[p.ID] = len(dedupedPackages)
+                       dedupedPackages = append(dedupedPackages, p)
+                       continue
+               }
+               prev := dedupedPackages[j]
+               switch {
+               case !sameIdentity(prev, p):
+                       return fmt.Errorf("SPDX ID %q names two packages that 
differ in name, version, "+
+                               "external references, or checksums: %q and %q",
+                               p.ID, prev.Name+"@"+prev.Version, 
p.Name+"@"+p.Version)
+               case !sameMetadata(prev, p):
+                       clog.WarnContext(ctx, "records sharing an SPDX ID 
disagree on metadata; keeping the first",
+                               "ID", p.ID, "package", p.Name+"@"+p.Version)
+               default:
+                       clog.DebugContext(ctx, "duplicate package ID found in 
SBOM, deduplicating package...", "ID", p.ID)
                }
        }
        doc.Packages = dedupedPackages
@@ -180,25 +202,53 @@
        return nil
 }
 
-// locateApkSBOM returns the path to the SBOM in the given filesystem, using 
the
-// given Package's name and version. It returns an empty string if the SBOM is
-// not found.
+// sameIdentity reports whether a and b identify one component to a scanner:
+// the same name, version, external references, and digests, in any order.
+func sameIdentity(a, b Package) bool {
+       return a.Name == b.Name && a.Version == b.Version &&
+               sets.New(a.ExternalRefs...).Equal(sets.New(b.ExternalRefs...)) 
&&
+               sets.New(a.Checksums...).Equal(sets.New(b.Checksums...)) &&
+               reflect.DeepEqual(a.VerificationCode, b.VerificationCode)
+}
+
+// sameMetadata reports whether a and b, which share an identity, also agree on
+// every other field.
+func sameMetadata(a, b Package) bool {
+       a.ExternalRefs, a.Checksums = b.ExternalRefs, b.Checksums
+       return reflect.DeepEqual(a, b)
+}
+
+// epochRe matches the -rN epoch that ends an apk version.
+var epochRe = regexp.MustCompile(`-r\d+$`)
+
+// locateApkSBOM returns the path of the SBOM that ipkg ships, or "" if it 
ships
+// none. Only paths the installed database lists for ipkg count, so an SBOM 
that
+// another package installs under ipkg's name is ignored.
 func locateApkSBOM(fsys apkfs.ReaderFS, ipkg *apk.InstalledPackage) (string, 
error) {
-       re := regexp.MustCompile(`-r\d+$`)
+       owned := map[string]struct{}{}
+       for _, f := range ipkg.Files {
+               if p := path.Clean("/" + f.Name); path.Dir(p) == apkSBOMdir {
+                       owned[p] = struct{}{}
+               }
+       }
+
        for _, s := range []string{
                fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, 
ipkg.Version),
-               fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, 
re.ReplaceAllString(ipkg.Version, "")),
+               fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, 
epochRe.ReplaceAllString(ipkg.Version, "")),
                fmt.Sprintf("%s/%s.spdx.json", apkSBOMdir, ipkg.Name),
        } {
+               if _, ok := owned[s]; !ok {
+                       continue
+               }
                info, err := fsys.Stat(s)
+               if errors.Is(err, fs.ErrNotExist) {
+                       continue
+               }
                if err != nil {
-                       if os.IsNotExist(err) {
-                               continue
-                       }
+                       return "", fmt.Errorf("inspecting %q: %w", s, err)
                }
-
                if info.IsDir() {
-                       return "", fmt.Errorf("directory found at SBOM path 
%s", s)
+                       return "", fmt.Errorf("directory found at SBOM path 
%q", s)
                }
                return s, nil
        }
@@ -206,104 +256,233 @@
        return "", nil
 }
 
+// ProcessInternalApkSBOM adds to doc the packages that ipkg's own SBOM
+// describes and everything they reach, or a record built from the apk database
+// when ipkg ships no SBOM. It fails when the SBOM does not parse or describes
+// anything other than ipkg.
 func (sx *SPDX) ProcessInternalApkSBOM(ctx context.Context, opts 
*options.Options, doc *Document, ipkg *apk.InstalledPackage) error {
-       // Check if apk installed an SBOM
-       path, err := locateApkSBOM(opts.FS, ipkg)
+       return sx.processInternalApkSBOM(ctx, opts, doc, ipkg, 
reservedIDs(opts.Packages))
+}
+
+func (sx *SPDX) processInternalApkSBOM(ctx context.Context, opts 
*options.Options, doc *Document, ipkg *apk.InstalledPackage, reserved 
map[string]struct{}) error {
+       sbomPath, err := locateApkSBOM(opts.FS, ipkg)
        if err != nil {
                return fmt.Errorf("inspecting FS for internal apk SBOM: %w", 
err)
        }
-       if path == "" {
-               // The SBOM does not exist.
-               // (So just ignore that the package was specified to the SPDX 
Generate method?)
+       if sbomPath == "" {
+               clog.WarnContext(ctx, "package ships no SBOM; describing it 
from the apk database",
+                       "package", ipkg.Name, "version", ipkg.Version)
+               p, licenses := installedPackage(opts, ipkg)
+               doc.Packages = append(doc.Packages, p)
+               mergeLicensingInfos(ctx, &Document{LicensingInfos: licenses}, 
doc)
+               addContains(doc, []string{p.ID})
                return nil
        }
 
-       apkSBOMDoc, err := sx.ParseInternalSBOM(opts, path)
+       apkSBOMDoc, err := sx.ParseInternalSBOM(opts, sbomPath)
        if err != nil {
-               // TODO: Log error parsing apk SBOM
-               return nil
+               return err
        }
 
-       // Cycle the top level elements...
-       // Find elements described by the document - check both 
documentDescribes array
-       // and DESCRIBES relationships (from SPDXRef-DOCUMENT)
-       idsDescribedByAPKSBOM := map[string]struct{}{}
-
-       // First check documentDescribes array
-       for _, elementID := range apkSBOMDoc.DocumentDescribes {
-               idsDescribedByAPKSBOM[elementID] = struct{}{}
+       described := describedIDs(apkSBOMDoc)
+       if len(described) == 0 {
+               return fmt.Errorf("%q describes no package", sbomPath)
+       }
+       todo := reachableIDs(apkSBOMDoc, described)
+       if err := checkIdentity(apkSBOMDoc, ipkg, described, todo, reserved); 
err != nil {
+               return fmt.Errorf("checking %q: %w", sbomPath, err)
+       }
+       if err := copySBOMElements(apkSBOMDoc, doc, todo); err != nil {
+               return fmt.Errorf("copying element: %w", err)
        }
 
-       // Also check for DESCRIBES relationships from SPDXRef-DOCUMENT
-       for _, rel := range apkSBOMDoc.Relationships {
-               if rel.Element == "SPDXRef-DOCUMENT" && rel.Type == "DESCRIBES" 
{
-                       idsDescribedByAPKSBOM[rel.Related] = struct{}{}
-               }
+       mergeLicensingInfos(ctx, apkSBOMDoc, doc)
+       addContains(doc, described)
+
+       return nil
+}
+
+// packageID is the SPDX ID melange gives an apk's own record.
+func packageID(ipkg *apk.InstalledPackage) string {
+       return stringToIdentifier(fmt.Sprintf("SPDXRef-Package-%s-%s", 
ipkg.Name, ipkg.Version))
+}
+
+// reservedIDs returns the SPDX ID of each installed package's own record.
+func reservedIDs(pkgs []*apk.InstalledPackage) map[string]struct{} {
+       ids := make(map[string]struct{}, len(pkgs))
+       for _, p := range pkgs {
+               ids[packageID(p)] = struct{}{}
        }
+       return ids
+}
 
-       // ... searching for a 1st level package
-       targetElementIDs := map[string]struct{}{}
-       for _, pkg := range apkSBOMDoc.Packages {
-               if _, ok := idsDescribedByAPKSBOM[pkg.ID]; !ok {
-                       continue
+// describedIDs returns, sorted, the elements a document names through
+// documentDescribes or a DESCRIBES relationship from the document itself.
+func describedIDs(d *Document) []string {
+       ids := slices.Clone(d.DocumentDescribes)
+       for _, r := range d.Relationships {
+               if r.Element == "SPDXRef-DOCUMENT" && r.Type == "DESCRIBES" {
+                       ids = append(ids, r.Related)
                }
+       }
+       slices.Sort(ids)
+       return slices.Compact(ids)
+}
 
-               targetElementIDs[pkg.ID] = struct{}{}
-               if len(targetElementIDs) == len(idsDescribedByAPKSBOM) {
-                       // Exit early if we found them all.
-                       break
+// reachableIDs returns roots and every element they reach through
+// relationships, other than files.
+func reachableIDs(d *Document, roots []string) map[string]struct{} {
+       edges := make(map[string][]string, len(d.Relationships))
+       for _, r := range d.Relationships {
+               if !strings.HasPrefix(r.Related, "SPDXRef-File-") {
+                       edges[r.Element] = append(edges[r.Element], r.Related)
                }
        }
 
-       sortedTargetElementIDs := make([]string, 0, len(targetElementIDs))
-       for id := range targetElementIDs {
-               sortedTargetElementIDs = append(sortedTargetElementIDs, id)
+       seen := make(map[string]struct{}, len(roots))
+       queue := slices.Clone(roots)
+       for len(queue) > 0 {
+               id := queue[0]
+               queue = queue[1:]
+               if _, ok := seen[id]; ok {
+                       continue
+               }
+               seen[id] = struct{}{}
+               queue = append(queue, edges[id]...)
        }
-       // Sort the element IDs so repeated builds produce the same 
relationship order.
-       sort.Strings(sortedTargetElementIDs)
+       return seen
+}
 
-       todo := make(map[string]struct{}, len(apkSBOMDoc.Relationships))
-       for _, id := range sortedTargetElementIDs {
-               todo[id] = struct{}{}
+// checkIdentity confirms that d describes ipkg alone: every described package
+// and every package with an apk PURL names ipkg at its version, and nothing it
+// reaches claims the SPDX ID of an installed package.
+func checkIdentity(d *Document, ipkg *apk.InstalledPackage, described 
[]string, reach, reserved map[string]struct{}) error {
+       own := packageID(ipkg)
+       byID := make(map[string]*Package, len(d.Packages))
+       for i := range d.Packages {
+               byID[d.Packages[i].ID] = &d.Packages[i]
        }
 
-       if err := copySBOMElements(apkSBOMDoc, doc, todo); err != nil {
-               return fmt.Errorf("copying element: %w", err)
+       for _, id := range described {
+               p, ok := byID[id]
+               if !ok {
+                       return fmt.Errorf("described element %q is not a 
package", id)
+               }
+               if !namesPackage(p.Name, p.Version, ipkg) {
+                       return fmt.Errorf("describes %q at %q rather than %q at 
%q", p.Name, p.Version, ipkg.Name, ipkg.Version)
+               }
+               purls, err := apkPURLs(p)
+               if err != nil {
+                       return err
+               }
+               for _, u := range purls {
+                       if !namesPackage(u.Name, u.Version, ipkg) {
+                               return fmt.Errorf("package %q carries PURL %q", 
id, u.String())
+                       }
+               }
+               if _, ok := reserved[id]; ok && id != own {
+                       return fmt.Errorf("package %q uses the SPDX ID of 
another installed package", id)
+               }
        }
 
-       mergeLicensingInfos(ctx, apkSBOMDoc, doc)
-
-       // Add CONTAINS relationships from the document root package to all 
top-level elements from the internal SBOM.
-       // This ensures they are reachable from the document root for tools 
that traverse the SBOM graph.
-       if len(doc.DocumentDescribes) > 0 {
-               rootPkgID := doc.DocumentDescribes[0]
-               for _, elementID := range sortedTargetElementIDs {
-                       doc.Relationships = append(doc.Relationships, 
Relationship{
-                               Element: rootPkgID,
-                               Type:    "CONTAINS",
-                               Related: elementID,
-                       })
+       for i := range d.Packages {
+               p := &d.Packages[i]
+               if _, ok := reach[p.ID]; !ok || slices.Contains(described, 
p.ID) {
+                       continue
+               }
+               if _, ok := reserved[p.ID]; ok || p.ID == own {
+                       return fmt.Errorf("reachable package %q uses the SPDX 
ID of an installed package", p.ID)
+               }
+               purls, err := apkPURLs(p)
+               if err != nil {
+                       return err
+               }
+               // Some generators catalog the package's own apk entry as a 
reachable package.
+               for _, u := range purls {
+                       if !namesPackage(p.Name, p.Version, ipkg) || 
!namesPackage(u.Name, u.Version, ipkg) {
+                               return fmt.Errorf("reachable package %q carries 
apk PURL %q", p.ID, u.String())
+                       }
                }
        }
 
        return nil
 }
 
-func copySBOMElements(sourceDoc, targetDoc *Document, todo 
map[string]struct{}) error {
-       // Walk the graph looking for things to copy.
-       // Loop until we don't find any new todos.
-       for prev, next := 0, len(todo); next != prev; prev, next = next, 
len(todo) {
-               for _, r := range sourceDoc.Relationships {
-                       if strings.HasPrefix(r.Related, "SPDXRef-File-") {
-                               continue
-                       }
-                       if _, ok := todo[r.Element]; ok {
-                               todo[r.Related] = struct{}{}
+// namesPackage reports whether name and version identify ipkg, with or without
+// its epoch.
+func namesPackage(name, version string, ipkg *apk.InstalledPackage) bool {
+       return name == ipkg.Name &&
+               (version == ipkg.Version || version == 
epochRe.ReplaceAllString(ipkg.Version, ""))
+}
+
+// apkPURLs returns the pkg:apk PURLs among p's external references.
+func apkPURLs(p *Package) ([]purl.PackageURL, error) {
+       var out []purl.PackageURL
+       for _, ref := range p.ExternalRefs {
+               if ref.Type != ExtRefTypePurl {
+                       continue
+               }
+               u, err := purl.FromString(ref.Locator)
+               if err != nil {
+                       if strings.HasPrefix(ref.Locator, "pkg:apk/") {
+                               return nil, fmt.Errorf("package %q carries 
malformed PURL %q: %w", p.ID, ref.Locator, err)
                        }
+                       continue
+               }
+               if u.Type == "apk" {
+                       out = append(out, u)
                }
        }
+       return out, nil
+}
 
-       // Now copy everything over.
+// installedPackage describes ipkg from its apk database entry, returning the
+// extracted licenses its license expression references.
+func installedPackage(opts *options.Options, ipkg *apk.InstalledPackage) 
(Package, []LicensingInfo) {
+       qualifiers := map[string]string{}
+       if arch := cmp.Or(ipkg.Arch, opts.ImageInfo.Arch.ToAPK()); arch != "" {
+               qualifiers["arch"] = arch
+       }
+       license, refs := licenseExpression(ipkg.License)
+       return Package{
+               ID:               packageID(ipkg),
+               Name:             ipkg.Name,
+               Version:          ipkg.Version,
+               LicenseConcluded: NOASSERTION,
+               LicenseDeclared:  license,
+               Description:      ipkg.Description,
+               DownloadLocation: NOASSERTION,
+               Originator:       supplier(opts),
+               Supplier:         supplier(opts),
+               SourceInfo:       "Package info from apk database",
+               CopyrightText:    NOASSERTION,
+               ExternalRefs: []ExternalRef{{
+                       Category: ExtRefPackageManager,
+                       Type:     ExtRefTypePurl,
+                       Locator: purl.NewPackageURL("apk", opts.OS.ID, 
ipkg.Name, ipkg.Version,
+                               purl.QualifiersFromMap(qualifiers), 
"").String(),
+               }},
+       }, refs
+}
+
+// addContains links the document root to each of ids, so tools that walk the
+// graph from the root reach them.
+func addContains(doc *Document, ids []string) {
+       if len(doc.DocumentDescribes) == 0 {
+               return
+       }
+       for _, id := range ids {
+               doc.Relationships = append(doc.Relationships, Relationship{
+                       Element: doc.DocumentDescribes[0],
+                       Type:    "CONTAINS",
+                       Related: id,
+               })
+       }
+}
+
+// copySBOMElements copies the packages in todo, and the relationships from
+// them other than to files, from sourceDoc to targetDoc.
+func copySBOMElements(sourceDoc, targetDoc *Document, todo 
map[string]struct{}) error {
        done := make(map[string]struct{}, len(todo))
 
        for _, p := range sourceDoc.Packages {
@@ -365,7 +544,7 @@
        }
 
        if err := json.Unmarshal(data, internalSBOM); err != nil {
-               return nil, fmt.Errorf("parsing internal apk sbom: %w", err)
+               return nil, fmt.Errorf("parsing internal apk sbom %q: %w", 
path, err)
        }
 
        // Fix up missing data, checkers require Originator &
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/spdx_test.go 
new/apko-1.4.6/pkg/sbom/generator/spdx/spdx_test.go
--- old/apko-1.4.5/pkg/sbom/generator/spdx/spdx_test.go 2026-09-22 
00:49:38.000000000 +0200
+++ new/apko-1.4.6/pkg/sbom/generator/spdx/spdx_test.go 2026-09-30 
14:19:13.000000000 +0200
@@ -15,6 +15,7 @@
 package spdx
 
 import (
+       "archive/tar"
        "encoding/json"
        "fmt"
        "os"
@@ -218,7 +219,7 @@
                        fsys := apkfs.NewMemFS()
                        tt.opts.FS = fsys
                        sbomDir := path.Join("var", "lib", "db", "sbom")
-                       err := fsys.MkdirAll(sbomDir, 0750)
+                       err := fsys.MkdirAll(sbomDir, 0o750)
                        require.NoError(t, err)
 
                        for _, apkPkg := range tt.opts.Packages {
@@ -228,8 +229,9 @@
                                require.NoError(t, err)
 
                                sbomDestPath := path.Join(sbomDir, apkSBOMName)
-                               err = fsys.WriteFile(sbomDestPath, 
apkSBOMBytes, 0644)
+                               err = fsys.WriteFile(sbomDestPath, 
apkSBOMBytes, 0o644)
                                require.NoError(t, err)
+                               apkPkg.Files = append(apkPkg.Files, 
tar.Header{Name: sbomDestPath})
                        }
 
                        sx := New()
@@ -281,7 +283,11 @@
        dir := t.TempDir()
        fsys := apkfs.NewMemFS()
        opts := testOpts(fsys)
-       opts.Packages = []*apk.InstalledPackage{{Name: "glibc", Version: 
"2.40-r0"}}
+       opts.Packages = []*apk.InstalledPackage{{
+               Name:    "glibc",
+               Version: "2.40-r0",
+               Files:   []tar.Header{{Name: internalSBOMPath}},
+       }}
 
        internalSBOM := Document{
                DocumentDescribes: []string{
@@ -289,14 +295,15 @@
                        documentRootID,
                },
                Packages: []Package{
-                       {ID: packageID, Name: "glibc"},
-                       {ID: documentRootID, Name: "/"},
+                       // Every described element must name the installed 
package.
+                       {ID: packageID, Name: "glibc", Version: "2.40-r0"},
+                       {ID: documentRootID, Name: "glibc", Version: "2.40-r0"},
                },
        }
        data, err := json.Marshal(internalSBOM)
        require.NoError(t, err)
-       require.NoError(t, fsys.MkdirAll("/var/lib/db/sbom", 0750))
-       require.NoError(t, fsys.WriteFile(internalSBOMPath, data, 0644))
+       require.NoError(t, fsys.MkdirAll("/var/lib/db/sbom", 0o750))
+       require.NoError(t, fsys.WriteFile(internalSBOMPath, data, 0o644))
 
        sx := New()
        generate := func(i int) []byte {
@@ -353,7 +360,7 @@
 }
 
 func TestStringToIdentifier(t *testing.T) {
-       var validIDRe = regexp.MustCompile(`^[a-zA-Z0-9-.]+$`)
+       validIDRe := regexp.MustCompile(`^[a-zA-Z0-9-.]+$`)
        for _, tc := range []string{
                "alpine",
                "kindest/node:v1.21.1",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/apko-1.4.5/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json
 
new/apko-1.4.6/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json
--- 
old/apko-1.4.5/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json
   1970-01-01 01:00:00.000000000 +0100
+++ 
new/apko-1.4.6/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json
   2026-09-30 14:19:13.000000000 +0200
@@ -0,0 +1,122 @@
+{
+  "spdxVersion": "SPDX-2.3",
+  "dataLicense": "CC0-1.0",
+  "SPDXID": "SPDXRef-DOCUMENT",
+  "name": "apk-wolfi-baselayout-20230201-r30",
+  "documentNamespace": 
"https://spdx.org/spdxdocs/chainguard/build/b5df68054c4f3e4ad79714cd9a3d39ca";,
+  "creationInfo": {
+    "licenseListVersion": "3.22",
+    "creators": [
+      "Tool: chainguard-build (v0.59.4)",
+      "Organization: Chainguard, Inc"
+    ],
+    "created": "2026-09-09T19:39:56Z"
+  },
+  "packages": [
+    {
+      "name": "wolfi",
+      "SPDXID": "SPDXRef-OperatingSystem",
+      "supplier": "Organization: Wolfi",
+      "originator": "Organization: Wolfi",
+      "downloadLocation": "NOASSERTION",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "NOASSERTION",
+      "description": "Operating System",
+      "primaryPackagePurpose": "OPERATING-SYSTEM"
+    },
+    {
+      "name": "wolfi-baselayout",
+      "SPDXID": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30",
+      "versionInfo": "20230201-r30",
+      "supplier": "Organization: Wolfi",
+      "originator": "Organization: Wolfi",
+      "downloadLocation": "NOASSERTION",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "MIT",
+      "copyrightText": "NOASSERTION",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceType": "purl",
+          "referenceLocator": 
"pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64\u0026distro=wolfi"
+        }
+      ],
+      "primaryPackagePurpose": "APPLICATION"
+    },
+    {
+      "name": "wolfi-baselayout.yaml",
+      "SPDXID": 
"SPDXRef-Package-Melange-wolfi-baselayout.yaml-a4472f6292a5e48d6d611bf24c6e119b1a483e66",
+      "versionInfo": "a4472f6292a5e48d6d611bf24c6e119b1a483e66",
+      "supplier": "Organization: Wolfi",
+      "originator": "Organization: Wolfi",
+      "downloadLocation": "NOASSERTION",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceType": "purl",
+          "referenceLocator": 
"pkg:github/chainguard-dev/stereo@a4472f6292a5e48d6d611bf24c6e119b1a483e66#wolfi-baselayout.yaml"
+        }
+      ],
+      "primaryPackagePurpose": "INSTALL"
+    },
+    {
+      "name": "wolfi-baselayout",
+      "SPDXID": "SPDXRef-Package-apk-wolfi-baselayout-d9db04555c7c746a",
+      "versionInfo": "20230201-r30",
+      "supplier": "Organization: chainguard",
+      "downloadLocation": "NOASSERTION",
+      "filesAnalyzed": false,
+      "sourceInfo": "acquired package info from APK DB: /.PKGINFO",
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "MIT",
+      "copyrightText": "NOASSERTION",
+      "description": "baselayout data for Wolfi",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceType": "purl",
+          "referenceLocator": 
"pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64\u0026origin=wolfi-baselayout"
+        }
+      ]
+    },
+    {
+      "name": "wolfi-baselayout",
+      "SPDXID": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout",
+      "versionInfo": "20230201-r30",
+      "supplier": "Organization: chainguard",
+      "downloadLocation": "NOASSERTION",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "NOASSERTION",
+      "copyrightText": "NOASSERTION",
+      "primaryPackagePurpose": "FILE"
+    }
+  ],
+  "relationships": [
+    {
+      "spdxElementId": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30",
+      "relatedSpdxElement": 
"SPDXRef-Package-Melange-wolfi-baselayout.yaml-a4472f6292a5e48d6d611bf24c6e119b1a483e66",
+      "relationshipType": "DESCRIBED_BY"
+    },
+    {
+      "spdxElementId": "SPDXRef-DOCUMENT",
+      "relatedSpdxElement": 
"SPDXRef-Package-apk-wolfi-baselayout-20230201-r30",
+      "relationshipType": "DESCRIBES"
+    },
+    {
+      "spdxElementId": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout",
+      "relatedSpdxElement": 
"SPDXRef-Package-apk-wolfi-baselayout-d9db04555c7c746a",
+      "relationshipType": "CONTAINS"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30",
+      "relatedSpdxElement": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout",
+      "relationshipType": "CONTAINS"
+    }
+  ]
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.Jc31Qi/_old  2026-10-01 16:47:31.547249644 +0200
+++ /var/tmp/diff_new_pack.Jc31Qi/_new  2026-10-01 16:47:31.554249938 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.4.5
-mtime: 1790030978
-commit: 739e7ce3f675ffb232e16849b51ff93657f1570c
+version: 1.4.6
+mtime: 1790770753
+commit: 51c298aa5249f43193e3cb8684a2a60facfcb69d
 

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

Reply via email to