diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0760269ed..9e453b7fa 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -41,6 +41,15 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} + # nfpm-rpm signing needs gpg provided as filepath + # https://goreleaser.com/customization/nfpm/ + - name: Create GPG key file + run: | + KEY_PATH="$RUNNER_TEMP/gpg-private-key.asc" + printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" > "$KEY_PATH" + chmod 600 "$KEY_PATH" + echo "GPG_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV" + - name: Set up keychain run: | echo -n $SIGNING_CERTIFICATE_BASE64 | base64 -d -o ./ApplicationID.p12 @@ -71,15 +80,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.CLI_RELEASE }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_KEY_PATH: ${{ env.GPG_KEY_PATH }} + # nfpm-rpm signing needs this env to be set. + NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - # artifacts need to be passed to the "publish-apt" job somehow + - name: Clean up GPG key file + if: always() + run: | + rm -f "$GPG_KEY_PATH" + - name: Upload artifacts to workflow uses: actions/upload-artifact@v4 with: name: goreleaser-dist-temp path: dist retention-days: 1 - + publish-apt: name: Publish APT runs-on: macOS-latest @@ -115,3 +131,42 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} run: ./scripts/publish-apt-packages.sh + + publish-rpm: + name: Publish RPM + runs-on: ubuntu-latest + needs: [goreleaser] + env: + # Needed to publish new packages to our S3-hosted RPM repo + AWS_ACCESS_KEY_ID: ${{ secrets.OBJECT_STORAGE_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: eu01 + AWS_ENDPOINT_URL: https://object.storage.eu01.onstackit.cloud + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Download artifacts from workflow + uses: actions/download-artifact@v5 + with: + name: goreleaser-dist-temp + path: dist + + - name: Install RPM tools + run: | + sudo apt-get update + sudo apt-get install -y createrepo-c + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + id: import_gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Publish RPM packages + if: contains(github.ref_name, '-') == false + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + run: ./scripts/publish-rpm-packages.sh \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 37412c183..b86115c5f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -99,17 +99,10 @@ nfpms: - deb - rpm -signs: - - artifacts: package - args: - [ - "-u", - "{{ .Env.GPG_FINGERPRINT }}", - "--output", - "${signature}", - "--detach-sign", - "${artifact}", - ] + rpm: + # The package is signed if a key_file is set + signature: + key_file: "{{ .Env.GPG_KEY_PATH }}" homebrew_casks: - name: stackit diff --git a/INSTALLATION.md b/INSTALLATION.md index 3a5045149..c2e7bf751 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -130,16 +130,54 @@ asset_filters=["stackit-cli_", "_linux_amd64.tar.gz"] eget stackitcloud/stackit-cli ``` -#### RPM package via dnf, yum and zypper +#### RHEL/Fedora/Rocky/Alma/openSUSE/... (`DNF/YUM/Zypper`) -The STACKIT CLI is available as [RPM Package](https://github.com/stackitcloud/stackit-cli/releases) and can be installed via dnf, yum and zypper package manager. +The STACKIT CLI can be installed through the [`DNF/YUM`](https://docs.fedoraproject.org/en-US/fedora/f40/system-administrators-guide/package-management/DNF/) / [`Zypper`](https://de.opensuse.org/Zypper) package managers. -Just download the rpm package from the [release page](https://github.com/stackitcloud/stackit-cli/releases) and run the install command like the following: +> Requires rpm version 4.15 or newer to support Ed25519 signatures. + +> `$basearch` is supported by modern distributions. On older systems that don't expand `$basearch`, replace it in the `baseurl` with your architecture explicitly (for example, `.../rpm/cli/x86_64` or `.../rpm/cli/aarch64`). + +##### Installation via DNF/YUM + +1. Add the repository: + +```shell +sudo tee /etc/yum.repos.d/stackit.repo > /dev/null << 'EOF' +[stackit] +name=STACKIT CLI +baseurl=https://packages.stackit.cloud/rpm/cli/$basearch +enabled=1 +gpgcheck=1 +gpgkey=https://packages.stackit.cloud/keys/key.gpg +EOF +``` + +2. Install the CLI: + +```shell +sudo dnf install stackit +``` + +##### Installation via Zypper + +1. Add the repository: + +```shell +sudo tee /etc/zypp/repos.d/stackit.repo > /dev/null << 'EOF' +[stackit] +name=STACKIT CLI +baseurl=https://packages.stackit.cloud/rpm/cli/$basearch +enabled=1 +gpgcheck=1 +gpgkey=https://packages.stackit.cloud/keys/key.gpg +EOF +``` + +2. Install the CLI: ```shell -dnf install stackitcli.rpm -yum install stackitcli.rpm -zypper install stackitcli.rpm +sudo zypper install stackit ``` #### Any distribution diff --git a/scripts/publish-apt-packages.sh b/scripts/publish-apt-packages.sh index 9d122d80b..81aa53cb4 100755 --- a/scripts/publish-apt-packages.sh +++ b/scripts/publish-apt-packages.sh @@ -49,4 +49,4 @@ aptly snapshot pull -no-remove -architectures="amd64,i386,arm64" current-snapsho # Publish the new snapshot to the remote repo printf "\n>>> Publishing updated snapshot \n" -aptly publish snapshot -keyring="${CUSTOM_KEYRING_FILE}" -gpg-key="${GPG_PRIVATE_KEY_FINGERPRINT}" -passphrase "${GPG_PASSPHRASE}" -config "${APTLY_CONFIG_FILE_PATH}" updated-snapshot "s3:${APT_BUCKET_NAME}:${APT_REPO_PATH}" +aptly publish snapshot -keyring="${CUSTOM_KEYRING_FILE}" -gpg-key="${GPG_PRIVATE_KEY_FINGERPRINT}" -passphrase "${GPG_PASSPHRASE}" -config "${APTLY_CONFIG_FILE_PATH}" updated-snapshot "s3:${APT_BUCKET_NAME}:${APT_REPO_PATH}" \ No newline at end of file diff --git a/scripts/publish-rpm-packages.sh b/scripts/publish-rpm-packages.sh new file mode 100755 index 000000000..d657d1e0d --- /dev/null +++ b/scripts/publish-rpm-packages.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +# This script is used to publish new RPM packages to the CLI RPM repository +# Usage: ./publish-rpm-packages.sh +set -eo pipefail + +PACKAGES_BUCKET_URL="https://packages.stackit.cloud" +PUBLIC_KEY_FILE_PATH="keys/key.gpg" +RPM_REPO_PATH="rpm/cli" +RPM_BUCKET_NAME="distribution" +GORELEASER_PACKAGES_FOLDER="dist/" + +# We need to disable the key database daemon (keyboxd) +# This can be done by removing "use-keyboxd" from ~/.gnupg/common.conf (see https://github.com/gpg/gnupg/blob/master/README) +echo -n >~/.gnupg/common.conf + +# Create RPM repository directory structure +printf ">>> Creating RPM repository structure \n" +mkdir -p rpm-repo/x86_64 +mkdir -p rpm-repo/i386 +mkdir -p rpm-repo/aarch64 + +# Copy RPM packages to appropriate architecture directories +printf "\n>>> Copying RPM packages to architecture directories \n" + +# Copy x86_64 packages (amd64) +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_amd64.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/x86_64/ + printf "Copied %s to x86_64/\n" "$(basename "$rpm_file")" + fi +done + +# Copy i386 packages +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_386.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/i386/ + printf "Copied %s to i386/\n" "$(basename "$rpm_file")" + fi +done + +# Copy aarch64 packages (arm64) +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_arm64.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/aarch64/ + printf "Copied %s to aarch64/\n" "$(basename "$rpm_file")" + fi +done + +# Download existing repository content (RPMs and metadata) if it exists +printf "\n>>> Downloading existing repository content \n" +aws s3 sync s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ rpm-repo/ --endpoint-url "${AWS_ENDPOINT_URL}" --exclude "*.asc" || echo "No existing repository found, creating new one" + +# Create repository metadata for each architecture +printf "\n>>> Creating repository metadata \n" +for arch in x86_64 i386 aarch64; do + if [ -d "rpm-repo/${arch}" ] && [ -n "$(find "rpm-repo/${arch}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + printf "Creating metadata for %s...\n" "$arch" + + # List what we're working with + file_list=$(find "rpm-repo/${arch}" -maxdepth 1 -type f -exec basename {} \; | tr '\n' ' ') + printf "Files in %s: %s\n" "$arch" "${file_list% }" + + # Create repository metadata + createrepo_c --update rpm-repo/${arch} + + # Sign the repository metadata + printf "Signing repository metadata for %s...\n" "$arch" + # Remove existing signature file if it exists + rm -f rpm-repo/${arch}/repodata/repomd.xml.asc + gpg --batch --pinentry-mode loopback --detach-sign --armor \ + --local-user "${GPG_PRIVATE_KEY_FINGERPRINT}" \ + --passphrase "${GPG_PASSPHRASE}" \ + rpm-repo/${arch}/repodata/repomd.xml + + # Verify the signature was created + if [ -f "rpm-repo/${arch}/repodata/repomd.xml.asc" ]; then + printf "Repository metadata signed successfully for %s\n" "$arch" + else + printf "WARNING: Repository metadata signature not created for %s\n" "$arch" + fi + else + printf "No packages found for %s, skipping...\n" "$arch" + fi +done + +# Upload the updated repository to S3 in two phases (repodata pointers last) +# clients reading the repo won't see a state where repomd.xml points to files not uploaded yet. +printf "\n>>> Uploading repository to S3 (phase 1: all except repomd*) \n" +aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \ + --endpoint-url "${AWS_ENDPOINT_URL}" \ + --delete \ + --exclude "*/repodata/repomd.xml" \ + --exclude "*/repodata/repomd.xml.asc" + +printf "\n>>> Uploading repository to S3 (phase 2: repomd* only) \n" +aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \ + --endpoint-url "${AWS_ENDPOINT_URL}" \ + --exclude "*" \ + --include "*/repodata/repomd.xml" \ + --include "*/repodata/repomd.xml.asc" + +# Upload the public key +# Also uploaded in APT publish; intentionally redundant +# Safe to overwrite and ensures updates if APT fails or key changes. +printf "\n>>> Uploading public key \n" +gpg --armor --export "${GPG_PRIVATE_KEY_FINGERPRINT}" > public-key.asc +aws s3 cp public-key.asc s3://${RPM_BUCKET_NAME}/${PUBLIC_KEY_FILE_PATH} --endpoint-url "${AWS_ENDPOINT_URL}" + +printf "\n>>> RPM repository published successfully! \n" +printf "Repository URL: %s/%s/ \n" "$PACKAGES_BUCKET_URL" "$RPM_REPO_PATH" +printf "Public key URL: %s/%s \n" "$PACKAGES_BUCKET_URL" "$PUBLIC_KEY_FILE_PATH"