Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.

Commit 1e8f0ef

Browse files
committed
init: first functional homebrew tarball generator
Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>
0 parents  commit 1e8f0ef

File tree

8 files changed

+591
-0
lines changed

8 files changed

+591
-0
lines changed

.github/renovate.json5

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"github>ublue-os/renovate-config:org-inherited-config"
5+
],
6+
7+
"regexManagers": [
8+
{
9+
"fileMatch": ["^Containerfile$"],
10+
"matchStrings": [
11+
"https://github\\.com/ublue-os/artwork/releases/download/bluefin-v(?<version>[0-9\\-]+)/bluefin-wallpapers\\.tar\\.zstd"
12+
],
13+
"datasourceTemplate": "github-releases",
14+
"depNameTemplate": "ublue-os/artwork"
15+
}
16+
]
17+
}

.github/workflows/brew-tarball.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Generate Homebrew Tarball and Release
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- ".github/workflows/brew-tarball.yml"
8+
schedule:
9+
- cron: "0 1 * * TUE" # Every Tuesday at 1am UTC
10+
11+
jobs:
12+
create_release:
13+
name: Create Release
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 30
16+
permissions:
17+
contents: write
18+
outputs:
19+
tag_date: ${{ steps.date.outputs.tag_date }}
20+
steps:
21+
- name: Get current date
22+
if: github.event_name != 'pull_request'
23+
id: date
24+
run: |
25+
set -x
26+
echo "date=$(date -u +%Y\-%m\-%d\ %H\:%M\:%S)" >> $GITHUB_OUTPUT
27+
echo "tag_date=$(date -u +%Y\-%m\-%d-%H-%M-%S)" >> $GITHUB_OUTPUT
28+
29+
- name: Create Release
30+
if: github.event_name != 'pull_request'
31+
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2
32+
with:
33+
name: Homebrew Tarball (${{ steps.date.outputs.date }})
34+
tag_name: "homebrew-${{ steps.date.outputs.tag_date }}"
35+
body: |
36+
This is a homebrew tarball generated for the [ublue-brew](https://github.com/${{ github.repository }}/blob/${{ github.sha }}/ublue/brew/ublue-brew.spec) package.
37+
It exists so that images can have a consistent tarball to base to install homebrew
38+
make_latest: true
39+
40+
push_tarballs:
41+
name: Generate Homebrew Tarball and Push to Release
42+
runs-on: ${{ matrix.platform == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
43+
needs: create_release
44+
timeout-minutes: 30
45+
permissions:
46+
contents: write
47+
strategy:
48+
matrix:
49+
platform: [arm64, amd64]
50+
fail-fast: false
51+
steps:
52+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
53+
- name: Setup Just
54+
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
55+
56+
- name: Generate Homebrew tarball
57+
id: generate_tarball
58+
run: |
59+
set -euox pipefail
60+
just=$(which just)
61+
62+
OUTDIR="./brew-out"
63+
SHASUM_DIR="./brew-sum"
64+
TARBALL_FILENAME="homebrew-$(arch).tar.zst"
65+
66+
mkdir -p $OUTDIR
67+
68+
podman run --rm -it \
69+
-v "$OUTDIR:/outdir:Z" \
70+
cgr.dev/chainguard/wolfi-base:latest \
71+
/bin/sh -c "
72+
set -o xtrace
73+
apk add curl git zstd posix-libc-utils uutils gnutar grep
74+
curl --retry 3 -Lo /tmp/brew-install https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh
75+
chmod +x /tmp/brew-install
76+
touch /.dockerenv
77+
ln -s /bin/bash /usr/bin/bash
78+
ls -s /usr/bin/grep /bin/grep
79+
env --ignore-environment PATH=/usr/bin:/bin:/usr/sbin:/sbin HOME=/home/linuxbrew NONINTERACTIVE=1 /usr/bin/bash /tmp/brew-install
80+
tar --zstd -cvf /outdir/${TARBALL_FILENAME} /home/linuxbrew/.linuxbrew"
81+
82+
mkdir -p $SHASUM_DIR
83+
for spice in 1 256 512 ; do
84+
"sha${spice}sum" "$OUTDIR/$TARBALL_FILENAME" | tee "$SHASUM_DIR/homebrew-$(arch).sha${spice}"
85+
done
86+
87+
echo "outdir=$(realpath $OUTDIR)" >> "$GITHUB_OUTPUT"
88+
echo "tarball_file=$(realpath $OUTDIR)/$TARBALL_FILENAME" >> "$GITHUB_OUTPUT"
89+
echo "shasum_dir=$(realpath $SHASUM_DIR)" >> "$GITHUB_OUTPUT"
90+
91+
- name: Upload to Job Artifacts
92+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
93+
with:
94+
name: brew-tarball-${{ matrix.platform }}
95+
if-no-files-found: error
96+
path: |
97+
${{ steps.generate_tarball.outputs.tarball_file }}
98+
${{ steps.generate_tarball.outputs.shasum_dir }}/*
99+
100+
- name: Upload Release Artifact
101+
if: github.event_name != 'pull_request'
102+
env:
103+
GITHUB_TOKEN: ${{ github.token }}
104+
TAG_DATE: ${{ needs.create_release.outputs.tag_date }}
105+
TARBALL_FILE: ${{ steps.generate_tarball.outputs.tarball_file }}
106+
SHASUM_DIR: ${{ steps.generate_tarball.outputs.shasum_dir }}
107+
run: |
108+
gh release upload --clobber \
109+
"homebrew-${TAG_DATE}" \
110+
"${TARBALL_FILE}" \
111+
"${SHASUM_DIR}"/*

.github/workflows/build.yml

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
name: Build container image
2+
on:
3+
pull_request:
4+
branches:
5+
- main
6+
schedule:
7+
- cron: "0 1 * * SUN"
8+
push:
9+
branches:
10+
- main
11+
paths-ignore:
12+
- "**/README.md"
13+
workflow_dispatch:
14+
15+
env:
16+
IMAGE_DESC: "OCI image for Homebrew"
17+
IMAGE_KEYWORDS: "homebrew"
18+
IMAGE_LOGO_URL: "https://avatars.githubusercontent.com/u/136393846?s=200&v=4"
19+
IMAGE_NAME: "${{ github.event.repository.name }}"
20+
IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}"
21+
DEFAULT_TAG: "latest"
22+
23+
jobs:
24+
build_push:
25+
name: Build and push image
26+
permissions:
27+
contents: read
28+
packages: write
29+
id-token: write
30+
timeout-minutes: 60
31+
strategy:
32+
fail-fast: true
33+
matrix:
34+
platform: ["amd64", "arm64"]
35+
runs-on: ${{ matrix.platform == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
39+
40+
- name: Build image
41+
run: podman build -t "${IMAGE_NAME}:${DEFAULT_TAG}" -f ./Containerfile .
42+
43+
- name: Login to GitHub Container Registry
44+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
LOGIN_USER: ${{ github.actor }}
48+
run: |
49+
echo "${GITHUB_TOKEN}" | podman login -u "${LOGIN_USER}" --password-stdin "ghcr.io"
50+
echo "${GITHUB_TOKEN}" | docker login -u "${LOGIN_USER}" --password-stdin "ghcr.io"
51+
52+
- name: Push to GHCR
53+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
54+
id: push
55+
env:
56+
IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }}
57+
IMAGE_NAME: ${{ env.IMAGE_NAME }}
58+
IMAGE_DIGEST: ${{ steps.load.outputs.digest }}
59+
PLATFORM: ${{ matrix.platform }}
60+
MAX_RETRIES: 3
61+
run: |
62+
set -x
63+
64+
for i in $(seq "${MAX_RETRIES}"); do
65+
podman push --digestfile=/tmp/digestfile "localhost/${IMAGE_NAME}:${DEFAULT_TAG}" "${IMAGE_REGISTRY}/${IMAGE_NAME}:${DEFAULT_TAG}-${PLATFORM}" && break || sleep $((5 * i));
66+
done
67+
echo "remote_image_digest=$(< /tmp/digestfile)" | tee "${GITHUB_OUTPUT}"
68+
69+
- name: Install Cosign
70+
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
71+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
72+
73+
- name: Sign Image
74+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
75+
env:
76+
REMOTE_IMAGE_DIGEST: ${{ steps.push.outputs.remote_image_digest }}
77+
COSIGN_EXPERIMENTAL: false
78+
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
79+
run: cosign sign -y --key env://COSIGN_PRIVATE_KEY "${IMAGE_REGISTRY}/${IMAGE_NAME}@${REMOTE_IMAGE_DIGEST}"
80+
81+
- name: Create Job Outputs
82+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
83+
env:
84+
IMAGE_NAME: ${{ env.IMAGE_NAME }}
85+
PLATFORM: ${{ matrix.platform }}
86+
DIGEST: ${{ steps.push.outputs.remote_image_digest }}
87+
run: |
88+
set -x
89+
mkdir -p /tmp/outputs/digests
90+
echo "${DIGEST}" | tee "/tmp/outputs/digests/${IMAGE_NAME}-${PLATFORM}.txt"
91+
92+
- name: Upload Output Artifacts
93+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
94+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
95+
with:
96+
name: ${{ env.IMAGE_NAME }}-${{ matrix.platform }}
97+
retention-days: 1
98+
if-no-files-found: error
99+
path: |
100+
/tmp/outputs/digests/*.txt
101+
102+
manifest:
103+
name: Create Manifest
104+
runs-on: ubuntu-latest
105+
if: always()
106+
needs:
107+
- build_push
108+
container:
109+
image: docker.io/library/alpine:latest
110+
options: --privileged --security-opt seccomp=unconfined
111+
permissions:
112+
contents: read
113+
packages: write
114+
id-token: write
115+
steps:
116+
- name: Install dependencies
117+
run: apk add bash coreutils curl docker findutils fuse-overlayfs git grep jq podman sed
118+
119+
# Necessary until bootc supports Cosign v3
120+
- name: Install old cosign
121+
env:
122+
GITHUB_TOKEN: ${{ github.token }}
123+
run: |
124+
# Thank you Bri! :3 @b-
125+
curl --retry 3 -H "Authorization: Bearer ${GITHUB_TOKEN}" -fsSLo "/usr/bin/cosign" "https://github.com/sigstore/cosign/releases/download/v2.6.1/cosign-linux-amd64"
126+
chmod +x /usr/bin/cosign
127+
128+
- name: Exit on failure
129+
shell: bash
130+
env:
131+
JOBS: ${{ toJson(needs) }}
132+
run: |
133+
for i in $(jq -r 'to_entries[] | .value.result' <<< "${JOBS}"); do
134+
if [ "$i" != "success" ] && [ "$i" != "skipped" ]; then
135+
echo "Status check not okay!"
136+
exit 1
137+
fi
138+
done
139+
140+
# This generates a timestamp like what is defined on the ArtifactHub documentation
141+
# E.G: 2022-02-08T15:38:15Z'
142+
# https://artifacthub.io/docs/topics/repositories/container-images/
143+
# https://linux.die.net/man/1/date
144+
- name: Get current date
145+
id: date
146+
run: echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" | tee "${GITHUB_OUTPUT}"
147+
148+
- name: Image Metadata
149+
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
150+
id: metadata
151+
with:
152+
tags: |
153+
type=raw,value=${{ env.DEFAULT_TAG }}
154+
type=raw,value=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}}
155+
type=raw,value={{date 'YYYYMMDD'}}
156+
type=ref,event=pr
157+
type=sha,enable=${{ github.event_name == 'pull_request' }}
158+
labels: |
159+
containers.bootc=1
160+
io.artifacthub.package.deprecated=false
161+
io.artifacthub.package.keywords=${{ env.IMAGE_KEYWORDS }}
162+
io.artifacthub.package.license=Apache-2.0
163+
io.artifacthub.package.logo-url=${{ env.IMAGE_LOGO_URL }}
164+
io.artifacthub.package.prerelease=false
165+
io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md
166+
org.opencontainers.image.created=${{ steps.date.outputs.date }}
167+
org.opencontainers.image.description=${{ env.IMAGE_DESC }}
168+
org.opencontainers.image.documentation=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md
169+
org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/blob/${{ github.sha }}/Containerfile
170+
org.opencontainers.image.title=${{ env.IMAGE_NAME }}
171+
org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/tree/${{ github.sha }}
172+
org.opencontainers.image.vendor=${{ github.repository_owner }}
173+
org.opencontainers.image.version=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}}
174+
175+
- name: Fetch Build Outputs
176+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
177+
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
178+
with:
179+
pattern: ${{ env.IMAGE_NAME }}-*
180+
merge-multiple: true
181+
path: /tmp/artifacts
182+
183+
- name: Load Outputs
184+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
185+
shell: bash
186+
id: load-outputs
187+
run: |
188+
set -x
189+
OUTCSV="$(mktemp)"
190+
for digest_file in /tmp/artifacts/*.txt; do
191+
# Extract the platform from the file name
192+
PLATFORM="$(basename "${digest_file}" ".txt" | sed 's/.*-//g')"
193+
IMAGE_NAME="$(basename "${digest_file}" ".txt" | grep -Po ".*-" | sed 's/-$//')"
194+
DIGEST="$(< "${digest_file}")"
195+
echo "${PLATFORM},${IMAGE_NAME},${DIGEST}" | tee -a "${OUTCSV}"
196+
done
197+
echo "OUTPUT_CSV=$(base32 -w 0 < "${OUTCSV}")" | tee -a "${GITHUB_OUTPUT}"
198+
199+
- name: Login to GitHub Container Registry
200+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
201+
env:
202+
REGISTRY: ghcr.io
203+
run: |
204+
echo "${{ secrets.GITHUB_TOKEN }}" | podman login -u "${{ github.actor }}" --password-stdin "${REGISTRY}"
205+
echo "${{ secrets.GITHUB_TOKEN }}" | docker login -u "${{ github.actor }}" --password-stdin "${REGISTRY}"
206+
207+
- name: Publish manifest with unified architecture and sign
208+
shell: bash
209+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
210+
env:
211+
COSIGN_EXPERIMENTAL: false
212+
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
213+
LABELS: ${{ steps.metadata.outputs.labels }}
214+
OUTPUT_CSV: ${{ steps.load-outputs.outputs.OUTPUT_CSV }}
215+
TAGS: ${{ steps.metadata.outputs.tags }}
216+
run: |
217+
set -x
218+
219+
OUTPUT_CSV_FILE="$(mktemp)"
220+
echo "${OUTPUT_CSV}" | base32 -d | tee "${OUTPUT_CSV_FILE}"
221+
222+
# This is insanely dumb, please PR if you have a better solution ~ @tulilirockz :3c
223+
IMAGES=$(cat "${OUTPUT_CSV_FILE}" | while IFS=$'\n' read -r ENTRY ; do echo "${ENTRY}" | cut -d, -f2 ; done | sort | uniq | tr "\n" " " | sed 's/.$//')
224+
for TARGET_IMAGE in ${IMAGES} ; do
225+
TARGET_MANIFEST="${IMAGE_REGISTRY}/${TARGET_IMAGE}"
226+
podman manifest create "${TARGET_MANIFEST}"
227+
228+
PLATFORMS=$(grep -F -e "${IMAGE}" "${OUTPUT_CSV_FILE}" | while IFS=$'\n' read -r ENTRY ; do echo "${ENTRY}" | cut -d, -f1 ; done | sort | uniq | tr "\n" " " | sed 's/.$//')
229+
for TARGET_PLATFORM in ${PLATFORMS} ; do
230+
TARGET_DIGEST="$(grep -F -e ",${TARGET_IMAGE}," "${OUTPUT_CSV_FILE}" | grep -F -e "${TARGET_PLATFORM}" | cut -d, -f3 | tr -d "\n")"
231+
podman manifest add "${TARGET_MANIFEST}" "${TARGET_MANIFEST}@${TARGET_DIGEST}" --arch "${TARGET_PLATFORM}"
232+
done
233+
234+
while IFS= read -r LABEL; do
235+
podman manifest annotate --index --annotation "${LABEL}" "${TARGET_MANIFEST}"
236+
done <<< "${LABELS}"
237+
238+
while IFS= read -r TAG; do
239+
podman manifest push --all=false --digestfile=/tmp/digestfile "${TARGET_MANIFEST}" "${TARGET_MANIFEST}:${TAG}"
240+
done <<< "${TAGS}"
241+
242+
cosign sign -y --key env://COSIGN_PRIVATE_KEY "${TARGET_MANIFEST}@$(< /tmp/digestfile)"
243+
done

Containerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM docker.io/library/alpine:latest AS build
2+
3+
RUN exit 1
4+
5+
FROM scratch AS ctx
6+
COPY /system_files/shared /system_files/shared/
7+
COPY --from=build /out/homebrew.tar.zstd /system_files/usr/share/homebrew.tar.zstd

Justfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build:
2+
podman build -t localhost/bluefin-brew:latest -f ./Containerfile .

0 commit comments

Comments
 (0)