packaging: add EL10 daily stable RPM archive

Why:
Provide unattended current-main packages for the newest Enterprise Linux
generation without maintaining duplicate repositories for each clone.

Impact:
Adds a disabled-by-default daily EL10 build, publish, and install flow.

Before/After:
Before, the archive covered Debian and Ubuntu; after, it also covers EL10.

Technical Overview:
Use the current CentOS Stream 10 spec as the distro packaging baseline.
Build current rsyslog main with Mock for the EL10 x86_64 target.
Apply explicit policy for upstreamed patches and new build requirements.
Sign RPMs and repository metadata with the existing archive key.
Publish clone-neutral x86_64 and SRPM repositories to DigitalOcean Spaces.
Merge prior metadata so every immutable daily package version remains usable.
Verify exact signed installs on CentOS Stream, Rocky, Alma, and Oracle Linux.
Create or update a GitHub issue when an enabled scheduled run fails.

With the help of AI-Agents: Codex
This commit is contained in:
Rainer Gerhards 2026-07-27 17:54:37 +02:00
parent b2faae551a
commit 020b4dc2f7
5 changed files with 1072 additions and 0 deletions

30
.github/el10-daily-stable-policy.yml vendored Normal file
View File

@ -0,0 +1,30 @@
{
"allowed_patch_skips": [
{
"patch": "imfile-inotify-fd-release-on-delete.patch",
"reason": "The EL10 deferred-delete fix is already represented by newer imfile lifecycle handling in current upstream."
},
{
"patch": "imjournal-warn-on-missing-MESSAGE-field.patch",
"reason": "The EL10 missing MESSAGE warning patch is already present in current upstream."
},
{
"patch": "omelasticsearch-pqc-tls.patch",
"reason": "The EL10 Elasticsearch TLS controls are already present in current upstream with later development."
},
{
"patch": "omelasticsearch-apply-tls-opts-during-detection.patch",
"reason": "The EL10 Elasticsearch detection TLS fix is already present in current upstream with later development."
}
],
"supplemental_build_requires": [
{
"package": "autoconf-archive",
"reason": "Current upstream configure.ac uses AX macros while the EL10 8.2604 source package does not declare the archive explicitly."
},
{
"package": "libyaml-devel",
"reason": "Current upstream enables YAML configuration support by default while the EL10 8.2604 source package predates that build dependency."
}
]
}

View File

@ -15,6 +15,7 @@ name: collect flake evidence
- impstats push to VictoriaMetrics
- debian daily stable
- ubuntu daily stable
- el10 daily stable
types: [completed]
workflow_dispatch:
inputs:

603
.github/workflows/el10_daily_stable.yml vendored Normal file
View File

@ -0,0 +1,603 @@
# Copyright 2026 Rainer Gerhards and Others
#
# https://github.com/rsyslog/rsyslog
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
---
name: el10 daily stable
'on':
workflow_dispatch:
inputs:
source_ref:
description: rsyslog source branch, tag, or commit to package
required: true
default: main
type: string
publish_to_archive:
description: Publish to the configured DigitalOcean Spaces archive
required: false
default: false
type: boolean
schedule:
- cron: '43 4 * * *'
concurrency:
group: el10-daily-stable
cancel-in-progress: false
env:
RPM_DAILY_STABLE_HELPER: devtools/release/rpm-daily-stable.sh
RPM_POLICY_FILE: .github/el10-daily-stable-policy.yml
RPM_MOCK_CONFIG: centos-stream+epel-10-x86_64
RPM_ARCH: x86_64
PACKAGE_CHANNEL: daily-stable
PACKAGE_DISTRO: el
PACKAGE_DISTRO_VERSION: '10'
SPACE_PREFIX: rpm/daily-stable/el/10
jobs:
preflight:
name: preflight
runs-on: ubuntu-24.04
if: github.repository == 'rsyslog/rsyslog'
permissions:
contents: read
outputs:
should_run: ${{ steps.decision.outputs.should_run }}
should_publish: ${{ steps.decision.outputs.should_publish }}
source_ref: ${{ steps.decision.outputs.source_ref }}
steps:
- name: Decide whether this run is active
id: decision
env:
EVENT_NAME: ${{ github.event_name }}
MANUAL_PUBLISH: ${{ inputs.publish_to_archive }}
MANUAL_SOURCE_REF: ${{ inputs.source_ref }}
SCHEDULE_ENABLED: ${{ vars.EL10_DAILY_STABLE_ENABLED }}
run: |
set -euo pipefail
should_run=false
should_publish=false
source_ref=main
case "$EVENT_NAME" in
workflow_dispatch)
should_run=true
source_ref="${MANUAL_SOURCE_REF:-main}"
if [ "${MANUAL_PUBLISH:-false}" = true ]; then
should_publish=true
fi
;;
schedule)
if [ "${SCHEDULE_ENABLED:-false}" = true ]; then
should_run=true
should_publish=true
fi
;;
esac
{
echo "should_run=$should_run"
echo "should_publish=$should_publish"
echo "source_ref=$source_ref"
} >> "$GITHUB_OUTPUT"
build:
name: build EL10 x86_64 packages
needs: preflight
if: needs.preflight.outputs.should_run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 150
permissions:
contents: read
outputs:
archive_date: ${{ steps.version.outputs.archive_date }}
expected_evr: ${{ steps.version.outputs.expected_evr }}
source_sha: ${{ steps.source.outputs.source_sha }}
steps:
- name: Checkout archive automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Checkout source to package
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
path: rsyslog-source
persist-credentials: false
ref: ${{ needs.preflight.outputs.source_ref }}
- name: Record packaged source revision
id: source
run: |
set -euo pipefail
source_sha="$(git -C rsyslog-source rev-parse HEAD)"
{
echo "source_sha=$source_sha"
echo "SOURCE_GIT_SHA=$source_sha"
echo "RSYSLOG_SOURCE_DIR=$GITHUB_WORKSPACE/rsyslog-source"
} | tee -a "$GITHUB_ENV" >> "$GITHUB_OUTPUT"
- name: Generate daily package version
id: version
run: "$RPM_DAILY_STABLE_HELPER version"
- name: Fetch official CentOS Stream 10 packaging baseline
id: packaging
run: |
git clone --depth 1 --branch c10s \
https://gitlab.com/redhat/centos-stream/rpms/rsyslog.git \
"$RUNNER_TEMP/el10-packaging"
baseline_sha="$(git -C "$RUNNER_TEMP/el10-packaging" rev-parse HEAD)"
echo "baseline_sha=$baseline_sha" >> "$GITHUB_OUTPUT"
- name: Generate rsyslog dist tarball
run: |
RSYSLOG_HOME="$GITHUB_WORKSPACE/rsyslog-source" \
RSYSLOG_DEV_CONTAINER=rsyslog/rsyslog_dev_base_ubuntu:26.04 \
devtools/devcontainer.sh --rm \
.github/scripts/debian_package_build.sh run_dist_build \
/rsyslog
dist_tarball="$(find rsyslog-source -maxdepth 1 -type f -name 'rsyslog-*.tar.gz' -print -quit)"
[ -n "$dist_tarball" ]
echo "DIST_TARBALL=$GITHUB_WORKSPACE/$dist_tarball" >> "$GITHUB_ENV"
- name: Prepare EL10 sources and run Mock build
id: package_build
env:
VERSION: ${{ steps.version.outputs.version }}
RELEASE: ${{ steps.version.outputs.release }}
EXPECTED_EVR: ${{ steps.version.outputs.expected_evr }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/el10-build"
devtools/ci-flake-phase.sh begin el10-package-build custom
set +e
docker run --rm --privileged \
-e EXPECTED_EVR -e RELEASE -e VERSION \
-e RPM_DAILY_STABLE_HELPER -e RPM_MOCK_CONFIG -e RPM_POLICY_FILE \
-e "DIST_TARBALL=/workspace/rsyslog-source/$(basename "$DIST_TARBALL")" \
-v "$GITHUB_WORKSPACE:/workspace" \
-v "$RUNNER_TEMP:/runner-temp" \
-w /workspace \
quay.io/rockylinux/rockylinux:10 bash -lc '
set -euo pipefail
dnf -q -y install epel-release >/dev/null
dnf -q -y install \
curl git mock mock-core-configs python3 rpm-build tar gzip >/dev/null
"$RPM_DAILY_STABLE_HELPER" prepare-sources \
/runner-temp/el10-packaging \
"$DIST_TARBALL" \
/runner-temp/el10-prepared \
"$RPM_POLICY_FILE" \
"$VERSION" \
"$RELEASE"
"$RPM_DAILY_STABLE_HELPER" build-package \
/runner-temp/el10-prepared \
"$RPM_MOCK_CONFIG" \
/runner-temp/el10-artifacts \
/runner-temp/el10-build.log \
"$EXPECTED_EVR"
'
build_status=$?
set -e
devtools/ci-flake-phase.sh end \
el10-package-build custom "$build_status"
if [ "$build_status" -eq 0 ]; then
sudo chown -R "$(id -u):$(id -g)" \
"$RUNNER_TEMP/el10-artifacts" "$RUNNER_TEMP/el10-build.log"
cp -a "$RUNNER_TEMP/el10-artifacts" el10-daily-stable-artifacts
fi
exit "$build_status"
- name: Upload package-build failure evidence
if: failure() && steps.package_build.outcome == 'failure'
uses: ./.github/actions/upload-flake-evidence
with:
job-name: EL10 daily stable package build
- name: Generate artifact manifest
env:
EXPECTED_EVR: ${{ steps.version.outputs.expected_evr }}
run: |
"$RPM_DAILY_STABLE_HELPER" manifest \
el10-daily-stable-artifacts \
"$EXPECTED_EVR" \
"$RPM_ARCH" \
"$PACKAGE_CHANNEL" \
"$PACKAGE_DISTRO" \
"$PACKAGE_DISTRO_VERSION"
- name: Upload EL10 package artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: el10-daily-stable-${{ steps.version.outputs.expected_evr }}
path: el10-daily-stable-artifacts/
retention-days: 14
if-no-files-found: error
- name: Summarize build
env:
BASELINE_SHA: ${{ steps.packaging.outputs.baseline_sha }}
EXPECTED_EVR: ${{ steps.version.outputs.expected_evr }}
SOURCE_REF: ${{ needs.preflight.outputs.source_ref }}
run: |
{
echo '### Enterprise Linux 10 daily stable build'
echo
echo "- EVR: \`$EXPECTED_EVR\`"
echo "- Source ref: \`$SOURCE_REF\`"
echo "- Source commit: \`$SOURCE_GIT_SHA\`"
echo "- CentOS Stream 10 packaging baseline: \`$BASELINE_SHA\`"
echo "- Mock target: \`$RPM_MOCK_CONFIG\`"
echo "- Archive prefix: \`$SPACE_PREFIX\`"
} >> "$GITHUB_STEP_SUMMARY"
publish:
name: publish DigitalOcean Spaces RPM archive
needs: [preflight, build]
if: needs.preflight.outputs.should_publish == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
# This legacy-named environment is the security boundary for the shared
# packages.rsyslog.com archive, not a Debian-only destination. Reusing it
# deliberately keeps every package family on the same Spaces bucket and
# archive signing key without duplicating long-lived credentials.
environment: debian-daily-stable
permissions:
contents: read
env:
# The DEBIAN_* names are retained for compatibility with the existing
# shared archive environment; the values are distribution-neutral.
AWS_ACCESS_KEY_ID: ${{ secrets.DEBIAN_DAILY_STABLE_SPACE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEBIAN_DAILY_STABLE_SPACE_SECRET_KEY }}
AWS_DEFAULT_REGION: ${{ vars.DEBIAN_DAILY_STABLE_SPACE_REGION }}
SPACE_BUCKET: ${{ vars.DEBIAN_DAILY_STABLE_SPACE_BUCKET }}
SPACE_ENDPOINT: ${{ vars.DEBIAN_DAILY_STABLE_SPACE_ENDPOINT }}
REPO_URL: ${{ vars.EL10_DAILY_STABLE_REPO_URL }}
ARCHIVE_GPG_PRIVATE_KEY: ${{ secrets.DEBIAN_DAILY_STABLE_GPG_PRIVATE_KEY }}
ARCHIVE_GPG_PASSPHRASE: ${{ secrets.DEBIAN_DAILY_STABLE_GPG_PASSPHRASE }}
ARCHIVE_GPG_FINGERPRINT: ${{ vars.DEBIAN_DAILY_STABLE_GPG_FINGERPRINT }}
steps:
- name: Checkout archive automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install RPM repository tools
run: |
command -v aws
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
createrepo-c gnupg rpm
- name: Validate archive configuration
run: |
set -euo pipefail
for variable in \
AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION \
SPACE_BUCKET SPACE_ENDPOINT REPO_URL ARCHIVE_GPG_PRIVATE_KEY \
ARCHIVE_GPG_FINGERPRINT; do
[ -n "${!variable:-}" ] || {
echo "$variable is empty" >&2
exit 1
}
done
- name: Download EL10 package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: el10-daily-stable-${{ needs.build.outputs.expected_evr }}
path: el10-daily-stable-artifacts
- name: Verify artifact checksums
run: |
cd el10-daily-stable-artifacts
sha256sum -c SHA256SUMS
- name: Import signing key and unlock its agent
run: |
set -euo pipefail
install -m 700 -d "$HOME/.gnupg"
echo allow-preset-passphrase > "$HOME/.gnupg/gpg-agent.conf"
gpgconf --kill gpg-agent || true
printf '%s\n' "$ARCHIVE_GPG_PRIVATE_KEY" | gpg --batch --import
keygrip="$(
gpg --batch --with-colons --with-keygrip --list-secret-keys \
"$ARCHIVE_GPG_FINGERPRINT" |
awk -F: '$1 == "grp" { print $10; exit }'
)"
[ -n "$keygrip" ]
preset_tool="$(command -v gpg-preset-passphrase || true)"
for candidate in \
/usr/lib/gnupg2/gpg-preset-passphrase \
/usr/lib/gnupg/gpg-preset-passphrase; do
if [ -z "$preset_tool" ] && [ -x "$candidate" ]; then
preset_tool="$candidate"
fi
done
[ -n "$preset_tool" ]
printf '%s' "$ARCHIVE_GPG_PASSPHRASE" |
"$preset_tool" --preset "$keygrip"
passphrase_file="$RUNNER_TEMP/archive-gpg-passphrase"
install -m 600 /dev/null "$passphrase_file"
printf '%s' "$ARCHIVE_GPG_PASSPHRASE" > "$passphrase_file"
echo "GPG_PASSPHRASE_FILE=$passphrase_file" >> "$GITHUB_ENV"
- name: Sign binary and source RPMs
run: |
"$RPM_DAILY_STABLE_HELPER" sign-rpms \
el10-daily-stable-artifacts \
"$ARCHIVE_GPG_FINGERPRINT"
- name: Download current repository metadata
run: |
set -euo pipefail
mkdir -p "rpm-repo/$RPM_ARCH" rpm-repo/SRPMS
for repo_subdir in "$RPM_ARCH" SRPMS; do
aws s3 sync \
--only-show-errors \
--endpoint-url "$SPACE_ENDPOINT" \
"s3://$SPACE_BUCKET/$SPACE_PREFIX/$repo_subdir/repodata" \
"rpm-repo/$repo_subdir/repodata"
done
- name: Generate signed incremental RPM repositories
run: |
"$RPM_DAILY_STABLE_HELPER" generate-repo \
el10-daily-stable-artifacts \
rpm-repo \
"$RPM_ARCH" \
"$ARCHIVE_GPG_FINGERPRINT" \
"$GPG_PASSPHRASE_FILE"
- name: Generate signed artifact manifest and snapshot
env:
ARCHIVE_DATE: ${{ needs.build.outputs.archive_date }}
EXPECTED_EVR: ${{ needs.build.outputs.expected_evr }}
run: |
"$RPM_DAILY_STABLE_HELPER" manifest \
el10-daily-stable-artifacts \
"$EXPECTED_EVR" "$RPM_ARCH" "$PACKAGE_CHANNEL" \
"$PACKAGE_DISTRO" "$PACKAGE_DISTRO_VERSION"
snapshot_dir="rpm-repo/snapshots/$ARCHIVE_DATE/$EXPECTED_EVR"
mkdir -p "$snapshot_dir"
cp -a el10-daily-stable-artifacts/manifest.json \
el10-daily-stable-artifacts/SHA256SUMS \
el10-daily-stable-artifacts/build.log \
el10-daily-stable-artifacts/rsyslog.spec \
"$snapshot_dir/"
- name: Generate repository configuration
run: |
cat > rpm-repo/rsyslog-daily-stable-el10.repo <<EOF
[rsyslog-daily-stable]
name=rsyslog daily stable for Enterprise Linux 10 - \$basearch
baseurl=$REPO_URL/\$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=$REPO_URL/rsyslog-archive-keyring.asc
EOF
- name: Publish immutable RPMs and snapshots
run: |
set -euo pipefail
upload_immutable() {
local path="$1"
local relative_path="${path#rpm-repo/}"
local key="$SPACE_PREFIX/$relative_path"
local local_hash remote_hash remote_key
remote_key="$(
aws s3api list-objects-v2 \
--endpoint-url "$SPACE_ENDPOINT" --bucket "$SPACE_BUCKET" \
--prefix "$key" --max-keys 1 \
--query 'Contents[0].Key' --output text
)"
local_hash="$(sha256sum "$path" | awk '{print $1}')"
if [ "$remote_key" = "$key" ]; then
remote_hash="$(
aws s3 cp --quiet --endpoint-url "$SPACE_ENDPOINT" \
"s3://$SPACE_BUCKET/$key" - | sha256sum | awk '{print $1}'
)"
[ "$remote_hash" = "$local_hash" ] || {
echo "immutable archive collision at $key" >&2
exit 1
}
return
fi
aws s3 cp --only-show-errors --endpoint-url "$SPACE_ENDPOINT" \
--acl public-read \
--cache-control 'public,max-age=31536000,immutable' \
--metadata "sha256=$local_hash" \
"$path" "s3://$SPACE_BUCKET/$key"
}
while IFS= read -r -d '' path; do
upload_immutable "$path"
done < <(
find "rpm-repo/$RPM_ARCH/Packages" rpm-repo/SRPMS/Packages \
rpm-repo/snapshots -type f -print0
)
- name: Publish archive key and mutable metadata
run: |
set -euo pipefail
upload_metadata() {
local path="$1"
local relative_path="${path#rpm-repo/}"
aws s3 cp --only-show-errors --endpoint-url "$SPACE_ENDPOINT" \
--acl public-read \
--cache-control 'public,max-age=60,must-revalidate' \
"$path" "s3://$SPACE_BUCKET/$SPACE_PREFIX/$relative_path"
}
upload_metadata rpm-repo/rsyslog-archive-keyring.asc
upload_metadata rpm-repo/rsyslog-daily-stable-el10.repo
for repo_subdir in "$RPM_ARCH" SRPMS; do
while IFS= read -r -d '' path; do
upload_metadata "$path"
done < <(
find "rpm-repo/$repo_subdir/repodata" -type f \
! -name repomd.xml ! -name repomd.xml.asc -print0
)
upload_metadata "rpm-repo/$repo_subdir/repodata/repomd.xml"
upload_metadata "rpm-repo/$repo_subdir/repodata/repomd.xml.asc"
done
verify:
name: verify ${{ matrix.name }} EL10 repository
needs: [preflight, build, publish]
if: needs.preflight.outputs.should_publish == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: CentOS Stream
image: quay.io/centos/centos:stream10
- name: Rocky Linux
image: quay.io/rockylinux/rockylinux:10
- name: AlmaLinux
image: almalinux:10
- name: Oracle Linux
image: container-registry.oracle.com/os/oraclelinux:10
container:
image: ${{ matrix.image }}
options: --user root
environment: debian-daily-stable
permissions:
contents: read
env:
REPO_URL: ${{ vars.EL10_DAILY_STABLE_REPO_URL }}
EXPECTED_GPG_FINGERPRINT: ${{ vars.DEBIAN_DAILY_STABLE_GPG_FINGERPRINT }}
EXPECTED_EVR: ${{ needs.build.outputs.expected_evr }}
steps:
- name: Checkout archive automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install verification tools
run: dnf -y install ca-certificates curl gnupg2 rpm
- name: Wait for CDN and verify signed metadata
id: published_metadata
run: |
set -euo pipefail
devtools/ci-flake-phase.sh begin el10-published-metadata custom
set +e
verification_rc=1
for attempt in $(seq 1 20); do
if "$RPM_DAILY_STABLE_HELPER" verify-repo \
"$REPO_URL" "$RPM_ARCH" "$EXPECTED_EVR" \
"$EXPECTED_GPG_FINGERPRINT"; then
verification_rc=0
break
fi
echo "Repository not ready yet, retrying ($attempt/20)..."
[ "$attempt" -eq 20 ] || sleep 30
done
set -e
devtools/ci-flake-phase.sh end \
el10-published-metadata custom "$verification_rc"
exit "$verification_rc"
- name: Install and smoke-test the exact published RPM
id: published_install
run: |
set -euo pipefail
mkdir -p .ci/flake-evidence/logs
devtools/ci-flake-phase.sh begin el10-published-install custom
set +e
(
set -euo pipefail
curl -fsSL "$REPO_URL/rsyslog-archive-keyring.asc" \
-o /tmp/rsyslog-archive-keyring.asc
rpm --import /tmp/rsyslog-archive-keyring.asc
curl -fsSL "$REPO_URL/rsyslog-daily-stable-el10.repo" \
-o /etc/yum.repos.d/rsyslog-daily-stable.repo
dnf -y makecache --refresh
dnf -y install "rsyslog-$EXPECTED_EVR"
installed_evr="$(rpm -q --qf '%{VERSION}-%{RELEASE}\n' rsyslog)"
[ "$installed_evr" = "$EXPECTED_EVR" ]
rsyslogd -v
rsyslogd -N1
) 2>&1 | tee .ci/flake-evidence/logs/el10-published-install.log
install_status=$?
set -e
devtools/ci-flake-phase.sh end \
el10-published-install custom "$install_status"
exit "$install_status"
- name: Upload publication failure evidence
if: >-
${{
failure() &&
(steps.published_metadata.outcome == 'failure' ||
steps.published_install.outcome == 'failure')
}}
uses: ./.github/actions/upload-flake-evidence
with:
job-name: ${{ matrix.name }} EL10 daily stable verification
report_failure:
name: report failure
needs: [preflight, build, publish, verify]
if: >-
always() &&
github.event_name == 'schedule' &&
(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled'))
runs-on: ubuntu-24.04
permissions:
issues: write
contents: read
steps:
- name: Create or update failure issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
VERSION: ${{ needs.build.outputs.expected_evr }}
PREFLIGHT_RESULT: ${{ needs.preflight.result }}
BUILD_RESULT: ${{ needs.build.result }}
PUBLISH_RESULT: ${{ needs.publish.result }}
VERIFY_RESULT: ${{ needs.verify.result }}
with:
script: |
const version = process.env.VERSION || `run-${context.runId}`;
const title = '[el10-daily-stable] package archive failure';
const body = [
`Automated EL10 daily stable failed for \`${version}\`.`,
'',
`Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
`Workflow commit: ${context.sha}`,
'',
'Job results:',
`- preflight: ${process.env.PREFLIGHT_RESULT}`,
`- build: ${process.env.BUILD_RESULT}`,
`- publish: ${process.env.PUBLISH_RESULT}`,
`- verify: ${process.env.VERIFY_RESULT}`
].join('\n');
const {owner, repo} = context.repo;
const existing = await github.paginate(github.rest.issues.listForRepo, {
owner, repo, state: 'open', per_page: 100
});
const issue = existing.find(item => item.title === title && !item.pull_request);
if (issue) {
await github.rest.issues.createComment({
owner, repo, issue_number: issue.number, body
});
return;
}
const created = await github.rest.issues.create({owner, repo, title, body});
try {
await github.rest.issues.addLabels({
owner, repo, issue_number: created.data.number,
labels: ['release', 'packaging', 'daily-stable', 'automated']
});
} catch (error) {
core.warning(`Could not add labels: ${error.message}`);
}

View File

@ -35,6 +35,7 @@ EXPECTED_UPLOADS = {
"impstats_push_victoriametrics.yml": 1,
"debian_daily_stable.yml": 2,
"ubuntu_daily_stable.yml": 2,
"el10_daily_stable.yml": 2,
}
TEST_COMMAND_RE = re.compile(r"run-ci\.sh|make\s+[^\n]*\b(?:check|distcheck)\b|devtools/test-[^\s]+\.sh")
UPLOAD_RE = re.compile(

View File

@ -0,0 +1,437 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: rpm-daily-stable.sh <command> [args...]
Commands:
version
prepare-sources <baseline-dir> <dist-tarball> <output-dir> <policy-file> <version> <release>
build-package <prepared-dir> <mock-config> <artifact-dir> <build-log> <expected-evr>
sign-rpms <artifact-dir> <fingerprint>
generate-repo <artifact-dir> <repo-dir> <arch> <fingerprint> <passphrase-file>
verify-repo <repo-url> <arch> <expected-evr> <expected-fingerprint>
manifest <artifact-dir> <expected-evr> <arch> <channel> <distro> <distro-version>
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
base_version() {
local configure_file script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
configure_file="${RSYSLOG_SOURCE_DIR:-$script_dir/../..}/configure.ac"
sed -n 's/AC_INIT(\[rsyslog\],\[\([^]]*\)\].*/\1/p' "$configure_file" |
sed 's/\.daily$//'
}
short_commit_sha() {
local candidate="${SOURCE_GIT_SHA:-${GITHUB_SHA:-}}"
if [ -n "$candidate" ] &&
printf '%s\n' "$candidate" | grep -Eq '^[0-9a-fA-F]{12,64}$'; then
printf '%.12s\n' "$candidate" | tr '[:upper:]' '[:lower:]'
return
fi
git rev-parse --verify HEAD >/dev/null 2>&1 ||
die "could not determine git commit"
git rev-parse --short=12 HEAD
}
cmd_version() {
local version date run attempt short_sha release expected_evr
version="$(base_version)"
[ -n "$version" ] || die "could not determine base version"
date="${RSYSLOG_BUILD_DATE:-$(date -u +%Y%m%d)}"
printf '%s\n' "$date" | grep -Eq '^[0-9]{8}$' ||
die "RSYSLOG_BUILD_DATE must use YYYYMMDD"
run="${GITHUB_RUN_NUMBER:-0}"
attempt="${GITHUB_RUN_ATTEMPT:-1}"
short_sha="$(short_commit_sha)"
release="0.daily${date}.${run}.${attempt}.git${short_sha}.adiscon1"
expected_evr="${version}-${release}.el10"
printf '%s\n' "$expected_evr"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
{
printf 'version=%s\n' "$version"
printf 'release=%s\n' "$release"
printf 'expected_evr=%s\n' "$expected_evr"
printf 'archive_date=%s-%s-%s\n' \
"${date:0:4}" "${date:4:2}" "${date:6:2}"
} >> "$GITHUB_OUTPUT"
fi
}
cmd_prepare_sources() {
local baseline_dir="$1"
local dist_tarball="$2"
local output_dir="$3"
local policy_file="$4"
local version="$5"
local release="$6"
local sources_dir specs_dir spec_file tmp_dir source_root
[ -f "$baseline_dir/rsyslog.spec" ] ||
die "missing EL packaging spec: $baseline_dir/rsyslog.spec"
[ -f "$dist_tarball" ] || die "missing dist tarball: $dist_tarball"
[ -f "$policy_file" ] || die "missing RPM policy: $policy_file"
rm -rf "$output_dir"
sources_dir="$output_dir/SOURCES"
specs_dir="$output_dir/SPECS"
mkdir -p "$sources_dir" "$specs_dir"
find "$baseline_dir" -maxdepth 1 -type f \
! -name rsyslog.spec ! -name sources \
-exec cp -a {} "$sources_dir/" \;
cp "$baseline_dir/rsyslog.spec" "$specs_dir/rsyslog.spec"
spec_file="$specs_dir/rsyslog.spec"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' RETURN
tar -xzf "$dist_tarball" -C "$tmp_dir"
source_root="$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d -print -quit)"
[ -n "$source_root" ] || die "dist tarball has no source directory"
[ "$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] ||
die "dist tarball must contain exactly one source directory"
mv "$source_root" "$tmp_dir/rsyslog-$version"
tar -C "$tmp_dir" -czf "$sources_dir/rsyslog-$version.tar.gz" \
"rsyslog-$version"
python3 - "$spec_file" "$policy_file" "$version" "$release" <<'PY'
import json
import pathlib
import re
import sys
from datetime import datetime, timezone
spec_path = pathlib.Path(sys.argv[1])
policy_path = pathlib.Path(sys.argv[2])
version = sys.argv[3]
release = sys.argv[4]
spec = spec_path.read_text(encoding="utf-8")
policy = json.loads(policy_path.read_text(encoding="utf-8"))
patches = {}
for match in re.finditer(r"(?m)^Patch(\d+):\s*(\S+)\s*$", spec):
patches[match.group(2)] = match.group(1)
allowed = policy.get("allowed_patch_skips", [])
allowed_names = [entry.get("patch", "").strip() for entry in allowed]
if any(not name for name in allowed_names):
raise SystemExit("policy contains an empty allowed patch name")
missing = sorted(set(allowed_names) - set(patches))
if missing:
raise SystemExit(f"allowed patch is absent from EL10 baseline: {missing}")
for name in allowed_names:
number = patches[name]
spec, declaration_count = re.subn(
rf"(?m)^Patch{re.escape(number)}:\s*{re.escape(name)}\s*\n", "", spec
)
spec, application_count = re.subn(
rf"(?m)^%patch\s+-P{re.escape(number)}(?:\s+[^\n]*)?\s*\n", "", spec
)
if declaration_count != 1 or application_count != 1:
raise SystemExit(
f"could not remove exactly one declaration/application for {name}"
)
build_requires = policy.get("supplemental_build_requires", [])
packages = [entry.get("package", "").strip() for entry in build_requires]
if any(not package for package in packages):
raise SystemExit("policy contains an empty supplemental BuildRequires package")
for package in packages:
if re.search(rf"(?m)^BuildRequires:\s*{re.escape(package)}(?:\s|$)", spec):
continue
spec, count = re.subn(
r"(?m)^(BuildRequires:\s*autoconf\s*)$",
rf"\1\nBuildRequires: {package}",
spec,
count=1,
)
if count != 1:
raise SystemExit(f"could not insert supplemental BuildRequires: {package}")
spec, version_count = re.subn(r"(?m)^Version:\s*.*$", f"Version: {version}", spec)
spec, release_count = re.subn(
r"(?m)^Release:\s*.*$", f"Release: {release}%{{?dist}}", spec
)
spec, source_count = re.subn(
r"(?m)^Source0:\s*.*$", "Source0: %{name}-%{version}.tar.gz", spec
)
if (version_count, release_count, source_count) != (1, 1, 1):
raise SystemExit("EL10 spec version/source fields did not match exactly once")
stamp = datetime.now(timezone.utc).strftime("%a %b %d %Y")
entry = (
f"%changelog\n* {stamp} Adiscon package maintainers "
f"<release-bot@adiscon.com> - {version}-{release}\n"
"- Automated rsyslog Enterprise Linux 10 daily stable build.\n\n"
)
spec, changelog_count = re.subn(r"(?m)^%changelog\s*$", entry.rstrip(), spec, count=1)
if changelog_count != 1:
raise SystemExit("EL10 spec has no unique %changelog marker")
spec_path.write_text(spec, encoding="utf-8")
PY
while IFS= read -r source_url; do
case "$source_url" in
http://*|https://*)
curl --fail --location --retry 4 --retry-delay 5 \
--output "$sources_dir/${source_url##*/}" "$source_url"
;;
esac
done < <(
rpmspec -P "$spec_file" |
sed -n 's/^Source[1-9][0-9]*:[[:space:]]*//p'
)
rm -rf "$tmp_dir"
trap - RETURN
}
cmd_build_package() {
local prepared_dir="$1"
local mock_config="$2"
local artifact_dir="$3"
local build_log="$4"
local expected_evr="$5"
local srpm rpm_file actual_evr rc
rm -rf "$artifact_dir"
mkdir -p "$artifact_dir/srpm" "$artifact_dir/rpms"
set +e
mock -r "$mock_config" --clean --buildsrpm \
--spec "$prepared_dir/SPECS/rsyslog.spec" \
--sources "$prepared_dir/SOURCES" \
--resultdir "$artifact_dir/srpm" 2>&1 | tee "$build_log"
rc=${PIPESTATUS[0]}
set -e
[ "$rc" -eq 0 ] || return "$rc"
srpm="$(find "$artifact_dir/srpm" -maxdepth 1 -type f -name '*.src.rpm' -print -quit)"
[ -n "$srpm" ] || die "mock did not produce a source RPM"
set +e
mock -r "$mock_config" --clean --rebuild "$srpm" \
--resultdir "$artifact_dir/rpms" 2>&1 | tee -a "$build_log"
rc=${PIPESTATUS[0]}
set -e
[ "$rc" -eq 0 ] || return "$rc"
rpm_file="$(find "$artifact_dir/rpms" -maxdepth 1 -type f \
-name 'rsyslog-[0-9]*.x86_64.rpm' ! -name '*-debuginfo-*' -print -quit)"
[ -n "$rpm_file" ] || die "mock did not produce the base rsyslog RPM"
actual_evr="$(rpm -qp --qf '%{VERSION}-%{RELEASE}\n' "$rpm_file")"
[ "$actual_evr" = "$expected_evr" ] ||
die "built RPM EVR $actual_evr does not match $expected_evr"
cp "$build_log" "$artifact_dir/build.log"
cp "$prepared_dir/SPECS/rsyslog.spec" "$artifact_dir/rsyslog.spec"
}
cmd_sign_rpms() {
local artifact_dir="$1"
local fingerprint="$2"
local rpm_file rpm_check
[ -n "$fingerprint" ] || die "RPM signing fingerprint is empty"
command -v rpmsign >/dev/null || die "rpmsign is not installed"
while IFS= read -r -d '' rpm_file; do
rpmsign --addsign \
--define "_gpg_name $fingerprint" \
--define '__gpg /usr/bin/gpg' \
"$rpm_file"
[ "$(rpm -qp --qf '%{RSAHEADER:pgpsig}' "$rpm_file")" != '(none)' ] ||
die "RPM has no RSA header signature: $rpm_file"
rpm_check="$(rpm --checksig --verbose "$rpm_file" 2>&1 || true)"
printf '%s\n' "$rpm_check" | grep -q 'Payload SHA256 digest: OK' ||
die "RPM signature verification failed: $rpm_file"
done < <(find "$artifact_dir" -type f -name '*.rpm' -print0)
}
create_repo_generation() {
local package_dir="$1"
local previous_dir="$2"
local output_dir="$3"
local package_kind="$4"
local new_dir merge_dir
new_dir="$(mktemp -d)"
merge_dir="$(mktemp -d)"
trap 'rm -rf "$new_dir" "$merge_dir"' RETURN
mkdir -p "$new_dir/Packages"
case "$package_kind" in
binary)
find "$package_dir" -maxdepth 1 -type f -name '*.rpm' \
! -name '*.src.rpm' -exec cp -a {} "$new_dir/Packages/" \;
;;
source)
find "$package_dir" -maxdepth 1 -type f -name '*.src.rpm' \
-exec cp -a {} "$new_dir/Packages/" \;
;;
*) die "unknown repository package kind: $package_kind" ;;
esac
createrepo_c --quiet "$new_dir"
rm -rf "$output_dir"
mkdir -p "$output_dir/Packages"
cp -a "$new_dir/Packages/." "$output_dir/Packages/"
if [ -d "$previous_dir/repodata" ]; then
mergerepo_c --all --omit-baseurl \
--repo "file://$previous_dir" \
--repo "file://$new_dir" \
--outputdir "$merge_dir"
cp -a "$merge_dir/repodata" "$output_dir/repodata"
else
cp -a "$new_dir/repodata" "$output_dir/repodata"
fi
rm -rf "$new_dir" "$merge_dir"
trap - RETURN
}
cmd_generate_repo() {
local artifact_dir="$1"
local repo_dir="$2"
local arch="$3"
local fingerprint="$4"
local passphrase_file="$5"
local previous_dir work_dir repo_subdir
[ -n "$fingerprint" ] || die "repository signing fingerprint is empty"
[ -n "$repo_dir" ] || die "repository output directory is empty"
[ -n "$arch" ] || die "repository architecture is empty"
[ -f "$passphrase_file" ] || die "missing repository signing passphrase file"
command -v createrepo_c >/dev/null || die "createrepo_c is not installed"
command -v mergerepo_c >/dev/null || die "mergerepo_c is not installed"
previous_dir="$(mktemp -d)"
work_dir="$(mktemp -d)"
trap 'rm -rf "$previous_dir" "$work_dir"' RETURN
for repo_subdir in "$arch" SRPMS; do
mkdir -p "$previous_dir/$repo_subdir"
if [ -d "$repo_dir/$repo_subdir/repodata" ]; then
cp -a "$repo_dir/$repo_subdir/repodata" \
"$previous_dir/$repo_subdir/repodata"
fi
done
create_repo_generation \
"$artifact_dir/rpms" "$previous_dir/$arch" "$work_dir/$arch" binary
create_repo_generation \
"$artifact_dir/srpm" "$previous_dir/SRPMS" "$work_dir/SRPMS" source
rm -rf "${repo_dir:?}/$arch" "$repo_dir/SRPMS"
mkdir -p "$repo_dir"
mv "$work_dir/$arch" "$repo_dir/$arch"
mv "$work_dir/SRPMS" "$repo_dir/SRPMS"
for repo_subdir in "$arch" SRPMS; do
gpg --batch --yes --pinentry-mode loopback \
--passphrase-file "$passphrase_file" \
--local-user "$fingerprint" \
--armor --detach-sign \
--output "$repo_dir/$repo_subdir/repodata/repomd.xml.asc" \
"$repo_dir/$repo_subdir/repodata/repomd.xml"
done
gpg --batch --armor --export "$fingerprint" \
> "$repo_dir/rsyslog-archive-keyring.asc"
rm -rf "$previous_dir" "$work_dir"
trap - RETURN
}
cmd_verify_repo() {
local repo_url="$1"
local arch="$2"
local expected_evr="$3"
local expected_fingerprint="$4"
local verify_dir actual_fingerprint
verify_dir="$(mktemp -d)"
trap 'rm -rf "$verify_dir"' RETURN
curl --fail --silent --show-error --location \
"$repo_url/rsyslog-archive-keyring.asc" --output "$verify_dir/key.asc"
actual_fingerprint="$(
gpg --batch --show-keys --with-colons "$verify_dir/key.asc" |
awk -F: '$1 == "fpr" { print $10; exit }'
)"
[ "$actual_fingerprint" = "$expected_fingerprint" ] ||
die "archive key fingerprint $actual_fingerprint does not match expected fingerprint"
gpg --batch --no-default-keyring --keyring "$verify_dir/keyring.gpg" \
--import "$verify_dir/key.asc" >/dev/null 2>&1
curl --fail --silent --show-error --location \
"$repo_url/$arch/repodata/repomd.xml" --output "$verify_dir/repomd.xml"
curl --fail --silent --show-error --location \
"$repo_url/$arch/repodata/repomd.xml.asc" --output "$verify_dir/repomd.xml.asc"
gpgv --keyring "$verify_dir/keyring.gpg" \
"$verify_dir/repomd.xml.asc" "$verify_dir/repomd.xml"
printf 'Verified signed repository metadata for %s (%s)\n' \
"$expected_evr" "$arch"
rm -rf "$verify_dir"
trap - RETURN
}
cmd_manifest() {
local artifact_dir="$1"
local expected_evr="$2"
local arch="$3"
local channel="$4"
local distro="$5"
local distro_version="$6"
python3 - "$artifact_dir" "$expected_evr" "$arch" "$channel" \
"$distro" "$distro_version" <<'PY'
import hashlib
import json
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
manifest = {
"version": sys.argv[2],
"architecture": sys.argv[3],
"channel": sys.argv[4],
"distribution": sys.argv[5],
"distribution_version": sys.argv[6],
"files": [],
}
for path in sorted(root.rglob("*")):
if not path.is_file() or path.name in {"manifest.json", "SHA256SUMS"}:
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
manifest["files"].append(
{"path": str(path.relative_to(root)), "sha256": digest, "size": path.stat().st_size}
)
(root / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
with (root / "SHA256SUMS").open("w", encoding="utf-8") as output:
for entry in manifest["files"]:
output.write(f"{entry['sha256']} {entry['path']}\n")
PY
}
command="${1:-}"
case "$command" in
version) shift; cmd_version "$@" ;;
prepare-sources) shift; cmd_prepare_sources "$@" ;;
build-package) shift; cmd_build_package "$@" ;;
sign-rpms) shift; cmd_sign_rpms "$@" ;;
generate-repo) shift; cmd_generate_repo "$@" ;;
verify-repo) shift; cmd_verify_repo "$@" ;;
manifest) shift; cmd_manifest "$@" ;;
*) usage; exit 2 ;;
esac