name: Publish merged packages # On every push to master: for each package directory the push touched and # each architecture it supports, find the PR build artifact for exactly that # tree (label = --), or build it now when there is # none, then publish that one artifact into every channel the package ships # to. One build, one file, several databases: a filename means one set of # bytes everywhere, and channels are views over a shared pool. # # Secrets live in the "publish" environment, restricted to master: # GPG_PRIVATE_KEY, GPG_PASSPHRASE the channel signing key # R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT # OMARCHY_PUBLISH_PREFIX (environment variable, not secret) points a proof # run at a scratch prefix inside the live bucket; empty means the real # channel paths. on: push: branches: [master] paths: ["pkgbuilds/**"] workflow_dispatch: inputs: packages: description: "Space-separated package directories to publish from master" required: true # Merges serialize. Two publishes into one channel at once would race on # the database; queued is fine, cancelled is not. concurrency: group: publish cancel-in-progress: false jobs: changes: runs-on: ubuntu-latest outputs: matrix: ${{ steps.list.outputs.matrix }} count: ${{ steps.list.outputs.count }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - id: list run: | if [[ -n "${{ github.event.inputs.packages }}" ]]; then names="${{ github.event.inputs.packages }}" else names=$(git diff --name-only "${{ github.event.before }}..${{ github.sha }}" -- pkgbuilds \ | awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u) fi matrix=$(printf '%s\n' $names | bin/build-matrix) echo "matrix=$matrix" >> "$GITHUB_OUTPUT" echo "count=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT" jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix" # One job for the whole merge. It collects every PR artifact for the # merged tree (building only what has none), then walks each channel and # architecture slot exactly once: pull that database, add every package # that belongs in it, upload. Six slots, six round trips, however many # packages the merge carried. One process is the only writer, so there # is no race between packages; the run-level concurrency group above # keeps one merge from overlapping the next. publish: needs: changes if: needs.changes.outputs.count != '0' runs-on: [self-hosted, omarchy-builder] environment: publish timeout-minutes: 240 steps: - uses: actions/checkout@v4 with: persist-credentials: false # Every matrix entry, as a file the shell steps can loop over: # package arch channels publish_arches - name: Plan run: | jq -r '.include[] | "\(.package) \(.arch) \(.channels|gsub(" ";",")) \(.publish_arches|gsub(" ";","))"' \ <<'EOF_MATRIX' > plan.txt ${{ needs.changes.outputs.matrix }} EOF_MATRIX cat plan.txt # Fetch each package's PR artifact into build-output/edge//, or # build it when no artifact exists for exactly this tree. - name: Collect artifacts env: GH_TOKEN: ${{ github.token }} CONTAINER_ENGINE: docker run: | set -euo pipefail while read -r package arch channels publish_arches; do hash=$(git rev-parse "HEAD:pkgbuilds/$package") label="$package-$arch-$hash" found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \ | jq -r '[.artifacts[] | select(.expired|not)] | sort_by(.created_at) | last | .archive_download_url // empty') mkdir -p "build-output/edge/$arch" if [[ -n "$found" ]]; then echo "==> $label: PR artifact" curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/artifact.zip "$found" unzip -oq /tmp/artifact.zip -d "build-output/edge/$arch" else echo "==> $label: no artifact for this tree, building" OMARCHY_KEEP_BUILD_WORKSPACE=1 bin/build --mirror edge --arch "$arch" --package "$package" fi done < plan.txt ls -1 build-output/edge/*/*.pkg.tar.zst - name: Publish env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} RCLONE_CONFIG_R2_TYPE: s3 RCLONE_CONFIG_R2_PROVIDER: Cloudflare # The token is scoped to the bucket; it may not CreateBucket, and # rclone's existence check is a CreateBucket in disguise. RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true" RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }} # repo-add, gpg and bsdtar are Arch tools; run the publish inside the # builder image (host-native, edge) with the workspace mounted. run: | set -euo pipefail docker image inspect omarchy-pkg-builder:latest-x86_64-edge >/dev/null 2>&1 \ || docker buildx build --load -t omarchy-pkg-builder:latest-x86_64-edge --build-arg MIRROR=edge build # Group the merge's files by the (channel, architecture) slot each # belongs to. A package's files live under build-output/edge// and are named --.pkg.tar.zst; a # split package's outputs share the pkgbase's directory, so match # on the artifact list rather than the name. # pkgbase is read inside the builder image: the Ubuntu host has no # bsdtar. One container call maps every file to its pkgbase. docker run --rm -v "$PWD:/w:ro" -w /w omarchy-pkg-builder:latest-x86_64-edge bash -c ' for f in build-output/edge/*/*.pkg.tar.zst; do printf "%s %s\n" "$f" "$(bsdtar -xOf "$f" .PKGINFO | awk -F" = " "\$1==\"pkgbase\"{print \$2}")" done' > pkgbase.txt declare -A slot_files=() while read -r package arch channels publish_arches; do for f in build-output/edge/"$arch"/*.pkg.tar.zst; do # Only files this package produced (its PKGINFO pkgbase). [[ $(awk -v f="$f" '$1==f{print $2}' pkgbase.txt) == "$package" ]] || continue for mirror in ${channels//,/ }; do for parch in ${publish_arches//,/ }; do slot_files["$mirror/$parch"]+="$f " done done done done < plan.txt # Deterministic slot order: edge before rc before stable, x86_64 # before aarch64, so a failure leaves the earlier rings consistent. for mirror in edge rc stable; do for parch in x86_64 aarch64; do files=${slot_files["$mirror/$parch"]:-} [[ -n "$files" ]] || continue echo "==> $mirror/$parch: $files" docker run --rm \ -e GPG_PRIVATE_KEY -e GPG_PASSPHRASE -e OMARCHY_PUBLISH_PREFIX \ -e RCLONE_CONFIG_R2_TYPE -e RCLONE_CONFIG_R2_PROVIDER -e RCLONE_CONFIG_R2_ENDPOINT -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET \ -e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY \ -v "$PWD:/w:ro" -w /w \ omarchy-pkg-builder:latest-x86_64-edge \ bin/publish-artifact --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$parch" $files done done result: needs: [changes, publish] if: always() runs-on: ubuntu-latest steps: - run: | echo "publish result: ${{ needs.publish.result }}" [[ "${{ needs.publish.result }}" == "success" || "${{ needs.publish.result }}" == "skipped" ]]