mirror of
https://github.com/KDE/kde-linux.git
synced 2026-08-02 10:17:36 -04:00
Reintroduce OpenQA testing
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.
This commit is contained in:
@@ -10,19 +10,33 @@ include:
|
||||
stages:
|
||||
- validate
|
||||
- start
|
||||
- test
|
||||
- publish
|
||||
|
||||
imaging:
|
||||
stage: start
|
||||
tags:
|
||||
- VM
|
||||
- amd64
|
||||
# IMAGING_MODE selects what we do with the built image:
|
||||
# test - throwaway image, uploaded to ci-artifacts and given to OpenQA for testing.
|
||||
# stage - signed and staged to storage for publishing, gated on OpenQA tests.
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_DRAFT == "true"
|
||||
when: manual
|
||||
variables:
|
||||
IMAGING_MODE: test
|
||||
- if: $CI_COMMIT_BRANCH =~ /^work\//
|
||||
when: manual
|
||||
- if: $CI_COMMIT_REF_PROTECTED != 'true' || $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME || $CI_PROJECT_PATH != 'kde-linux/kde-linux'
|
||||
variables:
|
||||
IMAGING_MODE: test
|
||||
- if: $CI_COMMIT_REF_PROTECTED == 'true' && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME && $CI_PROJECT_PATH == 'kde-linux/kde-linux'
|
||||
when: always
|
||||
variables:
|
||||
IMAGING_MODE: stage
|
||||
- when: always
|
||||
variables:
|
||||
IMAGING_MODE: test
|
||||
allow_failure: false
|
||||
image: storage.kde.org/vm-images/kde-linux-builder
|
||||
id_tokens: &tokens
|
||||
@@ -31,36 +45,67 @@ imaging:
|
||||
variables: &variables
|
||||
RUNNER_AFTER_SCRIPT_TIMEOUT: 1h
|
||||
script:
|
||||
- sudo cp /etc/pacman.conf.nolinux /etc/pacman.conf # can be dropped once an image rebuild has happened after 2026-05-27
|
||||
- sudo ./build.sh --force --debug # bootstap.sh happens as part of CI image build
|
||||
- sudo ./build.sh --force --debug
|
||||
- if [ "$IMAGING_MODE" = stage ]; then ./upload.sh --stage; fi
|
||||
- if [ "$IMAGING_MODE" = test ]; then ./upload-to-storage.sh; fi # Need this to have something to pass to OpenQA.
|
||||
after_script:
|
||||
- if [ "$IMAGING_MODE" = stage ] && [ "$CI_JOB_STATUS" = success ]; then exit 0; fi
|
||||
- if [ "$IMAGING_MODE" = test ]; then exit 0; fi
|
||||
- ./upload-to-storage.sh || true
|
||||
artifacts: &artifacts
|
||||
artifacts:
|
||||
expire_in: 4 weeks
|
||||
when: always
|
||||
paths:
|
||||
- "**/erofs.log"
|
||||
reports:
|
||||
dotenv: build.env
|
||||
|
||||
imaging+publish:
|
||||
stage: start
|
||||
# Run OpenQA testing on the built image.
|
||||
trigger-openqa:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH != 'kde-linux/kde-linux'
|
||||
when: never
|
||||
- if: $CI_MERGE_REQUEST_DRAFT == "true"
|
||||
when: manual
|
||||
- if: $CI_COMMIT_BRANCH =~ /^work\//
|
||||
when: manual
|
||||
- when: on_success
|
||||
needs:
|
||||
- job: imaging
|
||||
artifacts: true
|
||||
trigger:
|
||||
project: kde-linux/os-autoinst-distri-kdelinux
|
||||
branch: $OPENQA_BRANCH
|
||||
strategy: depend
|
||||
variables:
|
||||
IMAGE_URL: $IMAGE_URL
|
||||
STAGING_CHANNEL_URL: $STAGING_CHANNEL_URL
|
||||
SYSUPDATE_PUBKEY_B64: $SYSUPDATE_PUBKEY_B64
|
||||
OPENQA_BRANCH: master
|
||||
|
||||
# Publish the staged image in storage.
|
||||
publish:
|
||||
stage: publish
|
||||
tags:
|
||||
- VM
|
||||
- amd64
|
||||
rules:
|
||||
# NOTE: these are different from the regular imaging job!
|
||||
- if: $CI_COMMIT_REF_PROTECTED == 'true' && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME && $CI_PROJECT_PATH == 'kde-linux/kde-linux'
|
||||
when: always
|
||||
when: on_success
|
||||
needs:
|
||||
- job: imaging
|
||||
artifacts: true
|
||||
- job: trigger-openqa
|
||||
artifacts: false
|
||||
allow_failure: false
|
||||
image: storage.kde.org/vm-images/kde-linux-builder
|
||||
id_tokens: *tokens
|
||||
variables: *variables
|
||||
script:
|
||||
- sudo cp /etc/pacman.conf.nolinux /etc/pacman.conf # can be dropped once an image rebuild has happened after 2026-05-27
|
||||
- sudo pacman --sync --refresh --noconfirm curl which git
|
||||
- curl -s https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer | bash
|
||||
- sudo ./build.sh --force --debug # bootstap.sh happens as part of CI image build
|
||||
- ./upload.sh
|
||||
after_script:
|
||||
# Upload broken images to Harald for inspection
|
||||
- "[ $CI_JOB_STATUS == 'success' ] || ./upload-to-storage.sh || true"
|
||||
artifacts: *artifacts
|
||||
- ./upload.sh --publish
|
||||
artifacts:
|
||||
expire_in: 4 weeks
|
||||
when: always
|
||||
paths:
|
||||
- "**/erofs.log"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
# SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2026 Hadi Chokr <hadichokr@icloud.com>
|
||||
|
||||
set -eux
|
||||
|
||||
# ADDON_DIR is set by basic-test.py to the .extra.d directory inside the mounted ESP
|
||||
if [ -z "$ADDON_DIR" ]; then
|
||||
echo "ERROR: ADDON_DIR environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the addon UKI (systemd-stub addon) that appends the test cmdline
|
||||
ukify build \
|
||||
--cmdline "kde-linux.basic-test=1 kde-linux.basic-test-callback=http://10.0.2.2:${PORT}/good" \
|
||||
--output "$ADDON_DIR/basic-test.addon.efi"
|
||||
105
basic-test.py
105
basic-test.py
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
# SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2026 Hadi Chokr <hadichokr@icloud.com>
|
||||
|
||||
import atexit
|
||||
import http.server
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/good':
|
||||
sys.exit(0)
|
||||
if self.path == '/bad':
|
||||
print("==Received /bad callback==")
|
||||
content_len = int(self.headers.get('Content-Length'))
|
||||
body = self.rfile.read(content_len)
|
||||
print(body.decode('utf-8'))
|
||||
sys.exit(1)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
server = http.server.HTTPServer(server_address=('', 0), RequestHandlerClass=Handler)
|
||||
print("serving at port", server.server_port)
|
||||
|
||||
img = sys.argv[1]
|
||||
if not img:
|
||||
print("No image specified")
|
||||
sys.exit(1)
|
||||
|
||||
efi_base = sys.argv[2]
|
||||
if not efi_base:
|
||||
print("No EFI base image specified")
|
||||
sys.exit(1)
|
||||
|
||||
# Always test as ISO (a valid .iso9660 is also a valid .raw)
|
||||
test_img = img.replace('.raw', '.test.iso').replace('.iso', '.test.iso')
|
||||
subprocess.check_call(['cp', '--reflink=auto', img, test_img])
|
||||
|
||||
# Inject the EFI addon into the ESP partition of the test ISO
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_src = f'{script_dir}/basic-test-efi-addon.sh'
|
||||
|
||||
with tempfile.TemporaryDirectory() as mnt:
|
||||
# Find the ESP partition offset and size using sfdisk
|
||||
sfdisk = subprocess.check_output(['sfdisk', '--json', test_img]).decode()
|
||||
import json
|
||||
parts = json.loads(sfdisk)['partitiontable']['partitions']
|
||||
esp = next(p for p in parts if p.get('type', '') == 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B')
|
||||
sector_size = json.loads(sfdisk)['partitiontable']['sectorsize']
|
||||
offset = esp['start'] * sector_size
|
||||
size = esp['size'] * sector_size
|
||||
|
||||
subprocess.check_call([
|
||||
'mount', '-o', f'loop,offset={offset},sizelimit={size}',
|
||||
test_img, mnt
|
||||
])
|
||||
try:
|
||||
efi_extra_dir = f'{mnt}/EFI/Linux/{efi_base}.extra.d'
|
||||
os.makedirs(efi_extra_dir, exist_ok=True)
|
||||
subprocess.check_call([
|
||||
'bash', addon_src
|
||||
], env={
|
||||
'PORT': str(server.server_port),
|
||||
'UKI': efi_base,
|
||||
'ADDON_DIR': efi_extra_dir,
|
||||
})
|
||||
finally:
|
||||
subprocess.check_call(['umount', mnt])
|
||||
|
||||
qemu_cmd = [
|
||||
"qemu-system-x86_64",
|
||||
"-cdrom", test_img,
|
||||
"-m", "4G",
|
||||
"-enable-kvm",
|
||||
"-cpu", "host",
|
||||
"-bios", "/usr/share/OVMF/x64/OVMF.4m.fd",
|
||||
]
|
||||
|
||||
# I ought to point out that this leaks the process in case of failure. It will however get reaped by the docker container shutdown.
|
||||
qemu = subprocess.Popen(qemu_cmd)
|
||||
atexit.register(lambda: (qemu.kill()))
|
||||
|
||||
def on_timeout():
|
||||
print("\n\n\n== Test timed out ==")
|
||||
print("Download the image for inspection from http://images.kde-linux.haraldsitter.eu/")
|
||||
print("(location may have changed to something like https://qoomon.github.io/aws-s3-bucket-browser/index.html?bucket=https://storage.kde.org/ci-artifacts/#).")
|
||||
print("Once downloaded run:\n")
|
||||
print(f"./basic-test.py {img} {efi_base}\n\n\n")
|
||||
|
||||
qemu.kill()
|
||||
sys.exit(2)
|
||||
|
||||
server.timeout = 5 * 60 # 5 minutes
|
||||
server.handle_timeout = on_timeout
|
||||
while True: # kinda garbage but there seems to be no nice (non-private) poll-or-timeout api
|
||||
server.handle_request()
|
||||
time.sleep(8)
|
||||
qemu.kill()
|
||||
sys.exit(1) # if we get here we timed out = fail
|
||||
6
build.sh
6
build.sh
@@ -50,7 +50,7 @@ ROOTFS_CAIBX=${OUTPUT}_root-x86-64.caibx
|
||||
ROOTFS_EROFS=${OUTPUT}_root-x86-64.erofs # Output erofs image path
|
||||
ISO="${OUTPUT}.iso" # both a valid GPT disk image and a bootable ISO
|
||||
|
||||
EFI_BASE=kde-linux_${VERSION} # Base name of the UKI in the image's ESP (exported so it can be used in basic-test-efi-addon.sh)
|
||||
EFI_BASE=kde-linux_${VERSION} # Base name of the UKI in the image's ESP
|
||||
EFI=${EFI_BASE}+3.efi # Name of primary UKI in the image's ESP (with tries counter for installed system)
|
||||
LIVE_EFI=${EFI_BASE}.efi # Name of live UKI in the ESP (no tries counter — ESP is read-only on ISO)
|
||||
|
||||
@@ -204,10 +204,6 @@ systemd-repart \
|
||||
--el-torito-publisher="KDE" \
|
||||
"$ISO"
|
||||
|
||||
# Test the ISO (which is also a valid GPT image so no need to test as .raw separately)
|
||||
./basic-test.py "$ISO" "$LIVE_EFI" || exit 1
|
||||
rm ./mkosi.output/*.test.iso
|
||||
|
||||
# Incase the owner is root
|
||||
chown -R user:user mkosi.output
|
||||
|
||||
|
||||
31
minioauth/go.mod
Normal file
31
minioauth/go.mod
Normal file
@@ -0,0 +1,31 @@
|
||||
module invent.kde.org/kde-linux/kde-linux/minioauth
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/minio/crc64nvme v1.0.2 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
2
minioauth/go.mod.license
Normal file
2
minioauth/go.mod.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: None
|
||||
44
minioauth/go.sum
Normal file
44
minioauth/go.sum
Normal file
@@ -0,0 +1,44 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
2
minioauth/go.sum.license
Normal file
2
minioauth/go.sum.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: None
|
||||
71
minioauth/minioauth.go
Normal file
71
minioauth/minioauth.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
// SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
// SPDX-FileCopyrightText: 2026 Thomas Duckworth <tduck@filotimoproject.org>
|
||||
|
||||
package minioauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type AWSSection struct {
|
||||
AccessKeyID string `ini:"aws_access_key_id"`
|
||||
SecretKey string `ini:"aws_secret_access_key"`
|
||||
SessionToken string `ini:"aws_session_token"`
|
||||
}
|
||||
|
||||
func readConfigAWS(section string) (AWSSection, error) {
|
||||
awsSection := AWSSection{}
|
||||
|
||||
awsConfigPath := filepath.Join(os.Getenv("HOME"), ".aws", "credentials")
|
||||
cfg, err := ini.Load(awsConfigPath)
|
||||
if err != nil {
|
||||
return awsSection, fmt.Errorf("failed to load AWS credentials file: %w", err)
|
||||
}
|
||||
|
||||
err = cfg.Section(section).MapTo(&awsSection)
|
||||
if err != nil {
|
||||
return awsSection, fmt.Errorf("failed to map AWS credentials section: %w", err)
|
||||
}
|
||||
|
||||
return awsSection, nil
|
||||
}
|
||||
|
||||
func Connect(endpoint string) (*minio.Client, error) {
|
||||
awsSection, err := readConfigAWS("default")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if awsSection.AccessKeyID == "" {
|
||||
return nil, errors.New("AWS access key ID is empty")
|
||||
}
|
||||
if awsSection.SecretKey == "" {
|
||||
return nil, errors.New("AWS secret access key is empty")
|
||||
}
|
||||
if awsSection.SessionToken == "" {
|
||||
return nil, errors.New("AWS session token is empty")
|
||||
}
|
||||
|
||||
minioClient, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(awsSection.AccessKeyID, awsSection.SecretKey, awsSection.SessionToken),
|
||||
Secure: true,
|
||||
TrailingHeaders: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create MinIO client: %w", err)
|
||||
}
|
||||
|
||||
return minioClient, nil
|
||||
}
|
||||
|
||||
func ListBuckets(client *minio.Client) ([]minio.BucketInfo, error) {
|
||||
return client.ListBuckets(context.Background())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
# SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2026 Hadi Chokr <hadichokr@icloud.com>
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
# Units that are known to fail on a read-only ISO (e.g., /boot is not writable)
|
||||
IGNORED_FAILED_UNITS = {
|
||||
"systemd-boot-update.service",
|
||||
"systemd-boot-random-seed.service",
|
||||
}
|
||||
|
||||
callback = None
|
||||
with open('/proc/cmdline') as cmdline:
|
||||
data = cmdline.read()
|
||||
entries = data.split(' ')
|
||||
for entry in entries:
|
||||
if entry.startswith('kde-linux.basic-test-callback='):
|
||||
# We don't support quoting of any kind.
|
||||
callback = entry.split('=', 2)[1]
|
||||
callback = callback.strip()
|
||||
break
|
||||
|
||||
if callback is None:
|
||||
print('No callback specified. set kde-linux.basic-test-callback=')
|
||||
exit(1)
|
||||
|
||||
bad_callback = callback.replace('/good', '/bad')
|
||||
|
||||
while True:
|
||||
# 1000 is the uid of the live user. always.
|
||||
if Path('/run/user/1000/kde-linux-bless-session').is_file():
|
||||
failed = json.loads(subprocess.check_output(["systemctl", "--failed", "--output=json"]))
|
||||
# Filter out ignored units
|
||||
relevant_failed = [unit for unit in failed if unit['unit'] not in IGNORED_FAILED_UNITS]
|
||||
if len(relevant_failed) > 0:
|
||||
with open('data.file', 'w') as f:
|
||||
for unit in relevant_failed:
|
||||
f.write("\n")
|
||||
f.write(json.dumps(unit))
|
||||
f.write("\n")
|
||||
try:
|
||||
f.write(subprocess.check_output(['journalctl', '--no-pager', f'_SYSTEMD_UNIT={unit["unit"]}']).decode('utf-8'))
|
||||
except Exception as e:
|
||||
f.write(f"Failed to get journal for {unit['unit']}: {e}\n")
|
||||
f.write("\n") # make sure we have a final newline
|
||||
subprocess.check_call(['curl',
|
||||
'--request', 'GET',
|
||||
'--data-binary', '@data.file', # use binary as otherwise curl will eat the newlines
|
||||
bad_callback])
|
||||
else:
|
||||
subprocess.check_call(['curl', callback])
|
||||
time.sleep(30)
|
||||
@@ -90,7 +90,3 @@ cp /usr/share/applications/calamares.desktop /home/live/Desktop/
|
||||
chmod +x /home/live/Desktop/calamares.desktop # without this there'd be a dialog asking if it really should execute
|
||||
|
||||
chown -R live:live /home/live/
|
||||
|
||||
# Start the basic-test (if it wants to be started)
|
||||
systemctl daemon-reload
|
||||
systemctl start --no-block kde-linux-basic-test.service
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
# SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
|
||||
[Unit]
|
||||
Description=Test of KDE Linux disk image
|
||||
ConditionKernelCommandLine=kde-linux.basic-test=1
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/lib/basic-test
|
||||
34
publisher/go.mod
Normal file
34
publisher/go.mod
Normal file
@@ -0,0 +1,34 @@
|
||||
module invent.kde.org/kde-linux/kde-linux/publisher
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
invent.kde.org/kde-linux/kde-linux/minioauth v0.0.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
replace invent.kde.org/kde-linux/kde-linux/minioauth => ../minioauth
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/minio/crc64nvme v1.0.2 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
2
publisher/go.mod.license
Normal file
2
publisher/go.mod.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: none
|
||||
44
publisher/go.sum
Normal file
44
publisher/go.sum
Normal file
@@ -0,0 +1,44 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
2
publisher/go.sum.license
Normal file
2
publisher/go.sum.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: none
|
||||
220
publisher/main.go
Normal file
220
publisher/main.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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>
|
||||
// SPDX-FileCopyrightText: 2026 Thomas Duckworth <tduck@filotimoproject.org>
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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 prefix(path string) string {
|
||||
trimmed := strings.Trim(path, "/")
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
return trimmed + "/"
|
||||
}
|
||||
|
||||
func destinationPrefix(path string) string {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
log.Fatalln("Remote path must be a non-empty staging path")
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
for i, part := range parts {
|
||||
if part != "staging" {
|
||||
continue
|
||||
}
|
||||
if i == len(parts)-1 {
|
||||
log.Fatalln("Remote path must include a staging directory name")
|
||||
}
|
||||
if i == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts[:i], "/") + "/"
|
||||
}
|
||||
|
||||
log.Fatalln("Remote path must contain a staging directory")
|
||||
return ""
|
||||
}
|
||||
|
||||
func isChannelCommitMarker(key string) bool {
|
||||
base := filepath.Base(key)
|
||||
return base == "SHA256SUMS" || base == "SHA256SUMS.gpg"
|
||||
}
|
||||
|
||||
func removeObject(ctx context.Context, client *minio.Client, bucket string, objectKey string) {
|
||||
log.Printf("Removing staged object %s", objectKey)
|
||||
if err := client.RemoveObject(ctx, bucket, objectKey, minio.RemoveObjectOptions{}); err != nil {
|
||||
log.Fatalf("Failed to remove %s: %v", objectKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
// publish merges a staging tree into its channel root.
|
||||
func publish(client *minio.Client, bucket string, path string) {
|
||||
ctx := context.Background()
|
||||
srcPrefix := prefix(path)
|
||||
|
||||
destPrefix := destinationPrefix(path)
|
||||
|
||||
objects := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{
|
||||
Prefix: srcPrefix,
|
||||
Recursive: true,
|
||||
})
|
||||
|
||||
var objectKeys []string
|
||||
for object := range objects {
|
||||
if object.Err != nil {
|
||||
log.Fatalln("Failed to list objects:", object.Err)
|
||||
}
|
||||
objectKeys = append(objectKeys, object.Key)
|
||||
}
|
||||
sort.Strings(objectKeys)
|
||||
|
||||
for _, objectKey := range objectKeys {
|
||||
rel := strings.TrimPrefix(objectKey, srcPrefix)
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
if isChannelCommitMarker(rel) {
|
||||
log.Printf("Skipping staged commit marker %s", objectKey)
|
||||
removeObject(ctx, client, bucket, objectKey)
|
||||
continue
|
||||
}
|
||||
dstKey := destPrefix + rel
|
||||
if dstKey == objectKey {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Publishing %s -> %s", objectKey, dstKey)
|
||||
|
||||
_, err := client.ComposeObject(ctx,
|
||||
minio.CopyDestOptions{Bucket: bucket, Object: dstKey},
|
||||
minio.CopySrcOptions{Bucket: bucket, Object: objectKey},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to copy %s to %s: %v", objectKey, dstKey, err)
|
||||
}
|
||||
|
||||
removeObject(ctx, client, bucket, objectKey)
|
||||
}
|
||||
}
|
||||
|
||||
// artifactSuffixes are the build artifacts we download so they can be pushed to
|
||||
// files.kde.org and the chunk store. sha256sums are regenerated at publish time, so they're excluded here
|
||||
var artifactSuffixes = []string{".iso", ".torrent", ".efi", ".tar.zst", ".erofs", ".caibx"}
|
||||
|
||||
func isArtifact(key string) bool {
|
||||
for _, suffix := range artifactSuffixes {
|
||||
if strings.HasSuffix(key, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// download gets every build artifact under path to publish to files.kde.org and the chunk store
|
||||
func download(client *minio.Client, bucket string, path string, output string) {
|
||||
ctx := context.Background()
|
||||
|
||||
objects := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{
|
||||
Prefix: prefix(path),
|
||||
Recursive: true,
|
||||
})
|
||||
|
||||
for object := range objects {
|
||||
if object.Err != nil {
|
||||
log.Fatalln("Failed to list objects:", object.Err)
|
||||
}
|
||||
|
||||
if !isArtifact(object.Key) {
|
||||
continue
|
||||
}
|
||||
|
||||
dst := filepath.Join(output, filepath.Base(object.Key))
|
||||
log.Printf("Downloading %s -> %s", object.Key, dst)
|
||||
if err := client.FGetObject(ctx, bucket, object.Key, dst, minio.GetObjectOptions{}); err != nil {
|
||||
log.Fatalf("Failed to download %s: %v", object.Key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either downloads every build artifact in the staging directory into --output,
|
||||
// or promotes the staging tree into the channel root.
|
||||
func main() {
|
||||
remote := flag.String("remote", "", "remote url to publish from, e.g. s3+https://storage.kde.org/kde-linux/staging/1")
|
||||
output := flag.String("output", ".", "directory to download artifacts into")
|
||||
downloadMode := flag.Bool("download", false, "download artifacts from the staging tree")
|
||||
promoteMode := flag.Bool("promote", false, "promote the staging tree into the channel root")
|
||||
flag.Parse()
|
||||
|
||||
if *downloadMode == *promoteMode {
|
||||
log.Fatalln("Must choose exactly one of --download or --promote")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if *downloadMode {
|
||||
log.Println("Downloading artifacts from bucket", bucket, "with path prefix", path, "into", *output)
|
||||
download(minioClient, bucket, path, *output)
|
||||
}
|
||||
|
||||
if *promoteMode {
|
||||
log.Println("Publishing to bucket", bucket, "from source path", path)
|
||||
publish(minioClient, bucket, path)
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,46 @@ set -eu
|
||||
# Output Directory
|
||||
OUTDIR="${OUTDIR:-mkosi.output}"
|
||||
|
||||
# Do not blow the lid off the storage for now. Reset the tree and only publish a select few files
|
||||
# We need to wire up an ephemeral image-signing key pair for OpenQA. We can't use the production key, but we still need
|
||||
# to test if the image can be upgraded to. Hence, create a key pair and pass it into OpenQA, where it will be injected
|
||||
# into the system.
|
||||
GNUPGHOME="$PWD/.openqa-gpg"
|
||||
rm -rf "$GNUPGHOME"
|
||||
mkdir -p "$GNUPGHOME"
|
||||
chmod 700 "$GNUPGHOME"
|
||||
gpg --batch --pinentry-mode loopback --homedir="$GNUPGHOME" --passphrase "" --quick-generate-key "KDE Linux openQA <linux@kde.org>" ed25519 sign 3d
|
||||
SYSUPDATE_PUBKEY_B64=$(gpg --homedir="$GNUPGHOME" --export --armor "KDE Linux openQA <linux@kde.org>" | base64 -w0)
|
||||
|
||||
mv upload-tree upload-tree-old || true
|
||||
if [ ! -d upload-tree ]; then
|
||||
mkdir upload-tree
|
||||
for f in "$OUTDIR"/*.iso "$OUTDIR"/*.erofs "$OUTDIR"/*.efi; do
|
||||
if [[ $f == *.test.iso ]]; then
|
||||
# Skip test images
|
||||
continue
|
||||
fi
|
||||
mv "$f" upload-tree/
|
||||
done
|
||||
mkdir -p upload-tree/sysupdate/v2
|
||||
mv "$OUTDIR"/*.iso upload-tree/
|
||||
mv "$OUTDIR"/*.efi "$OUTDIR"/*.tar.zst "$OUTDIR"/*.erofs "$OUTDIR"/*.caibx upload-tree/sysupdate/v2/
|
||||
(
|
||||
cd upload-tree/sysupdate/v2
|
||||
# shellcheck disable=SC2129
|
||||
sha256sum -- *.efi >> SHA256SUMS
|
||||
sha256sum -- *.tar.zst >> SHA256SUMS
|
||||
sha256sum -- *.erofs >> SHA256SUMS
|
||||
# Don't put .erofs.caibx into SHA256SUMS, it will break file matching.
|
||||
# https://github.com/systemd/systemd/issues/38605
|
||||
sha256sum -- *-x86-64.caibx >> SHA256SUMS
|
||||
|
||||
# Sign, so test images can actually verify and upgrade from artifacts.
|
||||
gpg --homedir="$GNUPGHOME" --output SHA256SUMS.gpg --detach-sign SHA256SUMS
|
||||
)
|
||||
fi
|
||||
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./uploader/ run . --remote "s3+https://storage.kde.org/ci-artifacts/$CI_PROJECT_PATH/j/$CI_JOB_ID"
|
||||
|
||||
# Point OpenQA at the image we just uploaded.
|
||||
ISO_FILE=$(find upload-tree -maxdepth 1 -name '*.iso' | head -1 | xargs -r basename)
|
||||
echo "IMAGE_URL=https://storage.kde.org/ci-artifacts/$CI_PROJECT_PATH/j/$CI_JOB_ID/$ISO_FILE" >> build.env
|
||||
echo "STAGING_CHANNEL_URL=https://storage.kde.org/ci-artifacts/$CI_PROJECT_PATH/j/$CI_JOB_ID/sysupdate/v2/" >> build.env
|
||||
echo "SYSUPDATE_PUBKEY_B64=$SYSUPDATE_PUBKEY_B64" >> build.env
|
||||
|
||||
echo "𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏"
|
||||
echo "You can find the raw disk images at:"
|
||||
echo "You can find the raw disk images and sysupdate tree at:"
|
||||
echo "https://qoomon.github.io/aws-s3-bucket-browser/index.html?bucket=https://storage.kde.org/ci-artifacts/#$CI_PROJECT_PATH/j/$CI_JOB_ID/"
|
||||
echo "𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏𓃀𓂝𓏏"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
// SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type AWSSection struct {
|
||||
AccessKeyId string `ini:"aws_access_key_id"`
|
||||
SecretKey string `ini:"aws_secret_access_key"`
|
||||
SessionToken string `ini:"aws_session_token"`
|
||||
}
|
||||
|
||||
func readConfigAWS(section string) (AWSSection, error) {
|
||||
awsSection := AWSSection{}
|
||||
|
||||
awsConfigPath := filepath.Join(os.Getenv("HOME"), ".aws", "credentials")
|
||||
cfg, err := ini.Load(awsConfigPath)
|
||||
if err != nil {
|
||||
return awsSection, errors.New("failed to load AWS credentials file: " + err.Error())
|
||||
}
|
||||
|
||||
err = cfg.Section(section).MapTo(&awsSection)
|
||||
if err != nil {
|
||||
return awsSection, errors.New("failed to map AWS credentials section: " + err.Error())
|
||||
}
|
||||
|
||||
return awsSection, nil
|
||||
}
|
||||
@@ -6,10 +6,13 @@ require (
|
||||
github.com/minio/minio-go/v7 v7.0.100
|
||||
github.com/pkg/sftp v1.13.6
|
||||
golang.org/x/crypto v0.50.0
|
||||
invent.kde.org/kde-linux/kde-linux/minioauth v0.0.0
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
replace invent.kde.org/kde-linux/kde-linux/minioauth => ../minioauth
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"invent.kde.org/kde-linux/kde-linux/minioauth"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
type release struct {
|
||||
@@ -29,36 +29,12 @@ type release struct {
|
||||
func connectToMinIO() *minio.Client {
|
||||
endpoint := "storage.kde.org"
|
||||
|
||||
// Below is a huge code dupe from uploader/main.go, should be shared somehow
|
||||
awsSection, err := readConfigAWS("default")
|
||||
minioClient, err := minioauth.Connect(endpoint)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to read AWS config:", err)
|
||||
}
|
||||
accessKeyID := awsSection.AccessKeyId
|
||||
if accessKeyID == "" {
|
||||
log.Fatalln("AWS access key ID is empty")
|
||||
}
|
||||
secretAccessKey := awsSection.SecretKey
|
||||
if secretAccessKey == "" {
|
||||
log.Fatalln("AWS secret access key is empty")
|
||||
}
|
||||
sessionToken := awsSection.SessionToken
|
||||
if secretAccessKey == "" {
|
||||
log.Fatalln("AWS session token is empty")
|
||||
}
|
||||
useSSL := true
|
||||
|
||||
// Initialize minio client object.
|
||||
minioClient, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, sessionToken),
|
||||
Secure: useSSL,
|
||||
TrailingHeaders: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create MinIO client:", err)
|
||||
log.Fatalln("Failed to connect to MinIO:", err)
|
||||
}
|
||||
|
||||
buckets, err := minioClient.ListBuckets(context.Background())
|
||||
buckets, err := minioauth.ListBuckets(minioClient)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to list buckets:", err)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ func connectToHost(user, host, identity string) (*ssh.Client, *ssh.Session, erro
|
||||
log.Fatalf("unable to parse private key: %v", err)
|
||||
}
|
||||
|
||||
// You can use `ssh-keyscan origin.files.kde.org` to get the host key
|
||||
_, _, hostKey, _, _, err := ssh.ParseKnownHosts([]byte("origin.files.kde.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILUjdH4S7otYIdLUkOZK+owIiByjNQPzGi7GQ5HOWjO6"))
|
||||
// You can use `ssh-keyscan tinami.kde.org` to get the host key
|
||||
_, _, hostKey, _, _, err := ssh.ParseKnownHosts([]byte("tinami.kde.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILUjdH4S7otYIdLUkOZK+owIiByjNQPzGi7GQ5HOWjO6"))
|
||||
if err != nil {
|
||||
log.Fatalf("unable to parse host public key: %v", err)
|
||||
}
|
||||
@@ -120,6 +120,7 @@ func readSHA256SUMS(client *sftp.Client, path string) map[string]string {
|
||||
|
||||
func readSHA256s(toKeep []string, releases map[string]release, existingSums map[string]string) []string {
|
||||
sha256s := []string{}
|
||||
rootPath := strings.TrimRight(os.Getenv("SSH_ROOT_PATH"), "/")
|
||||
for _, key := range toKeep {
|
||||
artifacts := releases[key].artifacts
|
||||
sort.Strings(artifacts) // Sort artifacts to ensure consistent order
|
||||
@@ -129,7 +130,7 @@ func readSHA256s(toKeep []string, releases map[string]release, existingSums map[
|
||||
// https://github.com/systemd/systemd/issues/38605
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(artifact, "/home/kdeos/kde-linux/kdeos_") {
|
||||
if rootPath != "" && strings.HasPrefix(artifact, rootPath+"/kdeos_") {
|
||||
// HACK 2025-08-20 sha256s of the files are broken, only drop this if when they are fixed (possibly just a matter of time)
|
||||
continue
|
||||
}
|
||||
@@ -235,7 +236,10 @@ func main() {
|
||||
identity := os.Getenv("SSH_IDENTITY")
|
||||
host := os.Getenv("SSH_HOST")
|
||||
user := os.Getenv("SSH_USER")
|
||||
path := os.Getenv("SSH_PATH")
|
||||
path := os.Getenv("SSH_SYSUPDATE_PATH")
|
||||
if path == "" {
|
||||
path = os.Getenv("SSH_PATH")
|
||||
}
|
||||
root_path := os.Getenv("SSH_ROOT_PATH")
|
||||
|
||||
var errs []error
|
||||
@@ -249,7 +253,7 @@ func main() {
|
||||
errs = append(errs, errors.New("SSH_USER not set"))
|
||||
}
|
||||
if path == "" {
|
||||
errs = append(errs, errors.New("SSH_PATH not set"))
|
||||
errs = append(errs, errors.New("SSH_SYSUPDATE_PATH or SSH_PATH not set"))
|
||||
}
|
||||
if root_path == "" {
|
||||
errs = append(errs, errors.New("SSH_ROOT_PATH not set"))
|
||||
|
||||
221
upload.sh
221
upload.sh
@@ -2,89 +2,174 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
# SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2026 Hadi Chokr <hadichokr@icloud.com>
|
||||
# SPDX-FileCopyrightText: 2026 Thomas Duckworth <tduck@filotimoproject.org>
|
||||
|
||||
set -eux
|
||||
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Choice between --stage and --publish must be provided as argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STAGE=
|
||||
PUBLISH=
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--stage)
|
||||
STAGE=1
|
||||
shift
|
||||
;;
|
||||
--publish)
|
||||
PUBLISH=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$STAGE" -eq 1 && "$PUBLISH" -eq 1 ]] || [[ "$STAGE" -ne 1 && "$PUBLISH" -ne 1 ]]; then
|
||||
echo "Must choose exactly one of --stage or --publish."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTDIR=mkosi.output
|
||||
|
||||
# For the vacuum helper and this script
|
||||
export SSH_IDENTITY="$PWD/.secure_files/ssh.key"
|
||||
export SSH_USER=kdeos
|
||||
export SSH_HOST=origin.files.kde.org
|
||||
export SSH_ROOT_PATH=/home/kdeos/kde-linux/
|
||||
export SSH_PATH=$SSH_ROOT_PATH/sysupdate/v2/
|
||||
export SSH_HOST=tinami.kde.org
|
||||
export SSH_ROOT_PATH=/srv/archives/files/kde-linux
|
||||
export SSH_SYSUPDATE_PATH=$SSH_ROOT_PATH/sysupdate/v2/
|
||||
export SSH_REALLY_DELETE=1
|
||||
|
||||
chmod 600 "$SSH_IDENTITY"
|
||||
|
||||
go -C ./upload-vacuum/ build -o upload-vacuum .
|
||||
./upload-vacuum/upload-vacuum
|
||||
|
||||
# The following variables are for this script only. Not shared with the vacuum helper.
|
||||
sudo chown -Rvf "$(id -u):$(id -g)" "$PWD/.secure_files" # Make sure we have access
|
||||
export GNUPGHOME="$PWD/.secure_files/gpg"
|
||||
gpg --verbose --no-options --homedir="$GNUPGHOME" --import "$PWD/.secure_files/gpg.private.key"
|
||||
REMOTE_ROOT=$SSH_USER@$SSH_HOST:$SSH_ROOT_PATH
|
||||
REMOTE_PATH=$SSH_USER@$SSH_HOST:$SSH_PATH
|
||||
# You can use `ssh-keyscan origin.files.kde.org` to get the host key
|
||||
echo "origin.files.kde.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILUjdH4S7otYIdLUkOZK+owIiByjNQPzGi7GQ5HOWjO6" >> ~/.ssh/known_hosts
|
||||
|
||||
# The initial SHA256SUMS file is created by the vacuum script based on what is left on the server. We append to it.
|
||||
|
||||
sudo chown -R "$USER":"$USER" "$OUTDIR"
|
||||
cd "$OUTDIR"
|
||||
|
||||
# We need shell globs here! More readable this way. Ignore shellcheck.
|
||||
# shellcheck disable=SC2129
|
||||
sha256sum -- *.efi >> SHA256SUMS
|
||||
sha256sum -- *.tar.zst >> SHA256SUMS
|
||||
sha256sum -- *.erofs >> SHA256SUMS
|
||||
# Don't put .erofs.caibx into the SHA256SUMS, it will break file matching.
|
||||
# https://github.com/systemd/systemd/issues/38605
|
||||
sha256sum -- *-x86-64.caibx >> SHA256SUMS
|
||||
|
||||
gpg --homedir="$GNUPGHOME" --output SHA256SUMS.gpg --detach-sign SHA256SUMS
|
||||
|
||||
scp -i "$SSH_IDENTITY" ./*.iso ./*.torrent "$REMOTE_ROOT"
|
||||
scp -i "$SSH_IDENTITY" ./*.efi ./*.tar.zst ./*.erofs ./*.caibx "$REMOTE_PATH"
|
||||
scp -i "$SSH_IDENTITY" SHA256SUMS SHA256SUMS.gpg "$REMOTE_PATH" # upload as last artifact to finalize the upload
|
||||
|
||||
# The new s3 based upload system
|
||||
|
||||
export VACUUM_REALLY_DELETE=1
|
||||
S3_STORE="s3+https://storage.kde.org/kde-linux/sysupdate/store/"
|
||||
export GNUPGHOME="$PWD/.secure_files/gpg"
|
||||
|
||||
# upload tree built during staging
|
||||
V2_TREE="upload-tree/sysupdate/v2"
|
||||
|
||||
S3_TARGET="s3+https://storage.kde.org/kde-linux/"
|
||||
S3_STORE="${S3_TARGET}sysupdate/store/"
|
||||
S3_TARGET_STAGING="${S3_TARGET_STAGING:-}"
|
||||
|
||||
## Upload to the chunk store directly
|
||||
go install -v github.com/folbricht/desync/cmd/desync@latest
|
||||
export PATH="$HOME/go/bin:$PATH"
|
||||
go -C ../token-redeemer/ run .
|
||||
desync chop \
|
||||
--concurrency "$(nproc)" \
|
||||
--store "$S3_STORE" \
|
||||
./*-x86-64.caibx \
|
||||
./*-x86-64.erofs
|
||||
# files.kde.org scp targets. We don't stage on files.kde.org, only on the storage.kde.org bucket.
|
||||
# We download from the bucket then upload directly to files.kde.org to publish there.
|
||||
REMOTE_ROOT_PATH=$SSH_USER@$SSH_HOST:$SSH_ROOT_PATH
|
||||
REMOTE_SYSUPDATE_PATH=$SSH_USER@$SSH_HOST:$SSH_SYSUPDATE_PATH
|
||||
# You can use `ssh-keyscan tinami.kde.org` to get the host key
|
||||
echo "tinami.kde.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILUjdH4S7otYIdLUkOZK+owIiByjNQPzGi7GQ5HOWjO6" >> ~/.ssh/known_hosts
|
||||
|
||||
## Prepare the image upload tree
|
||||
cd ..
|
||||
rm -rf upload-tree
|
||||
V2_TREE="upload-tree/testing/sysupdate/v2"
|
||||
mkdir -p "$V2_TREE"
|
||||
stage() {
|
||||
S3_TARGET_STAGING="${S3_TARGET_STAGING:-${S3_TARGET}testing/staging/${CI_PIPELINE_ID}/${CI_JOB_ID}}"
|
||||
|
||||
mv "$OUTDIR"/*.iso "$OUTDIR"/*.torrent upload-tree/testing/
|
||||
mv "$OUTDIR"/*.efi "$OUTDIR"/*.tar.zst "$OUTDIR"/*.erofs "$OUTDIR"/*.caibx "$V2_TREE/"
|
||||
# Stage the freshly built image into the bucket.
|
||||
sudo chown -R "$USER":"$USER" "$OUTDIR"
|
||||
|
||||
### Upload
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./uploader/ run . --remote "$S3_TARGET"
|
||||
(
|
||||
cd "$OUTDIR"
|
||||
# We need shell globs here! More readable this way. Ignore shellcheck.
|
||||
# shellcheck disable=SC2129
|
||||
sha256sum -- *.efi >> SHA256SUMS
|
||||
sha256sum -- *.tar.zst >> SHA256SUMS
|
||||
sha256sum -- *.erofs >> SHA256SUMS
|
||||
# Don't put .erofs.caibx into the SHA256SUMS, it will break file matching.
|
||||
# https://github.com/systemd/systemd/issues/38605
|
||||
sha256sum -- *-x86-64.caibx >> SHA256SUMS
|
||||
|
||||
### Clean up previous images (this replaces the upload-tree with only SHA256SUMS!)
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./upload-vacuum-v3/ run .
|
||||
gpg --homedir="$GNUPGHOME" --output SHA256SUMS.gpg --detach-sign SHA256SUMS
|
||||
)
|
||||
|
||||
#### Sign SHA256SUMS
|
||||
gpg --homedir="$GNUPGHOME" --output "$V2_TREE/SHA256SUMS.gpg" --detach-sign "$V2_TREE/SHA256SUMS"
|
||||
# Prepare the staging upload tree.
|
||||
rm -rf upload-tree
|
||||
mkdir -p "$V2_TREE"
|
||||
mv "$OUTDIR"/*.iso "$OUTDIR"/*.torrent upload-tree/
|
||||
mv "$OUTDIR"/*.efi "$OUTDIR"/*.tar.zst "$OUTDIR"/*.erofs "$OUTDIR"/*.caibx "$V2_TREE/"
|
||||
mv "$OUTDIR"/SHA256SUMS "$OUTDIR"/SHA256SUMS.gpg "$V2_TREE/"
|
||||
|
||||
#### Upload SHA256SUMS
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./uploader/ run . --remote "$S3_TARGET"
|
||||
# Upload to the per-job staging prefix in the bucket.
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./uploader/ run . --remote "$S3_TARGET_STAGING"
|
||||
|
||||
# Emit the exact staging target for the publish job, plus public URLs for
|
||||
# OpenQA to test the staged image and sysupdate channel.
|
||||
echo "S3_TARGET_STAGING=$S3_TARGET_STAGING" >> build.env
|
||||
echo "IMAGE_URL=${S3_TARGET_STAGING#s3+}/$(basename upload-tree/*.iso)" >> build.env
|
||||
echo "STAGING_CHANNEL_URL=${S3_TARGET_STAGING#s3+}/sysupdate/v2/" >> build.env
|
||||
}
|
||||
|
||||
publish() {
|
||||
if [[ -z "$S3_TARGET_STAGING" ]]; then
|
||||
echo "S3_TARGET_STAGING must be supplied by the imaging job dotenv artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull the staged build down from the bucket into ./publish-artifacts.
|
||||
rm -rf publish-artifacts
|
||||
mkdir -p publish-artifacts
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./publisher/ build -o publisher .
|
||||
./publisher/publisher --remote "$S3_TARGET_STAGING" --output publish-artifacts --download
|
||||
|
||||
# Publish to files.kde.org.
|
||||
go -C ./upload-vacuum/ build -o upload-vacuum .
|
||||
(
|
||||
cd publish-artifacts
|
||||
../upload-vacuum/upload-vacuum
|
||||
|
||||
# shellcheck disable=SC2129
|
||||
sha256sum -- *.efi >> SHA256SUMS
|
||||
sha256sum -- *.tar.zst >> SHA256SUMS
|
||||
sha256sum -- *.erofs >> SHA256SUMS
|
||||
# https://github.com/systemd/systemd/issues/38605
|
||||
sha256sum -- *-x86-64.caibx >> SHA256SUMS
|
||||
|
||||
gpg --homedir="$GNUPGHOME" --output SHA256SUMS.gpg --detach-sign SHA256SUMS
|
||||
|
||||
scp -i "$SSH_IDENTITY" ./*.iso ./*.torrent "$REMOTE_ROOT_PATH"
|
||||
scp -i "$SSH_IDENTITY" ./*.efi ./*.tar.zst ./*.erofs ./*.caibx "$REMOTE_SYSUPDATE_PATH"
|
||||
)
|
||||
|
||||
# Push the rootfs chunks into the S3 chunk store.
|
||||
go install -v github.com/folbricht/desync/cmd/desync@f67d01e
|
||||
export PATH="$HOME/go/bin:$PATH"
|
||||
go -C ./token-redeemer/ run .
|
||||
desync chop \
|
||||
--concurrency "$(nproc)" \
|
||||
--store "$S3_STORE" \
|
||||
publish-artifacts/*-x86-64.caibx \
|
||||
publish-artifacts/*-x86-64.erofs
|
||||
|
||||
# Upload sums to files.kde.org only after the chunk store is ready.
|
||||
scp -i "$SSH_IDENTITY" publish-artifacts/SHA256SUMS publish-artifacts/SHA256SUMS.gpg "$REMOTE_SYSUPDATE_PATH"
|
||||
|
||||
# Merge the staged tree into the live S3 tree only after files.kde.org and the chunk store are ready.
|
||||
go -C ./token-redeemer/ run .
|
||||
./publisher/publisher --remote "$S3_TARGET_STAGING" --promote
|
||||
|
||||
# Regenerate, re-sign and re-upload SHA256SUMS.
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./upload-vacuum-v3/ run .
|
||||
|
||||
gpg --homedir="$GNUPGHOME" \
|
||||
--output "upload-tree/testing/sysupdate/v2/SHA256SUMS.gpg" \
|
||||
--detach-sign "upload-tree/testing/sysupdate/v2/SHA256SUMS"
|
||||
go -C ./token-redeemer/ run .
|
||||
go -C ./uploader/ run . --remote "$S3_TARGET"
|
||||
}
|
||||
|
||||
curl -s https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer | bash
|
||||
|
||||
sudo chown -Rvf "$(id -u):$(id -g)" "$PWD/.secure_files" # Make sure we have access
|
||||
chmod 600 "$SSH_IDENTITY"
|
||||
gpg --verbose --no-options --homedir="$GNUPGHOME" --import "$PWD/.secure_files/gpg.private.key"
|
||||
|
||||
|
||||
if [[ "${STAGE}" -eq 1 ]]; then
|
||||
stage
|
||||
fi
|
||||
|
||||
if [[ "${PUBLISH}" -eq 1 ]]; then
|
||||
publish
|
||||
fi
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
// SPDX-FileCopyrightText: 2025 Harald Sitter <sitter@kde.org>
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type AWSSection struct {
|
||||
AccessKeyId string `ini:"aws_access_key_id"`
|
||||
SecretKey string `ini:"aws_secret_access_key"`
|
||||
SessionToken string `ini:"aws_session_token"`
|
||||
}
|
||||
|
||||
func readConfigAWS(section string) (AWSSection, error) {
|
||||
awsSection := AWSSection{}
|
||||
|
||||
awsConfigPath := filepath.Join(os.Getenv("HOME"), ".aws", "credentials")
|
||||
cfg, err := ini.Load(awsConfigPath)
|
||||
if err != nil {
|
||||
return awsSection, errors.New("failed to load AWS credentials file: " + err.Error())
|
||||
}
|
||||
|
||||
err = cfg.Section(section).MapTo(&awsSection)
|
||||
if err != nil {
|
||||
return awsSection, errors.New("failed to map AWS credentials section: " + err.Error())
|
||||
}
|
||||
|
||||
return awsSection, nil
|
||||
}
|
||||
@@ -6,9 +6,12 @@ toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
invent.kde.org/kde-linux/kde-linux/minioauth v0.0.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
replace invent.kde.org/kde-linux/kde-linux/minioauth => ../minioauth
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
||||
@@ -17,40 +17,18 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"invent.kde.org/kde-linux/kde-linux/minioauth"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
func connectToMinIO(endpoint string) *minio.Client {
|
||||
awsSection, err := readConfigAWS("default")
|
||||
minioClient, err := minioauth.Connect(endpoint)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to read AWS config:", err)
|
||||
}
|
||||
accessKeyID := awsSection.AccessKeyId
|
||||
if accessKeyID == "" {
|
||||
log.Fatalln("AWS access key ID is empty")
|
||||
}
|
||||
secretAccessKey := awsSection.SecretKey
|
||||
if secretAccessKey == "" {
|
||||
log.Fatalln("AWS secret access key is empty")
|
||||
}
|
||||
sessionToken := awsSection.SessionToken
|
||||
if secretAccessKey == "" {
|
||||
log.Fatalln("AWS session token is empty")
|
||||
}
|
||||
useSSL := true
|
||||
|
||||
// Initialize minio client object.
|
||||
minioClient, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, sessionToken),
|
||||
Secure: useSSL,
|
||||
TrailingHeaders: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create MinIO client:", err)
|
||||
log.Fatalln("Failed to connect to MinIO:", err)
|
||||
}
|
||||
|
||||
buckets, err := minioClient.ListBuckets(context.Background())
|
||||
buckets, err := minioauth.ListBuckets(minioClient)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to list buckets:", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user