diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..f984bef --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,161 @@ +name: build development package +on: + pull_request: + merge_group: + push: + branches: + - main + workflow_dispatch: +env: + IMAGE_NAME: ublue-update + IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }} + +jobs: + push-ghcr: + name: Build and push image + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + id-token: write + strategy: + fail-fast: false + matrix: + major_version: [39, 40, 41] + include: + - major_version: 39 + is_latest_version: false + is_stable_version: true + - major_version: 40 + is_latest_version: true + is_stable_version: false + - major_version: 41 + is_latest_version: false + is_stable_version: false + steps: + # Checkout push-to-registry action GitHub repository + - name: Checkout Push to Registry action + uses: actions/checkout@v4 + + - name: Generate tags + id: generate-tags + shell: bash + run: | + # Generate a timestamp for creating an image version history + TIMESTAMP="$(date +%Y%m%d)" + MAJOR_VERSION="${{ matrix.major_version }}" + COMMIT_TAGS=() + BUILD_TAGS=() + # Have tags for tracking builds during pull request + SHA_SHORT="${GITHUB_SHA::7}" + COMMIT_TAGS+=("pr-${{ github.event.pull_request.number }}-${MAJOR_VERSION}") + COMMIT_TAGS+=("${SHA_SHORT}-${MAJOR_VERSION}") + if [[ "${{ matrix.is_latest_version }}" == "true" ]] && \ + [[ "${{ matrix.is_stable_version }}" == "true" ]]; then + COMMIT_TAGS+=("pr-${{ github.event.pull_request.number }}") + COMMIT_TAGS+=("${SHA_SHORT}") + fi + + BUILD_TAGS=("${MAJOR_VERSION}" "${MAJOR_VERSION}-${TIMESTAMP}") + + if [[ "${{ matrix.is_latest_version }}" == "true" ]] && \ + [[ "${{ matrix.is_stable_version }}" == "true" ]]; then + BUILD_TAGS+=("latest") + fi + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "Generated the following commit tags: " + for TAG in "${COMMIT_TAGS[@]}"; do + echo "${TAG}" + done + alias_tags=("${COMMIT_TAGS[@]}") + else + alias_tags=("${BUILD_TAGS[@]}") + fi + echo "Generated the following build tags: " + for TAG in "${BUILD_TAGS[@]}"; do + echo "${TAG}" + done + echo "alias_tags=${alias_tags[*]}" >> $GITHUB_OUTPUT + + # Build image using Buildah action + # - name: Build Image + # id: build_image + # uses: redhat-actions/buildah-build@v2 + # with: + # containerfiles: | + # ./Containerfile + # image: ${{ env.IMAGE_NAME }} + # tags: | + # ${{ steps.generate-tags.outputs.alias_tags }} + # build-args: | + # FEDORA_MAJOR_VERSION=${{ matrix.major_version }} + # oci: true + + - name: Build Image + id: build_image + env: + FEDORA_MAJOR_VERSION: ${{ matrix.major_version }} + run: | + just container-build + with: + containerfiles: | + ./Containerfile + image: ${{ env.IMAGE_NAME }} + tags: | + ${{ steps.generate-tags.outputs.alias_tags }} + build-args: | + FEDORA_MAJOR_VERSION=${{ matrix.major_version }} + oci: true + + + # Workaround bug where capital letters in your GitHub username make it impossible to push to GHCR. + # https://github.com/macbre/push-to-ghcr/issues/12 + - name: Lowercase Registry + id: registry_case + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ env.IMAGE_REGISTRY }} + + # Push the image to GHCR (Image Registry) + - name: Push To GHCR + uses: redhat-actions/push-to-registry@v2 + id: push + if: github.event_name != 'pull_request' + env: + REGISTRY_USER: ${{ github.actor }} + REGISTRY_PASSWORD: ${{ github.token }} + with: + image: ${{ steps.build_image.outputs.image }} + tags: ${{ steps.build_image.outputs.tags }} + registry: ${{ steps.registry_case.outputs.lowercase }} + username: ${{ env.REGISTRY_USER }} + password: ${{ env.REGISTRY_PASSWORD }} + extra-args: | + --disable-content-trust + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Sign container + - uses: sigstore/cosign-installer@v3.7.0 + if: github.event_name != 'pull_request' + + - name: Sign container image + if: github.event_name != 'pull_request' + run: | + cosign sign -y --key env://COSIGN_PRIVATE_KEY ${{ steps.registry_case.outputs.lowercase }}/${{ env.IMAGE_NAME }}@${TAGS} + env: + TAGS: ${{ steps.push.outputs.digest }} + COSIGN_EXPERIMENTAL: false + COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} + + - name: Echo outputs + if: github.event_name != 'pull_request' + run: | + echo "${{ toJSON(steps.push.outputs) }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b0a37c..f984bef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,9 +79,25 @@ jobs: echo "alias_tags=${alias_tags[*]}" >> $GITHUB_OUTPUT # Build image using Buildah action + # - name: Build Image + # id: build_image + # uses: redhat-actions/buildah-build@v2 + # with: + # containerfiles: | + # ./Containerfile + # image: ${{ env.IMAGE_NAME }} + # tags: | + # ${{ steps.generate-tags.outputs.alias_tags }} + # build-args: | + # FEDORA_MAJOR_VERSION=${{ matrix.major_version }} + # oci: true + - name: Build Image id: build_image - uses: redhat-actions/buildah-build@v2 + env: + FEDORA_MAJOR_VERSION: ${{ matrix.major_version }} + run: | + just container-build with: containerfiles: | ./Containerfile @@ -92,6 +108,7 @@ jobs: FEDORA_MAJOR_VERSION=${{ matrix.major_version }} oci: true + # Workaround bug where capital letters in your GitHub username make it impossible to push to GHCR. # https://github.com/macbre/push-to-ghcr/issues/12 - name: Lowercase Registry diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index bd519bc..900ab31 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -27,7 +27,7 @@ jobs: build-release: name: Build and push rpm package - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: write packages: write diff --git a/Containerfile b/Containerfile index 4d15ef1..a5dbd34 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,5 @@ -ARG TEST_IMAGE="${TEST_IMAGE:-ghcr.io/ublue-os/base-main:40}" -ARG FEDORA_MAJOR_VERSION="${FEDORA_MAJOR_VERSION:-40}" +ARG TEST_IMAGE="${TEST_IMAGE:-ghcr.io/ublue-os/base-main:41}" +ARG FEDORA_MAJOR_VERSION="${FEDORA_MAJOR_VERSION:-41}" FROM registry.fedoraproject.org/fedora:${FEDORA_MAJOR_VERSION} AS builder @@ -9,29 +9,9 @@ WORKDIR /app ADD . /app -RUN dnf install \ - --disablerepo='*' \ - --enablerepo='fedora,updates' \ - --setopt install_weak_deps=0 \ - --nodocs \ - --assumeyes \ - 'dnf-command(builddep)' \ - rpkg \ - rpm-build && \ - mkdir -p "$UBLUE_ROOT" && \ - rpkg spec --outdir "$UBLUE_ROOT" && \ - dnf builddep -y output/ublue-update.spec && \ - just build-rpm - -# Dump a file list for each RPM for easier consumption -RUN \ - for RPM in ${UBLUE_ROOT}/noarch/*.rpm; do \ - NAME="$(rpm -q $RPM --queryformat='%{NAME}')"; \ - mkdir -p "${UBLUE_ROOT}/ublue-os/files/${NAME}"; \ - rpm2cpio "${RPM}" | cpio -idmv --directory "${UBLUE_ROOT}/ublue-os/files/${NAME}"; \ - mkdir -p ${UBLUE_ROOT}/ublue-os/rpms/; \ - cp "${RPM}" "${UBLUE_ROOT}/ublue-os/rpms/$(rpm -q "${RPM}" --queryformat='%{NAME}.%{ARCH}.rpm')"; \ - done +RUN dnf install -y just + +RUN just build-rpm-container FROM ${TEST_IMAGE} diff --git a/Containerfile.builder b/Containerfile.builder new file mode 100644 index 0000000..11e2ca2 --- /dev/null +++ b/Containerfile.builder @@ -0,0 +1,18 @@ +ARG FEDORA_MAJOR_VERSION="${FEDORA_MAJOR_VERSION:-41}" + +FROM registry.fedoraproject.org/fedora:${FEDORA_MAJOR_VERSION} AS builder + +ENV UBLUE_ROOT=/app/output + +WORKDIR /app + +ADD . /app + +RUN dnf install -y just git + +RUN just build-rpm + +FROM scratch + +ENV UBLUE_ROOT=/app/output +COPY --from=builder ${UBLUE_ROOT}/ublue-os/rpms /tmp/rpms diff --git a/justfile b/justfile index 972322d..218a2f9 100644 --- a/justfile +++ b/justfile @@ -4,17 +4,17 @@ export TARGET := "ublue-update" export SOURCE_DIR := UBLUE_ROOT + "/" + TARGET export RPMBUILD := UBLUE_ROOT + "/rpmbuild" -export GITHUB_REF := env_var_or_default("GITHUB_REF","refs/tags/v1.0.0+" + `git rev-parse --short HEAD`) +# export GITHUB_REF := env_var_or_default("GITHUB_REF","refs/tags/v1.0.0+" + `git rev-parse --short HEAD`) # Define the GITHUB_REF variable if it's not already set +default: + just --list + venv-create: /usr/bin/python -m venv venv source venv/bin/activate && pip3 install . echo 'Enter: `source venv/bin/activate` to enter the venv' -default: - just --list - build: black --check src python3 -m build @@ -28,6 +28,31 @@ spec: output build-rpm: rpkg local --outdir "$PWD/output" +build-rpm-container: + #!/usr/bin/env bash + dnf install \ + --disablerepo='*' \ + --enablerepo='fedora,updates' \ + --setopt install_weak_deps=0 \ + --nodocs \ + --assumeyes \ + 'dnf-command(builddep)' \ + rpkg \ + rpm-build \ + git + mkdir -p "$UBLUE_ROOT" + rpkg spec --outdir "$UBLUE_ROOT" + dnf builddep -y output/ublue-update.spec + rpkg local --outdir "$PWD/output" + + # file rpm filelist + for RPM in ${UBLUE_ROOT}/noarch/*.rpm; do + NAME="$(rpm -q $RPM --queryformat='%{NAME}')" + mkdir -p "${UBLUE_ROOT}/ublue-os/rpms/" + cp "${RPM}" "${UBLUE_ROOT}/ublue-os/rpms/$(rpm -q "${RPM}" --queryformat='%{NAME}.%{ARCH}.rpm')" + done + + output: mkdir -p output @@ -35,15 +60,16 @@ format: black src flake8 src + dnf-install: dnf install -y "output/noarch/*.rpm" container-build: - podman build . -t testing -f Containerfile + podman build . -t test-container -f Containerfile container-test: #!/usr/bin/env bash - podman run -d --replace --name ublue_update_test --security-opt label=disable --device /dev/fuse:rw --privileged testing + podman run -d --replace --name ublue_update_test --security-opt label=disable --device /dev/fuse:rw --privileged test-container while [[ "$(podman exec ublue_update_test systemctl is-system-running)" != "running" && "$(podman exec ublue_update_test systemctl is-system-running)" != "degraded" ]]; do echo "Waiting for systemd to finish booting..." sleep 1 diff --git a/src/ublue_update/cli.py b/src/ublue_update/cli.py index 403dc9b..5cee9e8 100644 --- a/src/ublue_update/cli.py +++ b/src/ublue_update/cli.py @@ -16,7 +16,7 @@ from ublue_update.filelock import acquire_lock, release_lock -def notify(title: str, body: str, actions: list = [], urgency: str = "normal"): +def notify(title: str, body: str, actions: list = [], urgency: str = "normal") -> subprocess.CompletedProcess[bytes] | None: if not cfg.dbus_notify: return process_uid = os.getuid() @@ -31,17 +31,18 @@ def notify(title: str, body: str, actions: list = [], urgency: str = "normal"): if actions != []: for action in actions: args.append(f"--action={action}") + # If root run per user: if process_uid == 0: users = [] try: users = get_active_users() except KeyError as e: log.error("failed to get active logind session info", e) + out: subprocess.CompletedProcess[bytes] | None = None for user in users: out = run_uid(user[0], args) - if actions != []: - return out - return + return out + out = subprocess.run(args, capture_output=True) return out @@ -80,7 +81,7 @@ def run_updates(system, system_update_available, dry_run): filelock_path = "/run/ublue-update.lock" if process_uid != 0: xdg_runtime_dir = os.environ.get("XDG_RUNTIME_DIR") - if os.path.isdir(xdg_runtime_dir): + if xdg_runtime_dir is not None and os.path.isdir(xdg_runtime_dir): filelock_path = f"{xdg_runtime_dir}/ublue-update.lock" fd = acquire_lock(filelock_path) if fd is None: @@ -158,7 +159,7 @@ def run_updates(system, system_update_available, dry_run): ["universal-blue-update-reboot=Reboot Now"], ) # if the user has confirmed the reboot - if "universal-blue-update-reboot" in out.stdout.decode("utf-8"): + if out is not None and "universal-blue-update-reboot" in out.stdout.decode("utf-8"): subprocess.run(["systemctl", "reboot"]) else: if system: diff --git a/src/ublue_update/update_drivers/brew.py b/src/ublue_update/update_drivers/brew.py index 4d5591c..e971b89 100644 --- a/src/ublue_update/update_drivers/brew.py +++ b/src/ublue_update/update_drivers/brew.py @@ -10,7 +10,7 @@ def detect_user(): - if not os.exists(brew_prefix): + if not os.path.isdir(brew_prefix): return -1 return os.stat(brew_prefix).st_uid @@ -29,12 +29,16 @@ def brew_update(dry_run): ] out = run_uid(args + ["brew", "update"]) if out.returncode != 0: - log.error(f"brew update failed, returned code {out.returncode}, program output:") + log.error( + f"brew update failed, returned code {out.returncode}, program output:" + ) log.error(out.stdout.decode("utf-8")) return out = run_uid(args + ["brew", "upgrade"]) if out.returncode != 0: - log.error(f"brew upgrade failed, returned code {out.returncode}, program output:") + log.error( + f"brew upgrade failed, returned code {out.returncode}, program output:" + ) log.error(out.stdout.decode("utf-8")) return log.info("brew updates completed") diff --git a/src/ublue_update/update_inhibitors/custom.py b/src/ublue_update/update_inhibitors/custom.py index 6b85468..b587d60 100644 --- a/src/ublue_update/update_inhibitors/custom.py +++ b/src/ublue_update/update_inhibitors/custom.py @@ -65,7 +65,7 @@ def run_custom_check_scripts() -> List[dict]: return results -def check_custom_inhibitors() -> bool: +def check_custom_inhibitors() -> tuple[bool, list]: custom_inhibitors = run_custom_check_scripts() failures = [] diff --git a/src/ublue_update/update_inhibitors/hardware.py b/src/ublue_update/update_inhibitors/hardware.py index 81b42c3..df8b5b1 100644 --- a/src/ublue_update/update_inhibitors/hardware.py +++ b/src/ublue_update/update_inhibitors/hardware.py @@ -80,10 +80,11 @@ def check_battery_status() -> dict: def check_cpu_load() -> dict: - if cfg.max_cpu_load_percent: + cores = psutil.cpu_count() + if cfg.max_cpu_load_percent and cores is not None: # get load average percentage in last 5 minutes: # https://psutil.readthedocs.io/en/latest/index.html?highlight=getloadavg - cpu_load_percent = psutil.getloadavg()[1] / psutil.cpu_count() * 100 + cpu_load_percent = psutil.getloadavg()[1] / cores * 100 return { "passed": cpu_load_percent < cfg.max_cpu_load_percent, "message": f"CPU load is above {cfg.max_cpu_load_percent}%", @@ -109,7 +110,7 @@ def check_mem_percentage() -> dict: } -def check_hardware_inhibitors() -> bool: +def check_hardware_inhibitors() -> tuple[bool, list]: hardware_inhibitors = [ check_network_status(), check_network_not_metered(),