The split in #869 sent every tree without an artifact to the rebuild job, so a dispatch whose x86 halves were already published waited eight minutes for eleven droplets that each found nothing to build in five seconds. Make the publish job's plan check in the changes job, on its hosted runner, before a builder is requested.
456 lines
25 KiB
YAML
456 lines
25 KiB
YAML
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 = <pkg>-<arch>-<treehash>), 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
|
|
|
|
# Only the publish job serializes (see its concurrency group): two publishes
|
|
# into one channel at once would race on the database. Builds run outside the
|
|
# lock, so a merge waits behind another merge's signing and upload, seconds,
|
|
# never behind its kernel build.
|
|
|
|
jobs:
|
|
changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.list.outputs.matrix }}
|
|
count: ${{ steps.list.outputs.count }}
|
|
rebuild: ${{ steps.list.outputs.rebuild }}
|
|
rebuild_count: ${{ steps.list.outputs.rebuild_count }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- id: list
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
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"
|
|
# Reuse or rebuild, decided per entry and said out loud. A tree with
|
|
# no build artifact (PR artifacts last 7 days; a dispatch may name
|
|
# any package) goes to the rebuild job: aarch64 natively on GitHub's
|
|
# arm64 runner, x86_64 on a builder droplet, one runner per entry.
|
|
rebuild=()
|
|
echo "| package | arch | tree | decision |" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "| --- | --- | --- | --- |" >> "$GITHUB_STEP_SUMMARY"
|
|
while read -r entry; do
|
|
package=$(jq -r .package <<<"$entry"); arch=$(jq -r .arch <<<"$entry")
|
|
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 | select(. != null) | "run \(.workflow_run.id), expires \(.expires_at)"')
|
|
# The plan the publish job makes, made here before a runner is
|
|
# asked for: a tree the channel already holds at master's version
|
|
# (a re-run, or a dispatch naming a package that is fine) needs
|
|
# no build, and an x86 rebuild would wait minutes for a droplet
|
|
# to find that out in seconds.
|
|
plan=""
|
|
[[ -n "$found" ]] || plan=$(bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
|
|
if [[ -n "$found" ]]; then
|
|
decision="reuse the build artifact ($found)"
|
|
elif [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
|
decision="already published at master's version, nothing to build"
|
|
elif [[ $arch == aarch64 ]]; then
|
|
decision="no build artifact: rebuild natively on ubuntu-24.04-arm"
|
|
rebuild+=("$(jq -c '. + {runner: "[\"ubuntu-24.04-arm\"]"}' <<<"$entry")")
|
|
else
|
|
decision="no build artifact: rebuild on a builder droplet"
|
|
rebuild+=("$(jq -c '. + {runner: "[\"self-hosted\",\"omarchy-builder\"]"}' <<<"$entry")")
|
|
fi
|
|
echo "==> $label: $decision"
|
|
echo "| $package | $arch | ${hash:0:12} | $decision |" >> "$GITHUB_STEP_SUMMARY"
|
|
done < <(jq -c '.include[]' <<<"$matrix")
|
|
echo "rebuild=$(printf '%s\n' "${rebuild[@]}" | jq -sc '{include: .}')" >> "$GITHUB_OUTPUT"
|
|
echo "rebuild_count=${#rebuild[@]}" >> "$GITHUB_OUTPUT"
|
|
|
|
# "Build it now when there is none". Each entry builds exactly as
|
|
# build-pr.yml builds it (same runner kind, same builder image, same
|
|
# bin/build call) and uploads under the same label, so the publish job
|
|
# collects this run's artifact the way it collects a PR's. No secret
|
|
# reaches these runners, and they hold no lock: entries build in parallel,
|
|
# and other merges publish while they do.
|
|
rebuild:
|
|
needs: changes
|
|
if: needs.changes.outputs.rebuild_count != '0'
|
|
runs-on: ${{ fromJSON(matrix.runner) }}
|
|
# The droplets are x86, so aarch64 never builds there. omarchy-mac-boot
|
|
# under QEMU took 2h47m; native arm64 and x86 kernels fit easily.
|
|
timeout-minutes: 240
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.changes.outputs.rebuild) }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
persist-credentials: false
|
|
# The same check the publish job makes before building: a re-run for a
|
|
# package the channel already holds at master's version builds
|
|
# nothing, and uploads nothing that could shadow the published file.
|
|
- name: Build ${{ matrix.package }} (${{ matrix.arch }})
|
|
id: build
|
|
env:
|
|
CONTAINER_ENGINE: docker
|
|
run: |
|
|
set -euo pipefail
|
|
plan=$(bin/build --dry-run --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}" 2>&1 | grep -E '^==> Plan complete' || true)
|
|
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
|
echo "==> ${{ matrix.package }} (${{ matrix.arch }}): already published at master's version, nothing to build"
|
|
echo "built=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
bin/build --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}"
|
|
echo "built=true" >> "$GITHUB_OUTPUT"
|
|
- name: Pack artifact
|
|
if: steps.build.outputs.built == 'true'
|
|
id: pack
|
|
run: |
|
|
source helpers/artifact-helpers.sh
|
|
pack_packages build-output/edge/${{ matrix.arch }} packages.tar
|
|
tar -tvf packages.tar
|
|
echo "label=${{ matrix.package }}-${{ matrix.arch }}-$(git rev-parse "HEAD:pkgbuilds/${{ matrix.package }}")" >> "$GITHUB_OUTPUT"
|
|
- name: Upload artifact
|
|
if: steps.build.outputs.built == 'true'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ steps.pack.outputs.label }}
|
|
path: packages.tar
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
# One job for the whole merge. It collects every artifact for the merged
|
|
# tree (PR builds, or the rebuild job above), 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 concurrency group keeps one merge's
|
|
# publish from overlapping the next. It builds nothing, so it runs on a
|
|
# hosted runner in about a minute instead of waiting for a droplet.
|
|
# It waits for the rebuild job and runs whatever that job's result: a
|
|
# failed rebuild leaves its package without an artifact, and the collect
|
|
# step below records that and stops before any publish.
|
|
publish:
|
|
needs: [changes, rebuild]
|
|
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.count != '0' }}
|
|
runs-on: ubuntu-latest
|
|
environment: publish
|
|
timeout-minutes: 30
|
|
concurrency:
|
|
group: publish
|
|
cancel-in-progress: false
|
|
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 artifact into build-output/edge/<arch>/: the PR's,
|
|
# or the rebuild job's when the PR's had expired. An artifact
|
|
# carries its package files inside packages.tar (see build-pr.yml and
|
|
# helpers/artifact-helpers.sh: the upload action rejects the colon in
|
|
# an epoch filename).
|
|
- name: Collect artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
CONTAINER_ENGINE: docker
|
|
run: |
|
|
set -uo pipefail
|
|
source helpers/artifact-helpers.sh
|
|
# sources.jsonl: where each package's files came from, or that the
|
|
# build failed. A failed build ends the run before any publish, and
|
|
# the record says so instead of the report job finding nothing.
|
|
: > sources.jsonl
|
|
failed=0
|
|
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 | select(. != null) | "\(.archive_download_url) \(.workflow_run.id)"')
|
|
read -r found from_run <<<"$found" || true
|
|
mkdir -p "build-output/edge/$arch"
|
|
if [[ -n "$found" ]]; then
|
|
if [[ $from_run == "${{ github.run_id }}" ]]; then
|
|
kind=rebuild
|
|
echo "==> $label: artifact from this run's $arch rebuild"
|
|
else
|
|
kind=pr-artifact
|
|
echo "==> $label: reusing the build artifact from run $from_run"
|
|
fi
|
|
rm -rf /tmp/artifact; mkdir -p /tmp/artifact
|
|
if curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/artifact.zip "$found" \
|
|
&& unzip -oq /tmp/artifact.zip -d /tmp/artifact \
|
|
&& unpack_packages /tmp/artifact "build-output/edge/$arch"; then
|
|
jq -nc --arg p "$package" --arg a "$arch" --arg s "$kind" '{package:$p, arch:$a, source:$s}' >> sources.jsonl
|
|
else
|
|
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"artifact-download-failed"}' >> sources.jsonl; failed=1; break
|
|
fi
|
|
else
|
|
# bin/build plans against the public channel first. If the
|
|
# channel already holds master's version there is nothing to
|
|
# build and nothing to publish: a re-run for a package that
|
|
# turned out to be fine. Record it and move on.
|
|
plan=$(CONTAINER_ENGINE=docker bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
|
|
# "Packages that would build:" followed by nothing means none.
|
|
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
|
echo "==> $label: already published at master's version, nothing to do"
|
|
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"already-published"}' >> sources.jsonl
|
|
continue
|
|
fi
|
|
# Nothing builds here. No artifact means the rebuild failed (see
|
|
# the rebuild job), or an artifact expired between planning and
|
|
# now (re-run all jobs).
|
|
echo "::error::$label: no artifact from the rebuild job"
|
|
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"build-failed"}' >> sources.jsonl; failed=1; break
|
|
fi
|
|
done < plan.txt
|
|
ls -1 build-output/edge/*/*.pkg.tar.zst 2>/dev/null || true
|
|
if (( failed )); then
|
|
# Write the record now; the publish step will not run.
|
|
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
|
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
|
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
|
> publish-record.json
|
|
cat publish-record.json
|
|
exit 1
|
|
fi
|
|
|
|
- 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
|
|
if ! compgen -G "build-output/edge/*/*.pkg.tar.zst" >/dev/null; then
|
|
echo "Nothing to publish: every requested package is already published at master's version."
|
|
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
|
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
|
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
|
> publish-record.json
|
|
cat publish-record.json
|
|
exit 0
|
|
fi
|
|
# A fresh runner has no images, and building this one here cost
|
|
# every publish about 100 s (and 20 s more to start a container
|
|
# from it) for the 14 s of signing and upload it is needed for.
|
|
# builder-images.yml already publishes the tested image for exactly
|
|
# these build inputs under their key; pull that. Build only when no
|
|
# image carries the key: a merge that changed build/ publishes
|
|
# before the refresh it triggered has finished.
|
|
builder=omarchy-pkg-builder:latest-x86_64-edge
|
|
if ! docker image inspect "$builder" >/dev/null 2>&1; then
|
|
key=$(bin/builder-image key --arch x86_64 --mirror edge)
|
|
published="ghcr.io/omacom/omarchy-pkg-builder:$key"
|
|
if docker pull --quiet "$published" &&
|
|
[[ $(docker image inspect "$published" --format '{{index .Config.Labels "org.omarchy.builder.key"}}') == "$key" ]]; then
|
|
docker tag "$published" "$builder"
|
|
echo "==> Builder image: pulled $published"
|
|
else
|
|
echo "==> Builder image: none published for $key, building it"
|
|
docker buildx build --load -t "$builder" --build-arg MIRROR=edge build
|
|
fi
|
|
fi
|
|
|
|
# Group the merge's files by the (channel, architecture) slot each
|
|
# belongs to. A package's files live under build-output/edge/<built
|
|
# arch>/ and are named <pkgname>-<ver>-<arch|any>.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.
|
|
# Every slot's outcome goes into publish-record.json for the report
|
|
# job: what was published, where, from which artifact, and whether
|
|
# the slot succeeded. A failing slot stops the loop (set -e) but the
|
|
# record still shows everything before it landed.
|
|
: > slots.jsonl
|
|
record_slot() { jq -nc --arg m "$1" --arg a "$2" --arg st "$3" --arg files "$4" \
|
|
'{mirror:$m, arch:$a, status:$st, packages:[$files | split(" ") | .[] | select(length>0) | sub(".*/";"") | sub("\\.pkg\\.tar\\.zst$";"")]}' >> slots.jsonl; }
|
|
status=0
|
|
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"
|
|
if 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; then
|
|
record_slot "$mirror" "$parch" published "$files"
|
|
else
|
|
record_slot "$mirror" "$parch" failed "$files"
|
|
status=1
|
|
break 2
|
|
fi
|
|
done
|
|
done
|
|
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
|
--slurpfile slots slots.jsonl --slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
|
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:$slots}' \
|
|
> publish-record.json
|
|
cat publish-record.json
|
|
exit $status
|
|
|
|
- name: Keep the publish record
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: publish-record-${{ github.run_id }}
|
|
path: publish-record.json
|
|
retention-days: 90
|
|
|
|
# Tell people what happened. A comment on the merged PR (found by the
|
|
# merge commit, so squash and rebase merges work too) and a line appended
|
|
# to a running JSON log in the bucket, next to the packages it describes,
|
|
# so the history is public and can be rendered later.
|
|
report:
|
|
needs: [changes, publish]
|
|
if: always() && needs.publish.result != 'skipped'
|
|
runs-on: ubuntu-latest
|
|
environment: publish
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
steps:
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
name: publish-record-${{ github.run_id }}
|
|
- name: Render
|
|
id: render
|
|
run: |
|
|
jq -r --arg outcome "${{ needs.publish.result }}" '
|
|
def pkgs: [.packages[] | select(test("-debug-")|not)] | join(", ");
|
|
def src: if .source=="pr-artifact" then "PR artifact" elif .source=="rebuild" or .source=="native-rebuild" then "rebuilt at merge" elif .source=="built" then "built here" elif .source=="already-published" then "already published, skipped" else "**" + .source + "**" end;
|
|
"### Publish " + (if $outcome=="success" then "succeeded" else "FAILED" end) +
|
|
" → **" + .target + "**" + (if .target!="live" then " (proof prefix, not live)" else "" end),
|
|
"",
|
|
"Packages: " + ([.sources[] | .package + " (" + .arch + ", " + src + ")"] | join("; ")),
|
|
"",
|
|
(if (.slots|length) > 0 then (.slots[] | "- `" + .mirror + "/" + .arch + "`: " + (if .status=="published" then "" else "**" + .status + "** " end) + pkgs)
|
|
elif ([.sources[] | select(.source=="already-published")] | length) == (.sources|length) then "_Nothing to publish: already at master'"'"'s version everywhere._"
|
|
else "_Nothing was published._" end),
|
|
"",
|
|
(if ([.sources[] | select(.source|test("failed"))] | length) > 0 then "_A build failed, so no channel was touched._\n"
|
|
elif (.plan|length) > (.slots|length) then "_Some planned slots did not run because an earlier slot failed._\n" else "" end),
|
|
"Commit " + .commit[0:7] + " · [run](" + .run + ")"
|
|
' publish-record.json > comment.md
|
|
cat comment.md
|
|
- name: Append to the publish log in the bucket
|
|
env:
|
|
RCLONE_CONFIG_R2_TYPE: s3
|
|
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
|
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 }}
|
|
run: |
|
|
curl -fsSL https://downloads.rclone.org/rclone-current-linux-amd64.zip -o rclone.zip && unzip -jq rclone.zip '*/rclone' && chmod +x rclone
|
|
# One JSON object per line, newest last. Served at
|
|
# https://pkgs.omarchy.org/publish-log.jsonl
|
|
./rclone copy R2:omarchy-pkgs/publish-log.jsonl . --s3-no-head 2>/dev/null || : > publish-log.jsonl
|
|
jq -c . publish-record.json >> publish-log.jsonl
|
|
./rclone copyto publish-log.jsonl R2:omarchy-pkgs/publish-log.jsonl --s3-no-head
|
|
echo "log now has $(wc -l < publish-log.jsonl) entries"
|
|
|
|
- name: Comment on the merged PR
|
|
# Only for a push: the merge commit names its PR. A dispatch runs
|
|
# from master's head, whose PR merged something else entirely, so
|
|
# commenting there would attach this run's report to the wrong PR.
|
|
if: github.event_name == 'push'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
pr=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty')
|
|
if [[ -n "$pr" ]]; then
|
|
gh pr comment "$pr" -R "${{ github.repository }}" --body-file comment.md
|
|
echo "commented on #$pr"
|
|
else
|
|
echo "no PR for ${{ github.sha }} (manual dispatch?); skipping PR comment"
|
|
fi
|
|
result:
|
|
needs: [changes, publish]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: |
|
|
echo "changes result: ${{ needs.changes.result }}, publish result: ${{ needs.publish.result }}"
|
|
[[ "${{ needs.changes.result }}" == "success" ]]
|
|
[[ "${{ needs.publish.result }}" == "success" || "${{ needs.publish.result }}" == "skipped" ]]
|