mirror of
https://github.com/KDE/kde-linux.git
synced 2026-08-02 18:28:25 -04:00
Integrates OpenQA image testing into CI. Builds images once in the imaging job, then passes the resulting image and update channel to the OpenQA pipeline in kde-linux/os-autoinst-distri-kdelinux. For regular test builds, upload the built ISO and a temporary sysupdate tree to ci-artifacts on storage.kde.org. The resulting IMAGE_URL, STAGING_CHANNEL_URL and ephemeral update signing key are exported through the imaging job dotenv artifact so OpenQA can boot the image and run upgrade tests against the build under test. For protected default-branch builds, stage the release artifacts under a per-job staging prefix in the kde-linux bucket. OpenQA tests the staged ISO and sysupdate channel before the publish job is allowed to run. Publishing downloads the staged artifacts, uploads the public ISO/torrent and sysupdate assets to files.kde.org, chunks and uploads the root to the desync chunk store, then merges the staged S3 tree into the live tree on the bucket and regenerates the published SHA256SUMS.
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
// SPDX-FileCopyrightText: 2024-2025 Harald Sitter <sitter@kde.org>
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"io"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"invent.kde.org/kde-linux/kde-linux/minioauth"
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
)
|
|
|
|
func connectToMinIO(endpoint string) *minio.Client {
|
|
minioClient, err := minioauth.Connect(endpoint)
|
|
if err != nil {
|
|
log.Fatalln("Failed to connect to MinIO:", err)
|
|
}
|
|
|
|
buckets, err := minioauth.ListBuckets(minioClient)
|
|
if err != nil {
|
|
log.Fatalln("Failed to list buckets:", err)
|
|
}
|
|
for _, bucket := range buckets {
|
|
log.Println(bucket)
|
|
}
|
|
|
|
return minioClient
|
|
}
|
|
|
|
func sha256File(path string) string {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
log.Fatalf("unable to open file %s: %v", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, file); err != nil {
|
|
log.Fatalf("unable to hash file %s: %v", path, err)
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil))
|
|
}
|
|
|
|
type UploadObject struct {
|
|
ObjectName string
|
|
Path string
|
|
SHA256 string
|
|
}
|
|
|
|
func upload(client *minio.Client, bucket string, objectNamePrefix string) {
|
|
dir := "../upload-tree"
|
|
|
|
// Somewhat involved producer-consumer going on here.
|
|
// - We start N sha256 calculation workers.
|
|
// - We then feed the calcs channel with partial UploadObjects.
|
|
// - Close the calcs channel to eventually let the workers drain and finish.
|
|
// - The workers calculate the sha and then push the complete objects into the results channel.
|
|
// - Once the calcs waitgroup is done we close the results channel as well to let the uploader drain and finish.
|
|
|
|
calcs := make(chan UploadObject, 32) // mind that the size causes this channel to buffer which prevents deadlocks
|
|
results := make(chan UploadObject, 32)
|
|
|
|
calcsWg := sync.WaitGroup{}
|
|
|
|
// The sha256 calculation workers.
|
|
for i := 0; i < 4; i++ {
|
|
calcsWg.Add(1)
|
|
go func() {
|
|
defer calcsWg.Done()
|
|
for obj := range calcs {
|
|
obj.SHA256 = sha256File(obj.Path)
|
|
results <- obj
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Once they are all done we can close the results channel.
|
|
go func() {
|
|
calcsWg.Wait()
|
|
log.Println("All SHA256 calculations done, closing results channel")
|
|
close(results)
|
|
}()
|
|
|
|
// The calcs channel gets fed by the walking of the upload tree.
|
|
go func() {
|
|
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectName, err := filepath.Rel(dir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectName = filepath.Join(objectNamePrefix, objectName)
|
|
if objectName == "" {
|
|
return errors.New("object name cannot be empty")
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
calcs <- UploadObject{
|
|
ObjectName: objectName,
|
|
Path: path,
|
|
}
|
|
|
|
return nil
|
|
})
|
|
log.Println("Finished walking upload tree, closing calcs channel")
|
|
close(calcs)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}()
|
|
|
|
// Upload. This loop only ends when results is closed and drained.
|
|
for res := range results {
|
|
log.Println("Uploading", res.ObjectName, "from", res.Path)
|
|
_, err := client.FPutObject(context.Background(), bucket, res.ObjectName, res.Path, minio.PutObjectOptions{
|
|
UserMetadata: map[string]string{
|
|
"X-KDE-SHA256": res.SHA256,
|
|
},
|
|
})
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
remote := flag.String("remote", "", "remote url to upload to, e.g. s3+https://storage.kde.org/kde-linux/sysupdate/v2/store")
|
|
flag.Parse()
|
|
|
|
remoteURI, err := url.Parse(*remote)
|
|
if err != nil {
|
|
log.Fatalln("Failed to parse remote URL:", err)
|
|
}
|
|
if remoteURI.Scheme != "s3+https" {
|
|
log.Fatalln("Unsupported remote scheme:", remoteURI.Scheme)
|
|
}
|
|
if remoteURI.Host != "storage.kde.org" {
|
|
log.Fatalln("Unsupported remote host:", remoteURI.Host)
|
|
}
|
|
parts := strings.SplitN(remoteURI.Path[1:], "/", 2)
|
|
if len(parts) != 2 {
|
|
log.Fatalln("Invalid remote path, expected format: /bucket/path")
|
|
}
|
|
bucket := parts[0]
|
|
path := parts[1]
|
|
if bucket == "" {
|
|
log.Fatalln("Invalid remote path, expected format: /bucket/path")
|
|
}
|
|
if path == "" {
|
|
log.Println("Warning: path is empty, uploading to bucket root")
|
|
path = "/"
|
|
}
|
|
|
|
log.Println("Connecting to MinIO at", remoteURI.Host)
|
|
minioClient := connectToMinIO(remoteURI.Host)
|
|
|
|
log.Println("Uploading to bucket", bucket, "with path prefix", path)
|
|
upload(minioClient, bucket, path)
|
|
}
|