Skip to content

Commit 570f53d

Browse files
authored
feat(distribution): provide rpm packages via rpm repository (#1012)
1 parent 45b9020 commit 570f53d

File tree

5 files changed

+218
-20
lines changed

5 files changed

+218
-20
lines changed

.github/workflows/release.yaml

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ jobs:
4141
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
4242
passphrase: ${{ secrets.GPG_PASSPHRASE }}
4343

44+
# nfpm-rpm signing needs gpg provided as filepath
45+
# https://goreleaser.com/customization/nfpm/
46+
- name: Create GPG key file
47+
run: |
48+
KEY_PATH="$RUNNER_TEMP/gpg-private-key.asc"
49+
printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" > "$KEY_PATH"
50+
chmod 600 "$KEY_PATH"
51+
echo "GPG_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV"
52+
4453
- name: Set up keychain
4554
run: |
4655
echo -n $SIGNING_CERTIFICATE_BASE64 | base64 -d -o ./ApplicationID.p12
@@ -71,15 +80,22 @@ jobs:
7180
env:
7281
GITHUB_TOKEN: ${{ secrets.CLI_RELEASE }}
7382
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
83+
GPG_KEY_PATH: ${{ env.GPG_KEY_PATH }}
84+
# nfpm-rpm signing needs this env to be set.
85+
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
7486

75-
# artifacts need to be passed to the "publish-apt" job somehow
87+
- name: Clean up GPG key file
88+
if: always()
89+
run: |
90+
rm -f "$GPG_KEY_PATH"
91+
7692
- name: Upload artifacts to workflow
7793
uses: actions/upload-artifact@v4
7894
with:
7995
name: goreleaser-dist-temp
8096
path: dist
8197
retention-days: 1
82-
98+
8399
publish-apt:
84100
name: Publish APT
85101
runs-on: macOS-latest
@@ -115,3 +131,42 @@ jobs:
115131
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
116132
GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
117133
run: ./scripts/publish-apt-packages.sh
134+
135+
publish-rpm:
136+
name: Publish RPM
137+
runs-on: ubuntu-latest
138+
needs: [goreleaser]
139+
env:
140+
# Needed to publish new packages to our S3-hosted RPM repo
141+
AWS_ACCESS_KEY_ID: ${{ secrets.OBJECT_STORAGE_ACCESS_KEY_ID }}
142+
AWS_SECRET_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY }}
143+
AWS_DEFAULT_REGION: eu01
144+
AWS_ENDPOINT_URL: https://object.storage.eu01.onstackit.cloud
145+
steps:
146+
- name: Checkout
147+
uses: actions/checkout@v5
148+
149+
- name: Download artifacts from workflow
150+
uses: actions/download-artifact@v5
151+
with:
152+
name: goreleaser-dist-temp
153+
path: dist
154+
155+
- name: Install RPM tools
156+
run: |
157+
sudo apt-get update
158+
sudo apt-get install -y createrepo-c
159+
160+
- name: Import GPG key
161+
uses: crazy-max/ghaction-import-gpg@v6
162+
id: import_gpg
163+
with:
164+
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
165+
passphrase: ${{ secrets.GPG_PASSPHRASE }}
166+
167+
- name: Publish RPM packages
168+
if: contains(github.ref_name, '-') == false
169+
env:
170+
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
171+
GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
172+
run: ./scripts/publish-rpm-packages.sh

.goreleaser.yaml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,10 @@ nfpms:
9999
- deb
100100
- rpm
101101

102-
signs:
103-
- artifacts: package
104-
args:
105-
[
106-
"-u",
107-
"{{ .Env.GPG_FINGERPRINT }}",
108-
"--output",
109-
"${signature}",
110-
"--detach-sign",
111-
"${artifact}",
112-
]
102+
rpm:
103+
# The package is signed if a key_file is set
104+
signature:
105+
key_file: "{{ .Env.GPG_KEY_PATH }}"
113106

114107
homebrew_casks:
115108
- name: stackit

INSTALLATION.md

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,54 @@ asset_filters=["stackit-cli_", "_linux_amd64.tar.gz"]
130130
eget stackitcloud/stackit-cli
131131
```
132132

133-
#### RPM package via dnf, yum and zypper
133+
#### RHEL/Fedora/Rocky/Alma/openSUSE/... (`DNF/YUM/Zypper`)
134134

135-
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.
135+
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.
136136

137-
Just download the rpm package from the [release page](https://github.com/stackitcloud/stackit-cli/releases) and run the install command like the following:
137+
> Requires rpm version 4.15 or newer to support Ed25519 signatures.
138+
139+
> `$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`).
140+
141+
##### Installation via DNF/YUM
142+
143+
1. Add the repository:
144+
145+
```shell
146+
sudo tee /etc/yum.repos.d/stackit.repo > /dev/null << 'EOF'
147+
[stackit]
148+
name=STACKIT CLI
149+
baseurl=https://packages.stackit.cloud/rpm/cli/$basearch
150+
enabled=1
151+
gpgcheck=1
152+
gpgkey=https://packages.stackit.cloud/keys/key.gpg
153+
EOF
154+
```
155+
156+
2. Install the CLI:
157+
158+
```shell
159+
sudo dnf install stackit
160+
```
161+
162+
##### Installation via Zypper
163+
164+
1. Add the repository:
165+
166+
```shell
167+
sudo tee /etc/zypp/repos.d/stackit.repo > /dev/null << 'EOF'
168+
[stackit]
169+
name=STACKIT CLI
170+
baseurl=https://packages.stackit.cloud/rpm/cli/$basearch
171+
enabled=1
172+
gpgcheck=1
173+
gpgkey=https://packages.stackit.cloud/keys/key.gpg
174+
EOF
175+
```
176+
177+
2. Install the CLI:
138178

139179
```shell
140-
dnf install stackitcli.rpm
141-
yum install stackitcli.rpm
142-
zypper install stackitcli.rpm
180+
sudo zypper install stackit
143181
```
144182

145183
#### Any distribution

scripts/publish-apt-packages.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ aptly snapshot pull -no-remove -architectures="amd64,i386,arm64" current-snapsho
4949

5050
# Publish the new snapshot to the remote repo
5151
printf "\n>>> Publishing updated snapshot \n"
52-
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}"
52+
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}"

scripts/publish-rpm-packages.sh

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env bash
2+
3+
# This script is used to publish new RPM packages to the CLI RPM repository
4+
# Usage: ./publish-rpm-packages.sh
5+
set -eo pipefail
6+
7+
PACKAGES_BUCKET_URL="https://packages.stackit.cloud"
8+
PUBLIC_KEY_FILE_PATH="keys/key.gpg"
9+
RPM_REPO_PATH="rpm/cli"
10+
RPM_BUCKET_NAME="distribution"
11+
GORELEASER_PACKAGES_FOLDER="dist/"
12+
13+
# We need to disable the key database daemon (keyboxd)
14+
# This can be done by removing "use-keyboxd" from ~/.gnupg/common.conf (see https://github.com/gpg/gnupg/blob/master/README)
15+
echo -n >~/.gnupg/common.conf
16+
17+
# Create RPM repository directory structure
18+
printf ">>> Creating RPM repository structure \n"
19+
mkdir -p rpm-repo/x86_64
20+
mkdir -p rpm-repo/i386
21+
mkdir -p rpm-repo/aarch64
22+
23+
# Copy RPM packages to appropriate architecture directories
24+
printf "\n>>> Copying RPM packages to architecture directories \n"
25+
26+
# Copy x86_64 packages (amd64)
27+
for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_amd64.rpm; do
28+
if [ -f "$rpm_file" ]; then
29+
cp "$rpm_file" rpm-repo/x86_64/
30+
printf "Copied %s to x86_64/\n" "$(basename "$rpm_file")"
31+
fi
32+
done
33+
34+
# Copy i386 packages
35+
for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_386.rpm; do
36+
if [ -f "$rpm_file" ]; then
37+
cp "$rpm_file" rpm-repo/i386/
38+
printf "Copied %s to i386/\n" "$(basename "$rpm_file")"
39+
fi
40+
done
41+
42+
# Copy aarch64 packages (arm64)
43+
for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_arm64.rpm; do
44+
if [ -f "$rpm_file" ]; then
45+
cp "$rpm_file" rpm-repo/aarch64/
46+
printf "Copied %s to aarch64/\n" "$(basename "$rpm_file")"
47+
fi
48+
done
49+
50+
# Download existing repository content (RPMs and metadata) if it exists
51+
printf "\n>>> Downloading existing repository content \n"
52+
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"
53+
54+
# Create repository metadata for each architecture
55+
printf "\n>>> Creating repository metadata \n"
56+
for arch in x86_64 i386 aarch64; do
57+
if [ -d "rpm-repo/${arch}" ] && [ -n "$(find "rpm-repo/${arch}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
58+
printf "Creating metadata for %s...\n" "$arch"
59+
60+
# List what we're working with
61+
file_list=$(find "rpm-repo/${arch}" -maxdepth 1 -type f -exec basename {} \; | tr '\n' ' ')
62+
printf "Files in %s: %s\n" "$arch" "${file_list% }"
63+
64+
# Create repository metadata
65+
createrepo_c --update rpm-repo/${arch}
66+
67+
# Sign the repository metadata
68+
printf "Signing repository metadata for %s...\n" "$arch"
69+
# Remove existing signature file if it exists
70+
rm -f rpm-repo/${arch}/repodata/repomd.xml.asc
71+
gpg --batch --pinentry-mode loopback --detach-sign --armor \
72+
--local-user "${GPG_PRIVATE_KEY_FINGERPRINT}" \
73+
--passphrase "${GPG_PASSPHRASE}" \
74+
rpm-repo/${arch}/repodata/repomd.xml
75+
76+
# Verify the signature was created
77+
if [ -f "rpm-repo/${arch}/repodata/repomd.xml.asc" ]; then
78+
printf "Repository metadata signed successfully for %s\n" "$arch"
79+
else
80+
printf "WARNING: Repository metadata signature not created for %s\n" "$arch"
81+
fi
82+
else
83+
printf "No packages found for %s, skipping...\n" "$arch"
84+
fi
85+
done
86+
87+
# Upload the updated repository to S3 in two phases (repodata pointers last)
88+
# clients reading the repo won't see a state where repomd.xml points to files not uploaded yet.
89+
printf "\n>>> Uploading repository to S3 (phase 1: all except repomd*) \n"
90+
aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \
91+
--endpoint-url "${AWS_ENDPOINT_URL}" \
92+
--delete \
93+
--exclude "*/repodata/repomd.xml" \
94+
--exclude "*/repodata/repomd.xml.asc"
95+
96+
printf "\n>>> Uploading repository to S3 (phase 2: repomd* only) \n"
97+
aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \
98+
--endpoint-url "${AWS_ENDPOINT_URL}" \
99+
--exclude "*" \
100+
--include "*/repodata/repomd.xml" \
101+
--include "*/repodata/repomd.xml.asc"
102+
103+
# Upload the public key
104+
# Also uploaded in APT publish; intentionally redundant
105+
# Safe to overwrite and ensures updates if APT fails or key changes.
106+
printf "\n>>> Uploading public key \n"
107+
gpg --armor --export "${GPG_PRIVATE_KEY_FINGERPRINT}" > public-key.asc
108+
aws s3 cp public-key.asc s3://${RPM_BUCKET_NAME}/${PUBLIC_KEY_FILE_PATH} --endpoint-url "${AWS_ENDPOINT_URL}"
109+
110+
printf "\n>>> RPM repository published successfully! \n"
111+
printf "Repository URL: %s/%s/ \n" "$PACKAGES_BUCKET_URL" "$RPM_REPO_PATH"
112+
printf "Public key URL: %s/%s \n" "$PACKAGES_BUCKET_URL" "$PUBLIC_KEY_FILE_PATH"

0 commit comments

Comments
 (0)