diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml index c6e3dad5e..c8bd8b9e0 100644 --- a/.github/workflows/rpm.yml +++ b/.github/workflows/rpm.yml @@ -112,3 +112,139 @@ jobs: dpkg/*.orig.tar.* if-no-files-found: error + publish_master: + name: Publish master package snapshot + if: >- + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + github.ref == 'refs/heads/master' && + github.repository == 'SCST-project/scst' + needs: [prepare_matrix, build_rpm, build_deb] + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + concurrency: + group: packages-master-release + cancel-in-progress: false + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Download RPM packages + uses: actions/download-artifact@main + with: + pattern: scst-rpm-*-x86_64-${{ github.sha }} + path: package-artifacts/rpm + + - name: Download DEB packages + uses: actions/download-artifact@main + with: + name: scst-deb-ubuntu-24.04-x86_64-${{ github.sha }} + path: package-artifacts/deb + + - name: Prepare release assets + env: + RPM_MATRIX: ${{ needs.prepare_matrix.outputs.rpm }} + RELEASE_OUTPUT_DIR: ${{ runner.temp }}/master-package-release + run: bash scripts/prepare-package-release + + - name: Publish rolling master snapshot + env: + GH_TOKEN: ${{ github.token }} + RELEASE_OUTPUT_DIR: ${{ runner.temp }}/master-package-release + shell: bash + run: | + set -euo pipefail + + readonly tag=master-rpm + get_master_sha() { + gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/master" \ + --jq '.object.sha' + } + + delete_tag_releases() { + local release_ids release_id + + release_ids="$( + gh api --paginate \ + "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ + --jq '.[] | select(.tag_name == "master-rpm") | .id' + )" + while IFS= read -r release_id; do + [[ -n "${release_id}" ]] || continue + gh api --method DELETE \ + "repos/${GITHUB_REPOSITORY}/releases/${release_id}" + done <<<"${release_ids}" + } + + delete_release_tag() { + local tag_status + + tag_status="$( + curl --silent --show-error --output /dev/null \ + --write-out '%{http_code}' \ + --header 'Accept: application/vnd.github+json' \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" + )" + case "${tag_status}" in + 200) + gh api --method DELETE \ + "repos/${GITHUB_REPOSITORY}/git/refs/tags/${tag}" + ;; + 404) ;; + *) + echo "Unable to inspect ${tag} (HTTP ${tag_status})." >&2 + return 1 + ;; + esac + } + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Skipping stale package snapshot for ${GITHUB_SHA}." + exit 0 + fi + + delete_tag_releases + delete_release_tag + + shopt -s nullglob + assets=("${RELEASE_OUTPUT_DIR}"/scst-master-*) + [[ ${#assets[@]} -gt 0 ]] || { + echo 'No release assets were prepared.' >&2 + exit 1 + } + + notes="$(printf '%s\n\n%s\n%s\n' \ + 'Automated development package snapshot from the master branch.' \ + "Commit: ${GITHUB_SHA}" \ + "Workflow: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")" + gh release create "${tag}" "${assets[@]}" \ + --target "${GITHUB_SHA}" \ + --title 'Latest master package snapshot' \ + --notes "${notes}" \ + --prerelease \ + --latest=false \ + --draft + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Discarding stale package snapshot for ${GITHUB_SHA}." + delete_tag_releases + delete_release_tag + exit 0 + fi + + gh release edit "${tag}" \ + --draft=false \ + --prerelease \ + --latest=false + + master_sha="$(get_master_sha)" + if [[ "${master_sha}" != "${GITHUB_SHA}" ]]; then + echo "Removing stale package snapshot for ${GITHUB_SHA}." + delete_tag_releases + delete_release_tag + fi diff --git a/README.md b/README.md index 373dad033..56df0a072 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,20 @@ project includes: Instructions for building and installing SCST are available in the INSTALL.md file. +## Current master packages + +Prebuilt x86_64 packages from the latest successfully published `master` +build are available in the rolling +[`master-rpm` prerelease](https://github.com/SCST-project/scst/releases/tag/master-rpm). +These are development snapshots, not SCST releases. + +The snapshot contains RPM bundles for Rocky Linux 10.2, 9.8 and 8.10 and +Oracle Linux UEK 10, 9 and 8. It also contains an Ubuntu 24.04 bundle with +`scst-dkms`, `scstadmin` and `iscsi-scst` binary packages and their Debian +source package files. Every `.tar.gz` bundle has a matching `.sha256` file. +The complete snapshot is replaced after each successful package workflow for +the current `master` commit. + ## QLogic target driver Two QLogic target drivers are included in the SCST project. diff --git a/scripts/prepare-package-release b/scripts/prepare-package-release new file mode 100755 index 000000000..48896c73a --- /dev/null +++ b/scripts/prepare-package-release @@ -0,0 +1,116 @@ +#!/bin/bash + +set -euo pipefail + +die() { + echo "Error: $*" >&2 + exit 1 +} + +: "${RPM_MATRIX:?RPM_MATRIX is not set}" +: "${GITHUB_SHA:?GITHUB_SHA is not set}" +: "${RELEASE_OUTPUT_DIR:?RELEASE_OUTPUT_DIR is not set}" + +readonly rpm_artifact_root="${RPM_ARTIFACT_ROOT:-package-artifacts/rpm}" +readonly deb_artifact_dir="${DEB_ARTIFACT_DIR:-package-artifacts/deb}" +readonly release_output_dir="${RELEASE_OUTPUT_DIR}" + +[[ "${GITHUB_SHA}" =~ ^[0-9a-fA-F]{40,64}$ ]] || + die "GITHUB_SHA is not a full Git object ID." +jq -e '.include | type == "array" and length > 0' \ + <<<"${RPM_MATRIX}" >/dev/null || die "RPM_MATRIX is invalid." + +mapfile -t rpm_targets < <(jq -r '.include[].target' <<<"${RPM_MATRIX}") +declare -A seen_targets=() +for target in "${rpm_targets[@]}"; do + [[ "${target}" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || + die "Invalid RPM target: ${target}" + [[ -z "${seen_targets[${target}]+present}" ]] || + die "Duplicate RPM target: ${target}" + seen_targets["${target}"]=1 +done + +if [[ -e "${release_output_dir}" && ! -d "${release_output_dir}" ]]; then + die "Release output path is not a directory: ${release_output_dir}" +fi +mkdir -p -- "${release_output_dir}" +if [[ -n "$(find "${release_output_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + die "Release output directory is not empty: ${release_output_dir}" +fi + +create_bundle() { + local source_dir="$1" + local bundle_name="$2" + shift 2 + + tar --sort=name --mtime='UTC 1970-01-01' \ + --owner=0 --group=0 --numeric-owner \ + -C "${source_dir}" -cf - "$@" | + gzip -n >"${release_output_dir}/${bundle_name}" + ( + cd "${release_output_dir}" + sha256sum "${bundle_name}" >"${bundle_name}.sha256" + ) +} + +for target in "${rpm_targets[@]}"; do + artifact_name="scst-rpm-${target}-x86_64-${GITHUB_SHA}" + artifact_dir="${rpm_artifact_root}/${artifact_name}" + [[ -d "${artifact_dir}/RPMS" ]] || + die "Missing RPMS directory in ${artifact_name}." + [[ -d "${artifact_dir}/SRPMS" ]] || + die "Missing SRPMS directory in ${artifact_name}." + [[ -f "${artifact_dir}/SHA256SUMS" ]] || + die "Missing SHA256SUMS in ${artifact_name}." + [[ -n "$(find "${artifact_dir}/RPMS" -type f -name '*.rpm' -print -quit)" ]] || + die "No binary RPM packages found in ${artifact_name}." + [[ -n "$(find "${artifact_dir}/SRPMS" -type f -name '*.rpm' -print -quit)" ]] || + die "No source RPM packages found in ${artifact_name}." + ( + cd "${artifact_dir}" + sha256sum --check SHA256SUMS + ) || die "RPM checksum verification failed for ${artifact_name}." + + create_bundle "${artifact_dir}" \ + "scst-master-${target}-x86_64.tar.gz" \ + RPMS SRPMS SHA256SUMS +done + +[[ -d "${deb_artifact_dir}" ]] || + die "Missing DEB artifact directory: ${deb_artifact_dir}" + +find_one_deb_file() { + local pattern="$1" + local -a matches=() + + mapfile -d '' -t matches < <( + find "${deb_artifact_dir}" -maxdepth 1 -type f \ + -name "${pattern}" -print0 + ) + [[ ${#matches[@]} -eq 1 ]] || + die "Expected one DEB artifact matching ${pattern}, found ${#matches[@]}." + deb_files+=("${matches[0]##*/}") +} + +declare -a deb_files=() +find_one_deb_file 'scst-dkms_*.deb' +find_one_deb_file 'scstadmin_*.deb' +find_one_deb_file 'iscsi-scst_*.deb' +find_one_deb_file '*.dsc' +find_one_deb_file '*.debian.tar.*' +find_one_deb_file '*.orig.tar.*' + +mapfile -d '' -t all_deb_packages < <( + find "${deb_artifact_dir}" -maxdepth 1 -type f -name '*.deb' -print0 +) +[[ ${#all_deb_packages[@]} -eq 3 ]] || + die "Expected exactly three binary DEB packages, found ${#all_deb_packages[@]}." + +create_bundle "${deb_artifact_dir}" \ + "scst-master-ubuntu-24.04-x86_64.tar.gz" "${deb_files[@]}" + +expected_files="$(((${#rpm_targets[@]} + 1) * 2))" +actual_files="$(find "${release_output_dir}" -maxdepth 1 -type f | + wc -l)" +[[ "${actual_files}" -eq "${expected_files}" ]] || + die "Expected ${expected_files} release assets, found ${actual_files}."