name: OBS Publish # OBS builds RPMs asynchronously on shared openSUSE infrastructure # (~30-60 min per rebuild). Trying to trigger the rebuild AND download the # freshly-built RPMs in the same tag-push run was structurally broken: the # download ran minutes after the trigger, long before OBS had published the # new version, so every release push failed with "No RPMs were downloaded". # # This workflow is split to match OBS's async reality: # * `trigger` (tag push / dispatch) — fire the OBS rebuild, then exit # green. It never tries to download, so a release push can't go red here. # * `attach` (hourly schedule / dispatch) — once OBS has published the # RPMs for the latest release's version, download them and attach them to # the existing GitHub Release. It is a no-op (green) when the RPMs aren't # built yet (retries next hour) or are already attached (no churn). # # Single-writer rule: this workflow NEVER creates the GitHub Release or edits # its title/notes — release.yml (REL- marker tag) is the sole creator and the # only writer of the release body. `attach` only uploads assets to a release # that already exists, so it can't cause the "edited N times" / placeholder- # notes churn the old per-workflow `gh release create --notes …` stubs did. on: push: tags: - "v*.*.*" schedule: # Hourly sweep: attach OBS RPMs to the latest release once OBS finishes # building them. A no-op the rest of the time (already attached / not # built yet), so it never edits a release it has nothing new for. - cron: "17 * * * *" workflow_dispatch: inputs: tag: description: "Release tag to trigger / attach RPMs to (e.g. v0.6.0)" required: true force: description: "Force OBS runservice even if Tumbleweed RPM exists (use when source bumped without version change)" required: false default: "false" env: OBS_PROJECT: home:Kernalix7 OBS_PACKAGE: winpodx OBS_API: https://api.opensuse.org # /public/ is OBS's anonymous read-only API mirror — no auth needed for # public home: projects. The runservice token can't read /build/. OBS_PUBLIC_API: https://api.opensuse.org/public OBS_DOWNLOAD: https://download.opensuse.org/repositories/home:/Kernalix7 permissions: contents: write jobs: # Fire the OBS rebuild and exit. Runs on tag push and manual dispatch only. # Never downloads — so a release push is always green here; the RPMs land # asynchronously via the `attach` job once OBS finishes the server-side build. trigger: if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Resolve tag id: tag run: | if [ "${{ github.event_name }}" = "push" ]; then tag="${{ github.ref_name }}" else tag="${{ inputs.tag }}" fi # OBS feeds the version into both deb and rpm specs; rpm forbids # '-' in Version: (Version/Release delimiter). Strip the RTM # gate suffix so `0.3.0-RTM1` lands as `0.3.0`. version="${tag#v}" version="${version%%-RTM*}" echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" - name: Install curl + jq run: sudo apt-get update && sudo apt-get install -y curl jq - name: Probe whether OBS already has target version built # Avoid triggering an unnecessary OBS rebuild when the target version # is already published. A single rebuild costs ~30-60min on OBS shared # infrastructure and is pure waste when the RPMs we'd download are # byte-identical to what's already on the server. Probe a representative # shipping repo (Tumbleweed) for a winpodx--*.rpm filename in # the public download index; if found, OBS has the build cached. id: probe run: | version="${{ steps.tag.outputs.version }}" if [ "${{ inputs.force }}" = "true" ]; then echo "force=true requested — bypassing Tumbleweed RPM probe and triggering runservice." echo "skip_runservice=false" >> "$GITHUB_OUTPUT" exit 0 fi listing_url="${OBS_DOWNLOAD}/openSUSE_Tumbleweed/noarch/" html=$(curl -sS -f -L "$listing_url" 2>/dev/null || echo "") if echo "$html" | grep -qE "winpodx-${version}-[0-9]+\.[0-9]+\.noarch\.rpm"; then echo "OBS already has openSUSE_Tumbleweed RPM for v${version} — skipping runservice." echo "skip_runservice=true" >> "$GITHUB_OUTPUT" else echo "OBS doesn't yet have openSUSE_Tumbleweed RPM for v${version} — will trigger runservice." echo "skip_runservice=false" >> "$GITHUB_OUTPUT" fi - name: Trigger OBS service run (token) if: steps.probe.outputs.skip_runservice == 'false' env: OBS_TOKEN: ${{ secrets.OBS_TOKEN }} run: | if [ -z "$OBS_TOKEN" ]; then echo "OBS_TOKEN secret is not set" >&2 exit 1 fi curl -sS -f -X POST \ -H "Authorization: Token $OBS_TOKEN" \ "$OBS_API/trigger/runservice?project=$OBS_PROJECT&package=$OBS_PACKAGE" echo "OBS rebuild triggered for v${{ steps.tag.outputs.version }}. RPMs will be attached by the hourly 'attach' job once OBS finishes (~30-60 min)." # Download OBS-built RPMs once they're published and attach them to the # existing GitHub Release. Runs hourly (sweeps the latest release) and on # manual dispatch (targets inputs.tag). Never creates or edits the release. attach: if: github.event_name != 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install curl + jq run: sudo apt-get update && sudo apt-get install -y curl jq - name: Resolve target tag id: tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then tag="${{ inputs.tag }}" else # schedule: sweep the most recent published release. tag=$(gh release list --repo "${{ github.repository }}" --limit 1 \ --json tagName --jq '.[0].tagName' 2>/dev/null || echo "") fi if [ -z "$tag" ]; then echo "No target tag (no releases yet?) — nothing to attach." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi version="${tag#v}" version="${version%%-RTM*}" echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT" - name: Skip if release already has OBS RPMs attached id: gate if: steps.tag.outputs.skip != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${{ steps.tag.outputs.tag }}" # release.yml owns creation — only attach to a release that exists. if ! gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then echo "Release $tag does not exist yet (release.yml owns creation) — nothing to attach." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi # Tumbleweed is always built and is the canary for "OBS RPMs already # attached". If its asset is present, the sweep already ran for this # release — exit green without re-uploading (no edit churn). assets=$(gh release view "$tag" --repo "${{ github.repository }}" \ --json assets --jq '.assets[].name' 2>/dev/null || echo "") if echo "$assets" | grep -q "openSUSE_Tumbleweed\.rpm$"; then echo "OBS RPMs already attached to $tag — no-op." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Discover repos and download RPMs id: download if: steps.gate.outputs.skip == 'false' run: | mkdir -p dist-rpms version="${{ steps.tag.outputs.version }}" # Parse enabled repos from project _meta (public, no auth) meta=$(curl -sS -f "$OBS_PUBLIC_API/source/$OBS_PROJECT/_meta") repos=$(echo "$meta" | grep -oP '(?<=&2 exit 1 fi for repo in $repos; do # openSUSE_Factory_{ARM,PowerPC,RISCV,zSystems} builds the same # noarch content as openSUSE_Tumbleweed — publishing all five would # just list four identical RPMs with misleading architecture-looking # names. Skip the non-x86 Factory rebuilds; Tumbleweed users on # aarch64/ppc64/riscv64/s390x install the Tumbleweed noarch RPM. case "$repo" in openSUSE_Factory_*) echo "Skipping $repo (noarch duplicate of Tumbleweed)"; continue ;; esac # Tumbleweed / Fedora / Slowroll all produce the same filename # (winpodx--.noarch.rpm) because their release tags lack # a distro suffix. Without renaming, only the last-iterated repo's # file survives in dist-rpms/. Append the repo name to every RPM so # each distro's build gets a stable, unique asset name on the # Release — matches the RHEL convention (.el9.rpm / .el10.rpm). # Normalize short Leap repo names ("15.6", "16.0") to the familiar # openSUSE_Leap_* form so the final filename is self-describing # instead of ending in a bare version number like ".15.6.rpm". case "$repo" in 1[0-9].[0-9]|1[0-9].[0-9][0-9]) repo_slug="openSUSE_Leap_$repo" ;; *) repo_slug=$(echo "$repo" | tr '/:' '__') ;; esac for arch in x86_64 noarch; do base="$OBS_DOWNLOAD/$repo/$arch" # MirrorCache listing hrefs have a "./" prefix # (href="./winpodx--.noarch.rpm"); match the filename # directly instead of anchoring on `href="winpodx-`. listing=$(curl -sSL "$base/" || true) [ -z "$listing" ] && continue # Anchor the filename match to the current tag's version so a # stale older RPM still in the OBS publish index (Tumbleweed / # Slowroll rolling-release repos are slow to garbage-collect) # doesn't get downloaded and re-uploaded as a v${version} # release asset. rpms=$(echo "$listing" | grep -oE "winpodx-${version}-[^\"]+\.rpm" | sort -u) for f in $rpms; do case "$f" in *debuginfo*|*debugsource*|*.src.rpm) continue ;; esac out="dist-rpms/${f%.rpm}.${repo_slug}.rpm" echo "Fetching $repo/$arch/$f -> $(basename "$out")" curl -sS -fL -o "$out" "$base/$f" || true done done done ls -la dist-rpms/ if ! ls dist-rpms/*.rpm >/dev/null 2>&1; then echo "::notice::OBS hasn't published RPMs for v${version} yet — they build asynchronously (~30-60 min). The hourly sweep will retry; or re-run this workflow once the build finishes." echo "have_rpms=false" >> "$GITHUB_OUTPUT" else echo "have_rpms=true" >> "$GITHUB_OUTPUT" fi - name: Attach RPMs to GitHub release if: steps.download.outputs.have_rpms == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release upload "${{ steps.tag.outputs.tag }}" dist-rpms/*.rpm --clobber \ --repo "${{ github.repository }}" echo "Attached OBS RPMs to ${{ steps.tag.outputs.tag }}." - uses: actions/upload-artifact@v4 if: steps.download.outputs.have_rpms == 'true' with: name: rpms-${{ steps.tag.outputs.version }} path: dist-rpms/