Author SHA1 Message Date
ZacharyZhang-NYandAndy Stewart 32ea8966fa Add rime-ice-installer, the Rime Ice Chinese input method setup TUI
Co-authored-by: Andy Stewart <lazycat.manatee@gmail.com>
2026-08-31 21:43:02 -04:00
1592 changed files with 4319 additions and 442489 deletions

No files matched your search

-27
View File
@@ -1,27 +0,0 @@
# Trust list for PR builds.
#
# A pull request only builds packages (and spins up builder droplets) when
# its author is trusted: repository collaborators are trusted automatically
# and do not need listing; external contributors listed here are trusted
# too. Anyone else gets the plan only, until a maintainer either adds them
# here or applies the "build-approved" label to that one PR. The label also
# releases GitHub's approval hold for that PR's build and test workflows.
# It remains effective while attached, without vouching for the author's
# other PRs. An explicit denouncement cannot be overridden by the label.
#
# Syntax:
# github:username
# -github:username reason for denouncement
#
# Keep entries sorted alphabetically.
github:bjarneo
github:DanWahlin
github:dhh
github:f-trycua
github:HANCORE-linux
github:kwilczynski
github:ryanrhughes
github:scottjones
github:spencerbull
github:tcballard
github:tobi
-88
View File
@@ -1,88 +0,0 @@
const BUILD = '.github/workflows/build-pr.yml';
const TESTS = '.github/workflows/test.yml';
// pullRequest/action/since default to the pull_request_target event. The
// sync workflows pass them explicitly: GitHub creates no pull_request_target
// run for a GITHUB_TOKEN push, so they release their own pushes' held runs.
module.exports = async function approve({ github, context, core, vouchStatus,
pullRequest = context.payload.pull_request, action = context.payload.action, since,
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), attempts = 36 }) {
// Missing/failed vouch lookups must not become approval. Denouncements
// remain absolute, just as they are in the package build gate.
if (!['unknown', 'bot', 'collaborator', 'vouched'].includes(vouchStatus)) {
throw new Error(`Cannot approve workflows: vouch status is ${vouchStatus || 'missing'}.`);
}
const expected = pullRequest;
const eventTime = Date.parse(since ?? expected.updated_at);
if (!Number.isFinite(eventTime)) throw new Error('Missing PR event timestamp.');
const approved = new Set();
let precedingBuild;
const stillApproved = async () => {
const { data: pr } = await github.rest.pulls.get({
...context.repo, pull_number: expected.number,
});
return pr.state === 'open' && pr.head.sha === expected.head.sha &&
pr.labels.some(label => label.name === 'build-approved');
};
// The label and PR-run events arrive independently. Wait for the build
// belonging to this event, rather than returning after approving an older
// run and leaving the new label-triggered run stuck behind GitHub's gate.
for (let attempt = 0; attempt < attempts; attempt++) {
if (attempt) await sleep(5000);
if (!await stillApproved()) {
core.info('PR closed, head changed, or build-approved removed; stopping.');
return;
}
const all = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
...context.repo, event: 'pull_request', head_sha: expected.head.sha, per_page: 100,
});
const runs = all.filter(run =>
run.event === 'pull_request' && run.head_sha === expected.head.sha &&
run.head_repository?.id === expected.head.repo.id && run.head_branch === expected.head.ref &&
[BUILD, TESTS].includes(run.path) &&
// Fork runs awaiting approval often have no pull_requests entries.
(!run.pull_requests?.length || run.pull_requests.some(pr => pr.number === expected.number))
).sort((a, b) => a.id - b.id);
const newestBuild = runs.findLast(run => run.path === BUILD);
if (!newestBuild || !(Date.parse(newestBuild.created_at) >= eventTime) ||
!runs.some(run => run.path === TESTS &&
(action === 'labeled' || Date.parse(run.created_at) >= eventTime))) continue;
if (precedingBuild) {
const { data: run } = await github.rest.actions.getWorkflowRun({
...context.repo, run_id: precedingBuild,
});
// Approve older builds first, and let them acquire concurrency before
// releasing a newer build. Otherwise an older queued run could start
// last and cancel the label-triggered build that carries approval.
if (!['in_progress', 'completed'].includes(run.status) || run.conclusion === 'action_required') continue;
precedingBuild = undefined;
}
const pending = runs.filter(run => run.conclusion === 'action_required' && !approved.has(run.id) &&
// If the newest build already runs (e.g. a maintainer approved it),
// don't resurrect an obsolete hold that could cancel that newer run.
(run.path !== BUILD || run.id === newestBuild.id || newestBuild.conclusion === 'action_required'));
if (!pending.length) return;
const run = pending[0];
// Recheck after the API reads, immediately before exercising write access.
if (!await stillApproved()) return;
await github.rest.actions.approveWorkflowRun({ ...context.repo, run_id: run.id });
approved.add(run.id);
core.info(`Approved ${run.path} run ${run.id} for PR #${expected.number}.`);
// Only a newer held build needs this one to take the concurrency slot
// first. A lone build may sit pending behind an in-flight build of an
// older commit (sync branches queue rather than cancel); waiting for it
// to start would time out before the tests run was released.
if (run.path === BUILD && pending.some(other => other.path === BUILD && other.id > run.id)) {
precedingBuild = run.id;
}
if (pending.length === 1) return;
}
throw new Error('Timed out waiting for PR workflows. Remove and reapply build-approved to retry.');
};
-76
View File
@@ -1,76 +0,0 @@
// Whether a PR rides to master on its own once the required checks pass.
//
// The rule is the build gate's, from build-pr.yml: a PR trusted to build is
// trusted to ship. Collaborators, vouched authors and bots are trusted; an
// unknown author is trusted while the PR carries build-approved; a
// denouncement is absolute. Two limits on top of it:
//
// - Only package changes. A PR's workflows, scripts and build tooling never
// run in its own build (build-pr.yml overlays only its package directories
// onto base tooling), so green checks say nothing about them, and after
// merge they run with the publish secrets. Allowed: anything under
// pkgbuilds/, plus the test a package PR brings with it, which is a new
// file under tests/ and lines in test.yml that only run new tests/*.sh.
// test.yml runs on PRs only; an existing test may also run on master
// (builder-images.yml runs tests/build-isolation.sh with packages: write),
// so editing one still needs a maintainer.
const PACKAGES = 'pkgbuilds/';
const TESTS = 'tests/';
const TEST_WORKFLOW = '.github/workflows/test.yml';
const TEST_LINE = /^\+\s*\.\/tests\/[A-Za-z0-9._-]+\.sh\s*$/;
// Every changed line adds a ./tests/<name>.sh call; nothing removed. A diff
// too large for the API has no patch and does not qualify.
function onlyRunsTests(patch) {
if (!patch) return false;
const changed = patch.split('\n').filter(line =>
/^[+-]/.test(line) && !line.startsWith('+++') && !line.startsWith('---'));
return changed.length > 0 && changed.every(line => TEST_LINE.test(line));
}
function packageChange(file) {
if (file.previous_filename && !file.previous_filename.startsWith(PACKAGES)) return false;
if (file.filename.startsWith(PACKAGES)) return true;
if (file.filename.startsWith(TESTS)) return file.status === 'added';
if (file.filename === TEST_WORKFLOW) return file.status === 'modified' && onlyRunsTests(file.patch);
return false;
}
function decide({ pr, files, vouchStatus, repository }) {
if (pr.state !== 'open') return { enable: false, reason: 'PR is not open' };
if (pr.draft) return { enable: false, reason: 'PR is a draft' };
const labelled = pr.labels.some(label => label.name === 'build-approved');
let trusted;
switch (vouchStatus) {
case 'bot': case 'collaborator': case 'vouched': trusted = true; break;
case 'unknown': trusted = labelled; break;
default: trusted = false; // denounced, or a failed lookup
}
if (!trusted) {
return { enable: false, reason: `author not trusted to build (${vouchStatus || 'missing'}${labelled ? ', labelled' : ''})` };
}
if (!files.length) return { enable: false, reason: 'PR changes no files' };
const outside = files.filter(file => !packageChange(file));
if (outside.length) {
const names = outside.slice(0, 3).map(file => file.filename).join(', ');
return { enable: false, reason: `changes more than packages and their new tests: ${names}${outside.length > 3 ? ', ...' : ''}` };
}
return { enable: true, reason: `${vouchStatus === 'unknown' ? 'build-approved' : vouchStatus} author, package changes only` };
}
module.exports = async function autoMerge({ github, context, core, number, vouchStatus }) {
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
const files = await github.paginate(github.rest.pulls.listFiles, {
...context.repo, pull_number: number, per_page: 100,
});
const decision = decide({
pr, files, vouchStatus, repository: `${context.repo.owner}/${context.repo.repo}`,
});
core.info(`#${number}: ${decision.enable ? 'auto-merge' : 'leave for a maintainer'} (${decision.reason})`);
core.setOutput('enable', String(decision.enable));
core.setOutput('head_sha', pr.head.sha);
return decision;
};
module.exports.decide = decide;
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash
# Usage: package-pr-groups.sh BRANCH_PREFIX PATCH_OUT
#
# Run from the repository root after a sync has rewritten recipes in the
# working tree. Writes those changes to PATCH_OUT and prints, as one JSON
# line, the PRs they split into: [{"branch": ..., "packages": "a b"}, ...].
#
# One PR per package, so a package whose build fails holds back only itself.
# Packages pinned from the same upstream branch (omarchy-dev and
# omarchy-settings-dev) share a PR: sync-upstream moves them in lockstep, and
# merging one without the other would ship the pair from different commits.
# Branch names come from sync-pr-branch.sh, so a scheduled run and a dispatch
# naming the same packages update the same PR.
set -euo pipefail
prefix=${1:?branch prefix required}
patch=${2:?patch path required}
scripts=$(dirname "$0")
git diff --binary -- pkgbuilds > "$patch"
declare -A members=()
while IFS= read -r package; do
key=$(jq -r '(.upstream.watch? | objects | select(has("git_branch")) | "\(.git_branch)#\(.branch)") // empty' \
"pkgbuilds/$package/.omarchy/package.json" 2>/dev/null || true)
members["${key:-$package}"]+="$package "
done < <(git diff --name-only -- pkgbuilds | cut -d/ -f2 | sort -u)
groups='[]'
for key in "${!members[@]}"; do
read -r -a packages <<<"${members[$key]}"
branch=$("$scripts/sync-pr-branch.sh" "$prefix" "${packages[@]}" | sed -n 's/^branch=//p')
groups=$(jq -c --arg branch "$branch" --arg packages "${packages[*]}" \
'. + [{branch: $branch, packages: $packages}]' <<<"$groups")
done
jq -c 'sort_by(.branch)' <<<"$groups"
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
# Usage: sync-pr-branch.sh BASE_BRANCH [PACKAGE...]
#
# Prints the branch a sync workflow run pushes to, as branch=/scope= lines for
# $GITHUB_OUTPUT. An unscoped (scheduled) run owns BASE_BRANCH and regenerates
# it from master every time. A run scoped to named packages regenerates only
# those, so it gets its own branch and PR: pushing it to BASE_BRANCH would
# replace every other pending update there with just the named packages.
set -euo pipefail
base=${1:?base branch required}
shift
if (( $# == 0 )); then
printf 'branch=%s\nscope=\n' "$base"
exit 0
fi
names=()
for name in "$@"; do
# Package directory names, as pacman allows them. Anything else is a typo
# or an attempt to smuggle something into a ref name or PR title.
if [[ ! $name =~ ^[a-z0-9@_+][a-z0-9@._+-]*$ ]]; then
echo "invalid package name: $name" >&2
exit 1
fi
names+=("$name")
done
mapfile -t names < <(printf '%s\n' "${names[@]}" | sort -u)
scope="${names[*]}"
slug=$(printf '%s\n' "${names[@]}" | sed 's/[^a-z0-9]\{1,\}/-/g; s/^-//; s/-$//' | paste -sd- -)
# Keep long package lists to a readable ref; the hash keeps distinct lists apart.
hash=$(printf '%s' "$scope" | sha256sum | cut -c1-10)
if [[ -z $slug ]]; then
slug=$hash
elif (( ${#slug} > 60 )); then
slug="${slug:0:48}"
slug="${slug%-}-$hash"
fi
printf 'branch=%s-%s\nscope=%s\n' "$base" "$slug" "$scope"
-46
View File
@@ -1,46 +0,0 @@
name: Approve PR workflows
# A pull_request workflow cannot approve itself: GitHub can hold it before
# any job starts. This workflow only runs trusted default-branch code and
# releases the ordinary, unprivileged PR workflows after build approval.
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
pull-requests: read
actions: write
concurrency:
group: approve-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
approve:
# Match build-pr.yml's events, including other labels applied while this
# PR still carries build-approved: each labeled event creates a build.
if: contains(github.event.pull_request.labels.*.name, 'build-approved')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Never check out the PR head or its merge ref with this write token.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- id: vouch
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
with:
user: ${{ github.event.pull_request.user.login }}
allow-fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Approve this PR's pending build and test runs
uses: actions/github-script@v7
env:
VOUCH_STATUS: ${{ steps.vouch.outputs.status }}
with:
script: |
const approve = require('./.github/scripts/approve-pr-workflows.cjs');
await approve({ github, context, core, vouchStatus: process.env.VOUCH_STATUS });
-108
View File
@@ -1,108 +0,0 @@
name: Auto-merge approved package PRs
# A package PR trusted to build is trusted to ship: once its builds are
# green it should merge and publish without a maintainer pressing the
# button. This enables GitHub's auto-merge on such PRs; branch protection
# still holds the merge until `result`, `self-tests` and `build-isolation`
# pass, and a red build stays an open PR. .github/scripts/auto-merge-pr.cjs
# has the rule.
#
# Auto-merge is enabled with the PAT in PKGS_BOT_TOKEN: a merge made with the
# built-in GITHUB_TOKEN does not start publish.yml.
#
# pull_request_target runs this default-branch code with secrets; the PR's
# code is never checked out here.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
workflow_dispatch:
inputs:
pr:
description: 'PR number to evaluate'
required: true
permissions:
contents: read
pull-requests: read
concurrency:
group: auto-merge-pr-${{ github.event.pull_request.number || github.event.inputs.pr }}
cancel-in-progress: true
jobs:
auto-merge:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Find the PR's author
id: pr
uses: actions/github-script@v7
env:
NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr }}
with:
script: |
const { data: pr } = await github.rest.pulls.get({
...context.repo, pull_number: Number(process.env.NUMBER),
});
core.setOutput('number', String(pr.number));
core.setOutput('author', pr.user.login);
- id: vouch
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
with:
user: ${{ steps.pr.outputs.author }}
allow-fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Decide
id: decide
uses: actions/github-script@v7
env:
NUMBER: ${{ steps.pr.outputs.number }}
VOUCH_STATUS: ${{ steps.vouch.outputs.status }}
with:
script: |
const autoMerge = require('./.github/scripts/auto-merge-pr.cjs');
await autoMerge({ github, context, core,
number: Number(process.env.NUMBER), vouchStatus: process.env.VOUCH_STATUS });
- name: Enable auto-merge
if: steps.decide.outputs.enable == 'true'
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.number }}
HEAD_SHA: ${{ steps.decide.outputs.head_sha }}
run: |
if [[ -z "$GH_TOKEN" ]]; then
echo "::error::Set PKGS_BOT_TOKEN; a GITHUB_TOKEN merge would not publish."
exit 1
fi
# Idempotent: enabling twice errors. Bot lanes enable their own.
if [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
echo "auto-merge already enabled on #$PR"
exit 0
fi
# --match-head-commit: never arm a merge for a commit newer than
# the one just judged.
gh pr merge --auto --squash --match-head-commit "$HEAD_SHA" "$PR" -R "$GITHUB_REPOSITORY"
# Removing build-approved withdraws the approval, so withdraw the
# auto-merge it armed too. Only on that event: auto-merge a maintainer
# enabled by hand on any other PR is theirs to keep.
- name: Withdraw auto-merge
if: >-
steps.decide.outputs.enable == 'false' &&
github.event.action == 'unlabeled' && github.event.label.name == 'build-approved'
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.number }}
run: |
if [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
gh pr merge --disable-auto "$PR" -R "$GITHUB_REPOSITORY"
fi
-321
View File
@@ -1,321 +0,0 @@
name: Build changed packages
# Build every package directory a PR touches, one job per package per arch:
# x86_64 on the self-hosted droplet pool, aarch64 natively on GitHub's arm64
# runners. Artifacts are unsigned; publish.yml signs and publishes them on
# merge.
#
# Tooling runs from the base branch; a PR supplies only pkgbuilds/. The
# vouch gate limits who may spend compute; this limits what their PR can run.
# No paths filter: approved PRs must report the required `result` even when
# no package directory changed. Those PRs get an empty matrix and a passing
# result in seconds; unapproved PRs wait for maintainer approval.
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
packages:
description: "Space-separated package directories to build"
required: true
# A new push normally cancels the PR's in-flight build. The bots' branches
# (auto/sync-* and auto/track-branches-*) are the exception: they are
# force-pushed with fresh upstream releases several times a day, which kept
# cancelling multi-hour aarch64 builds before they could finish. There the
# newest run waits instead (GitHub keeps at most one pending run per group,
# replacing older pending ones), and when it starts it reuses every artifact
# the finished build uploaded, so only packages whose tree changed are built
# again. The tracker labels the PR it opens, so a new tracker PR starts two
# runs at once; cancelling the first left a red `result` on the PR (#901).
concurrency:
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && (startsWith(github.head_ref, 'auto/sync-') || startsWith(github.head_ref, 'auto/track-branches'))) }}
jobs:
# Builds cost real machines, so they run only for trusted authors:
# collaborators, anyone in .github/VOUCHED.td (read from the default
# branch, so a PR cannot vouch for itself), or a PR a maintainer has
# labelled "build-approved". Everyone else gets this job's plan output
# while the required `result` stays pending until a maintainer approves.
changes:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.list.outputs.matrix }}
count: ${{ steps.gate.outputs.count }}
trusted: ${{ steps.gate.outputs.trusted }}
vouch_status: ${{ steps.vouch.outputs.status }}
empty: ${{ steps.list.outputs.empty }}
steps:
# Same rule as the build job: bin/build-matrix comes from the base
# branch tip, the package directories from the PR head.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref || github.sha }}
fetch-depth: 0
persist-credentials: false
- if: github.event_name == 'pull_request'
run: |
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
# Bootstrap: the PR that introduces this tooling has a base without
# it. Take the plan helper from the PR head in that one case; it
# runs on a hosted runner and only prints a plan.
if [[ ! -x bin/build-matrix ]]; then
git checkout "${{ github.event.pull_request.head.sha }}" -- bin/build-matrix helpers/
echo "::notice::base branch has no bin/build-matrix; using the PR's copy for planning"
fi
- id: vouch
if: github.event_name == 'pull_request'
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
with:
user: ${{ github.event.pull_request.user.login }}
allow-fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- id: approval
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
...context.repo, pull_number: context.payload.pull_request.number,
});
// Approving or rerunning a held run keeps its original event,
// which may predate the label. Read the current approval instead.
core.setOutput('approved', pr.state === 'open' &&
pr.head.sha === context.payload.pull_request.head.sha &&
pr.labels.some(label => label.name === 'build-approved'));
# One matrix entry per package per architecture. Every package builds
# once, against edge; the channels it ships to on merge are carried
# along for information. A filename means one set of bytes.
- id: list
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ -n "${{ github.event.inputs.packages }}" ]]; then
names="${{ github.event.inputs.packages }}"
else
# The PR's own files, as GitHub lists them against the merge base.
# A two-dot diff against the current base tip also counted every
# package master changed after the PR branched, so a stale PR
# planned dozens of unrelated packages at its old versions. The
# checkout here is shallow, so there is no merge base to diff from.
# A package the PR deletes has nothing to build.
names=$(gh api --paginate "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --jq '.[].filename' \
| awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u \
| while read -r name; do
if git cat-file -e "${{ github.event.pull_request.head.sha }}:pkgbuilds/$name" 2>/dev/null; then echo "$name"; fi
done)
fi
matrix=$(printf '%s\n' $names | bin/build-matrix)
# A package directory whose exact tree already has a build artifact
# (label <pkg>-<arch>-<tree hash>, uploaded only after a successful
# build) is not built again. Pushing a fix for one package to a PR
# that touches fifty rebuilds one, not fifty; publish.yml finds the
# same artifacts on merge. workflow_dispatch is an explicit request
# and always builds.
if [[ "${{ github.event_name }}" == pull_request ]]; then
head="${{ github.event.pull_request.head.sha }}"
kept=(); reused=()
while read -r entry; do
package=$(jq -r .package <<<"$entry"); arch=$(jq -r .arch <<<"$entry")
label="$package-$arch-$(git rev-parse "$head:pkgbuilds/$package")"
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)] | length' || echo 0)
if (( found > 0 )); then reused+=("$label"); else kept+=("$entry"); fi
done < <(jq -c '.include[]' <<<"$matrix")
matrix=$(printf '%s\n' "${kept[@]}" | jq -sc '{include: .}')
if (( ${#reused[@]} )); then
printf '==> already built, reusing the artifact: %s\n' "${reused[@]}"
{ echo "Reused existing build artifacts (${#reused[@]}):"; printf -- '- %s\n' "${reused[@]}"; } >> "$GITHUB_STEP_SUMMARY"
fi
fi
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "planned=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix"
# A PR whose diff against its base is empty changes nothing: its
# content already landed some other way (a sync PR beat it, or a
# merge from master swallowed it). Merging it would record a change
# that isn't one. Flag it so `result` fails rather than passes.
if [[ "${{ github.event_name }}" == pull_request ]]; then
total=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" | wc -l)
echo "empty=$([[ $total -eq 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "files changed vs base: $total"
else
echo "empty=false" >> "$GITHUB_OUTPUT"
fi
- id: gate
env:
STATUS: ${{ github.event_name == 'workflow_dispatch' && 'dispatch' || steps.vouch.outputs.status }}
AUTHOR: ${{ github.event.pull_request.user.login }}
APPROVED: ${{ steps.approval.outputs.approved || 'false' }}
PLANNED: ${{ steps.list.outputs.planned }}
run: |
case "$STATUS" in
bot|collaborator|vouched|dispatch) trusted=true ;;
# A denouncement is absolute: the label cannot override it.
denounced) trusted=false ;;
unknown) trusted=$APPROVED ;;
*) trusted=false ;;
esac
echo "trusted=$trusted" >> "$GITHUB_OUTPUT"
if [[ $trusted == true ]]; then
echo "count=$PLANNED" >> "$GITHUB_OUTPUT"
echo "Author $AUTHOR is trusted ($STATUS); building $PLANNED package(s)."
else
echo "count=0" >> "$GITHUB_OUTPUT"
echo "::warning::Author $AUTHOR is not trusted ($STATUS). $PLANNED package build(s) planned but not run."
if [[ $STATUS == denounced ]]; then
echo "::warning::The author is denounced in .github/VOUCHED.td; the build-approved label does not apply."
else
echo "::warning::A maintainer can add the author to .github/VOUCHED.td, or apply the 'build-approved' label to build this PR."
fi
fi
build:
needs: changes
if: needs.changes.outputs.count != '0'
# The droplets are x86, so aarch64 there runs under QEMU: omarchy-mac-boot
# took 2h47m of the 180 minutes, most of it in check().
# GitHub's arm64 runners (4 vCPU, 16 GB; ~100 GB disk free in our pilots)
# build it natively in 11 minutes, and linux-aurora in 30 (72 under QEMU).
runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || fromJSON('["self-hosted","omarchy-builder"]') }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.changes.outputs.matrix) }}
steps:
# Tooling from base: everything that executes on this runner's host
# (bin/, helpers/, build/) comes from the base branch. Only the PR's
# package directories are overlaid. A PR can therefore change what
# gets built, never how the runner builds it. A PR that changes both
# tooling and a package builds the package with the OLD tooling; land
# the tooling first. workflow_dispatch has no PR and runs as checked out.
# The base branch tip, not the event's base.sha: that sha is a snapshot
# taken at the PR's last push, so a tooling fix on master would never
# reach an open PR until someone pushed to it (seen on the daily sync
# PR after the artifact packing fix landed).
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref || github.sha }}
persist-credentials: false
- name: Overlay the PR's package directories onto base tooling
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
echo "tooling: $(git rev-parse --short HEAD) (base) packages: ${{ github.event.pull_request.head.sha }} (PR head)"
# A preview only. `head` exits after ten lines and, under pipefail,
# git's SIGPIPE (141) failed the step for any PR far enough behind
# master to differ in more files; sed reads the whole stream.
git status --short | sed -n '1,10p'
# A PR can change a package's directory without changing its version:
# an upstream hook, its tests, a README. edge already holds that
# version, so the planner builds nothing and there is nothing to pack.
# The same check publish.yml's rebuild job makes; without it the pack
# step failed on the empty output and the required result went red.
- name: Build ${{ matrix.package }} (${{ matrix.arch }}, ships to ${{ matrix.channels }})
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 }}): edge already holds this version, nothing to build"
echo "built=false" >> "$GITHUB_OUTPUT"
# Arch bumps pkgrel only when the built package changes, which is
# the reviewer's call. When the recipe itself changed (comments and
# blank lines aside), flag it on the PKGBUILD without failing: the
# change ships only once pkgver or pkgrel moves. HEAD is the base
# branch; the PR's package directories are overlaid on it.
recipe="pkgbuilds/${{ matrix.package }}/PKGBUILD"
recipe_lines() { grep -vE '^[[:space:]]*(#|$)' || true; }
if ! cmp -s <(git show "HEAD:$recipe" 2>/dev/null | recipe_lines) <(recipe_lines < "$recipe"); then
note="PKGBUILD changed, but edge already holds this version of ${{ matrix.package }}, so the change will not ship until pkgver or pkgrel changes. Bump pkgrel if it affects the built package."
echo "::warning file=$recipe,title=Change will not ship::$note"
echo "$note" >> "$GITHUB_STEP_SUMMARY"
fi
exit 0
fi
bin/build --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}"
echo "built=true" >> "$GITHUB_OUTPUT"
# makepkg's check() leaves meson's per-test output in the build tree,
# never on stdout, so a failing test shows only a summary line in this
# job log. bin/build bind-mounts $SRC_DIR at /src, so those logs outlive
# the container. Without this upload an arch-specific test failure
# cannot be diagnosed from CI at all (seen on owe 0.2.7, aarch64).
- name: Upload test logs
if: always() && steps.build.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: test-logs-${{ matrix.package }}-${{ matrix.arch }}
path: src/**/meson-logs/
if-no-files-found: ignore
retention-days: 14
# The artifact label carries the package directory's git tree hash so
# the publish step can find the build for exactly the tree that merged.
# The package file inside keeps makepkg's standard name untouched.
# The artifact label uses the PR head's tree for this package: that is
# the tree that merges, and what publish looks up.
- name: Tree hash
id: tree
run: echo "hash=$(git rev-parse "${{ github.event.pull_request.head.sha || github.sha }}:pkgbuilds/${{ matrix.package }}")" >> "$GITHUB_OUTPUT"
# The upload action rejects a path containing ':', which is how makepkg
# names a package with an epoch. The files ride inside packages.tar
# (helpers/artifact-helpers.sh); publish.yml unpacks it. Only a
# successful build uploads: the artifact's existence is what lets the
# planner above and publish.yml skip rebuilding this exact tree.
- 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
- name: Upload artifact
if: steps.build.outputs.built == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package }}-${{ matrix.arch }}-${{ steps.tree.outputs.hash }}
path: packages.tar
if-no-files-found: error
retention-days: 7
# `result` is required by branch protection. An unvouched author awaiting
# approval gets a differently named informational check, leaving `result`
# unreported (pending). Skipping or passing a job named `result` would count
# as satisfying the requirement even though no build was authorized.
# Actual planning/build failures and denouncements still report `result`.
result:
name: ${{ needs.changes.result == 'success' && needs.changes.outputs.trusted == 'false' && needs.changes.outputs.vouch_status == 'unknown' && needs.changes.outputs.empty == 'false' && 'Awaiting build approval' || 'result' }}
needs: [changes, build]
if: always()
runs-on: ubuntu-latest
steps:
- run: |
echo "trusted=${{ needs.changes.outputs.trusted }} build=${{ needs.build.result }}"
if [[ "${{ needs.changes.result }}" != "success" ]]; then
echo "::error::Build planning or the trust check failed. See the changes job."
exit 1
fi
# Nothing to merge: the PR's diff against its base is empty. Its
# change already landed elsewhere. Close it rather than merge it.
if [[ "${{ needs.changes.outputs.empty }}" == "true" ]]; then
echo "::error::This PR changes no files relative to its base. Its content is already on the target branch; close it instead of merging."
exit 1
fi
if [[ "${{ needs.changes.outputs.trusted }}" == "false" && "${{ needs.changes.outputs.vouch_status }}" == "unknown" && "${{ needs.changes.outputs.empty }}" == "false" ]]; then
echo "::notice::Awaiting maintainer build approval. Apply 'build-approved' to this PR or vouch for the author in .github/VOUCHED.td."
echo "Package builds are waiting for maintainer approval. Apply **build-approved** to this PR to start them. The required **result** check remains pending." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [[ "${{ needs.changes.outputs.trusted }}" != "true" ]]; then
echo "::error::Builds are blocked: the author is denounced or the trust result is invalid. The build-approved label cannot override this."
exit 1
fi
[[ "${{ needs.build.result }}" == "success" || "${{ needs.build.result }}" == "skipped" ]]
-118
View File
@@ -1,118 +0,0 @@
name: Refresh builder images
on:
schedule:
- cron: '23 4 * * *'
push:
branches: [master]
paths:
- build/**
- bin/builder-image
- helpers/paths.sh
- helpers/docker-helpers.sh
- tests/build-isolation.sh
- .github/workflows/builder-images.yml
workflow_dispatch:
pull_request:
paths:
- build/**
- bin/builder-image
- helpers/paths.sh
- helpers/docker-helpers.sh
- tests/build-isolation.sh
- .github/workflows/builder-images.yml
# Complete each refresh before another can replace its tested image tags.
concurrency:
group: builder-images-${{ github.event.pull_request.number || 'master' }}
cancel-in-progress: false
permissions:
contents: read
jobs:
# Exercise proposed image changes on native runners with a read-only token.
# Publishing is a separate master-only job with its own write permission.
validate:
if: github.event_name == 'pull_request'
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
env:
CONTAINER_ENGINE: docker
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build a fresh environment
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
- name: Test isolated package builds
env:
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
run: tests/build-isolation.sh
refresh:
if: github.repository == 'omacom/omarchy-pkgs' && github.ref == 'refs/heads/master'
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
permissions:
contents: read
packages: write
env:
CONTAINER_ENGINE: docker
REGISTRY_IMAGE: ghcr.io/omacom/omarchy-pkg-builder
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build a fresh environment
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
- name: Test isolated package builds
env:
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
run: tests/build-isolation.sh
- name: Publish tested image
env:
GH_TOKEN: ${{ github.token }}
GH_ACTOR: ${{ github.actor }}
DOCKER_CONFIG: ${{ runner.temp }}/builder-registry-auth
run: |
set -euo pipefail
mkdir -p "$DOCKER_CONFIG"
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GH_ACTOR" --password-stdin
key=$(bin/builder-image key --arch "${{ matrix.arch }}" --mirror edge)
version="$REGISTRY_IMAGE:$key-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
docker tag "$CANDIDATE_IMAGE" "$version"
docker push "$version"
# GHCR creates new packages private. Do not advertise an image to
# fork PRs until it is public. This is a one-time package setting.
anonymous_config=$(mktemp -d "$RUNNER_TEMP/builder-anonymous.XXXXXX")
if ! DOCKER_CONFIG="$anonymous_config" docker manifest inspect "$version" >/dev/null; then
rm -rf "$anonymous_config"
echo "::error::Make the omacom/omarchy-pkg-builder GHCR package public, then rerun this job. The previous matching image remains selected."
exit 1
fi
rm -rf "$anonymous_config"
docker tag "$CANDIDATE_IMAGE" "$REGISTRY_IMAGE:$key"
docker push "$REGISTRY_IMAGE:$key"
digest=$(docker image inspect "$version" --format '{{index .RepoDigests 0}}')
printf '### Builder image (%s)\n\nInput key: `%s`\n\nImage: `%s`\n' \
"${{ matrix.arch }}" "$key" "$digest" >> "$GITHUB_STEP_SUMMARY"
-134
View File
@@ -1,134 +0,0 @@
name: Open package PRs
# Shared by the three bot lanes (sync-upstream.yml, sync-rebuilds.yml and
# track-branches.yml). The caller's sync job rewrites recipes, groups the
# changes with .github/scripts/package-pr-groups.sh and uploads them as the
# `package-pins` artifact. This opens or updates one PR per group and arms
# auto-merge, so every green package lands on its own and only the failures
# stay open. A single batch PR let one red package hold back every other one
# (#788, #837).
#
# Everything goes through the PAT in PKGS_BOT_TOKEN: its pushes build without
# an approval hold, and a merge made with it starts publish.yml. A merge made
# with the built-in GITHUB_TOKEN would not publish.
on:
workflow_call:
inputs:
groups:
description: 'JSON from package-pr-groups.sh: [{"branch", "packages"}]'
required: true
type: string
prefix:
description: 'Branch prefix the caller owns, e.g. auto/sync-upstream'
required: true
type: string
title:
description: 'PR title; ": <package> <version>, ..." is appended'
required: true
type: string
body:
description: 'PR body'
required: true
type: string
secrets:
PKGS_BOT_TOKEN:
required: true
permissions:
contents: read
jobs:
pr:
if: ${{ inputs.groups != '[]' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 4
matrix:
group: ${{ fromJSON(inputs.groups) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Fetch the pins
uses: actions/download-artifact@v4
with:
name: package-pins
path: ${{ runner.temp }}
# The PR title names what moved, so the merged history reads like a
# changelog: "Sync upstream releases: zed 1.23.2-1".
- name: Apply this PR's packages
id: apply
env:
PACKAGES: ${{ matrix.group.packages }}
TITLE: ${{ inputs.title }}
run: |
read -r -a packages <<<"$PACKAGES"
git apply $(printf -- '--include=pkgbuilds/%s/* ' "${packages[@]}") "$RUNNER_TEMP/pins.patch"
git status --porcelain
field() { sed -n "s/^$2=//p" "pkgbuilds/$1/PKGBUILD" | head -1 | tr -d "\"'"; }
versions=$(for package in "${packages[@]}"; do
printf '%s %s-%s, ' "$package" "$(field "$package" pkgver)" "$(field "$package" pkgrel)"
done)
title="$TITLE: ${versions%, }"
echo "title=$title" >> "$GITHUB_OUTPUT"
echo "- $title" >> "$GITHUB_STEP_SUMMARY"
- name: Open or update the PR
id: pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.PKGS_BOT_TOKEN }}
commit-message: ${{ steps.apply.outputs.title }}
title: ${{ steps.apply.outputs.title }}
body: ${{ inputs.body }}
branch: ${{ matrix.group.branch }}
delete-branch: true
labels: automated
# Auto-merge, not a direct merge: branch protection still has to see
# `result`, `self-tests` and `build-isolation` green. auto-merge-pr.yml
# may arm the same PR at the same moment; either one arming it is the
# outcome wanted.
- name: Enable auto-merge
if: ${{ steps.pr.outputs.pull-request-number != '' }}
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.pull-request-number }}
run: |
armed() { [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; }
if armed; then
echo "auto-merge already enabled on #$PR"
exit 0
fi
gh pr merge --auto --merge "$PR" -R "$GITHUB_REPOSITORY" || armed
# A PR on this lane's prefix that the run did not refresh, and that
# conflicts with master, is stale: master moved its packages on without it.
# Close it rather than leave a red PR behind. Not being refreshed is not
# enough on its own, since a feed that failed this run (one of dozens timing
# out is routine) leaves a still-valid PR unrefreshed too. The lane's old
# batch PR, on the bare prefix, is retired outright.
close-superseded:
runs-on: ubuntu-latest
steps:
- name: Close superseded PRs
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
GROUPS_JSON: ${{ inputs.groups }}
PREFIX: ${{ inputs.prefix }}
run: |
gh pr list -R "$GITHUB_REPOSITORY" --state open --limit 500 --json number,headRefName,mergeable \
--jq '.[] | select(.headRefName == env.PREFIX or (.headRefName | startswith(env.PREFIX + "-"))) | "\(.number) \(.headRefName) \(.mergeable)"' |
while read -r number branch mergeable; do
jq -e --arg branch "$branch" 'any(.[]; .branch == $branch)' <<<"$GROUPS_JSON" >/dev/null && continue
[[ $branch == "$PREFIX" || $mergeable == CONFLICTING ]] || continue
echo "closing #$number ($branch): superseded"
gh pr close "$number" -R "$GITHUB_REPOSITORY" --delete-branch \
--comment "Superseded: master has moved these packages on, or per-package PRs carry them now."
done
-459
View File
@@ -1,459 +0,0 @@
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
# queue: max, because by default a group holds one waiting job: a third
# merge arriving cancels the second one's publish, and nothing reruns it.
# With it up to 100 wait, in the order they reach this job.
concurrency:
group: publish
cancel-in-progress: false
queue: max
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" ]]
+91
View File
@@ -0,0 +1,91 @@
name: Sync AUR Packages
on:
schedule:
# Every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
packages:
description: 'Specific packages to sync (space-separated, leave empty for all)'
required: false
default: ''
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Sync AUR packages
run: |
docker run --rm \
-e PACKAGES="$PACKAGES" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
-v "$PWD/bin:/workspace/bin:ro" \
-v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq
groupadd -g "$HOST_GID" runner
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
chown -R runner:runner /workspace/pkgbuilds
if [[ -n "${PACKAGES:-}" ]]; then
read -r -a package_args <<< "$PACKAGES"
runuser -u runner -- ./bin/sync-aur "${package_args[@]}"
else
runuser -u runner -- ./bin/sync-aur
fi
'
env:
PACKAGES: ${{ github.event.inputs.packages }}
- name: Check for changes
id: changes
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Create Pull Request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AUR packages'
title: 'chore: sync AUR packages'
body: |
Automated AUR package sync.
Package sync behavior is controlled by `.omarchy/package.json`.
branch: auto/sync-aur
delete-branch: true
labels: automated
reviewers: ryanrhughes
- name: Notify Basecamp on failure
if: failure() && env.BASECAMP_CHATBOT_URL != ''
env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: |
curl -s -o /dev/null \
-H "Content-Type: application/json" \
-d "$(jq -n --arg content \
"🔴 <strong>AUR sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
'{content: $content}')" \
"$BASECAMP_CHATBOT_URL"
+29 -72
View File
@@ -1,16 +1,5 @@
name: Sync Rebuild Triggers name: Sync Rebuild Triggers
# Rebuilds ride the unattended lane, like track-branches.yml: each package's
# pkgrel bump gets its own PR on auto/sync-rebuilds-<package>, opened by
# package-prs.yml. It builds on the droplets, auto-merge lands it once
# `result`, `self-tests` and `build-isolation` are green, and the merge
# publishes. A rebuild that fails stays an unmerged red PR for a maintainer
# without holding back any other package.
#
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN, because a merge made
# with the built-in GITHUB_TOKEN does not start publish.yml, and its pushes
# are held for approval instead of building.
on: on:
schedule: schedule:
# Every 6 hours, off the hour to dodge the scheduling backlog at :00 # Every 6 hours, off the hour to dodge the scheduling backlog at :00
@@ -22,30 +11,14 @@ on:
required: false required: false
default: '' default: ''
# One sync at a time: two runs racing on one auto/sync-rebuilds-* branch
# would each force-push their own bump over the other's.
concurrency:
group: sync-rebuilds
cancel-in-progress: false
jobs: jobs:
sync: sync:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write
outputs: pull-requests: write
groups: ${{ steps.groups.outputs.groups }}
steps: steps:
- name: Require the bot token
env:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
run: |
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
exit 1
fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -57,7 +30,6 @@ jobs:
# version the build never saw is the one failure this command must not # version the build never saw is the one failure this command must not
# have: nothing re-fires once the record matches. # have: nothing re-fires once the record matches.
- name: Bump pkgrel for packages whose dependencies moved - name: Bump pkgrel for packages whose dependencies moved
id: sync
run: | run: |
docker run --rm \ docker run --rm \
-e PACKAGES="$PACKAGES" \ -e PACKAGES="$PACKAGES" \
@@ -67,7 +39,7 @@ jobs:
-v "$PWD/helpers:/workspace/helpers:ro" \ -v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \ -v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \ -w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc ' archlinux:base-devel bash -lc '
set -euo pipefail set -euo pipefail
printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist
@@ -87,53 +59,38 @@ jobs:
env: env:
PACKAGES: ${{ github.event.inputs.packages }} PACKAGES: ${{ github.event.inputs.packages }}
- name: Group the bumps into PRs - name: Check for changes
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }} id: changes
id: groups
run: | run: |
groups=$(.github/scripts/package-pr-groups.sh auto/sync-rebuilds "$RUNNER_TEMP/pins.patch") if [ -z "$(git status --porcelain)" ]; then
echo "groups=$groups" >> "$GITHUB_OUTPUT" echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Hand the bumps to the PR jobs - name: Create Pull Request
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }} if: steps.changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4 uses: peter-evans/create-pull-request@v7
with: with:
name: package-pins token: ${{ secrets.GITHUB_TOKEN }}
path: ${{ runner.temp }}/pins.patch commit-message: 'chore: rebuild against updated dependencies'
retention-days: 1 title: 'chore: rebuild against updated dependencies'
body: |
Automated pkgrel bump for packages that link against a dependency
which has moved in the official repositories.
prs: Each package names those dependencies in `rebuild_on` and carries the
needs: sync versions its current pkgrel was bumped for in `rebuilt_against`. The
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }} bump is what makes the rebuilt package an upgrade pacman will offer;
permissions: without it the build produces the version already published and no
contents: read one receives it.
uses: ./.github/workflows/package-prs.yml branch: auto/sync-rebuilds
with: delete-branch: true
groups: ${{ needs.sync.outputs.groups }} labels: automated
prefix: auto/sync-rebuilds reviewers: ryanrhughes
title: Rebuild against updated dependencies
body: |
Automated pkgrel bump for a package that links against a dependency
which has moved in the official repositories.
Each package names those dependencies in `rebuild_on` and carries the
versions its current pkgrel was bumped for in `rebuilt_against`. The
bump is what makes the rebuilt package an upgrade pacman will offer;
without it the build produces the version already published and no
one receives it.
This PR auto-merges once the build checks pass. A failing rebuild
leaves it open for a maintainer without holding back any other package.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
notify:
needs: [sync, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure - name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }} if: failure() && env.BASECAMP_CHATBOT_URL != ''
env: env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }} BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: | run: |
+29 -78
View File
@@ -1,17 +1,5 @@
name: Sync Upstream Releases name: Sync Upstream Releases
# Bumps every package whose upstream release feed has moved, except the
# "auto_merge": true packages that track-branches.yml checks hourly. Each
# package gets its own PR on auto/sync-upstream-<package>, opened by
# package-prs.yml: the PR builds on the droplets, auto-merge lands it when
# `result` is green, and the merge publishes. A package that fails to build
# stays an open red PR for a maintainer without holding back any other; the
# next run updates it if upstream moves again. `min_release_age` still
# quarantines fresh releases before they are proposed.
#
# The PRs and auto-merge use the PAT in PKGS_BOT_TOKEN, so their pushes
# build without an approval hold and their merges start publish.yml.
on: on:
schedule: schedule:
# Every 6 hours, off the hour to dodge the scheduling backlog at :00 # Every 6 hours, off the hour to dodge the scheduling backlog at :00
@@ -23,30 +11,14 @@ on:
required: false required: false
default: '' default: ''
# One sync at a time: two runs racing on one auto/sync-upstream-* branch
# would each force-push their own update over the other's.
concurrency:
group: sync-upstream
cancel-in-progress: false
jobs: jobs:
sync: sync:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write
outputs: pull-requests: write
groups: ${{ steps.groups.outputs.groups }}
steps: steps:
- name: Require the bot token
env:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
run: |
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
exit 1
fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -55,84 +27,63 @@ jobs:
# Runs in an Arch container for vercmp: whether a release is an upgrade has # Runs in an Arch container for vercmp: whether a release is an upgrade has
# to be decided by the same comparator pacman will use on users' machines. # to be decided by the same comparator pacman will use on users' machines.
- name: Update packages from upstream release feeds - name: Update packages from upstream release feeds
id: sync
run: | run: |
docker run --rm \ docker run --rm \
-e PACKAGES="$PACKAGES" \ -e PACKAGES="$PACKAGES" \
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
-e HOST_UID="$(id -u)" \ -e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \ -e HOST_GID="$(id -g)" \
-v "$PWD/bin:/workspace/bin:ro" \ -v "$PWD/bin:/workspace/bin:ro" \
-v "$PWD/helpers:/workspace/helpers:ro" \ -v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \ -v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \ -w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc ' archlinux:base-devel bash -lc '
set -euo pipefail set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive pacman -Syu --noconfirm jq
groupadd -g "$HOST_GID" runner groupadd -g "$HOST_GID" runner
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
chown -R runner:runner /workspace/pkgbuilds chown -R runner:runner /workspace/pkgbuilds
# Packages marked auto_merge ride track-branches.yml instead,
# which checks them hourly.
if [[ -n "${PACKAGES:-}" ]]; then if [[ -n "${PACKAGES:-}" ]]; then
read -r -a package_args <<< "$PACKAGES" read -r -a package_args <<< "$PACKAGES"
runuser -u runner -- ./bin/sync-upstream --lane reviewed "${package_args[@]}" runuser -u runner -- ./bin/sync-upstream "${package_args[@]}"
else else
runuser -u runner -- ./bin/sync-upstream --lane reviewed runuser -u runner -- ./bin/sync-upstream
fi fi
' '
env: env:
PACKAGES: ${{ github.event.inputs.packages }} PACKAGES: ${{ github.event.inputs.packages }}
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Failed feeds leave their recipes untouched; completed updates still - name: Check for changes
# get their PRs. The failed sync step keeps the workflow red and notifies. id: changes
- name: Group the updates into PRs
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: groups
run: | run: |
groups=$(.github/scripts/package-pr-groups.sh auto/sync-upstream "$RUNNER_TEMP/pins.patch") if [ -z "$(git status --porcelain)" ]; then
echo "groups=$groups" >> "$GITHUB_OUTPUT" echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Hand the updates to the PR jobs - name: Create Pull Request
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }} if: steps.changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4 uses: peter-evans/create-pull-request@v7
with: with:
name: package-pins token: ${{ secrets.GITHUB_TOKEN }}
path: ${{ runner.temp }}/pins.patch commit-message: 'chore: sync upstream releases'
retention-days: 1 title: 'chore: sync upstream releases'
body: |
Automated update of packages that track an upstream vendor release
feed rather than the AUR.
prs: Each package reports its newest release through
needs: sync `.omarchy/upstream.sh`.
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }} branch: auto/sync-upstream
permissions: delete-branch: true
contents: read labels: automated
uses: ./.github/workflows/package-prs.yml reviewers: ryanrhughes
with:
groups: ${{ needs.sync.outputs.groups }}
prefix: auto/sync-upstream
title: Sync upstream releases
body: |
Automated update of a package that tracks an upstream vendor release
feed rather than the AUR.
Release watches and providers are declared in `.omarchy/package.json`;
exceptional feeds use `.omarchy/upstream.sh`. This PR auto-merges once
the build checks pass. A failing build leaves it open for a maintainer
without holding back any other package.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
notify:
needs: [sync, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure - name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }} if: failure() && env.BASECAMP_CHATBOT_URL != ''
env: env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }} BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: | run: |
+4 -52
View File
@@ -1,28 +1,12 @@
name: Tests name: Tests
# PR-only. Publishing on push has its own workflow and is what verifies the
# merged tree: it resolves every package against the live channel and refuses
# a filename that already exists with different bytes, so two PRs cannot land
# the same version twice. A post-merge test run would only repeat the PR's.
on: on:
pull_request: pull_request:
push:
branches: [master]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build-isolation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Prepare fixture builder
run: docker build -t omarchy-build-isolation-test -f tests/build-isolation.Dockerfile tests
- name: Verify isolated builds with real pacman transactions
env:
CONTAINER_ENGINE: docker
TEST_BUILDER_IMAGE: omarchy-build-isolation-test
run: tests/build-isolation.sh
self-tests: self-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -32,12 +16,6 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Test PR workflow approval
run: node --test tests/pr-workflow-approval.cjs
- name: Test builder images
run: node --test tests/builder-image.cjs
# An Arch container for vercmp: version ordering has to be decided by # An Arch container for vercmp: version ordering has to be decided by
# the same comparator pacman uses on users' machines. # the same comparator pacman uses on users' machines.
- name: Run self-tests - name: Run self-tests
@@ -45,36 +23,10 @@ jobs:
docker run --rm \ docker run --rm \
-v "$PWD:/workspace:ro" \ -v "$PWD:/workspace:ro" \
-w /workspace \ -w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc ' archlinux:base-devel bash -lc '
set -euo pipefail set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive neovim tmux pacman -Syu --noconfirm jq
python tests/owe-package-restart.py
python tests/oma-service-removal.py
python tests/passwordless-package-lifecycle.py
python tests/upstream-watch.py
bash tests/ipu7-install.sh
bash tests/ipu7-headers.sh
./bin/sync-upstream self-test ./bin/sync-upstream self-test
./bin/sync-rebuilds --self-test
./bin/omarchy-pkgs self-test ./bin/omarchy-pkgs self-test
./bin/omarchy-release self-test ./bin/omarchy-release self-test
./tests/neovim-remote-clipboard.sh
python tests/neovim-clipboard-tmux.py
./tests/partial-release.sh
./tests/published-build-plan.sh
./tests/settings-boot-config.sh
./tests/settings-runtime-profile.sh
./tests/passwordless-package-files.sh
./tests/pinned-sources.sh
./tests/controller.sh
./tests/artifact-helpers.sh
./tests/limine-mkinitcpio-hook.sh
./tests/voxtype-bin-install.sh
./tests/superwhisper-bin-install.sh
pacman -S --noconfirm --quiet rclone >/dev/null
./tests/publish-artifact.sh
shopt -s nullglob
for test in pkgbuilds/*/test.sh; do
bash "$test"
done
' '
-153
View File
@@ -1,153 +0,0 @@
name: Track upstream branches
# The hourly lane. Packages marked "auto_merge": true update here instead of
# in sync-upstream.yml's 6-hourly run: some follow a moving upstream branch
# (omarchy-dev and omarchy-settings-dev on quattro, omasnap-git on main)
# rather than tagged releases, so nothing in this repository changes when
# their source does; the rest are release feeds worth shipping within the
# hour (browsers, large vendors, Omacom's own projects). This workflow makes
# each new branch tip a commit pin in the recipe, which publish.yml then
# treats like any other version bump: the PR builds on the droplets,
# auto-merge lands it when `result` is green, and the merge publishes the
# artifacts. A tip that fails to build stays an unmerged red PR that the next
# tick supersedes.
#
# One PR per package, on auto/track-branches-<package>, opened by
# package-prs.yml. Packages pinned from the same upstream branch
# (omarchy-dev and omarchy-settings-dev) share one, because sync-upstream
# moves them in lockstep.
#
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN so they trigger the
# build and publish workflows. The built-in GITHUB_TOKEN cannot drive this
# unattended chain. The PAT needs Contents: write and Pull requests: write
# on this repository, and its owner must be trusted by the build workflow.
on:
schedule:
# Hourly, off the hour to dodge the scheduling backlog at :00. GitHub
# drops scheduled runs under load: the old every-2-hours entry fired
# every 6 to 9 hours in practice.
- cron: '35 * * * *'
workflow_dispatch:
inputs:
packages:
description: 'Specific packages to track (space-separated, leave empty for every auto_merge package)'
required: false
default: ''
# One tracker at a time: two runs racing on one auto/track-branches-* branch
# would each force-push their own pin over the other's.
concurrency:
group: track-branches
cancel-in-progress: false
jobs:
track:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
groups: ${{ steps.groups.outputs.groups }}
steps:
- name: Require the tracking token
env:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
run: |
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
# Same container as sync-upstream.yml: vercmp decides whether a pin is
# an upgrade with the comparator pacman uses on users' machines.
- name: Pin tracked branches to their current tips
id: sync
run: |
docker run --rm \
-e PACKAGES="$PACKAGES" \
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
-v "$PWD/bin:/workspace/bin:ro" \
-v "$PWD/helpers:/workspace/helpers:ro" \
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
-w /workspace \
ghcr.io/archlinux/archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive
groupadd -g "$HOST_GID" runner
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
chown -R runner:runner /workspace/pkgbuilds
if [[ -n "${PACKAGES:-}" ]]; then
read -r -a package_args <<< "$PACKAGES"
runuser -u runner -- ./bin/sync-upstream --lane auto-merge "${package_args[@]}"
else
runuser -u runner -- ./bin/sync-upstream --lane auto-merge
fi
'
env:
PACKAGES: ${{ github.event.inputs.packages }}
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Group the pins into PRs
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: groups
run: |
groups=$(.github/scripts/package-pr-groups.sh auto/track-branches "$RUNNER_TEMP/pins.patch")
echo "groups=$groups" >> "$GITHUB_OUTPUT"
- name: Hand the pins to the PR jobs
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
uses: actions/upload-artifact@v4
with:
name: package-pins
path: ${{ runner.temp }}/pins.patch
retention-days: 1
prs:
needs: track
if: ${{ !cancelled() && needs.track.outputs.groups != '' }}
permissions:
contents: read
uses: ./.github/workflows/package-prs.yml
with:
groups: ${{ needs.track.outputs.groups }}
prefix: auto/track-branches
title: Track upstream branches
body: |
Automated pin of a package that follows a moving upstream branch or a
trusted release feed (`"auto_merge": true` in `.omarchy/package.json`).
Its recipe now points at the newest upstream release, or the newest
branch tip whose commit timestamp is at least `min_release_age` old.
This PR auto-merges once the build checks pass. A failing build leaves
it open without holding back any other package; the next tracker run
replaces it with the newer tip.
secrets:
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
notify:
needs: [track, prs]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- name: Notify Basecamp on failure
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
env:
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
run: |
curl -s -o /dev/null \
-H "Content-Type: application/json" \
-d "$(jq -n --arg content \
"🔴 <strong>Branch tracking failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
'{content: $content}')" \
"$BASECAMP_CHATBOT_URL"
-81
View File
@@ -1,81 +0,0 @@
name: Unpublish retired packages
# Takes packages out of the channel databases after their recipes are gone
# from master. Deleting a recipe stops it building; this stops pacman
# offering it. Files stay in the bucket (see bin/unpublish-packages).
#
# Only packages with no recipe on master: one that still has a recipe would
# come back on its next publish, and refusing it keeps a typo from pulling a
# live package out of every channel.
on:
workflow_dispatch:
inputs:
packages:
description: "Space-separated pkgbases whose recipes were removed from master"
required: true
channels:
description: "Channels to remove them from"
required: true
default: "edge rc stable"
jobs:
unpublish:
runs-on: ubuntu-latest
environment: publish
timeout-minutes: 15
# The publish job's group: one writer per channel database at a time.
# queue: max, as there: a waiting job is queued, not replaced.
concurrency:
group: publish
cancel-in-progress: false
queue: max
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Refuse packages that still have a recipe
env:
PACKAGES: ${{ github.event.inputs.packages }}
CHANNELS: ${{ github.event.inputs.channels }}
run: |
set -euo pipefail
for c in $CHANNELS; do
[[ $c == edge || $c == rc || $c == stable ]] || { echo "::error::Unknown channel: $c"; exit 1; }
done
for p in $PACKAGES; do
[[ $p =~ ^[a-z0-9@._+-]+$ ]] || { echo "::error::Not a package name: $p"; exit 1; }
if [[ -e pkgbuilds/$p ]]; then
echo "::error::pkgbuilds/$p still exists on master; remove the recipe first"
exit 1
fi
done
- name: Unpublish
env:
PACKAGES: ${{ github.event.inputs.packages }}
CHANNELS: ${{ github.event.inputs.channels }}
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 }}
OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }}
run: |
set -euo pipefail
# repo-remove and bsdtar are Arch tools: run in the tested builder
# image, as the publish job does.
builder=ghcr.io/omacom/omarchy-pkg-builder:$(bin/builder-image key --arch x86_64 --mirror edge)
docker pull --quiet "$builder"
for mirror in $CHANNELS; do
for arch in x86_64 aarch64; do
docker run --rm \
-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 "$builder" \
bin/unpublish-packages --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$arch" $PACKAGES
done
done
-6
View File
@@ -1,8 +1,6 @@
.firecrawl/
src/ src/
logs/ logs/
build-output/ build-output/
cache/
pkgs.omarchy.org/ pkgs.omarchy.org/
pkgbuilds/*/.SRCINFO pkgbuilds/*/.SRCINFO
pkgbuilds/*/.gitignore pkgbuilds/*/.gitignore
@@ -37,7 +35,3 @@ pkgbuilds/yay/yay/
.srcdest/ .srcdest/
.repo-host .repo-host
.worktrees/ .worktrees/
# Python helpers and offline tests
__pycache__/
.release-verification/
+796 -427
View File
File diff suppressed because it is too large. Load diff
+20 -19
View File
@@ -6,7 +6,7 @@ source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/paths.sh"
PACKAGE="" PACKAGE=""
SOURCE="local" SOURCE="aur"
SYNC="true" SYNC="true"
RELEASE_RING="" RELEASE_RING=""
AUR_PACKAGE="" AUR_PACKAGE=""
@@ -17,14 +17,14 @@ usage() {
cat <<EOF cat <<EOF
Usage: $0 <package> [OPTIONS] Usage: $0 <package> [OPTIONS]
Create an Omarchy-owned package. --source aur imports a starting recipe once; Create pkgbuilds/<package> with Omarchy metadata. AUR packages are synced
future releases must use an upstream watch, provider, or hook. immediately after metadata is written.
Options: Options:
--source <aur|local> Initial recipe source (default: local) --source <aur|local> Package source (default: aur)
--local Shortcut for --source local --local Shortcut for --source local
--aur <name> AUR package name when different from local directory --aur <name> AUR package name when different from local directory
--no-sync Keep the imported recipe manually maintained --no-sync For AUR packages, mark sync disabled after initial setup
--fast Put package in the fast release ring --fast Put package in the fast release ring
--release-ring <ring> Release ring (currently: fast) --release-ring <ring> Release ring (currently: fast)
--scaffold For local packages, create a starter PKGBUILD --scaffold For local packages, create a starter PKGBUILD
@@ -32,9 +32,9 @@ Options:
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
$0 yay --source aur $0 yay
$0 spotify --source aur --fast $0 spotify --fast
$0 signal-desktop --source aur --no-sync $0 signal-desktop --no-sync
$0 omarchy-zsh --local --scaffold $0 omarchy-zsh --local --scaffold
EOF EOF
} }
@@ -97,10 +97,6 @@ if [[ -z "$PACKAGE" ]]; then
usage usage
exit 1 exit 1
fi fi
if [[ ! $PACKAGE =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
print_error "Invalid package name: $PACKAGE"
exit 1
fi
case "$SOURCE" in case "$SOURCE" in
aur|local) ;; aur|local) ;;
@@ -116,11 +112,6 @@ PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
OMARCHY_DIR="$PACKAGE_DIR/.omarchy" OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
METADATA_FILE="$OMARCHY_DIR/package.json" METADATA_FILE="$OMARCHY_DIR/package.json"
if [[ "$SOURCE" == aur && -f "$PACKAGE_DIR/PKGBUILD" ]]; then
print_error "Refusing to replace the maintained recipe for $PACKAGE"
exit 1
fi
if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then
print_error "Package metadata already exists: $METADATA_FILE" print_error "Package metadata already exists: $METADATA_FILE"
print_info "Use --force to overwrite it" print_info "Use --force to overwrite it"
@@ -155,8 +146,18 @@ jq -n "${jq_args[@]}" "$jq_filter" > "$METADATA_FILE"
print_success "Wrote $METADATA_FILE" print_success "Wrote $METADATA_FILE"
if [[ "$SOURCE" == "aur" ]]; then if [[ "$SOURCE" == "aur" ]]; then
"$BUILD_ROOT/bin/import-aur" "$PACKAGE" if [[ "$SYNC" == "false" ]]; then
# Temporarily sync once, then restore sync=false so future automated syncs skip it.
tmpfile=$(mktemp)
jq 'del(.sync)' "$METADATA_FILE" > "$tmpfile"
mv "$tmpfile" "$METADATA_FILE"
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
jq '. + {sync: false}' "$METADATA_FILE" > "$tmpfile"
mv "$tmpfile" "$METADATA_FILE"
print_info "AUR sync disabled for future runs"
else
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
fi
else else
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
+10 -57
View File
@@ -25,22 +25,6 @@ SKIP_PROD_CHECK=false
FAST_RING_ONLY=false FAST_RING_ONLY=false
BOOTSTRAP=false BOOTSTRAP=false
PACKAGES="" PACKAGES=""
ALL_ARCHES=false
# Kept for --arch all, which re-invokes this script per architecture with the
# other arguments unchanged (minus the --arch all pair itself).
ORIGINAL_ARGS=()
arch_option=false
for arg in "$@"; do
if [[ "$arch_option" == true ]]; then
[[ "$arg" != "all" ]] && ORIGINAL_ARGS+=("--arch" "$arg")
arch_option=false
elif [[ "$arg" == "--arch" ]]; then
arch_option=true
else
ORIGINAL_ARGS+=("$arg")
fi
done
usage() { usage() {
echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]" echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]"
@@ -52,8 +36,7 @@ usage() {
echo " or --bootstrap (one-time initial seed)" echo " or --bootstrap (one-time initial seed)"
echo "" echo ""
echo "Options:" echo "Options:"
echo " --arch <arch>|all Target architecture (x86_64 or aarch64, default: x86_64);" echo " --arch <arch> Target architecture (x86_64 or aarch64, default: x86_64)"
echo " all = every published architecture, one after another"
echo " --package <names...> Advance only the named package(s)" echo " --package <names...> Advance only the named package(s)"
echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)" echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)"
echo " --bootstrap One-time stable -> rc seed before forward-only enforcement" echo " --bootstrap One-time stable -> rc seed before forward-only enforcement"
@@ -82,14 +65,8 @@ while [[ $# -gt 0 ]]; do
shift 2 shift 2
;; ;;
--arch) --arch)
if [[ "$2" == "all" ]]; then ARCH="$2"
ALL_ARCHES=true update_arch_paths
ARCH=all
else
require_valid_arch "$2"
ARCH="$2"
update_arch_paths
fi
shift 2 shift 2
;; ;;
--package) --package)
@@ -157,31 +134,6 @@ case "$FROM->$TO" in
;; ;;
esac esac
if [[ "$ALL_ARCHES" == true ]]; then
# Hold one lock across the whole operation so a timer cannot mutate a
# channel between architectures. Check every source database before moving
# the first one; a failed sync can still require an idempotent retry, but a
# missing architecture never creates a knowingly partial advance.
if [[ "$DRY_RUN" != true ]]; then
acquire_release_lock || exit 1
fi
ARCHES=$(published_arches)
for arch in $ARCHES; do
source_db="$REPO_ROOT/$FROM/$arch/omarchy.db.tar.zst"
if [[ ! -f "$source_db" ]]; then
print_error "Source database not found: $source_db"
echo "Nothing has been published to the $FROM channel for $arch."
exit 1
fi
done
for arch in $ARCHES; do
"$0" --arch "$arch" "${ORIGINAL_ARGS[@]}" || exit $?
done
exit 0
fi
SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH" SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH"
TARGET_DIR="$REPO_ROOT/$TO/$ARCH" TARGET_DIR="$REPO_ROOT/$TO/$ARCH"
SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst" SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst"
@@ -326,7 +278,7 @@ while IFS=$'\t' read -r name base filename; do
done < <(read_manifest) done < <(read_manifest)
echo "" echo ""
print_info "Copied: $COPIED | Already present: $PRESENT | Differing (kept): ${#DIFFERING[@]} | Not eligible: $SKIPPED" print_info "Copied: $COPIED | Already present: $PRESENT | Not eligible: $SKIPPED"
if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then
print_error "${#MISSING_FILES[@]} package(s) named in the $FROM database are missing on disk:" print_error "${#MISSING_FILES[@]} package(s) named in the $FROM database are missing on disk:"
@@ -345,12 +297,13 @@ if [[ ${#MISSING_SIGS[@]} -gt 0 ]]; then
fi fi
if [[ ${#DIFFERING[@]} -gt 0 ]]; then if [[ ${#DIFFERING[@]} -gt 0 ]]; then
print_warning "${#DIFFERING[@]} file(s) already published in $TO with different bytes — kept as-is:" print_error "${#DIFFERING[@]} file(s) already published in $TO with DIFFERENT bytes:"
printf ' %s\n' "${DIFFERING[@]}" printf ' %s\n' "${DIFFERING[@]}"
echo "Published filenames are never rewritten, so the $TO copy stays authoritative." echo "Published filenames are never rewritten, and two artifacts fighting over"
echo "A same-name collision means this version reached $TO through another lineage" echo "one name means something built twice from different inputs. Resolve it"
echo "(native build, bootstrap seed, or a same-version rebuild) and the package has" echo "deliberately: bump pkgrel and rebuild so the new artifact gets a new"
echo "not moved in $FROM since; it advances under a new filename when it does." echo "filename, or remove the source copy if the destination is correct."
exit 1
fi fi
if [[ "$DRY_RUN" == true ]]; then if [[ "$DRY_RUN" == true ]]; then
+56 -103
View File
@@ -1,10 +1,6 @@
#!/bin/bash #!/bin/bash
# Run the release workflow for a channel when work is queued. # Run the release workflow for a channel when work is queued.
# Usage: auto-release <edge|rc|stable> [<arch>|all] # Usage: auto-release <edge|rc|stable>
#
# With no architecture (what the timers pass), every published architecture
# is processed in turn, each against its own queue and its own backoff, so a
# failing aarch64 build never holds up x86_64 or the other way round.
# #
# Safe to run on a tight schedule. Three guards make that true: # Safe to run on a tight schedule. Three guards make that true:
# #
@@ -25,7 +21,7 @@ source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/lock-helpers.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh"
MIRROR="${1:-}" MIRROR="${1:-}"
ARCH_ARG="${2:-all}" STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
# Backoff schedule: 10m, 20m, 40m, 80m, 160m, 320m, then hourly-ish forever # Backoff schedule: 10m, 20m, 40m, 80m, 160m, 320m, then hourly-ish forever
# (capped at 6h, the cadence this system ran at before frequent timers). # (capped at 6h, the cadence this system ran at before frequent timers).
@@ -33,9 +29,8 @@ BACKOFF_BASE_SECONDS="${OMARCHY_BACKOFF_BASE:-600}"
BACKOFF_MAX_SECONDS="${OMARCHY_BACKOFF_MAX:-21600}" BACKOFF_MAX_SECONDS="${OMARCHY_BACKOFF_MAX:-21600}"
if [[ -z "$MIRROR" ]]; then if [[ -z "$MIRROR" ]]; then
print_error "Usage: $0 <mirror> [<arch>|all]" print_error "Usage: $0 <mirror>"
echo " mirror: edge, rc, or stable" echo " mirror: edge, rc, or stable"
echo " arch: one of $VALID_ARCHES, or all (default) for every published architecture"
exit 1 exit 1
fi fi
@@ -44,13 +39,17 @@ if [[ "$MIRROR" != "edge" && "$MIRROR" != "rc" && "$MIRROR" != "stable" ]]; then
exit 1 exit 1
fi fi
if [[ "$ARCH_ARG" == "all" ]]; then STATE_FILE="$STATE_DIR/.sync-needed-$MIRROR"
ARCHES=$(published_arches) FAIL_FILE="$STATE_DIR/.build-failed-$MIRROR"
else
require_valid_arch "$ARCH_ARG" # Nothing queued: stay quiet. At a 5-minute cadence this is most invocations,
ARCHES="$ARCH_ARG" # and a header for each would bury the runs that matter in the journal.
if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi fi
print_header "Processing Sync for $MIRROR"
# The build inputs are this checkout's contents; its HEAD identifies them. # The build inputs are this checkout's contents; its HEAD identifies them.
current_fingerprint() { current_fingerprint() {
git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown" git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown"
@@ -66,103 +65,57 @@ backoff_seconds() {
echo "$delay" echo "$delay"
} }
# Returns 0 when this architecture's queue was processed (or was empty), 1 when FAIL_COUNT=0
# the release failed. Backoff and "someone else holds the lock" are not if [[ -f "$FAIL_FILE" ]]; then
# failures: they are this tick deciding to do nothing. # shellcheck disable=SC1090
release_arch() { source "$FAIL_FILE" 2>/dev/null || true
local arch="$1" FAIL_COUNT="${FAILURE_COUNT:-0}"
local state_file fail_file failed_at="${FAILURE_AT:-0}"
state_file=$(sync_queue_file "$MIRROR" "$arch") failed_fingerprint="${FAILURE_FINGERPRINT:-}"
fail_file=$(sync_fail_file "$MIRROR" "$arch")
# A queue written under the pre-architecture name belongs to x86_64. if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then
if [[ "$arch" == "x86_64" && ! -f "$state_file" && -f "$(legacy_sync_queue_file "$MIRROR")" ]]; then print_info "Repository changed since the last failure — clearing backoff and retrying"
state_file=$(legacy_sync_queue_file "$MIRROR") rm -f "$FAIL_FILE"
fi FAIL_COUNT=0
if [[ "$arch" == "x86_64" && ! -f "$fail_file" && -f "$(legacy_sync_fail_file "$MIRROR")" ]]; then else
fail_file=$(legacy_sync_fail_file "$MIRROR") delay=$(backoff_seconds "$FAIL_COUNT")
fi now=$(date +%s)
retry_at=$((failed_at + delay))
# Nothing queued: stay quiet. At a 5-minute cadence this is most if ((now < retry_at)); then
# invocations, and a header for each would bury the runs that matter in print_warning "$MIRROR has failed $FAIL_COUNT time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
# the journal. echo " Push a fix (any new commit clears this), or: rm $FAIL_FILE"
[[ -f "$state_file" ]] || return 0 exit 0
print_header "Processing Sync for $MIRROR ($arch)"
local fail_count=0 failed_at failed_fingerprint delay now retry_at
if [[ -f "$fail_file" ]]; then
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
# shellcheck disable=SC1090
source "$fail_file" 2>/dev/null || true
fail_count="${FAILURE_COUNT:-0}"
failed_at="${FAILURE_AT:-0}"
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then
print_info "Repository changed since the last failure — clearing backoff and retrying"
rm -f "$fail_file"
fail_count=0
else
delay=$(backoff_seconds "$fail_count")
now=$(date +%s)
retry_at=$((failed_at + delay))
if ((now < retry_at)); then
print_warning "$MIRROR ($arch) has failed $fail_count time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
echo " Push a fix (any new commit clears this), or: rm $fail_file"
return 0
fi
print_info "Backoff elapsed — retrying $MIRROR ($arch) (failure #$((fail_count + 1)) if this fails)"
fi fi
print_info "Backoff elapsed — retrying $MIRROR (failure #$((FAIL_COUNT + 1)) if this fails)"
fi fi
fi
# Non-blocking: a build in progress means this tick has nothing to do. The # Non-blocking: a build in progress means this tick has nothing to do.
# lock is reentrant, so once this run holds it the remaining architectures if ! try_release_lock; then
# run under the same acquisition. holder=$(release_lock_holder)
if ! try_release_lock; then print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
local holder exit 0
holder=$(release_lock_holder) fi
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
return 0
fi
print_info "State file found: $state_file" print_info "State file found: $STATE_FILE"
print_info "Starting release workflow for $MIRROR ($arch)..." print_info "Starting release workflow for $MIRROR..."
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --arch "$arch" --skip-prod-check; then if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --skip-prod-check; then
print_success "Release completed successfully for $MIRROR ($arch)" print_success "Release completed successfully for $MIRROR"
local completed_state=("$state_file" "$fail_file") rm -f "$STATE_FILE"
if [[ "$arch" == "x86_64" ]]; then rm -f "$FAIL_FILE"
completed_state+=("$(legacy_sync_queue_file "$MIRROR")" "$(legacy_sync_fail_file "$MIRROR")") print_success "State file removed: $STATE_FILE"
fi else
if ! rm -f "${completed_state[@]}"; then status=$?
print_error "Release succeeded, but its queue state could not be cleared" FAIL_COUNT=$((FAIL_COUNT + 1))
return 1 cat >"$FAIL_FILE" <<EOF
fi FAILURE_COUNT=$FAIL_COUNT
print_success "State file removed: $state_file"
return 0
fi
fail_count=$((fail_count + 1))
if ! cat >"$fail_file" <<EOF
FAILURE_COUNT=$fail_count
FAILURE_AT=$(date +%s) FAILURE_AT=$(date +%s)
FAILURE_FINGERPRINT=$(current_fingerprint) FAILURE_FINGERPRINT=$(current_fingerprint)
EOF EOF
then next=$(backoff_seconds "$FAIL_COUNT")
print_error "Could not record failure state: $fail_file" print_error "Release failed for $MIRROR (attempt $FAIL_COUNT)"
return 1 print_warning "State file retained for retry: $STATE_FILE"
fi
local next
next=$(backoff_seconds "$fail_count")
print_error "Release failed for $MIRROR ($arch) (attempt $fail_count)"
print_warning "State file retained for retry: $state_file"
print_warning "Backing off $((next / 60))m before the next attempt; a new commit retries sooner" print_warning "Backing off $((next / 60))m before the next attempt; a new commit retries sooner"
return 1 exit "$status"
} fi
status=0
for arch in $ARCHES; do
release_arch "$arch" || status=1
done
exit "$status"
+29 -218
View File
@@ -13,27 +13,6 @@ print_header "Omarchy Package Builder"
DRY_RUN=false DRY_RUN=false
# Knobs for builds driven from CI or resumed by hand. Each defaults to the
# historical behaviour, so an unadorned `bin/build` is unchanged.
#
# OMARCHY_KEEP_BUILD_WORKSPACE=1 keep build-output/$MIRROR/$ARCH instead of
# wiping it, so packages built by an earlier
# job (or a previous, interrupted run) seed
# the build database and resolve as
# dependencies of what builds now.
# OMARCHY_SKIP_BUILDER_IMAGE=1 use the omarchy-pkg-builder image already
# present instead of building it; a workflow
# that builds the image once with an external
# BuildKit cache can then fan out over many
# package jobs without each one rebuilding it.
# OMARCHY_DEFER_RUNTIME_DEPS=true build the omarchy/omarchy-settings pair
# with --nodeps (see build/build.sh); only
# for a pipeline that verifies the install
# transaction afterwards.
KEEP_BUILD_WORKSPACE=${OMARCHY_KEEP_BUILD_WORKSPACE:-0}
SKIP_BUILDER_IMAGE=${OMARCHY_SKIP_BUILDER_IMAGE:-0}
DEFER_RUNTIME_DEPS=${OMARCHY_DEFER_RUNTIME_DEPS:-false}
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
@@ -86,14 +65,7 @@ while [[ $# -gt 0 ]]; do
echo " $0 --arch aarch64" echo " $0 --arch aarch64"
echo " $0 --mirror stable" echo " $0 --mirror stable"
echo " $0 --package yay" echo " $0 --package yay"
echo " $0 --package yay walker cursor-bin" echo " $0 --package yay elephant cursor-bin"
echo ""
echo "Environment (for CI and resumed builds; defaults keep today's behaviour):"
echo " OMARCHY_KEEP_BUILD_WORKSPACE=1 keep build-output and reuse packages already there"
echo " OMARCHY_SKIP_BUILDER_IMAGE=1 use the existing builder image instead of building it"
echo " OMARCHY_DEFER_RUNTIME_DEPS=true build the omarchy pair with --nodeps (transaction verified later)"
echo " OMARCHY_PUBLISHED_REPO_URL=<url> channel to plan and resolve against when no local tree exists"
echo " (default https://pkgs.omarchy.org; empty disables the fallback)"
echo "" echo ""
exit 0 exit 0
;; ;;
@@ -104,55 +76,18 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
require_valid_arch "$ARCH"
if [[ $DEFER_RUNTIME_DEPS != "false" && $DEFER_RUNTIME_DEPS != "true" ]]; then
print_error "OMARCHY_DEFER_RUNTIME_DEPS must be true or false"
exit 1
fi
# Deferring runtime dependencies is only sound for the omarchy pair, and only
# when both halves are built together: the pair depends on each other and on
# packages that a sharded pipeline builds in other jobs, and the consumer of
# this mode installs the assembled set in one verified transaction. Check the
# request here so a misuse fails before Docker starts.
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
deferred_runtime=0
deferred_settings=0
deferred_count=0
for package in $PACKAGES; do
((deferred_count += 1))
case $package in
omarchy|omarchy-dev) deferred_runtime=1 ;;
omarchy-settings|omarchy-settings-dev) deferred_settings=1 ;;
*)
print_error "OMARCHY_DEFER_RUNTIME_DEPS only applies to the omarchy/omarchy-settings pair, not $package"
exit 1
;;
esac
done
if (( deferred_runtime != 1 || deferred_settings != 1 || deferred_count != 2 )); then
print_error "OMARCHY_DEFER_RUNTIME_DEPS requires --package with exactly the omarchy pair"
exit 1
fi
fi
# Show target architecture and mirror after parsing args # Show target architecture and mirror after parsing args
print_info "Target architecture: $ARCH" print_info "Target architecture: $ARCH"
print_info "Mirror: $MIRROR" print_info "Mirror: $MIRROR"
print_info "Build workspace: $BUILD_OUTPUT_DIR" print_info "Build workspace: $BUILD_OUTPUT_DIR"
print_info "Final output: $REPO_DIR" print_info "Final output: $REPO_DIR"
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
print_info "Runtime dependency checks: deferred to the install transaction"
fi
if [[ "$DRY_RUN" == true ]]; then if [[ "$DRY_RUN" == true ]]; then
print_warning "DRY RUN MODE - build plan only; no container or makepkg will run" print_warning "DRY RUN MODE - build plan only; no Docker or makepkg will run"
ARCH="$ARCH" \ ARCH="$ARCH" \
MIRROR="$MIRROR" \ MIRROR="$MIRROR" \
PACKAGES="$PACKAGES" \ PACKAGES="$PACKAGES" \
DRY_RUN=true \ DRY_RUN=true \
DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS" \
PKGBUILDS_DIR="$PKGBUILDS_DIR" \ PKGBUILDS_DIR="$PKGBUILDS_DIR" \
BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \ BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
FINAL_OUTPUT_DIR="$REPO_DIR" \ FINAL_OUTPUT_DIR="$REPO_DIR" \
@@ -165,45 +100,21 @@ fi
# Create directories if they don't exist # Create directories if they don't exist
mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR" mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
# Check the selected container engine is available # Check Docker is available
check_engine check_docker
# A foreign target architecture runs under QEMU user emulation. Probe by # Setup QEMU for aarch64 builds on x86_64 hosts
# actually running a container for the target platform: that is the only if [[ "$(uname -m)" == "x86_64" && "$ARCH" == "aarch64" ]]; then
# test that covers both "binfmt not registered" and "registered but broken". # Check if QEMU is already working
HOST_ARCH=$(uname -m) if ! docker run --rm --platform linux/arm64 alpine:3.21 /bin/true >/dev/null 2>&1; then
[[ "$HOST_ARCH" == "arm64" ]] && HOST_ARCH=aarch64 print_info "Setting up QEMU for ARM64 emulation..."
if [[ "$HOST_ARCH" != "$ARCH" ]]; then setup_qemu
PROBE_IMAGE="alpine:3.21"
[[ "$CONTAINER_ENGINE" == "podman" ]] && PROBE_IMAGE="docker.io/library/alpine:3.21"
# Rootless Podman cannot repair host binfmt state itself. Validate the flags
# before the basic probe, because an F-only registration can start an ARM
# container but silently breaks sudo inside it.
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
setup_qemu "$ARCH"
fi
if ! "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$ARCH")" "$PROBE_IMAGE" /bin/true >/dev/null 2>&1; then
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
print_error "QEMU $ARCH is registered, but the container probe failed"
print_info "Refresh the registration with: sudo systemctl restart systemd-binfmt"
exit 1
else
print_info "Setting up QEMU for $ARCH emulation on this $HOST_ARCH host..."
setup_qemu "$ARCH"
fi
fi fi
fi fi
# Clean build-output directory to start fresh, unless the caller seeded it # Clean build-output directory to start fresh
# with packages from an earlier job or is resuming an interrupted run. print_info "Cleaning build workspace..."
if [[ $KEEP_BUILD_WORKSPACE == "1" ]]; then rm -rf "$BUILD_OUTPUT_DIR"/*
print_info "Keeping existing build workspace..."
else
print_info "Cleaning build workspace..."
rm -rf "${BUILD_OUTPUT_DIR:?}"/*
fi
mkdir -p "$BUILD_OUTPUT_DIR" mkdir -p "$BUILD_OUTPUT_DIR"
# Show package info # Show package info
@@ -213,43 +124,18 @@ else
print_info "Building unscoped packages for $MIRROR mirror" print_info "Building unscoped packages for $MIRROR mirror"
fi fi
# Build/update the Docker image, unless the caller prepared the exact image # Build/update the Docker image
# already (a workflow building it once with an external BuildKit cache). A build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
# missing image is an error rather than a silent rebuild: the point of the
# flag is that every job runs the same bytes.
IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
if [[ $SKIP_BUILDER_IMAGE == "1" ]]; then
if ! "$CONTAINER_ENGINE" image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
print_error "Prepared builder image is unavailable: $IMAGE_TAG"
exit 1
fi
print_info "Using prepared builder image: $IMAGE_TAG"
else
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
fi
print_info "Planning isolated package builds..." print_info "Running package build..."
# Create output directories if they don't exist # Create output directories if they don't exist
mkdir -p "$BUILD_OUTPUT_DIR" mkdir -p "$BUILD_OUTPUT_DIR"
mkdir -p "$REPO_DIR" mkdir -p "$REPO_DIR"
# Share downloaded archives, never /var/lib/pacman or an installed root. # Ensure output directories are writable by container user
# Channel/architecture separation preserves each mirror's dependency set. make_dir_writable "$BUILD_OUTPUT_DIR"
PACKAGE_CACHE_DIR="$BUILD_ROOT/cache/pacman/$MIRROR/$ARCH" make_dir_writable "$REPO_DIR"
mkdir -p "$PACKAGE_CACHE_DIR"
PLAN_DIR=$(mktemp -d "$SRC_DIR/build-plan.XXXXXX")
trap 'rm -rf "$PLAN_DIR"' EXIT
# Keep manifest directories host-owned so cleanup also works when Docker's
# builder uid differs from the caller (as on GitHub runners).
mkdir -p "$PLAN_DIR/artifacts"
# Rootful Docker writes as the image uid, so retain its existing permission
# workaround. Rootless Podman uses keep-id and must leave ownership/modes alone.
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
make_dir_writable "$BUILD_OUTPUT_DIR"
make_dir_writable "$PLAN_DIR"
fi
# Build Docker arguments # Build Docker arguments
DOCKER_ARGS=( DOCKER_ARGS=(
@@ -258,101 +144,26 @@ DOCKER_ARGS=(
-e MIRROR="$MIRROR" -e MIRROR="$MIRROR"
-e PACKAGES="$PACKAGES" -e PACKAGES="$PACKAGES"
-e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}" -e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}"
-e OMARCHY_PUBLISHED_REPO_URL="${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}"
-e DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS"
-e BUILD_PLAN_DIR=/build-plan
-v "$PLAN_DIR:/build-plan"
-v "$PACKAGE_CACHE_DIR:/var/cache/pacman/pkg"
-v "$BUILD_ROOT/build-output:/build-output" -v "$BUILD_ROOT/build-output:/build-output"
-v "$REPO_ROOT:/pkgs.omarchy.org:ro" -v "$REPO_ROOT:/pkgs.omarchy.org"
-v "$BUILD_DIR:/build:ro" -v "$BUILD_DIR:/build:ro"
-v "$BUILD_ROOT/helpers:/helpers:ro" -v "$BUILD_ROOT/helpers:/helpers:ro"
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro" -v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
) )
# Podman-created images can leave WORKDIR owned by a remapped uid. Mount the # Run the builder with assembled args
# existing host-user-owned workspace so the builder can write there. IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
DOCKER_ARGS+=(-v "$SRC_DIR:/src")
fi
# Plan once against the published database, then keep that order throughout
# the run. Each package sees the staged artifacts but starts with a fresh
# pacman database and root filesystem, even after a failed build.
PLATFORM_ARG=$(get_platform_arg "$ARCH") PLATFORM_ARG=$(get_platform_arg "$ARCH")
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \ docker run $PLATFORM_ARG "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh
-e DRY_RUN=true "$IMAGE_TAG" /build/build.sh
mapfile -t ORDERED_PACKAGES < "$PLAN_DIR/packages" BUILD_RESULT=$?
mapfile -t SKIPPED_PACKAGES < "$PLAN_DIR/skipped"
SUCCESSFUL_PACKAGES=()
FAILED_PACKAGES=()
BLOCKED_PACKAGES=()
declare -A BUILD_STATUS=()
for package in "${ORDERED_PACKAGES[@]}"; do
blocked_by=""
while read -r consumer dependency; do
if [[ "$consumer" == "$package" && "${BUILD_STATUS[$dependency]:-}" != success ]]; then
blocked_by="$dependency"
break
fi
done < "$PLAN_DIR/dependencies"
if [[ -n "$blocked_by" ]]; then
print_warning "$package blocked by unsuccessful dependency: $blocked_by"
BUILD_STATUS[$package]=blocked
BLOCKED_PACKAGES+=("$package")
continue
fi
print_info "Building $package in a fresh container..."
if "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \
-e BUILD_PACKAGE="$package" "$IMAGE_TAG" /build/build.sh; then
BUILD_STATUS[$package]=success
SUCCESSFUL_PACKAGES+=("$package")
else
BUILD_STATUS[$package]=failed
FAILED_PACKAGES+=("$package")
fi
done
# Summary
echo "" echo ""
print_header "Build Summary" if [[ $BUILD_RESULT -eq 0 ]]; then
echo " Total packages: ${#ORDERED_PACKAGES[@]}" print_success "Build completed successfully!"
echo " Built: ${#SUCCESSFUL_PACKAGES[@]}" else
echo " Skipped: ${#SKIPPED_PACKAGES[@]} (up-to-date or excluded)"
echo " Failed: ${#FAILED_PACKAGES[@]}"
echo " Blocked: ${#BLOCKED_PACKAGES[@]}"
# The release caller supplies a fresh directory. A completed result
# distinguishes package failures from an interrupted/failed orchestrator;
# only artifacts belonging to fully successful builds may be published.
if [[ -n "${OMARCHY_BUILD_RESULT_DIR:-}" ]]; then
mkdir -p "$OMARCHY_BUILD_RESULT_DIR"
: > "$OMARCHY_BUILD_RESULT_DIR/artifacts"
for package in "${SUCCESSFUL_PACKAGES[@]}"; do
cat "$PLAN_DIR/artifacts/$package" >> "$OMARCHY_BUILD_RESULT_DIR/artifacts"
done
printf '%s\n' "${FAILED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/failed"
printf '%s\n' "${BLOCKED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/blocked"
touch "$OMARCHY_BUILD_RESULT_DIR/complete"
fi
if (( ${#FAILED_PACKAGES[@]} + ${#BLOCKED_PACKAGES[@]} )); then
if (( ${#FAILED_PACKAGES[@]} )); then
echo "Failed packages:"
printf ' - %s\n' "${FAILED_PACKAGES[@]}"
fi
if (( ${#BLOCKED_PACKAGES[@]} )); then
echo "Packages blocked by failed dependencies:"
printf ' - %s\n' "${BLOCKED_PACKAGES[@]}"
fi
print_warning "Some packages failed (see details above)" print_warning "Some packages failed (see details above)"
# Reserved for a completed run with unsuccessful packages. Other failures exit $BUILD_RESULT
# must not let release publish arbitrary files left in the workspace.
exit 2
fi fi
print_success "Build completed successfully!"
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
# Print the PR build matrix for a set of package directories as JSON: one
# entry per package per supported architecture. Every package builds exactly
# once, against edge, and that one artifact is what every channel ships:
# channels are databases over a shared pool of files, and a filename must
# mean one set of bytes. "channels" lists where the artifact is published on
# merge: edge for everything, plus rc and stable immediately for the fast
# ring. Eligibility comes from package_builds_for_mirror, the rule the
# release host uses, so CI and the host cannot disagree.
#
# Usage: build-matrix [--arch <arch>|all] <package>...
# Reads package names on stdin when none are given. With no --arch, every
# architecture in CI_ARCHES (default "x86_64 aarch64") the package supports.
# Output: {"include":[{"package":"x","arch":"x86_64","channels":"edge rc stable","publish_arches":"x86_64"},...]}
# arch is where it builds; publish_arches lists every architecture
# database the file goes into (all of them for arch=any).
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/package-metadata.sh"
ARCHES=$CI_ARCHES
if [[ "${1:-}" == --arch ]]; then [[ $2 == all ]] || ARCHES=$2; shift 2; fi
for a in $ARCHES; do require_valid_arch "$a"; done
if (( $# )); then names=("$@"); else mapfile -t names; fi
entries=()
for name in "${names[@]}"; do
[[ -n "$name" ]] || continue
pkgdir="$PKGBUILDS_DIR/$name"
[[ -d "$pkgdir" ]] || continue
# skip_build packages still build on their own PR (explicit --package
# semantics); the host's unscoped runs are what skip them.
channels=""
for mirror in $VALID_MIRRORS; do
package_builds_for_mirror "$pkgdir" "$mirror" && channels="$channels $mirror"
done
channels=${channels# }
[[ -n "$channels" ]] || continue
# An arch=any package produces one architecture-independent file, so it
# builds once, on the first architecture, and that file serves every
# channel database of every architecture.
if [[ " $(package_arches "$pkgdir" "${ARCHES%% *}") " == *" any "* ]]; then
entries+=("$(jq -nc --arg p "$name" --arg a "${ARCHES%% *}" --arg c "$channels" --arg pa "$ARCHES" '{package:$p, arch:$a, channels:$c, publish_arches:$pa}')")
continue
fi
for arch in $ARCHES; do
package_supports_arch "$pkgdir" "$arch" || continue
entries+=("$(jq -nc --arg p "$name" --arg a "$arch" --arg c "$channels" '{package:$p, arch:$a, channels:$c, publish_arches:$a}')")
done
done
printf '%s\n' "${entries[@]}" | jq -sc '{include: .}'
-66
View File
@@ -1,66 +0,0 @@
#!/bin/bash
# Build a reusable package environment from this checkout's own inputs.
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/paths.sh"
usage() {
echo "Usage: bin/builder-image {key|build} [--arch x86_64|aarch64] [--mirror edge|rc|stable] [--tag IMAGE] [--fresh]"
}
command=${1:-}
[[ $# -eq 0 ]] || shift
tag=""
fresh=false
while (( $# )); do
case "$1" in
--arch) ARCH=${2:?Missing architecture}; shift 2 ;;
--mirror) MIRROR=${2:?Missing mirror}; shift 2 ;;
--tag) tag=${2:?Missing image tag}; shift 2 ;;
--fresh) fresh=true; shift ;;
*) usage >&2; exit 1 ;;
esac
done
require_valid_arch "$ARCH"
validate_mirror "$MIRROR" || { echo "Invalid mirror: $MIRROR" >&2; exit 1; }
case "$command" in key|build) ;; *) usage >&2; exit 1 ;; esac
if [[ $command == key && ( -n $tag || $fresh == true ) ]]; then
usage >&2
exit 1
fi
# Include the whole build context, conservatively including mounted build
# scripts too. Normalize timestamps and ownership so fresh checkouts agree;
# retain file contents, names, executable bits and symlink targets. Bump v1
# if the image build invocation or this compatibility contract changes.
hash=$(tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner \
--format=gnu -cf - -C "$BUILD_DIR" . | sha256sum | cut -d' ' -f1)
key="v1-$ARCH-$MIRROR-$hash"
if [[ $command == key ]]; then
echo "$key"
exit 0
fi
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
check_engine
platform=$(get_platform_arg "$ARCH")
tag=${tag:-omarchy-pkg-builder:latest-$ARCH-$MIRROR}
revision=$(git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)
args=("$platform" --build-arg "MIRROR=$MIRROR"
--label "org.omarchy.builder.key=$key"
--label "org.opencontainers.image.source=https://github.com/omacom/omarchy-pkgs"
--label "org.opencontainers.image.revision=$revision"
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--tag "$tag" --file "$BUILD_DIR/Dockerfile")
if [[ $fresh == true ]]; then
# A daily build must refresh Arch even when its Dockerfile has not changed.
args+=(--no-cache)
if [[ $CONTAINER_ENGINE == docker ]]; then args+=(--pull); else args+=(--pull=always); fi
fi
if [[ $CONTAINER_ENGINE == docker ]]; then
docker buildx build --load "${args[@]}" "$BUILD_DIR"
else
podman build "${args[@]}" "$BUILD_DIR"
fi
+22 -63
View File
@@ -1,10 +1,6 @@
#!/bin/bash #!/bin/bash
# Check PKGBUILD versions against published repo versions # Check PKGBUILD versions against published repo versions
# Creates a state file per channel and architecture when packages need building # Creates state files for edge and/or stable if any packages need building
#
# Usage: check-versions [--pull] [--arch <arch>]
# --pull pull the repository first (the scheduled run does this)
# --arch <arch> check one architecture; default: every published one
set -e set -e
@@ -14,27 +10,11 @@ source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/package-metadata.sh" source "$BUILD_ROOT/helpers/package-metadata.sh"
source "$BUILD_ROOT/helpers/lock-helpers.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh"
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
ARCH="${ARCH:-x86_64}"
PULL=false PULL=false
ARCHES=""
while [[ $# -gt 0 ]]; do [[ "${1:-}" == "--pull" ]] && PULL=true
case $1 in
--pull)
PULL=true
shift
;;
--arch)
require_valid_arch "$2"
ARCHES="$2"
shift 2
;;
*)
print_error "Unknown option: $1"
exit 1
;;
esac
done
[[ -n "$ARCHES" ]] || ARCHES=$(published_arches)
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
@@ -79,26 +59,10 @@ get_repo_version() {
' '
} }
# The PKGBUILD's full version (epoch:pkgver-pkgrel) for the architecture being
# checked. Prints nothing when pkgver or pkgrel cannot be read: a partial
# version like "-" would compare unequal to everything published and queue a
# rebuild on every tick, so an unreadable PKGBUILD must not queue at all.
get_pkgbuild_version() { get_pkgbuild_version() {
local pkgdir="$1" local pkgdir="$1"
local epoch pkgver pkgrel if [[ -f "$pkgdir/PKGBUILD" ]]; then
(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi')
[[ -f "$pkgdir/PKGBUILD" ]] || return 1
epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH")
pkgver=$(package_pkgbuild_var "$pkgdir" pkgver "$ARCH")
pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH")
[[ -n "$pkgver" && -n "$pkgrel" ]] || return 1
if [[ -n "$epoch" ]]; then
echo "${epoch}:${pkgver}-${pkgrel}"
else
echo "${pkgver}-${pkgrel}"
fi fi
} }
@@ -113,8 +77,8 @@ check_vcs_unchanged() {
# If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged. # If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged.
local pkgbuild_epoch pkgbuild_pkgrel local pkgbuild_epoch pkgbuild_pkgrel
pkgbuild_epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH") pkgbuild_epoch=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${epoch:-}"')
pkgbuild_pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH") pkgbuild_pkgrel=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel}"')
local repo_pkgrel="${repo_version##*-}" local repo_pkgrel="${repo_version##*-}"
local repo_no_pkgrel="${repo_version%-*}" local repo_no_pkgrel="${repo_version%-*}"
@@ -142,11 +106,12 @@ check_package() {
local mirror="$3" local mirror="$3"
local pkgbuild_version repo_version local pkgbuild_version repo_version
if ! pkgbuild_version=$(get_pkgbuild_version "$pkgdir") || [[ -z "$pkgbuild_version" ]]; then pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
print_warning "$pkg: could not read pkgver/pkgrel from its PKGBUILD for $ARCH — not queueing" repo_version=$(get_repo_version "$pkg" "$mirror")
if [[ -z "$pkgbuild_version" ]]; then
return 1 return 1
fi fi
repo_version=$(get_repo_version "$pkg" "$mirror")
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; then if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; then
if [[ -n "$repo_version" && -n "$(package_extract_vcs_hash_from_version "$repo_version")" ]]; then if [[ -n "$repo_version" && -n "$(package_extract_vcs_hash_from_version "$repo_version")" ]]; then
@@ -172,8 +137,8 @@ check_package() {
# it because that exact filename is already published with different bytes. # it because that exact filename is already published with different bytes.
# If the artifact for this version already exists in the channel, there is # If the artifact for this version already exists in the channel, there is
# nothing to build regardless of which direction the versions differ. # nothing to build regardless of which direction the versions differ.
if package_version_is_published "$REPO_ROOT/$mirror/$ARCH" "$pkg" "$pkgbuild_version" "$ARCH"; then if compgen -G "$REPO_ROOT/$mirror/$ARCH/${pkg}-${pkgbuild_version}-*.pkg.tar."[!s]* >/dev/null 2>&1; then
print_warning "$pkg $pkgbuild_version is already published; not queueing" print_warning "$pkg $pkgbuild_version is already published — this checkout is behind the channel; not queueing"
return 1 return 1
fi fi
@@ -182,12 +147,11 @@ check_package() {
check_mirror() { check_mirror() {
local mirror="$1" local mirror="$1"
local state_file local state_file="$STATE_DIR/.sync-needed-$mirror"
state_file=$(sync_queue_file "$mirror" "$ARCH")
local needs_build=false local needs_build=false
local packages=() local packages=()
print_info "Checking $mirror packages for $ARCH..." print_info "Checking $mirror packages..."
while IFS= read -r pkg; do while IFS= read -r pkg; do
local pkgdir="$PKGBUILDS_DIR/$pkg" local pkgdir="$PKGBUILDS_DIR/$pkg"
@@ -195,7 +159,7 @@ check_mirror() {
needs_build=true needs_build=true
packages+=("$pkg") packages+=("$pkg")
fi fi
done < <(packages_for_unscoped_build "$mirror" "$ARCH") done < <(packages_for_unscoped_build "$mirror")
echo "" echo ""
@@ -204,23 +168,18 @@ check_mirror() {
# this to name the packages in its start report, which is the difference # this to name the packages in its start report, which is the difference
# between "a build is running" and "your package is in this build". # between "a build is running" and "your package is in this build".
printf '%s\n' "${packages[@]}" >"$state_file" printf '%s\n' "${packages[@]}" >"$state_file"
print_success "${mirror^} ($ARCH) needs building (${#packages[@]} packages)" print_success "${mirror^} needs building (${#packages[@]} packages)"
print_info "Packages: ${packages[*]}" print_info "Packages: ${packages[*]}"
print_info "State file created: $state_file" print_info "State file created: $state_file"
else else
print_info "${mirror^} ($ARCH) is up to date" print_info "${mirror^} is up to date"
fi fi
echo "" echo ""
} }
# One pass per published architecture: the version comparison reads that check_mirror edge
# architecture's channel databases, and each queue is its own file. check_mirror rc
for ARCH in $ARCHES; do check_mirror stable
update_arch_paths
check_mirror edge
check_mirror rc
check_mirror stable
done
print_success "Version check complete!" print_success "Version check complete!"
+7 -13
View File
@@ -1,27 +1,21 @@
#!/bin/bash #!/bin/bash
# Clean builder images and cache for the selected container engine # Clean Docker builder images and cache
# Forces a fresh image build on next run # Forces a fresh image build on next run
set -e set -e
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
check_engine print_header "Cleaning Docker Builder Images"
print_header "Cleaning Container Builder Images"
# Remove all omarchy-pkg-builder images # Remove all omarchy-pkg-builder images
print_info "Removing omarchy-pkg-builder images..." print_info "Removing omarchy-pkg-builder images..."
"$CONTAINER_ENGINE" images omarchy-pkg-builder -q | xargs -r "$CONTAINER_ENGINE" rmi -f 2>/dev/null || true docker images omarchy-pkg-builder -q | xargs -r docker rmi -f 2>/dev/null || true
# Clear this engine's build cache. # Clear buildx cache for these builds
print_info "Clearing build cache..." print_info "Clearing buildx cache..."
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then docker buildx prune -a -f
"$CONTAINER_ENGINE" buildx prune -a -f
else
"$CONTAINER_ENGINE" image prune --build-cache -f
fi
print_success "Container builder cache cleared" print_success "Docker builder cache cleared"
print_info "Next build will create fresh images" print_info "Next build will create fresh images"
+3 -7
View File
@@ -31,13 +31,9 @@ clean_packages() {
# Skip signature files # Skip signature files
[[ "$pkg" == *.sig ]] && continue [[ "$pkg" == *.sig ]] && continue
# Format: name-version-release-arch.pkg.tar.*. Work from the right because # Extract package name (remove version and architecture)
# package names can themselves contain version-like pieces (qt6-5compat, # Format: name-version-release-arch.pkg.tar.*
# nvidia-580xx-utils), while pkgver and pkgrel cannot contain hyphens. local pkgname=$(echo "$pkg" | sed -E 's/-[0-9]+.*-(any|x86_64|i686)\.pkg\.tar\..*//')
local stem="${pkg%%.pkg.tar.*}"
stem="${stem%-*}" # architecture
stem="${stem%-*}" # pkgrel
local pkgname="${stem%-*}" # pkgver
# Add to array # Add to array
if [[ -n "${packages[$pkgname]}" ]]; then if [[ -n "${packages[$pkgname]}" ]]; then
-62
View File
@@ -1,62 +0,0 @@
#!/bin/bash
# Internal initial-recipe import used by add-package. Maintained recipes are
# never replaced; subsequent releases go through sync-upstream.
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
echo "Usage: bin/add-package <package> --source aur [--aur <upstream-name>]"
exit 0
fi
if [[ $# != 1 || ! $1 =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
print_error "Use bin/add-package <package> --source aur for an initial import"
exit 1
fi
package=$1
package_dir="$PKGBUILDS_DIR/$package"
metadata="$package_dir/.omarchy/package.json"
if [[ -f "$package_dir/PKGBUILD" ]]; then
print_error "Refusing to replace the maintained recipe for $package"
exit 1
fi
if [[ ! -f "$metadata" ]] || [[ $(jq -r .source "$metadata") != aur ]]; then
print_error "Initial import requires metadata created by bin/add-package --source aur"
exit 1
fi
aur_package=$(jq -r --arg name "$package" '.aur // $name' "$metadata")
if [[ ! $aur_package =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
print_error "Invalid AUR package name"
exit 1
fi
# Refuse unrelated package files: an import only starts from .omarchy metadata.
shopt -s dotglob nullglob
for item in "$package_dir"/*; do
if [[ ${item##*/} != .omarchy ]]; then
print_error "Initial import needs an empty package directory: $package_dir"
exit 1
fi
done
work=$(mktemp -d "$PKGBUILDS_DIR/.import-aur.XXXXXX")
trap 'rm -rf "$work"' EXIT
print_info "Importing $package from AUR package $aur_package..."
git clone --quiet "https://aur.archlinux.org/${aur_package}.git" "$work/recipe"
[[ -f "$work/recipe/PKGBUILD" ]] || { print_error "AUR package has no PKGBUILD"; exit 1; }
commit=$(git -C "$work/recipe" rev-parse HEAD)
rm -rf "$work/recipe/.git" "$work/recipe/.omarchy"
rm -f "$work/recipe/.SRCINFO" "$work/recipe/.gitignore"
cp -a "$package_dir/.omarchy" "$work/recipe/.omarchy"
jq --arg name "$aur_package" --arg commit "$commit" '
.source = "local" | .origin = {aur: $name, commit: $commit}
| del(.aur, .upstream_commit)
' "$metadata" > "$work/recipe/.omarchy/package.json"
# Stage on the same filesystem, restoring the metadata directory on failure.
mv "$package_dir" "$work/original"
if ! mv "$work/recipe" "$package_dir"; then
mv "$work/original" "$package_dir"
exit 1
fi
print_success "Imported $package; review the recipe and configure its direct upstream watch"
+7 -14
View File
@@ -21,10 +21,9 @@ BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/host-helpers.sh" source "$BUILD_ROOT/helpers/host-helpers.sh"
source "$BUILD_ROOT/helpers/package-metadata.sh"
UPSTREAM_URL="${OMARCHY_UPSTREAM_URL:-https://github.com/basecamp/omarchy.git}" UPSTREAM_URL="${OMARCHY_UPSTREAM_URL:-https://github.com/basecamp/omarchy.git}"
EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/$(reference_arch)/omarchy.db.tar.zst}" EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/x86_64/omarchy.db.tar.zst}"
RELEASE_PACKAGES=(omarchy omarchy-settings) RELEASE_PACKAGES=(omarchy omarchy-settings)
DEFAULT_RC_REF="quattro" DEFAULT_RC_REF="quattro"
SRCDEST_DIR="$BUILD_ROOT/.srcdest" SRCDEST_DIR="$BUILD_ROOT/.srcdest"
@@ -93,7 +92,7 @@ version_is_rc() { version_is_prerelease "$1"; }
pkgbuild_var() { pkgbuild_var() {
local pkg="$1" var="$2" local pkg="$1" var="$2"
package_pkgbuild_var "$BUILD_ROOT/pkgbuilds/$pkg" "$var" (cd "$BUILD_ROOT/pkgbuilds/$pkg" && bash -c "source PKGBUILD 2>/dev/null; echo \"\${$var}\"")
} }
# Prints the published version of $pkg from the edge DB. Distinguishes # Prints the published version of $pkg from the edge DB. Distinguishes
@@ -328,19 +327,13 @@ regenerate_checksums() {
trigger_build_host() { trigger_build_host() {
local host local host
queue_edge_builds() {
mkdir -p "$STATE_DIR" || return 1
local arch
for arch in $(published_arches); do
touch "$(sync_queue_file edge "$arch")" || return 1
done
}
# Explicit host configuration outranks the local-host inference (a # Explicit host configuration outranks the local-host inference (a
# workstation that ran a full local release carries the db marker too). # workstation that ran a full local release carries the db marker too).
if ! resolve_repo_host "${REPO_HOST_OVERRIDE:-}" >/dev/null && on_repo_host; then if ! resolve_repo_host "${REPO_HOST_OVERRIDE:-}" >/dev/null && on_repo_host; then
print_info "Triggering edge build locally (this is the build host)..." print_info "Triggering edge build locally (this is the build host)..."
if queue_edge_builds && systemctl start --no-block omarchy-auto-release-edge.service; then if mkdir -p "${OMARCHY_STATE_DIR:-/root/.state}" &&
touch "${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-edge" &&
systemctl start --no-block omarchy-auto-release-edge.service; then
print_success "Edge build triggered" print_success "Edge build triggered"
else else
print_warning "Could not start the edge release service — the 6-hourly timer will pick it up" print_warning "Could not start the edge release service — the 6-hourly timer will pick it up"
@@ -350,11 +343,11 @@ trigger_build_host() {
if ! host=$(resolve_repo_host "${REPO_HOST_OVERRIDE:-}"); then if ! host=$(resolve_repo_host "${REPO_HOST_OVERRIDE:-}"); then
print_info "No repository host configured (set OMARCHY_REPO_HOST or $BUILD_ROOT/.repo-host — any ssh destination, e.g. root@<host> or an ssh-config alias)." print_info "No repository host configured (set OMARCHY_REPO_HOST or $BUILD_ROOT/.repo-host — any ssh destination, e.g. root@<host> or an ssh-config alias)."
print_info "The 6-hourly auto-release timer will pick this up, or trigger manually:" print_info "The 6-hourly auto-release timer will pick this up, or trigger manually:"
echo " ssh <build-host> 'git -C /root/omarchy-pkgs pull --ff-only && systemctl start omarchy-check-versions.service omarchy-auto-release-edge.service'" echo " ssh <build-host> 'git -C /root/omarchy-pkgs pull --ff-only && touch /root/.state/.sync-needed-edge && systemctl start omarchy-auto-release-edge.service'"
return 0 return 0
fi fi
print_info "Triggering edge build on $host..." print_info "Triggering edge build on $host..."
if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && cd /root/omarchy-pkgs && export BUILD_ROOT=/root/omarchy-pkgs && source helpers/paths.sh && mkdir -p "$STATE_DIR" && for arch in $(published_arches); do touch "$(sync_queue_file edge "$arch")"; done && systemctl start --no-block omarchy-auto-release-edge.service'; then if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && mkdir -p /root/.state && touch /root/.state/.sync-needed-edge && systemctl start --no-block omarchy-auto-release-edge.service'; then
print_success "Edge build triggered on $host" print_success "Edge build triggered on $host"
else else
print_warning "Could not trigger $host — the 6-hourly timer will pick it up" print_warning "Could not trigger $host — the 6-hourly timer will pick it up"
+55 -93
View File
@@ -18,7 +18,6 @@ set -e
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/host-helpers.sh" source "$BUILD_ROOT/helpers/host-helpers.sh"
@@ -29,11 +28,7 @@ ISO_REPO="${OMARCHY_ISO_REPO:-omacom-io/omarchy-iso}"
DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}" DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}"
PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}" PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}"
# The first published architecture supplies the version-ordering floor when RC_DB_URL="$PKGS_DB_BASE/rc/x86_64/omarchy.db.tar.zst"
# cutting pins. Readiness checks below still verify every published
# architecture before an RC or final release can move forward.
OBSERVED_ARCH=$(reference_arch)
RC_DB_URL="$PKGS_DB_BASE/rc/$OBSERVED_ARCH/omarchy.db.tar.zst"
SRCDEST_DIR="$BUILD_ROOT/.srcdest" SRCDEST_DIR="$BUILD_ROOT/.srcdest"
MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs) MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs)
@@ -186,12 +181,12 @@ ensure_work_clone() {
# Prints omarchy's published version in a channel; empty when absent, rc 2 when # Prints omarchy's published version in a channel; empty when absent, rc 2 when
# the database cannot be read (callers must not mistake an outage for absence). # the database cannot be read (callers must not mistake an outage for absence).
published_version() { published_version() {
local channel="$1" arch="${2:-$OBSERVED_ARCH}" tmp descs local channel="$1" tmp descs
tmp=$(mktemp) || return 2 tmp=$(mktemp) || return 2
# A unique query string busts the CDN cache: right after a sync the plain # A unique query string busts the CDN cache: right after a sync the plain
# URL can keep serving the previous db for a while, which reads as "not # URL can keep serving the previous db for a while, which reads as "not
# published yet" to status, the wait loop, and ship's pre-checks. # published yet" to status, the wait loop, and ship's pre-checks.
if ! curl -sf "$PKGS_DB_BASE/$channel/$arch/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then if ! curl -sf "$PKGS_DB_BASE/$channel/x86_64/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then
rm -f "$tmp" rm -f "$tmp"
return 2 return 2
fi fi
@@ -212,14 +207,6 @@ published_version() {
' <<<"$descs" ' <<<"$descs"
} }
all_arches_at_version() { # all_arches_at_version <channel> <pkgver>
local channel="$1" want="$2" arch got
for arch in $(published_arches); do
got=$(published_version "$channel" "$arch" 2>/dev/null) || return 1
[[ "${got%-*}" == "$want" ]] || return 1
done
}
# The rc branch of THIS repo carries the current pins. Read them without # The rc branch of THIS repo carries the current pins. Read them without
# touching the working tree. # touching the working tree.
rc_branch_pin() { # prints "pkgver commit", empty when no rc branch rc_branch_pin() { # prints "pkgver commit", empty when no rc branch
@@ -264,12 +251,8 @@ fi
git -C /root/omarchy-pkgs-rc fetch origin rc git -C /root/omarchy-pkgs-rc fetch origin rc
git -C /root/omarchy-pkgs-rc reset --hard origin/rc git -C /root/omarchy-pkgs-rc reset --hard origin/rc
cd /root/omarchy-pkgs-rc cd /root/omarchy-pkgs-rc
# The pair is built once per published architecture (helpers/paths.sh on the OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
# host decides which), so every architecture'"'"'s rc channel carries the pins. bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check
for arch in $(BUILD_ROOT=/root/omarchy-pkgs-rc bash -c "source helpers/paths.sh; published_arches"); do
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
bin/repo release --mirror rc --arch "$arch" --package omarchy omarchy-settings --skip-prod-check
done
' '
trigger_rc_build() { trigger_rc_build() {
@@ -295,25 +278,19 @@ host_advance() { # host_advance <from> <to> [extra args...]
# against this machine's (likely stale) local tree would be wrong. # against this machine's (likely stale) local tree would be wrong.
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
print_no_host_help "advance $from -> $to" \ print_no_host_help "advance $from -> $to" \
" cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --arch all --skip-prod-check $*" " cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --skip-prod-check $*"
return 1 return 1
fi fi
# --arch all: the host advances every architecture it publishes, so a train "$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --skip-prod-check "$@"
# never moves a channel for one architecture and not another.
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --arch all --skip-prod-check "$@"
} }
wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds] wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds]
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 arch got pending local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got
print_info "Waiting for $want in $channel for: $(published_arches | tr '\n' ' ')" print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..."
while ((waited < timeout)); do while ((waited < timeout)); do
pending="" got=$(published_version "$channel" 2>/dev/null) || got=""
for arch in $(published_arches); do if [[ "${got%-*}" == "$want" ]]; then
got=$(published_version "$channel" "$arch" 2>/dev/null) || got="" print_success "$channel now serves omarchy $got"
[[ "${got%-*}" == "$want" ]] || pending+=" $arch=${got:-unreachable}"
done
if [[ -z "$pending" ]]; then
print_success "$channel now serves omarchy $want on every published architecture"
return 0 return 0
fi fi
sleep 60 sleep 60
@@ -321,7 +298,7 @@ wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds
printf '.' >&2 printf '.' >&2
done done
echo "" >&2 echo "" >&2
print_warning "Timed out waiting for $want in $channel (pending:$pending)" print_warning "Timed out waiting for $want in $channel (currently: ${got:-unknown})"
print_info "The build may still be running — re-run this command to resume." print_info "The build may still be running — re-run this command to resume."
return 1 return 1
} }
@@ -375,8 +352,8 @@ iso_checkout() { # prints a usable omarchy-iso checkout, cloning to tmp if neede
build_iso() { # build_iso <version> [--rc] build_iso() { # build_iso <version> [--rc]
local version="$1" rc_flag="${2:-}" dir local version="$1" rc_flag="${2:-}" dir
dir=$(iso_checkout) || return 1 dir=$(iso_checkout) || return 1
if [[ -z "$CONTAINER_ENGINE" ]] || ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
print_warning "No container engine is available — cannot build the ISO here. Run on a Docker or Podman machine:" print_warning "Docker unavailable — cannot build the ISO here. Run on a Docker machine:"
echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version" echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version"
return 1 return 1
fi fi
@@ -685,13 +662,15 @@ cmd_rc() {
if [[ -n "$pin" ]]; then if [[ -n "$pin" ]]; then
local pin_ver="${pin%% *}" pin_commit="${pin##* }" local pin_ver="${pin%% *}" pin_commit="${pin##* }"
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
if all_arches_at_version rc "$pin_ver"; then local pub
print_success "$pin_ver is already cut from this head and published to rc on every architecture" pub=$(published_version rc 2>/dev/null) || pub=""
if [[ "${pub%-*}" == "$pin_ver" ]]; then
print_success "$pin_ver is already cut from this head and published to rc"
maybe_iso "$pin_ver" rc "$iso_mode" maybe_iso "$pin_ver" rc "$iso_mode"
return 0 return 0
fi fi
print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build" print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build"
trigger_rc_build || return 1 trigger_rc_build || true
[[ "$wait" == true ]] && wait_for_published rc "$pin_ver" [[ "$wait" == true ]] && wait_for_published rc "$pin_ver"
maybe_iso "$pin_ver" rc "$iso_mode" maybe_iso "$pin_ver" rc "$iso_mode"
return 0 return 0
@@ -707,7 +686,7 @@ cmd_rc() {
new_ver="${new_pin%% *}" new_ver="${new_pin%% *}"
print_success "Pinned $new_ver (rc branch pushed)" print_success "Pinned $new_ver (rc branch pushed)"
trigger_rc_build || return 1 trigger_rc_build || true
if [[ "$wait" == true ]]; then if [[ "$wait" == true ]]; then
wait_for_published rc "$new_ver" || return 1 wait_for_published rc "$new_ver" || return 1
fi fi
@@ -729,7 +708,9 @@ cmd_ship() {
exit 1 exit 1
fi fi
version=$(branch_to_version "$branch") version=$(branch_to_version "$branch")
if all_arches_at_version stable "$version" && local stable_now
stable_now=$(published_version stable 2>/dev/null) || stable_now=""
if [[ "${stable_now%-*}" == "$version" ]] &&
gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
print_success "Nothing to ship — $version is tagged, released, and live on stable" print_success "Nothing to ship — $version is tagged, released, and live on stable"
exit 0 exit 0
@@ -771,8 +752,10 @@ cmd_ship() {
echo " omarchy-release rc" echo " omarchy-release rc"
exit 1 exit 1
fi fi
if ! all_arches_at_version rc "$pin_ver"; then local pub
print_error "$pin_ver is pinned but is not published for every architecture" pub=$(published_version rc 2>/dev/null) || pub=""
if [[ "${pub%-*}" != "$pin_ver" ]]; then
print_error "$pin_ver is pinned but rc serves '${pub:-nothing}' — the candidate build hasn't published"
echo "Wait for it (or re-run: omarchy-release rc), then ship." echo "Wait for it (or re-run: omarchy-release rc), then ship."
exit 1 exit 1
fi fi
@@ -816,9 +799,10 @@ cmd_ship() {
# 2. Final pins into rc. Resolve the tag we just established so the final # 2. Final pins into rc. Resolve the tag we just established so the final
# PKGBUILDs record both its provenance and its exact commit. # PKGBUILDs record both its provenance and its exact commit.
local stable_pub local rc_pub stable_pub
if all_arches_at_version rc "$version"; then rc_pub=$(published_version rc 2>/dev/null) || rc_pub=""
print_success "2/7 Final $version already published to rc on every architecture" if [[ "${rc_pub%-*}" == "$version" ]]; then
print_success "2/7 Final $version already published to rc"
else else
if [[ "$pin_ver" != "$version" ]]; then if [[ "$pin_ver" != "$version" ]]; then
print_info "2/7 Pinning final $version from tag v$version..." print_info "2/7 Pinning final $version from tag v$version..."
@@ -826,21 +810,22 @@ cmd_ship() {
else else
print_info "2/7 Final $version pinned — re-triggering build" print_info "2/7 Final $version pinned — re-triggering build"
fi fi
trigger_rc_build || exit 1 trigger_rc_build || true
wait_for_published rc "$version" || exit 1 wait_for_published rc "$version" || exit 1
fi fi
# 3. Promote rc -> stable # 3. Promote rc -> stable
if all_arches_at_version stable "$version"; then stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
print_success "3/7 Stable already serves $version on every architecture" if [[ "${stable_pub%-*}" == "$version" ]]; then
print_success "3/7 Stable already serves $version"
else else
host_advance rc stable || exit 1 host_advance rc stable || exit 1
if ! all_arches_at_version stable "$version"; then stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
print_error "Promotion ran but stable does not serve $version on every architecture" if [[ "${stable_pub%-*}" != "$version" ]]; then
print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing"
exit 1 exit 1
fi fi
stable_pub=$(published_version stable 2>/dev/null) || stable_pub="$version" print_success "3/7 Promoted to stable: omarchy $stable_pub"
print_success "3/7 Promoted to stable: omarchy $stable_pub on every architecture"
fi fi
# 4. Final pins onto master (keeps edge overlap publishing and the repo record) # 4. Final pins onto master (keeps edge overlap publishing and the repo record)
@@ -923,7 +908,7 @@ next_step() { # prints "<command>|<description>"
last=$(newest_release_branch 2>/dev/null) || last="" last=$(newest_release_branch 2>/dev/null) || last=""
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
last_ver=$(branch_to_version "$last") last_ver=$(branch_to_version "$last")
if ! all_arches_at_version stable "$last_ver" || if [[ "${STABLE_VER%-*}" != "$last_ver" ]] ||
{ command -v gh >/dev/null && ! gh release view "v$last_ver" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; }; then { command -v gh >/dev/null && ! gh release view "v$last_ver" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; }; then
echo "ship|$last_ver is tagged but not fully shipped — resume ship" echo "ship|$last_ver is tagged but not fully shipped — resume ship"
return return
@@ -939,7 +924,7 @@ next_step() { # prints "<command>|<description>"
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)" echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate" echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate"
elif ! all_arches_at_version rc "$pin_ver"; then elif [[ "${RC_VER%-*}" != "$pin_ver" ]]; then
echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait" echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait"
else else
echo "ship|$pin_ver is published to rc — test it, then ship" echo "ship|$pin_ver is published to rc — test it, then ship"
@@ -1009,24 +994,20 @@ cmd_doctor() {
check "makepkg available (checksums)" command -v makepkg check "makepkg available (checksums)" command -v makepkg
check "curl available" command -v curl check "curl available" command -v curl
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
local ch arch local ch
for ch in edge stable; do for ch in edge stable; do
for arch in $(published_arches); do if published_version "$ch" >/dev/null 2>&1; then
if published_version "$ch" "$arch" >/dev/null 2>&1; then print_success "$ch channel db readable"
print_success "$ch/$arch channel db readable"
else
print_error "$ch/$arch channel db readable"
failures=$((failures + 1))
fi
done
done
for arch in $(published_arches); do
if published_version rc "$arch" >/dev/null 2>&1; then
print_success "rc/$arch channel db readable"
else else
print_warning "rc/$arch channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)" print_error "$ch channel db readable"
failures=$((failures + 1))
fi fi
done done
if published_version rc >/dev/null 2>&1; then
print_success "rc channel db readable"
else
print_warning "rc channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)"
fi
local host local host
if host=$(repo_host); then if host=$(repo_host); then
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
@@ -1035,10 +1016,10 @@ cmd_doctor() {
else else
print_warning "no build host configured — set OMARCHY_REPO_HOST, --host, or write an ssh destination (root@<host> or an ssh-config alias) to $BUILD_ROOT/.repo-host; until then builds trigger on the 6h timer only" print_warning "no build host configured — set OMARCHY_REPO_HOST, --host, or write an ssh destination (root@<host> or an ssh-config alias) to $BUILD_ROOT/.repo-host; until then builds trigger on the 6h timer only"
fi fi
if [[ -n "$CONTAINER_ENGINE" ]] && "$CONTAINER_ENGINE" info >/dev/null 2>&1; then if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
print_success "$CONTAINER_ENGINE available (ISO builds possible here)" print_success "docker available (ISO builds possible here)"
else else
print_warning "container engine unavailable — ISO builds will print instructions instead" print_warning "docker unavailable — ISO builds will print instructions instead"
fi fi
echo "" echo ""
if ((failures == 0)); then if ((failures == 0)); then
@@ -1073,25 +1054,6 @@ cmd_self_test() {
expect "version_is_patch 5.0.0" "$(version_is_patch 5.0.0 && echo yes || echo no)" "no" expect "version_is_patch 5.0.0" "$(version_is_patch 5.0.0 && echo yes || echo no)" "no"
expect "previous_patch_tag 4.0.2" "$(previous_patch_tag 4.0.2)" "v4.0.1" expect "previous_patch_tag 4.0.2" "$(previous_patch_tag 4.0.2)" "v4.0.1"
expect "previous_patch_tag 4.0.10" "$(previous_patch_tag 4.0.10)" "v4.0.9" expect "previous_patch_tag 4.0.10" "$(previous_patch_tag 4.0.10)" "v4.0.9"
# Keep release readiness fail-closed when only one architecture has reached
# the requested version. This replaces the network reader for this process;
# self-test exits immediately afterwards.
published_version() {
case "$2" in
x86_64) echo "${TEST_X86_VERSION:-4.0.2-1}" ;;
aarch64) echo "${TEST_ARM_VERSION:-4.0.2-1}" ;;
esac
}
PUBLISHED_ARCHES=x86_64
expect "x86-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
PUBLISHED_ARCHES=aarch64
expect "ARM-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
PUBLISHED_ARCHES="x86_64 aarch64"
TEST_ARM_VERSION=4.0.1-1
expect "mixed versions block release" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "blocked"
TEST_ARM_VERSION=4.0.2-1
expect "both architectures ready" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
echo "" echo ""
if ((failures == 0)); then if ((failures == 0)); then
print_success "Self-test passed" print_success "Self-test passed"
+6 -6
View File
@@ -17,13 +17,13 @@ Usage: $0 <package> [OPTIONS]
Create a scratch workspace for inspecting an AUR-backed package. Create a scratch workspace for inspecting an AUR-backed package.
The workspace contains: The workspace contains:
upstream/ Raw AUR package at the recorded origin.commit, or HEAD upstream/ Raw AUR package at .omarchy/package.json upstream_commit, or HEAD
patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied
current/ Current checked-in package directory current/ Current checked-in package directory
Options: Options:
--dir <path> Workspace directory (default: mktemp under /tmp) --dir <path> Workspace directory (default: mktemp under /tmp)
--commit <sha> Use a specific AUR commit instead of origin.commit --commit <sha> Use a specific AUR commit instead of upstream_commit
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
@@ -75,13 +75,13 @@ if [[ ! -f "$METADATA" ]]; then
exit 1 exit 1
fi fi
if [[ "$(jq -r '.origin.aur // .aur // (if .source == "aur" then "legacy" else "" end)' "$METADATA")" == "" ]]; then if [[ "$(jq -r '.source // ""' "$METADATA")" != "aur" ]]; then
print_error "package-worktree only supports AUR-backed packages" print_error "package-worktree only supports AUR-backed packages"
exit 1 exit 1
fi fi
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.origin.aur // .aur // $package' "$METADATA") AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.aur // $package' "$METADATA")
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.origin.commit // .upstream_commit // ""' "$METADATA")} UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.upstream_commit // ""' "$METADATA")}
if [[ -z "$DEST_DIR" ]]; then if [[ -z "$DEST_DIR" ]]; then
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX") DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
@@ -224,7 +224,7 @@ print_info "AUR package: $AUR_PACKAGE"
if [[ -n "$UPSTREAM_COMMIT" ]]; then if [[ -n "$UPSTREAM_COMMIT" ]]; then
print_info "Upstream commit: $UPSTREAM_COMMIT" print_info "Upstream commit: $UPSTREAM_COMMIT"
else else
print_warning "No origin.commit recorded; using AUR HEAD" print_warning "No upstream_commit recorded; using AUR HEAD"
fi fi
rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR" rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR"
-118
View File
@@ -1,118 +0,0 @@
#!/bin/bash
# Publish built packages into one channel of the remote repository,
# incrementally and immutably.
#
# publish-artifact --mirror <edge|rc|stable> --arch <arch> <pkg files...>
#
# What it does, in order:
# 1. pull the channel's current database from the remote
# 2. refuse if any package filename already exists on the remote
# 3. sign each package (GPG_PRIVATE_KEY / GPG_PASSPHRASE)
# 4. repo-add the packages into the pulled database (replaces the entry
# for that name; nothing else in the channel is touched)
# 5. upload packages, then signatures, then the database last
#
# Never overwrites: uploads use --ignore-existing for packages and the
# pre-check in step 2 makes a same-name collision a hard failure rather than
# a silent skip. The database is the only object rewritten, and it is
# uploaded only after every file it references is present.
#
# The remote is an rclone remote (REMOTE, default the production one);
# OMARCHY_PUBLISH_PREFIX can point a proof run at a scratch prefix.
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
REMOTE=${REMOTE:-pkgs.omarchy.org:omarchy-pkgs}
PREFIX=${OMARCHY_PUBLISH_PREFIX:-}
FILES=()
while [[ $# -gt 0 ]]; do
case $1 in
--mirror) MIRROR=$2; require_valid_mirror "$MIRROR"; shift 2 ;;
--arch) ARCH=$2; require_valid_arch "$ARCH"; shift 2 ;;
--remote) REMOTE=$2; shift 2 ;;
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
-*) print_error "Unknown option: $1"; exit 1 ;;
*) FILES+=("$1"); shift ;;
esac
done
(( ${#FILES[@]} )) || { print_error "No package files given"; exit 1; }
: "${GPG_PRIVATE_KEY:?}"; GPG_PASSPHRASE=${GPG_PASSPHRASE-}
DEST="$REMOTE/${PREFIX:+$PREFIX/}$MIRROR/$ARCH"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
print_header "Publish to $DEST"
# --- 0. sanity: every file is a package, named as makepkg names it ---------
for f in "${FILES[@]}"; do
[[ -f "$f" && "$f" == *.pkg.tar.* && "$f" != *.sig ]] || { print_error "Not a package file: $f"; exit 1; }
name=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgname"{print $2}')
ver=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgver"{print $2}')
pkgarch=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="arch"{print $2}')
[[ "$(basename "$f")" == "$name-$ver-$pkgarch.pkg.tar."* ]] || {
print_error "Filename does not match PKGINFO ($name-$ver-$pkgarch): $(basename "$f")"; exit 1; }
[[ "$pkgarch" == any || "$pkgarch" == "$ARCH" ]] || { print_error "$f is $pkgarch, publishing to $ARCH"; exit 1; }
done
# --- 1. pull the current database -----------------------------------------
mkdir -p "$WORK/repo"
listing=$(rclone lsf "$DEST/" --s3-no-head 2>/dev/null || true)
if grep -q '^omarchy.db.tar.zst$' <<<"$listing"; then
rclone copy "$DEST/omarchy.db.tar.zst" "$WORK/repo" --s3-no-head
rclone copy "$DEST/omarchy.files.tar.zst" "$WORK/repo" --s3-no-head 2>/dev/null || true
print_info "Pulled current database ($(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries)"
else
print_warning "No database at $DEST — creating a new one"
fi
# --- 2. same-name collisions ----------------------------------------------
# A filename must mean one set of bytes across every channel. The same file
# reaching a channel that already holds it (a fast-ring publish after edge,
# a re-run, a later promotion) is fine: it is skipped on upload and only the
# database entry is added. Different bytes under a name the channel already
# has is the one thing this must never do.
for f in "${FILES[@]}"; do
b=$(basename "$f")
grep -qxF "$b" <<<"$listing" || continue
remote_sum=$(rclone hashsum md5 "$DEST/$b" --s3-no-head 2>/dev/null | awk '{print $1}')
local_sum=$(md5sum "$f" | awk '{print $1}')
if [[ -n "$remote_sum" && "$remote_sum" == "$local_sum" ]]; then
print_info "Already published with identical bytes, adding to the database only: $b"
else
print_error "Already published with DIFFERENT bytes, refusing to overwrite: $b"
echo " Bump pkgrel; published filenames are immutable."
exit 1
fi
done
# --- 3. sign ---------------------------------------------------------------
export GNUPGHOME="$WORK/gnupg"; mkdir -m 700 "$GNUPGHOME"
echo "$GPG_PRIVATE_KEY" | gpg --batch --quiet --import
KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '$1=="sec"{print $5; exit}')
[[ -n "$KEY_ID" ]] || { print_error "No secret key imported"; exit 1; }
for f in "${FILES[@]}"; do
cp "$f" "$WORK/repo/"
gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \
--detach-sign --no-armor --local-user "$KEY_ID" "$WORK/repo/$(basename "$f")"
print_step "signed $(basename "$f")"
done
# --- 4. repo-add (replaces the entry for each pkgname) ---------------------
( cd "$WORK/repo" && repo-add --quiet omarchy.db.tar.zst "${FILES[@]/#*\//}" )
ln -sf omarchy.db.tar.zst "$WORK/repo/omarchy.db"
ln -sf omarchy.files.tar.zst "$WORK/repo/omarchy.files"
print_info "Database now has $(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries"
# --- 5. upload: packages, signatures, database last -----------------------
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '- *.sig' --filter '+ *.pkg.tar.*' --filter '- *'
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '+ *.pkg.tar.*.sig' --filter '- *'
# Re-verify every referenced file is really there before the db goes up.
listing=$(rclone lsf "$DEST/" --s3-no-head)
for f in "${FILES[@]}"; do
b=$(basename "$f")
grep -qxF "$b" <<<"$listing" && grep -qxF "$b.sig" <<<"$listing" || { print_error "Upload incomplete: $b"; exit 1; }
done
rclone copy "$WORK/repo" "$DEST" --s3-no-head --checksum --copy-links --filter '+ omarchy.db*' --filter '+ omarchy.files*' --filter '- *'
print_success "Published ${#FILES[@]} package(s) to $DEST"
+8 -19
View File
@@ -149,16 +149,11 @@ fi
# leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name # leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name
# still matches, for pushing just one of them on purpose. # still matches, for pushing just one of them on purpose.
# #
# Package identity comes from .PKGINFO rather than the filename or PKGBUILD: it # pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg
# is what makepkg actually recorded, and it handles epochs and split packages. # actually recorded, and it needs no guessing about which directory built what.
package_identity_of() { pkgbase_of() {
bsdtar -xOf "$1" .PKGINFO 2>/dev/null | bsdtar -xOf "$1" .PKGINFO 2>/dev/null |
awk -F ' = ' ' awk -F ' = ' '$1 == "pkgbase" { print $2; exit }'
$1 == "pkgname" { name = $2 }
$1 == "pkgbase" { base = $2 }
$1 == "pkgver" { version = $2 }
END { print name "\t" base "\t" version }
'
} }
FILES=() FILES=()
@@ -166,18 +161,14 @@ if [[ -z "$PACKAGES" ]]; then
FILES=("${ALL_FILES[@]}") FILES=("${ALL_FILES[@]}")
else else
declare -A MATCHED=() declare -A MATCHED=()
declare -A LATEST_FILE=()
declare -A LATEST_VERSION=()
for file in "${ALL_FILES[@]}"; do for file in "${ALL_FILES[@]}"; do
IFS=$'\t' read -r pkgname pkgbase pkgver < <(package_identity_of "$BUILD_OUTPUT_DIR/$file") # name-version-release-arch.pkg.tar.zst -> name
pkgname="${file%-*-*-*.pkg.tar.*}"
pkgbase=$(pkgbase_of "$BUILD_OUTPUT_DIR/$file")
for wanted in $PACKAGES; do for wanted in $PACKAGES; do
if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then
FILES+=("$file")
MATCHED["$wanted"]=1 MATCHED["$wanted"]=1
if [[ -z "${LATEST_FILE[$pkgname]:-}" ]] ||
[[ $(vercmp "$pkgver" "${LATEST_VERSION[$pkgname]}") -gt 0 ]]; then
LATEST_FILE["$pkgname"]="$file"
LATEST_VERSION["$pkgname"]="$pkgver"
fi
break break
fi fi
done done
@@ -190,8 +181,6 @@ else
exit 1 exit 1
fi fi
done done
mapfile -t FILES < <(printf '%s\n' "${LATEST_FILE[@]}" | sort)
fi fi
if [[ ${#FILES[@]} -eq 0 ]]; then if [[ ${#FILES[@]} -eq 0 ]]; then
+4 -61
View File
@@ -114,11 +114,7 @@ BUILT_FILES=""
# What the scheduled version check queued, when it was the one that asked for # What the scheduled version check queued, when it was the one that asked for
# this run. Absent for a manual run, which is fine — the report just omits it. # this run. Absent for a manual run, which is fine — the report just omits it.
QUEUED_PACKAGES="" QUEUED_PACKAGES=""
QUEUE_FILE=$(sync_queue_file "$MIRROR" "$ARCH") QUEUE_FILE="${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-$MIRROR"
# A queue written under the pre-architecture name belongs to x86_64.
if [[ "$ARCH" == "x86_64" && ! -s "$QUEUE_FILE" && -s "$(legacy_sync_queue_file "$MIRROR")" ]]; then
QUEUE_FILE=$(legacy_sync_queue_file "$MIRROR")
fi
[[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE") [[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE")
if [[ "$DRY_RUN" != true ]]; then if [[ "$DRY_RUN" != true ]]; then
@@ -138,43 +134,11 @@ if [[ "$DRY_RUN" == true ]]; then
else else
print_info "Step 1/6: Building packages..." print_info "Step 1/6: Building packages..."
fi fi
BUILD_RESULT_DIR=$(mktemp -d) "$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || {
trap 'rm -rf "$BUILD_RESULT_DIR"' EXIT
build_status=0
OMARCHY_BUILD_RESULT_DIR="$BUILD_RESULT_DIR" "$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || build_status=$?
partial=false
if [[ "$build_status" == 2 && -f "$BUILD_RESULT_DIR/complete" && "$DRY_RUN" != true ]]; then
if [[ "${OMARCHY_DEFER_RUNTIME_DEPS:-false}" == true ]]; then
print_error "The deferred release pair must succeed together; nothing will be published"
notify_error "Release failed: Incomplete release pair" "$RELEASE_CONTEXT"
exit 1
fi
partial=true
while IFS= read -r file; do
[[ -f "$BUILD_OUTPUT_DIR/$file" ]] || {
print_error "Completed build artifact is missing: $file"
notify_error "Release failed: Missing completed artifact" "$RELEASE_CONTEXT"
exit 1
}
done < "$BUILD_RESULT_DIR/artifacts"
# Exclude partial split outputs or leftovers from failed builds. Moving
# them out of the flat publication directory keeps them available for
# diagnosis without handing them to sign/promote.
unpublished=""
for path in "$BUILD_OUTPUT_DIR"/*.pkg.tar.*; do
[[ -f "$path" ]] || continue
file=${path##*/}
if ! grep -Fxq -- "${file%.sig}" "$BUILD_RESULT_DIR/artifacts"; then
[[ -n "$unpublished" ]] || unpublished=$(mktemp -d "$BUILD_OUTPUT_DIR/.unpublished.XXXXXX")
mv -- "$path" "$unpublished/"
fi
done
print_warning "Some packages failed; publishing the completed packages before retrying the failures"
elif [[ "$build_status" != 0 ]]; then
print_error "Build failed" print_error "Build failed"
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT" notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
exit 1 exit 1
fi }
if [[ "$DRY_RUN" == true ]]; then if [[ "$DRY_RUN" == true ]]; then
echo "" echo ""
@@ -187,12 +151,6 @@ BUILT_COUNT=$(grep -c '' <<<"$BUILT_FILES")
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0 [[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
print_info "Built $BUILT_COUNT package(s) this run" print_info "Built $BUILT_COUNT package(s) this run"
if [[ "$partial" == true && "$BUILT_COUNT" == 0 ]]; then
print_error "No completed packages to publish"
notify_error "Release failed: No completed packages" "$RELEASE_CONTEXT"
exit 1
fi
# Step 2: Sign # Step 2: Sign
echo "" echo ""
print_info "Step 2/6: Signing packages..." print_info "Step 2/6: Signing packages..."
@@ -251,16 +209,7 @@ if ((BUILT_COUNT > 0)); then
summary+="<br><br><strong>$BUILT_COUNT package(s) published:</strong>" summary+="<br><br><strong>$BUILT_COUNT package(s) published:</strong>"
summary+="$(format_package_list_html "$BUILT_FILES")" summary+="$(format_package_list_html "$BUILT_FILES")"
summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/" summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/"
if [[ "$partial" == true ]]; then notify_success "Release published: $MIRROR" "$summary"
failed=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/failed" | basecamp_html_escape)
blocked=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/blocked" | basecamp_html_escape)
[[ -z "$failed" ]] || summary+="<br><br>Failed: $failed"
[[ -z "$blocked" ]] || summary+="<br>Blocked by failed dependencies: $blocked"
summary+="<br>Published packages will be skipped on retry; failed packages remain queued."
notify_info "Partial release published: $MIRROR" "$summary"
else
notify_success "Release published: $MIRROR" "$summary"
fi
else else
# Rare by construction: a release only runs when the version check queued # Rare by construction: a release only runs when the version check queued
# work, so publishing nothing means the check and the builder disagreed # work, so publishing nothing means the check and the builder disagreed
@@ -275,10 +224,4 @@ else
fi fi
echo "" echo ""
if [[ "$partial" == true ]]; then
print_warning "Completed packages published; unsuccessful packages remain for retry"
# Preserve the scheduled queue and failure backoff. The next version
# check/build compares against the updated repository and skips successes.
exit 1
fi
print_success "Release workflow completed successfully!" print_success "Release workflow completed successfully!"
+10 -15
View File
@@ -62,8 +62,8 @@ if [[ ! -d "$REPO_DIR" ]]; then
exit 1 exit 1
fi fi
# Check the selected container engine is available # Check Docker is available
check_engine check_docker
# Find package files to confirm before running Docker # Find package files to confirm before running Docker
cd "$REPO_DIR" cd "$REPO_DIR"
@@ -88,29 +88,24 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0 exit 0
fi fi
# repo-remove is architecture-independent, so use a host-native edge image. # Build/update the Docker image (always use x86_64 for removal - it's architecture independent)
TOOL_ARCH=$(docker_native_arch) || { # repo-remove is mirror-independent — always use the edge x86_64 image
print_error "Unsupported host architecture: $(uname -m)" build_docker_image "$BUILD_DIR" "x86_64" "edge"
exit 1
}
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "edge"
acquire_release_lock || exit 1 acquire_release_lock || exit 1
print_info "Removing package..." print_info "Removing package..."
# Rootless Podman uses keep-id and leaves host ownership/modes intact. # Ensure directory is writable by container user
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then make_dir_writable "$REPO_DIR"
make_dir_writable "$REPO_DIR"
fi
# Run the removal script in the host-native image. # Run the removal script in Docker (always use x86_64 image)
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \ docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \ -e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \ -e MIRROR="$MIRROR" \
-v "$REPO_ROOT:/pkgs.omarchy.org" \ -v "$REPO_ROOT:/pkgs.omarchy.org" \
-v "$BUILD_DIR:/build:ro" \ -v "$BUILD_DIR:/build:ro" \
"omarchy-pkg-builder:latest-$TOOL_ARCH-edge" /build/remove-package.sh "$PACKAGE_NAME" omarchy-pkg-builder:latest-x86_64-edge /build/remove-package.sh "$PACKAGE_NAME"
RESULT=$? RESULT=$?
+21 -58
View File
@@ -15,8 +15,6 @@ set -e
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
CHECK_ONLY=false CHECK_ONLY=false
SKIP_TIMERS=false SKIP_TIMERS=false
@@ -63,12 +61,10 @@ if command -v apt-get >/dev/null 2>&1; then
DISTRO="debian" DISTRO="debian"
PKG_BSDTAR="libarchive-tools" PKG_BSDTAR="libarchive-tools"
PKG_DOCKER="docker.io" PKG_DOCKER="docker.io"
PKG_PODMAN="podman"
elif command -v pacman >/dev/null 2>&1; then elif command -v pacman >/dev/null 2>&1; then
DISTRO="arch" DISTRO="arch"
PKG_BSDTAR="libarchive" PKG_BSDTAR="libarchive"
PKG_DOCKER="docker" PKG_DOCKER="docker"
PKG_PODMAN="podman"
else else
print_error "Unsupported distribution — need apt-get or pacman" print_error "Unsupported distribution — need apt-get or pacman"
exit 1 exit 1
@@ -76,36 +72,18 @@ fi
print_info "Distribution: $DISTRO" print_info "Distribution: $DISTRO"
# A fresh repository host still defaults to Docker. An explicit Podman choice,
# or a working Podman selected by the shared helper, is left alone.
if [[ -z "$CONTAINER_ENGINE" ]]; then
CONTAINER_ENGINE=docker
export CONTAINER_ENGINE
fi
if ! container_engine_supported; then
print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)"
exit 1
fi
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
PKG_ENGINE="$PKG_DOCKER"
ENGINE_NAME="Docker"
else
PKG_ENGINE="$PKG_PODMAN"
ENGINE_NAME="Podman"
fi
if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then
print_error "Run as root (installing packages and systemd units)" print_error "Run as root (installing packages and systemd units)"
exit 1 exit 1
fi fi
# The release timers are systemd units. Say so plainly rather than failing # Docker and the release timers are both systemd units. Say so plainly rather
# later on a missing command — a container is the usual way to end up here, # than failing later on a missing command — a container is the usual way to end
# and it cannot be a repository host. # up here, and it cannot be a repository host.
if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then
print_error "systemctl not found — the repository host must run systemd" print_error "systemctl not found — the repository host must run systemd"
echo "" echo ""
echo "The release timers are systemd units. This looks like a" echo "Docker and the release timers are systemd units. This looks like a"
echo "container; run setup on the host itself." echo "container; run setup on the host itself."
exit 1 exit 1
fi fi
@@ -161,27 +139,24 @@ else
fi fi
echo "" echo ""
# --- container engine -------------------------------------------------------- # --- docker ------------------------------------------------------------------
# The selected engine is left alone when it already works. A host may be running a # Docker is left alone when it already works. A host may well be running a
# version from Docker's own repository rather than the distribution's, and # version from Docker's own repository rather than the distribution's, and
# replacing that underneath a working builder would be a poor trade for # replacing that underneath a working builder would be a poor trade for
# tidiness. # tidiness.
print_info "Checking $ENGINE_NAME..." print_info "Checking Docker..."
if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
print_step "$CONTAINER_ENGINE present: $("$CONTAINER_ENGINE" --version 2>/dev/null | head -1)" print_step "docker present: $(docker --version 2>/dev/null | head -1)"
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then
print_success "$ENGINE_NAME is installed and available — leaving it alone" print_success "Docker is installed and running — leaving it alone"
elif [[ "$CHECK_ONLY" == true ]]; then elif [[ "$CHECK_ONLY" == true ]]; then
print_warning "$ENGINE_NAME is installed but unavailable" print_warning "Docker is installed but not running; would start it"
elif [[ "$CONTAINER_ENGINE" == "podman" ]]; then
print_error "Podman is installed but unavailable to the current user"
exit 1
else else
print_info "Docker is installed but not running — starting it" print_info "Docker is installed but not running — starting it"
systemctl enable --now docker.service systemctl enable --now docker.service
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then
print_success "Docker started" print_success "Docker started"
else else
print_error "Docker is installed but still not responding" print_error "Docker is installed but still not responding"
@@ -190,24 +165,16 @@ if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
fi fi
fi fi
elif [[ "$CHECK_ONLY" == true ]]; then elif [[ "$CHECK_ONLY" == true ]]; then
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then print_warning "Would install $PKG_DOCKER and enable it"
print_warning "Would install $PKG_ENGINE and enable it"
else
print_warning "Would install $PKG_ENGINE"
fi
else else
print_info "Installing $PKG_ENGINE..." print_info "Installing $PKG_DOCKER..."
install_packages "$PKG_ENGINE" install_packages "$PKG_DOCKER"
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then systemctl enable --now docker.service
systemctl enable --now docker.service if docker info >/dev/null 2>&1; then
fi print_success "Docker installed and running"
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
print_success "$ENGINE_NAME installed and available"
else else
print_error "$ENGINE_NAME installed but not responding" print_error "Docker installed but not responding"
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then echo " Check 'systemctl status docker' — builds cannot run without it."
echo " Check 'systemctl status docker' — builds cannot run without it."
fi
exit 1 exit 1
fi fi
fi fi
@@ -270,10 +237,6 @@ echo ""
# --- release timers ---------------------------------------------------------- # --- release timers ----------------------------------------------------------
print_info "Published architectures: $(published_arches | tr '\n' ' ')"
echo " (PUBLISHED_ARCHES in helpers/paths.sh; OMARCHY_ARCHES overrides a one-off command)"
echo ""
TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-rc omarchy-auto-release-stable) TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-rc omarchy-auto-release-stable)
if [[ "$SKIP_TIMERS" == true ]]; then if [[ "$SKIP_TIMERS" == true ]]; then
+9 -16
View File
@@ -52,8 +52,8 @@ if [[ ! -d "$BUILD_OUTPUT_DIR" ]]; then
exit 1 exit 1
fi fi
# Check the selected container engine is available # Check Docker is available
check_engine check_docker
# Check GPG credentials are in environment # Check GPG credentials are in environment
if [[ -z "$GPG_PRIVATE_KEY" ]]; then if [[ -z "$GPG_PRIVATE_KEY" ]]; then
@@ -66,30 +66,23 @@ if [[ -z "$GPG_PASSPHRASE" ]]; then
exit 1 exit 1
fi fi
# Signing is architecture-independent, so run its utility container natively # Build/update the Docker image (always use x86_64 for signing - it's architecture independent)
# on either an x86_64 or ARM host. build_docker_image "$BUILD_DIR" "x86_64" "$MIRROR"
TOOL_ARCH=$(docker_native_arch) || {
print_error "Unsupported host architecture: $(uname -m)"
exit 1
}
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "$MIRROR"
print_info "Running package signing..." print_info "Running package signing..."
# Rootless Podman uses keep-id and leaves host ownership/modes intact. # Ensure output directory is writable by container user
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then make_dir_writable "$BUILD_OUTPUT_DIR"
make_dir_writable "$BUILD_OUTPUT_DIR"
fi
# Run the signing script in the host-native image. # Run the signing script in Docker (always use x86_64 image)
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \ docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \ -e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \ -e MIRROR="$MIRROR" \
-e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \ -e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \
-e GPG_PASSPHRASE="$GPG_PASSPHRASE" \ -e GPG_PASSPHRASE="$GPG_PASSPHRASE" \
-v "$BUILD_ROOT/build-output:/build-output" \ -v "$BUILD_ROOT/build-output:/build-output" \
-v "$BUILD_DIR:/build:ro" \ -v "$BUILD_DIR:/build:ro" \
"omarchy-pkg-builder:latest-$TOOL_ARCH-$MIRROR" /build/sign.sh omarchy-pkg-builder:latest-x86_64-$MIRROR /build/sign.sh
SIGN_RESULT=$? SIGN_RESULT=$?
Executable
+434
View File
@@ -0,0 +1,434 @@
#!/bin/bash
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/package-metadata.sh"
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
SPECIFIC_PACKAGES=()
usage() {
cat <<EOF
Usage: $0 [PACKAGE...]
Sync AUR-backed packages into pkgbuilds/<package>/.
Package selection is driven by pkgbuilds/<package>/.omarchy/package.json:
{ "source": "aur" } # synced from matching AUR package name
{ "source": "aur", "aur": "yaru" } # synced from different AUR package name
{ "source": "aur", "sync": false } # AUR-origin, but not auto-synced
Arguments:
PACKAGE One or more package names to sync (optional)
Examples:
$0 # Sync all AUR packages with sync enabled
$0 yay cursor-bin # Sync specific packages
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--tier)
print_error "--tier is no longer supported; package metadata controls sync behavior"
exit 1
;;
--*)
print_error "Unknown option: $1"
exit 1
;;
*)
SPECIFIC_PACKAGES+=("$1")
shift
;;
esac
done
print_header "AUR Package Sync"
mkdir -p "$PKGBUILDS_DIR"
SYNCED=0
SKIPPED=0
FAILED=0
SYNCED_PACKAGES=()
SPECIFIC_MODE=false
get_pkgbuild_field() {
local package_dir="$1"
local field="$2"
local value=""
if [[ -f "$package_dir/PKGBUILD" ]]; then
value=$(grep -m1 "^${field}=" "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'") || true
if [[ -n "$value" ]]; then
echo "$value"
return
fi
fi
if [[ -f "$package_dir/.SRCINFO" ]]; then
awk -F' = ' -v field="$field" '$1 ~ "^[[:space:]]*" field "$" { print $2; exit }' "$package_dir/.SRCINFO"
fi
}
set_pkgrel() {
local package_dir="$1"
local pkgrel="$2"
local pkgbuild="$package_dir/PKGBUILD"
if [[ -f "$pkgbuild" ]]; then
sed -i "s/^pkgrel=.*/pkgrel=$pkgrel/" "$pkgbuild"
fi
}
display_package_name() {
local package_dir="$1"
local name
name=$(basename "$package_dir")
echo "${name%.work}"
}
remove_aur_only_files() {
local package_dir="$1"
rm -f "$package_dir/.SRCINFO" "$package_dir/.gitignore"
}
copy_aur_contents() {
local aur_dir="$1"
local target_dir="$2"
mkdir -p "$target_dir"
shopt -s dotglob nullglob
local item base
for item in "$aur_dir"/*; do
base=$(basename "$item")
[[ "$base" == ".git" ]] && continue
cp -a "$item" "$target_dir/"
done
shopt -u dotglob nullglob
}
commit_synced_worktree() {
local work_dir="$1"
local target_dir="$2"
local parent base staged_dir backup_root backup_dir
parent=$(dirname "$target_dir")
base=$(basename "$target_dir")
staged_dir=$(mktemp -d "$parent/.${base}.staged.XXXXXX")
backup_root=$(mktemp -d "$parent/.${base}.backup.XXXXXX")
backup_dir="$backup_root/$base"
# Copy to the package filesystem before swapping. This keeps the original
# package directory intact if copying from /tmp fails or is interrupted.
if ! cp -a "$work_dir/." "$staged_dir/"; then
print_error "Failed to stage synced package for $base"
rm -rf "$staged_dir" "$backup_root"
return 1
fi
if [[ -e "$target_dir" ]]; then
if ! mv "$target_dir" "$backup_dir"; then
print_error "Failed to back up existing package directory: $target_dir"
rm -rf "$staged_dir" "$backup_root"
return 1
fi
fi
if ! mv "$staged_dir" "$target_dir"; then
print_error "Failed to install synced package directory: $target_dir"
if [[ -e "$backup_dir" ]]; then
mv "$backup_dir" "$target_dir" || true
fi
rm -rf "$staged_dir" "$backup_root"
return 1
fi
rm -rf "$backup_root" "$work_dir"
}
set_upstream_commit() {
local package_dir="$1"
local commit="$2"
local metadata="$package_dir/.omarchy/package.json"
local tmpfile
tmpfile=$(mktemp)
jq --arg commit "$commit" '.upstream_commit = $commit' "$metadata" > "$tmpfile"
mv "$tmpfile" "$metadata"
}
apply_omarchy_patches() {
local package_dir="$1"
local patches_dir="$package_dir/.omarchy/patches"
local applied=false
[[ -d "$patches_dir" ]] || return 1
shopt -s nullglob
local patch_files=("$patches_dir"/*.patch)
local patch_dir_files=("$patches_dir"/*)
shopt -u nullglob
if [[ ${#patch_files[@]} -eq 0 ]]; then
if [[ ${#patch_dir_files[@]} -gt 0 ]]; then
print_warning "No .patch files found in $patches_dir"
fi
return 1
fi
print_info "Applying Omarchy patches for $(display_package_name "$package_dir")..."
local patch_file
for patch_file in "${patch_files[@]}"; do
print_info " $(basename "$patch_file")"
if ! (cd "$package_dir" && patch -p1 --forward --batch --no-backup-if-mismatch < "$patch_file"); then
print_error "Failed to apply patch: $patch_file"
return 2
fi
applied=true
done
[[ "$applied" == true ]]
}
run_omarchy_post_sync_hook() {
local package_dir="$1"
local package="$2"
local aur_package="$3"
local aur_pkgrel="$4"
local hook="$package_dir/.omarchy/post-sync.sh"
[[ -f "$hook" ]] || return 1
print_info "Running Omarchy post-sync hook for $(display_package_name "$package_dir")..."
if ! (
cd "$package_dir"
PACKAGE_NAME="$package" \
AUR_PACKAGE_NAME="$aur_package" \
AUR_PKGREL="$aur_pkgrel" \
bash ".omarchy/post-sync.sh"
); then
print_error "Failed to run post-sync hook: $hook"
return 2
fi
return 0
}
apply_pkgrel_suffix_if_customized() {
local package_dir="$1"
local aur_pkgrel="$2"
[[ -n "$aur_pkgrel" ]] || return 0
print_info "Applying Omarchy pkgrel suffix for $(display_package_name "$package_dir"): pkgrel=$aur_pkgrel.1"
set_pkgrel "$package_dir" "$aur_pkgrel.1"
}
apply_pkgrel_override() {
local package_dir="$1"
local aur_pkgrel="$2"
local previous_pkgver="$3"
local metadata="$package_dir/.omarchy/package.json"
local pkgbuild="$package_dir/PKGBUILD"
[[ -f "$metadata" ]] || return 1
jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1
local current_pkgver suffix offset base rel tmpfile
# Read through the same accessor that produced previous_pkgver. Parsing it a
# second time here let a quoted pkgver= compare unequal to itself, which threw
# away the pkgrel metadata of an unchanged package on every sync.
current_pkgver=$(get_pkgbuild_field "$package_dir" pkgver)
if [[ -n "$previous_pkgver" && "$current_pkgver" != "$previous_pkgver" ]]; then
print_info "Removing stale pkgrel metadata for $(display_package_name "$package_dir") (pkgver changed: $previous_pkgver -> $current_pkgver)"
tmpfile=$(mktemp)
jq 'del(.pkgrel)' "$metadata" > "$tmpfile"
mv "$tmpfile" "$metadata"
return 1
fi
suffix=$(jq -r '.pkgrel.suffix // 1' "$metadata")
offset=$(jq -r '.pkgrel.offset // 0' "$metadata")
if [[ ! "$offset" =~ ^[0-9]+$ || ! "$aur_pkgrel" =~ ^[0-9]+$ ]]; then
print_error "pkgrel offset requires numeric AUR pkgrel for $(display_package_name "$package_dir")"
return 2
fi
base=$((aur_pkgrel + offset))
rel="$base.$suffix"
print_info "Applying pkgrel suffix for $(display_package_name "$package_dir"): AUR pkgrel=$aur_pkgrel, offset=$offset, suffix=$suffix -> pkgrel=$rel"
set_pkgrel "$package_dir" "$rel"
return 0
}
clone_aur_package() {
local aur_package="$1"
local dest="$2"
local clone_log="$TEMP_DIR/clone.log"
local attempt
for attempt in 1 2 3; do
rm -rf "$dest"
if git clone "https://aur.archlinux.org/${aur_package}.git" "$dest" >"$clone_log" 2>&1; then
return 0
fi
if [[ $attempt -lt 3 ]]; then
print_warning "Clone of $aur_package failed (attempt $attempt/3), retrying in 10s..."
sleep 10
fi
done
print_warning "Failed to clone $aur_package after 3 attempts: $(tail -n 1 "$clone_log")"
return 1
}
sync_package() {
local package="$1"
local package_dir="$PKGBUILDS_DIR/$package"
local metadata="$package_dir/.omarchy/package.json"
if [[ ! -f "$metadata" ]]; then
if [[ "$SPECIFIC_MODE" == true ]]; then
print_error "Package $package is missing .omarchy/package.json"
((++FAILED))
else
print_warning "Skipping $package: missing .omarchy/package.json"
((++SKIPPED))
fi
return 0
fi
if [[ "$(jq -r '.source // ""' "$metadata")" != "aur" ]]; then
print_info "Skipping $package: source is not AUR"
((++SKIPPED))
return 0
fi
if [[ "$(jq -r 'if has("sync") then .sync else true end' "$metadata")" == "false" ]]; then
print_info "Skipping $package: AUR sync disabled"
((++SKIPPED))
return 0
fi
local aur_package
aur_package=$(jq -r --arg package "$package" '.aur // $package' "$metadata")
if [[ "$aur_package" == "$package" ]]; then
print_info "Syncing $package from AUR..."
else
print_info "Syncing $package from AUR package $aur_package..."
fi
cd "$TEMP_DIR"
if ! clone_aur_package "$aur_package" "$TEMP_DIR/$aur_package"; then
((++FAILED))
return 0
fi
if [[ ! -f "$TEMP_DIR/$aur_package/PKGBUILD" ]]; then
print_warning "AUR repository for $aur_package is empty (package does not exist in AUR)"
((++FAILED))
return 0
fi
local aur_dir="$TEMP_DIR/$aur_package"
local work_dir="$TEMP_DIR/${package}.work"
local aur_pkgrel previous_pkgver upstream_commit
aur_pkgrel=$(get_pkgbuild_field "$aur_dir" pkgrel)
previous_pkgver=$(get_pkgbuild_field "$package_dir" pkgver || true)
upstream_commit=$(git -C "$aur_dir" rev-parse HEAD)
rm -rf "$work_dir"
copy_aur_contents "$aur_dir" "$work_dir"
rm -rf "$work_dir/.omarchy"
cp -a "$package_dir/.omarchy" "$work_dir/.omarchy"
local customized=false
if apply_omarchy_patches "$work_dir"; then
customized=true
else
local patch_status=$?
if [[ $patch_status -eq 2 ]]; then
((++FAILED))
return 0
fi
fi
if run_omarchy_post_sync_hook "$work_dir" "$package" "$aur_package" "$aur_pkgrel"; then
customized=true
else
local hook_status=$?
if [[ $hook_status -eq 2 ]]; then
((++FAILED))
return 0
fi
fi
local pkgrel_overridden=false
set +e
apply_pkgrel_override "$work_dir" "$aur_pkgrel" "$previous_pkgver"
local pkgrel_status=$?
set -e
case "$pkgrel_status" in
0) pkgrel_overridden=true ;;
1) ;;
*) ((++FAILED)); return 0 ;;
esac
if [[ "$customized" == true && "$pkgrel_overridden" == false ]]; then
apply_pkgrel_suffix_if_customized "$work_dir" "$aur_pkgrel"
fi
remove_aur_only_files "$work_dir"
set_upstream_commit "$work_dir" "$upstream_commit"
if ! commit_synced_worktree "$work_dir" "$package_dir"; then
((++FAILED))
return 0
fi
SYNCED_PACKAGES+=("$package")
((++SYNCED))
}
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
SPECIFIC_MODE=true
for package in "${SPECIFIC_PACKAGES[@]}"; do
sync_package "$package"
done
else
while IFS= read -r package; do
sync_package "$package"
done < <(packages_for_aur_sync)
fi
echo ""
if [[ $FAILED -gt 0 ]]; then
print_error "Sync completed with failures"
else
print_success "Sync complete!"
fi
echo " Target: $PKGBUILDS_DIR"
echo " Synced: $SYNCED"
echo " Skipped: $SKIPPED"
echo " Failed: $FAILED"
if [[ $FAILED -gt 0 ]]; then
exit 1
fi
+75 -470
View File
@@ -17,14 +17,11 @@ SELF_TEST=false
# rebuilding for it would ship a package built against the wrong ABI. # rebuilding for it would ship a package built against the wrong ABI.
OFFICIAL_REPOS=" core extra multilib core-debug extra-debug " OFFICIAL_REPOS=" core extra multilib core-debug extra-debug "
# The published repositories are the floor a bumped pkgrel has to clear. # The published repository, used as the floor a bumped pkgrel has to clear.
# Only x86_64 is published today; aarch64 has no repository to compare against.
PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}" PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}"
PUBLISHED_MIRRORS=(edge stable) PUBLISHED_MIRRORS=(edge stable)
PUBLISHED_ARCH=x86_64
# Arch Linux ARM has no dated snapshots. This is the same live repository the
# aarch64 builder resolves; override it only when the builder mirror changes.
ALARM_BASE_URL="${OMARCHY_ALARM_BASE_URL:-https://fl.us.mirror.archlinuxarm.org/aarch64}"
ALARM_REPOS=(core extra alarm aur)
usage() { usage() {
cat <<EOF cat <<EOF
@@ -37,13 +34,10 @@ A package opts in by naming those dependencies in .omarchy/package.json:
{ "source": "aur", "sync": false, "rebuild_on": ["qt6-base"] } { "source": "aur", "sync": false, "rebuild_on": ["qt6-base"] }
The versions the current pkgrel was bumped for are recorded per published The versions the current pkgrel was bumped for are recorded alongside, in
architecture in rebuilt_against, and written by this command: rebuilt_against, and written by this command:
{ "rebuild_on": ["qt6-base"], "rebuilt_against": { { "rebuild_on": ["qt6-base"], "rebuilt_against": { "qt6-base": "6.11.2-2" } }
"x86_64": { "qt6-base": "6.11.2-3" },
"aarch64": { "qt6-base": "6.11.2-2" }
} }
pkgrel is bumped unless every package named in rebuild_on is recorded and still pkgrel is bumped unless every package named in rebuild_on is recorded and still
matches. A name that is missing from the record counts as changed, so opting a matches. A name that is missing from the record counts as changed, so opting a
@@ -66,11 +60,8 @@ Examples:
$0 # Update every package that declares rebuild_on $0 # Update every package that declares rebuild_on
$0 quickshell-git # Update specific packages $0 quickshell-git # Update specific packages
x86_64 trigger versions come from the local pacman database; aarch64 versions Trigger versions are read from the local pacman database, so sync it first
come from the live Arch Linux ARM repository database. A trigger this (pacman -Sy) or this reports whatever that database last saw.
repository carries for an architecture shadows both, so its version is the
checked-in recipe's once edge publishes it. Every architecture in CI_ARCHES
(what a merge builds) that the package supports is considered.
EOF EOF
} }
@@ -103,7 +94,7 @@ SPECIFIC_MODE=false
# The version of a trigger package as the build container would resolve it. # The version of a trigger package as the build container would resolve it.
# A name pacman does not know reports nothing rather than failing, so the caller # A name pacman does not know reports nothing rather than failing, so the caller
# gets to say which package was left alone instead of the run dying here. # gets to say which package was left alone instead of the run dying here.
native_repo_version() { repo_version() {
local package="$1" local package="$1"
local info local info
@@ -117,61 +108,9 @@ native_repo_version() {
' <<<"$info" ' <<<"$info"
} }
load_alarm_repo() {
local repo="$1" db index
index="$TEMP_DIR/alarm-$repo.index"
[[ -f "$index" ]] && return 0
db="$TEMP_DIR/alarm-$repo.db"
if ! curl -fsSL --max-time 120 -o "$db" "$ALARM_BASE_URL/$repo/$repo.db" 2>/dev/null; then
print_error "Could not read the aarch64 $repo repository database"
return 1
fi
if ! tar -tf "$db" >/dev/null 2>&1; then
print_error "Unreadable aarch64 $repo repository database"
return 1
fi
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
function emit() {
if (name != "" && version != "") {
print name "\t" version
if (base != "" && base != name) print base "\t" version
}
name=""; base=""; version=""
}
$0 == "%FILENAME%" { emit(); next }
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
$0 == "%BASE%" { getline; base=$0; next }
$0 == "%VERSION%" { getline; version=$0; next }
END { emit() }
' >"$index"
}
alarm_repo_version() {
local package="$1" repo version
for repo in "${ALARM_REPOS[@]}"; do
load_alarm_repo "$repo" || return 1
version=$(awk -F '\t' -v package="$package" '$1 == package { print $2; exit }' "$TEMP_DIR/alarm-$repo.index")
if [[ -n "$version" ]]; then
echo "$version"
return 0
fi
done
}
repo_version() { # repo_version <arch> <package>
case "$1" in
x86_64) native_repo_version "$2" ;;
aarch64) alarm_repo_version "$2" ;;
*) return 1 ;;
esac
}
declare -A PUBLISHED_VERSION=() declare -A PUBLISHED_VERSION=()
declare -A EDGE_VERSION=()
declare -A EDGE_READ=()
PUBLISHED_LOADED=false PUBLISHED_LOADED=false
PUBLISHED_AVAILABLE=true
remember_published() { remember_published() {
local name="$1" local name="$1"
@@ -192,44 +131,34 @@ load_published_versions() {
[[ "$PUBLISHED_LOADED" == true ]] && return 0 [[ "$PUBLISHED_LOADED" == true ]] && return 0
PUBLISHED_LOADED=true PUBLISHED_LOADED=true
local arch mirror db name base version local mirror db name base version
for arch in $(ci_arches); do for mirror in "${PUBLISHED_MIRRORS[@]}"; do
for mirror in "${PUBLISHED_MIRRORS[@]}"; do db="$TEMP_DIR/published-$mirror.db.tar.zst"
db="$TEMP_DIR/published-$mirror-$arch.db.tar.zst"
if ! curl -fsSL --max-time 120 -o "$db" \ if ! curl -fsSL --max-time 120 -o "$db" \
"$PUBLISHED_BASE_URL/$mirror/$arch/omarchy.db.tar.zst" 2>/dev/null; then "$PUBLISHED_BASE_URL/$mirror/$PUBLISHED_ARCH/omarchy.db.tar.zst" 2>/dev/null; then
print_warning "Could not read the published $mirror/$arch database; bumps are not checked against it this run" print_warning "Could not read the published $mirror database; bumps are not checked against it this run"
continue PUBLISHED_AVAILABLE=false
fi continue
if ! tar -tf "$db" >/dev/null 2>&1; then fi
print_warning "Unreadable published $mirror/$arch database; bumps are not checked against it this run"
continue
fi
[[ "$mirror" == edge ]] && EDGE_READ["$arch"]=1
while IFS=$'\t' read -r name base version; do while IFS=$'\t' read -r name base version; do
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version" [[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version" [[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
if [[ "$mirror" == edge && -n "$version" ]]; then done < <(
[[ -z "$name" ]] || EDGE_VERSION["$arch/$name"]="$version" tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
[[ -z "$base" ]] || EDGE_VERSION["$arch/$base"]="$version" function emit() {
fi if (name != "" && version != "") print name "\t" base "\t" version
done < <( name=""; base=""; version=""
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk ' }
function emit() { $0 == "%FILENAME%" { emit(); next }
if (name != "" && version != "") print name "\t" base "\t" version $0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
name=""; base=""; version="" $0 == "%BASE%" { getline; base=$0; next }
} $0 == "%VERSION%" { getline; version=$0; next }
$0 == "%FILENAME%" { emit(); next } END { emit() }
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next } '
$0 == "%BASE%" { getline; base=$0; next } )
$0 == "%VERSION%" { getline; version=$0; next }
END { emit() }
'
)
done
done done
} }
@@ -240,29 +169,6 @@ published_version() {
echo "${PUBLISHED_VERSION[$package]:-}" echo "${PUBLISHED_VERSION[$package]:-}"
} }
# A trigger this repository builds for the architecture. The builder lists
# [omarchy] ahead of the distribution repositories, so this recipe, not Arch or
# Arch Linux ARM, decides what a dependent links against.
carried_trigger_dir() { # carried_trigger_dir <arch> <trigger>
local pkgdir
pkgdir=$(package_dir_for_name "$2") || return 1
package_has_metadata "$pkgdir" || return 1
package_builds_for_mirror "$pkgdir" edge || return 1
package_supports_arch "$pkgdir" "$1" || return 1
echo "$pkgdir"
}
carried_version() { # carried_version <arch> <pkgdir>
local epoch pkgver pkgrel
epoch=$(package_pkgbuild_var "$2" epoch "$1") || return 1
pkgver=$(package_pkgbuild_var "$2" pkgver "$1") || return 1
pkgrel=$(package_pkgbuild_var "$2" pkgrel "$1") || return 1
[[ -n "$pkgver" && -n "$pkgrel" ]] || return 1
# makepkg leaves a zero epoch out of the published version
[[ "$epoch" != 0 ]] || epoch=""
echo "${epoch:+$epoch:}$pkgver-$pkgrel"
}
# The pkgver of a full version string, with any epoch and pkgrel removed. # The pkgver of a full version string, with any epoch and pkgrel removed.
version_pkgver() { version_pkgver() {
local version="${1#*:}" local version="${1#*:}"
@@ -273,7 +179,7 @@ pkgbuild_field() {
local package_dir="$1" local package_dir="$1"
local field="$2" local field="$2"
package_pkgbuild_var "$package_dir" "$field" (cd "$package_dir" && env -u OMARCHY_SRC bash -c "source PKGBUILD 2>/dev/null; echo \"\${$field:-}\"")
} }
# 2 -> 3, and 1.1 -> 1.2. Anything else is a pkgrel this command has no business # 2 -> 3, and 1.1 -> 1.2. Anything else is a pkgrel this command has no business
@@ -329,43 +235,6 @@ write_pkgrel() {
mv "$scratch" "$pkgbuild" mv "$scratch" "$pkgbuild"
} }
# A package built against a trigger's ABI pins it exactly, once per architecture
# because Arch Linux ARM can trail x86_64: depends_x86_64=('hyprland=0.56.2').
# Rebuilding against a newer trigger has to move that pin with it, or the
# rebuilt package demands the version it no longer loads into. Unpinned
# dependencies, and pins outside the per-architecture arrays, are left alone.
write_pins() {
local package_dir="$1"
local current="$2"
local pkgbuild="$package_dir/PKGBUILD"
local scratch="$pkgbuild.sync-rebuilds"
local arch trigger version pattern
cp "$pkgbuild" "$scratch" || return 1
while IFS=$'\t' read -r arch trigger version; do
pattern=$(printf '%s' "$trigger" | sed 's/[][\.*^$+?(){}|/]/\\&/g')
if ! awk -v array="depends_$arch" -v pattern="$pattern" -v pin="$trigger=$(version_pkgver "$version")" '
$0 ~ "^" array "=\\(" { inside = 1 }
inside { gsub("[\"\047]" pattern "=[^\"\047]*[\"\047]", "\047" pin "\047") }
inside && /\)/ { inside = 0 }
{ print }
' "$scratch" > "$scratch.new"; then
rm -f "$scratch" "$scratch.new"
return 1
fi
mv "$scratch.new" "$scratch"
done < <(jq -r 'to_entries[] | .key as $arch | .value | to_entries[] | [$arch, .key, .value] | @tsv' <<<"$current")
if ! bash -n "$scratch" 2>/dev/null; then
print_error "PKGBUILD with moved pins is not valid shell"
rm -f "$scratch"
return 1
fi
chmod --reference="$pkgbuild" "$scratch"
mv "$scratch" "$pkgbuild"
}
write_metadata() { write_metadata() {
local package_dir="$1" local package_dir="$1"
local filter="$2" local filter="$2"
@@ -383,17 +252,7 @@ record_triggers() {
local package_dir="$1" local package_dir="$1"
local current="$2" local current="$2"
# A flat record is the legacy x86_64 shape. Preserve records for write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current"
# architectures outside this run, then replace the ones just rebuilt.
write_metadata "$package_dir" '
(.rebuilt_against // {}) as $old |
(if ($old | length) == 0 then {}
elif ($old | to_entries | all(.value | type == "string"))
then {x86_64: $old}
else $old
end) as $by_arch |
.rebuilt_against = ($by_arch * $current)
' --argjson current "$current"
} }
# Metadata that does not parse would otherwise drop its package out of the run # Metadata that does not parse would otherwise drop its package out of the run
@@ -441,61 +300,21 @@ sync_package() {
print_info "Checking $package against ${triggers[*]}..." print_info "Checking $package against ${triggers[*]}..."
local current="{}" arch arch_current trigger version carried considered=0 local current="{}" trigger version
for arch in $(ci_arches); do for trigger in "${triggers[@]}"; do
package_supports_arch "$package_dir" "$arch" || continue version=$(repo_version "$trigger")
considered=$((considered + 1)) if [[ -z "$version" ]]; then
arch_current="{}" print_error " $trigger is in no official repository; leaving $package alone"
for trigger in "${triggers[@]}"; do ((++FAILED))
if carried=$(carried_trigger_dir "$arch" "$trigger"); then return 0
if ! version=$(carried_version "$arch" "$carried"); then fi
print_error " Could not read the $arch version of $trigger from its recipe; leaving $package alone" if ! current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$current"); then
((++FAILED)) print_error " Could not record $trigger $version for $package"
return 0
fi
load_published_versions
if [[ -z "${EDGE_READ[$arch]:-}" ]]; then
print_error " Could not read the published edge/$arch database for $trigger; leaving $package alone"
((++FAILED))
return 0
fi
# Until edge publishes the recipe's version the builder still links
# against the previous one, and recording the new version now would
# certify a build that never saw it.
if [[ "${EDGE_VERSION[$arch/$trigger]:-}" != "$version" ]]; then
print_warning " $trigger $version is not published on edge/$arch yet; leaving $package alone until it is"
((++SKIPPED))
return 0
fi
elif ! version=$(repo_version "$arch" "$trigger"); then
print_error " Could not read $arch repository versions; leaving $package alone"
((++FAILED))
return 0
fi
if [[ -z "$version" ]]; then
print_error " $trigger is in no $arch repository; leaving $package alone"
((++FAILED))
return 0
fi
if ! arch_current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$arch_current"); then
print_error " Could not record $arch/$trigger $version for $package"
((++FAILED))
return 0
fi
done
if ! current=$(jq -c --arg arch "$arch" --argjson versions "$arch_current" '.[$arch] = $versions' <<<"$current"); then
print_error " Could not record $arch trigger versions for $package"
((++FAILED)) ((++FAILED))
return 0 return 0
fi fi
done done
if ((considered == 0)); then
print_info " Skipping: not built for any published architecture"
((++SKIPPED))
return 0
fi
local recorded local recorded
if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then
print_error " Could not read .omarchy/package.json for $package" print_error " Could not read .omarchy/package.json for $package"
@@ -504,20 +323,13 @@ sync_package() {
fi fi
[[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}" [[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}"
# Before architecture-specific records existed, rebuilt_against described
# x86_64. Read it that way without forcing a metadata-only migration.
if jq -e 'to_entries | all(.value | type == "string")' >/dev/null <<<"$recorded"; then
recorded=$(jq -c '{x86_64: .}' <<<"$recorded")
fi
# Walk the declared triggers rather than the record, so a name the record does # Walk the declared triggers rather than the record, so a name the record does
# not carry reads as changed instead of going unexamined forever. # not carry reads as changed instead of going unexamined forever.
local moved local moved
if ! moved=$(jq -r --argjson recorded "$recorded" ' if ! moved=$(jq -r --argjson recorded "$recorded" '
[to_entries[] as $arch to_entries
| $arch.value | to_entries[] as $trigger | map(select($recorded[.key] != .value)
| select($recorded[$arch.key][$trigger.key] != $trigger.value) | "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)")
| "\($arch.key)/\($trigger.key) \($recorded[$arch.key][$trigger.key] // "unrecorded") -> \($trigger.value)"]
| join(", ") | join(", ")
' <<<"$current"); then ' <<<"$current"); then
print_error " Could not compare recorded trigger versions for $package" print_error " Could not compare recorded trigger versions for $package"
@@ -526,7 +338,7 @@ sync_package() {
fi fi
if [[ -z "$moved" ]]; then if [[ -z "$moved" ]]; then
print_info " Already rebuilt against every published architecture" print_info " Already rebuilt against $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")"
((++SKIPPED)) ((++SKIPPED))
return 0 return 0
fi fi
@@ -572,7 +384,6 @@ sync_package() {
# checkout that has fallen behind the repository can otherwise be bumped to # checkout that has fallen behind the repository can otherwise be bumped to
# something pacman orders below what it would replace. # something pacman orders below what it would replace.
local floor local floor
load_published_versions
floor=$(published_version "$package") floor=$(published_version "$package")
if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then
if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then
@@ -588,12 +399,6 @@ sync_package() {
return 0 return 0
fi fi
if ! write_pins "$package_dir" "$current"; then
print_error " Failed to move pinned trigger versions for $package"
((++FAILED))
return 0
fi
if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then
print_error " Failed to record pkgrel suffix for $package" print_error " Failed to record pkgrel suffix for $package"
((++FAILED)) ((++FAILED))
@@ -661,11 +466,11 @@ selftest_root() {
} }
selftest_package() { selftest_package() {
local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}" arches="${6:-x86_64}" local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}"
local dir="$root/pkgbuilds/$name" local dir="$root/pkgbuilds/$name"
mkdir -p "$dir/.omarchy" mkdir -p "$dir/.omarchy"
printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(%s)\n' "$name" "$pkgver" "$pkgrel" "$arches" > "$dir/PKGBUILD" printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(x86_64)\n' "$name" "$pkgver" "$pkgrel" > "$dir/PKGBUILD"
printf '%s\n' "$metadata" > "$dir/.omarchy/package.json" printf '%s\n' "$metadata" > "$dir/.omarchy/package.json"
} }
@@ -684,97 +489,44 @@ STUB
chmod +x "$root/stub/pacman" chmod +x "$root/stub/pacman"
} }
# A repository database assembled by hand from name=version pairs. # Serves a repository database assembled by hand from name=version pairs. With
selftest_db() { # none given the stub fails, which is how the unreachable-repository path is
local out="$1"
shift
local staging="$out.d" entry name version
rm -rf "$staging"
mkdir -p "$staging"
for entry in "$@"; do
name="${entry%=*}"
version="${entry#*=}"
mkdir -p "$staging/$name-$version"
printf '%%FILENAME%%\n%s-%s-x86_64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
done
tar --zstd -cf "$out" -C "$staging" .
}
# Serves one published database for every channel and architecture. With no
# pairs given the stub fails, which is how the unreachable-repository path is
# exercised. # exercised.
selftest_published() { selftest_published() {
local root="$1" local root="$1"
shift shift
local staging="$root/stub/db"
local entry name version
if [[ $# -gt 0 ]]; then if [[ $# -gt 0 ]]; then
selftest_db "$root/stub/omarchy.db.tar.zst" "$@" rm -rf "$staging"
mkdir -p "$staging"
for entry in "$@"; do
name="${entry%=*}"
version="${entry#*=}"
mkdir -p "$staging/$name-$version"
printf '%%FILENAME%%\n%s-%s-x86_64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
done
tar --zstd -cf "$root/stub/omarchy.db.tar.zst" -C "$staging" .
fi fi
cat > "$root/stub/curl" <<'STUB' cat > "$root/stub/curl" <<'STUB'
#!/bin/bash #!/bin/bash
out="" out=""
url=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-o) out="$2"; shift 2 ;; -o) out="$2"; shift 2 ;;
*) url="$1"; shift ;; *) shift ;;
esac esac
done done
case "$url" in db="$(dirname "$0")/omarchy.db.tar.zst"
*/omarchy.db.tar.zst)
channel="${url%/omarchy.db.tar.zst}"
arch="${channel##*/}"
channel="${channel%/*}"
db="$(dirname "$0")/omarchy-${channel##*/}-$arch.db.tar.zst"
[[ -f "$db" ]] || db="$(dirname "$0")/omarchy.db.tar.zst"
;;
*/aarch64/*)
repo="${url%/*}"
db="$(dirname "$0")/alarm-${repo##*/}.db"
;;
*) db="" ;;
esac
[[ -f "$db" && -n "$out" ]] || exit 22 [[ -f "$db" && -n "$out" ]] || exit 22
cp "$db" "$out" cp "$db" "$out"
STUB STUB
chmod +x "$root/stub/curl" chmod +x "$root/stub/curl"
} }
# Serves one channel and architecture its own published database.
selftest_channel() { # selftest_channel <root> <channel> <arch> [name=version...]
local root="$1" channel="$2" arch="$3"
shift 3
selftest_db "$root/stub/omarchy-$channel-$arch.db.tar.zst" "$@"
}
selftest_alarm() {
local root="$1"
shift
local repo staging="$root/stub/alarm-db" entry name version
for repo in core extra; do
rm -rf "$staging"
mkdir -p "$staging"
if [[ "$repo" == "core" ]]; then
mkdir -p "$staging/unrelated-1-1"
printf '%%FILENAME%%\nunrelated-1-1-aarch64.pkg.tar.zst\n\n%%NAME%%\nunrelated\n\n%%BASE%%\nunrelated\n\n%%VERSION%%\n1-1\n' \
> "$staging/unrelated-1-1/desc"
else
for entry in "$@"; do
name="${entry%=*}"
version="${entry#*=}"
mkdir -p "$staging/$name-$version"
printf '%%FILENAME%%\n%s-%s-aarch64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
done
fi
tar -czf "$root/stub/alarm-$repo.db" -C "$staging" .
done
}
cmd_self_test() { cmd_self_test() {
local failures=0 local failures=0
local root local root
@@ -789,15 +541,11 @@ cmd_self_test() {
fi fi
} }
# SELFTEST_ARCHES stands in for CI_ARCHES; "default" leaves it unset, as the
# scheduled workflow does.
run_case() { run_case() {
local root="$1" local root="$1"
shift shift
local status=0 arches=(CI_ARCHES="${SELFTEST_ARCHES:-x86_64}") local status=0
[[ "${SELFTEST_ARCHES:-}" != default ]] || arches=(-u CI_ARCHES) PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
env "${arches[@]}" PATH="$root/stub:$PATH" \
"$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
echo "$status" echo "$status"
} }
@@ -815,7 +563,7 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")" check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")" check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")"
check "unrecorded trigger now recorded" "2-2" \ check "unrecorded trigger now recorded" "2-2" \
"$(jq -r '.rebuilt_against.x86_64["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")" "$(jq -r '.rebuilt_against["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")"
echo "Opting a package in buys a rebuild rather than a bare record:" echo "Opting a package in buys a rebuild rather than a bare record:"
root=$(selftest_root fresh) root=$(selftest_root fresh)
@@ -825,7 +573,7 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")" check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")" check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")"
check "trigger recorded" "1-1" \ check "trigger recorded" "1-1" \
"$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")" "$(jq -r '.rebuilt_against["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")"
echo "An unchanged package is left alone:" echo "An unchanged package is left alone:"
root=$(selftest_root current) root=$(selftest_root current)
@@ -835,17 +583,6 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")" check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")" check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")"
echo "A rebuild moves the trigger's exact pin for the architecture it ran for:"
root=$(selftest_root pins)
selftest_package "$root" t-pins 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1.0-1"}}'
printf "depends_x86_64=('dep-a=1.0' 'dep-b')\ndepends_aarch64=('dep-a=1.0')\n" >> "$root/pkgbuilds/t-pins/PKGBUILD"
selftest_pacman "$root" dep-a=2.0-1
selftest_published "$root"
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-pins")"
check "x86_64 pin moved" "depends_x86_64=('dep-a=2.0' 'dep-b')" "$(grep '^depends_x86_64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
check "aarch64 pin left for its own run" "depends_aarch64=('dep-a=1.0')" "$(grep '^depends_aarch64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
echo "Metadata that does not parse fails the run instead of vanishing from it:" echo "Metadata that does not parse fails the run instead of vanishing from it:"
root=$(selftest_root unreadable) root=$(selftest_root unreadable)
selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"' selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"'
@@ -875,138 +612,6 @@ cmd_self_test() {
check "suffix recorded for the next AUR sync" 1 \ check "suffix recorded for the next AUR sync" 1 \
"$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")" "$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")"
echo "A dependency is tracked independently for both published architectures:"
root=$(selftest_root multiarch)
selftest_package "$root" t-multi 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}' 1.0 'x86_64 aarch64'
selftest_pacman "$root" dep-a=1-1
selftest_published "$root"
selftest_alarm "$root" dep-a=2-1
SELFTEST_ARCHES="x86_64 aarch64"
check "run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel bumped once" 2 "$(pkgrel_of "$root/pkgbuilds/t-multi")"
check "x86_64 trigger recorded" "1-1" \
"$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")"
check "aarch64 trigger recorded" "2-1" \
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")"
echo "An ARM-only run records only the ARM dependency state:"
root=$(selftest_root arm-only)
selftest_package "$root" t-arm 1 '{"source":"local","rebuild_on":["dep-a"]}' 1.0 'x86_64 aarch64'
selftest_pacman "$root"
selftest_published "$root"
selftest_alarm "$root" dep-a=2-1
SELFTEST_ARCHES=aarch64
check "run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-arm")"
check "ARM trigger recorded" "2-1" \
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-arm/.omarchy/package.json")"
check "x86_64 was not consulted" "false" \
"$(jq -r '.rebuilt_against | has("x86_64")' "$root/pkgbuilds/t-arm/.omarchy/package.json")"
echo "An x86-only package ignores ARM during a dual-architecture run:"
root=$(selftest_root x86-package)
selftest_package "$root" t-x86 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}'
selftest_pacman "$root" dep-a=1-1
selftest_published "$root"
selftest_alarm "$root" dep-a=2-1
SELFTEST_ARCHES="x86_64 aarch64"
check "run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-x86")"
echo "An ARM-only package is checked when no architecture list is given:"
root=$(selftest_root arm-default)
selftest_package "$root" t-arm-default 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"aarch64":{"dep-a":"1-1"}}}' 1.0 aarch64
selftest_pacman "$root"
selftest_published "$root"
selftest_alarm "$root" dep-a=2-1
SELFTEST_ARCHES=default
check "run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-arm-default")"
check "ARM trigger recorded" "2-1" \
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-arm-default/.omarchy/package.json")"
echo "A dependency carried here for ARM is read from its recipe once edge publishes it:"
root=$(selftest_root carried)
selftest_package "$root" t-carried 1.1 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
selftest_package "$root" t-linked 3 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"x86_64":{"t-carried":"0.15.1-1"},"aarch64":{"t-carried":"0.15.0-2"}}}' 1.0 'x86_64 aarch64'
selftest_pacman "$root" t-carried=0.15.1-1
selftest_published "$root"
selftest_alarm "$root" t-carried=0.15.1-1
selftest_channel "$root" edge aarch64 t-carried=0.15.1-1.1
SELFTEST_ARCHES=default
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
check "the carried version is recorded, not Arch Linux ARM's" "0.15.1-1.1" \
"$(jq -r '.rebuilt_against.aarch64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
check "x86_64, where nothing is carried, still reads the distribution" "0.15.1-1" \
"$(jq -r '.rebuilt_against.x86_64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
check "a second run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "a second run leaves it alone" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
echo "A carried dependency that edge does not publish yet leaves its dependents alone:"
root=$(selftest_root carried-in-flight)
selftest_package "$root" t-carried 1.2 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
selftest_package "$root" t-linked 4 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"aarch64":{"t-carried":"0.15.1-1.1"}}}' 1.0 aarch64
selftest_pacman "$root"
selftest_published "$root"
selftest_alarm "$root" t-carried=0.15.1-1
selftest_channel "$root" edge aarch64 t-carried=0.15.1-1.1
SELFTEST_ARCHES=aarch64
check "run succeeds" 0 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel untouched" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
check "record untouched" "0.15.1-1.1" \
"$(jq -r '.rebuilt_against.aarch64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
echo "A carried dependency whose edge database cannot be read fails the run:"
root=$(selftest_root carried-unreadable)
selftest_package "$root" t-carried 1.1 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
selftest_package "$root" t-linked 3 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"aarch64":{"t-carried":"0.15.0-2"}}}' 1.0 aarch64
selftest_pacman "$root"
selftest_published "$root"
selftest_alarm "$root" t-carried=0.15.1-1
SELFTEST_ARCHES=aarch64
check "run fails" 1 "$(run_case "$root")"
check "pkgrel untouched" 3 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
printf 'not a database\n' > "$root/stub/omarchy-edge-aarch64.db.tar.zst"
check "a corrupt download fails the run too" 1 "$(run_case "$root")"
unset SELFTEST_ARCHES
check "pkgrel still untouched" 3 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
echo "A PKGBUILD that branches on CARCH at file scope still reads its version:"
root=$(selftest_root carch-branch)
mkdir -p "$root/pkgbuilds/t-carch/.omarchy"
cat > "$root/pkgbuilds/t-carch/PKGBUILD" <<'PKG'
pkgname=t-carch
case "$CARCH" in
x86_64) _suffix=x64 ;;
aarch64) _suffix=arm64 ;;
*) return 1 ;;
esac
pkgver=1.0
pkgrel=3
arch=(x86_64 aarch64)
PKG
echo '{"source":"local"}' > "$root/pkgbuilds/t-carch/.omarchy/package.json"
check "pkgver read for x86_64" "1.0" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgver x86_64)"
check "pkgrel read for x86_64" "3" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgrel x86_64)"
check "arch-specific value follows CARCH" "arm64" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" _suffix aarch64)"
check "unsupported arch reports failure" 1 "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgver armv7h >/dev/null; echo $?)"
echo "A carried recipe's version is spelled the way makepkg publishes it:"
root=$(selftest_root carried-epoch)
selftest_package "$root" t-epoch 1.1 '{"source":"local"}' 0.15.1 aarch64
check "no epoch" "0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
echo 'epoch=0' >> "$root/pkgbuilds/t-epoch/PKGBUILD"
check "a zero epoch is left out" "0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
echo 'epoch=2' >> "$root/pkgbuilds/t-epoch/PKGBUILD"
check "a real epoch is kept" "2:0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
echo "" echo ""
if [[ "$failures" -eq 0 ]]; then if [[ "$failures" -eq 0 ]]; then
print_success "Self-test passed" print_success "Self-test passed"
@@ -1021,9 +626,9 @@ if [[ "$SELF_TEST" == true ]]; then
exit $? exit $?
fi fi
for tool in pacman vercmp jq curl tar; do for tool in pacman vercmp jq curl; do
if ! command -v "$tool" >/dev/null 2>&1; then if ! command -v "$tool" >/dev/null 2>&1; then
print_error "$tool not found: rebuild trigger sync cannot run" print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman"
exit 1 exit 1
fi fi
done done
+23 -486
View File
@@ -9,10 +9,8 @@ source "$BUILD_ROOT/helpers/upstream-github.sh"
TEMP_DIR=$(mktemp -d) TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT trap 'rm -rf "$TEMP_DIR"' EXIT
export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache"
SPECIFIC_PACKAGES=() SPECIFIC_PACKAGES=()
LANE=all
usage() { usage() {
cat <<EOF cat <<EOF
@@ -20,11 +18,11 @@ Usage: $0 [PACKAGE...]
Update packages that track an upstream vendor release feed instead of the AUR. Update packages that track an upstream vendor release feed instead of the AUR.
Packages opt in declaratively through "upstream" in .omarchy/package.json. A package whose upstream ships tagged GitHub releases with a checksum manifest
Providers cover GitHub Releases with checksum manifests or API asset digests, opts in declaratively, via "upstream" in .omarchy/package.json (see
semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See helpers/upstream-github.sh for the schema); no code needed. Anything with a
docs/upstream-sources.md for the schemas. Anything outside those conventions may provide bespoke feed provides pkgbuilds/<package>/.omarchy/upstream.sh instead, a hook
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout: that reports the newest upstream release as JSON on stdout:
{ {
"pkgver": "1.2.3", "pkgver": "1.2.3",
@@ -50,14 +48,6 @@ the bypass, so the resulting change still goes through a reviewed PR.
Arguments: Arguments:
PACKAGE One or more package names to update (optional) PACKAGE One or more package names to update (optional)
Options:
--lane <reviewed|auto-merge|all>
Which delivery lane to sync (default: all). Packages marked
"auto_merge": true ride the unattended lane (the branch tracker
opens and auto-merges their PR); everything else is reviewed.
The scheduled workflows each pass their own lane so a moving
branch tip never waits on a vendor release, or the reverse.
Commands: Commands:
self-test Run the offline fixture tests for release selection, the self-test Run the offline fixture tests for release selection, the
quarantine backstop, and metadata parsing quarantine backstop, and metadata parsing
@@ -74,14 +64,6 @@ while [[ $# -gt 0 ]]; do
usage usage
exit 0 exit 0
;; ;;
--lane)
LANE="${2:-}"
case "$LANE" in
reviewed|auto-merge|all) ;;
*) print_error "Invalid --lane '$LANE' (expected reviewed, auto-merge, or all)"; exit 1 ;;
esac
shift 2
;;
--*) --*)
print_error "Unknown option: $1" print_error "Unknown option: $1"
exit 1 exit 1
@@ -158,22 +140,17 @@ set_pkgbuild_array() {
local rewritten="$TEMP_DIR/pkgbuild-rewritten" local rewritten="$TEMP_DIR/pkgbuild-rewritten"
if ! awk -v prefix="${name}=(" -v block="$block" ' if ! awk -v prefix="${name}=(" -v block="$block" '
# The code on a line, minus any comment. Checksum arrays hold quoted hex
# (or SKIP), so a "#" can only ever start a comment here.
function code(s) { sub(/#.*/, "", s); return s }
!replaced && index($0, prefix) == 1 { !replaced && index($0, prefix) == 1 {
while ((getline line < block) > 0) print line while ((getline line < block) > 0) print line
close(block) close(block)
replaced = 1 replaced = 1
# A ")" anywhere past the opening closes the array; testing for one at end # A ")" anywhere past the opening closes the array; testing for one at end
# of line instead would treat a trailing comment as a continuation and eat # of line instead would treat a trailing comment as a continuation and eat
# every line up to the next ")". A ")" inside a comment -- "# sidecar # every line up to the next ")".
# (new in v0.7.5)" -- closes nothing, and ending the skip there would if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
# leave the rest of the old array behind a stray ")".
if (index(code(substr($0, length(prefix) + 1)), ")") == 0) skipping = 1
next next
} }
skipping { if (code($0) ~ /\)/) skipping = 0; next } skipping { if ($0 ~ /\)/) skipping = 0; next }
{ print } { print }
' "$pkgbuild" > "$rewritten"; then ' "$pkgbuild" > "$rewritten"; then
print_error "Failed to rewrite ${name} in $pkgbuild" print_error "Failed to rewrite ${name} in $pkgbuild"
@@ -355,33 +332,27 @@ sync_package() {
return 0 return 0
fi fi
if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then local github_repo has_upstream=false
print_info "Skipping $package: upstream updates held by sync=false" github_repo=$(package_upstream_github_repo "$package_dir")
((++SKIPPED))
return 0
fi
local provider has_upstream=false
provider=$(package_upstream_provider "$package_dir")
if package_has_upstream_provider "$package_dir"; then if package_has_upstream_provider "$package_dir"; then
has_upstream=true has_upstream=true
fi fi
# A present-but-unusable declaration fails loudly; treating it like "no # A present-but-unusable declaration fails loudly; treating it like "no
# upstream source" would silently drop the package from scheduled runs. # upstream source" would silently drop the package from scheduled runs.
if [[ "$has_upstream" == true && -z "$provider" ]]; then if [[ "$has_upstream" == true && -z "$github_repo" ]]; then
print_error "Package $package has an unusable or ambiguous upstream declaration" print_error "Package $package has an unusable upstream declaration (needs a github owner/repo)"
((++FAILED)) ((++FAILED))
return 0 return 0
fi fi
if [[ -n "$provider" && -f "$hook" ]]; then if [[ -n "$github_repo" && -f "$hook" ]]; then
print_error "Package $package declares both an upstream provider and an upstream.sh hook; keep exactly one" print_error "Package $package declares both upstream.github and an upstream.sh hook; keep exactly one"
((++FAILED)) ((++FAILED))
return 0 return 0
fi fi
if [[ -z "$provider" && ! -f "$hook" ]]; then if [[ -z "$github_repo" && ! -f "$hook" ]]; then
if [[ "$SPECIFIC_MODE" == true ]]; then if [[ "$SPECIFIC_MODE" == true ]]; then
print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh" print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh"
((++FAILED)) ((++FAILED))
@@ -401,31 +372,10 @@ sync_package() {
print_info "Checking $package for upstream releases..." print_info "Checking $package for upstream releases..."
if [[ "$provider" == watch ]]; then local release
local result if [[ -n "$github_repo" ]]; then
if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then if ! release=$(github_upstream_release "$package_dir" "$min_age"); then
print_error "Upstream watch failed for $package" print_error "GitHub release provider failed for $package"
((++FAILED))
elif [[ $(jq -r .status <<<"$result") == updated ]]; then
print_success " $(jq -r '.before + " -> " + .after' <<<"$result")"
((++UPDATED))
else
print_info " $(jq -r '.reason' <<<"$result")"
((++SKIPPED))
fi
return 0
fi
local release release_status=0
if [[ -n "$provider" ]]; then
case "$provider" in
github) release=$(github_upstream_release "$package_dir" "$min_age") || release_status=$? ;;
git_tags) release=$(git_tags_upstream_release "$package_dir") || release_status=$? ;;
npm) release=$(npm_upstream_release "$package_dir") || release_status=$? ;;
debian) release=$(debian_upstream_release "$package_dir") || release_status=$? ;;
esac
if [[ ${release_status:-0} -ne 0 ]]; then
print_error "$provider upstream provider failed for $package"
((++FAILED)) ((++FAILED))
return 0 return 0
fi fi
@@ -511,8 +461,8 @@ sync_package() {
# paths. Covers release selection (fallback past quarantined releases, # paths. Covers release selection (fallback past quarantined releases,
# draft/prerelease filtering, bypass, unchanged version), failure paths # draft/prerelease filtering, bypass, unchanged version), failure paths
# (unusable tags/timestamps, missing checksums), checksum template mapping # (unusable tags/timestamps, missing checksums), checksum template mapping
# for both architectures, release API digests, the min_release_age backstop, # for both architectures, the min_release_age backstop, the duration parser,
# the duration parser, and manifest validation. # and manifest validation.
cmd_self_test() { cmd_self_test() {
local failures=0 local failures=0
@@ -589,31 +539,6 @@ EOF
check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")" check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")" check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
cp "$pkg/.omarchy/package.json" "$pkg/normal.json"
jq '.upstream.latest_only = true' "$pkg/normal.json" > "$pkg/.omarchy/package.json"
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false},
{tag_name: "v0.4.1-8", published_at: $old, draft: false, prerelease: false}
]')
FIXTURE_CHECKSUMS=$(printf '%s\n' \
"$sum_x19 ./tool-v1.9.0-x64.tar.xz" \
"$sum_a19 ./tool-v1.9.0-arm64.tar.xz")
out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="<error>"
check "latest_only ignores incompatible historical tags" "1.9.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
cp "$pkg/normal.json" "$pkg/.omarchy/package.json"
FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" --arg old3 "$old3d" '[
{tag_name: "v2.0.0", published_at: $young, draft: false, prerelease: false},
{tag_name: "v1.9.9", published_at: $old2, draft: false, prerelease: true},
{tag_name: "v1.9.8", published_at: $old2, draft: true, prerelease: false},
{tag_name: "v1.9.0", published_at: $old2, draft: false, prerelease: false},
{tag_name: "v1.8.0", published_at: $old3, draft: false, prerelease: false}
]')
FIXTURE_CHECKSUMS=$(printf '%s\n' \
"$sum_x19 ./tool-v1.9.0-x64.tar.xz" \
"$sum_a19 tool-v1.9.0-arm64.tar.xz" \
"$sum_x20 *tool-v2.0.0-x64.tar.xz" \
"$sum_a20 tool-v2.0.0-arm64.tar.xz")
out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="<error>" out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="<error>"
check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")" check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")"
@@ -637,190 +562,6 @@ EOF
rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$? rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$?
check "missing aarch64 checksum fails the sync" "1" "$rc" check "missing aarch64 checksum fails the sync" "1" "$rc"
# A vendor publishing no manifest: checksums come from the digests the
# release API reports per asset, with nothing fetched beyond the feed.
echo "Release API digests:"
local digpkg="$TEMP_DIR/selftest-digests"
mkdir -p "$digpkg/.omarchy"
printf 'pkgver=1.0.0\npkgrel=1\n' > "$digpkg/PKGBUILD"
cat > "$digpkg/.omarchy/package.json" <<'EOF'
{
"source": "local",
"upstream": {
"github": "example/tool",
"digests": true,
"assets": {
"x86_64": "tool-{pkgver}-1-x86_64.pkg.tar.zst",
"aarch64": "tool-{pkgver}-1-aarch64.pkg.tar.zst"
}
}
}
EOF
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)},
{name: "tool-1.9.0-1-x86_64.rpm", digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"}
]}
]')
FIXTURE_CHECKSUMS="manifest must not be consulted"
out=$(github_upstream_release "$digpkg" 0 2>/dev/null) || out="<error>"
check "x86_64 checksum via the asset digest" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
check "aarch64 checksum via the asset digest" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // "<none>"' <<<"$out")"
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst"}
]}
]')
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
check "asset without a digest fails the sync" "1" "$rc"
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
{name: "tool-1.9.0-2-aarch64.pkg.tar.zst", digest: ("sha256:" + $x)}
]}
]')
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
check "asset re-cut under another release number fails the sync" "1" "$rc"
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: $x},
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)}
]}
]')
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
check "digest without the sha256: prefix fails the sync" "1" "$rc"
# The provider enforces the declaration shape itself: scheduled runs reach
# it without validate_package_metadata.
jq '.upstream.checksums = "SHASUMS256.txt"' "$digpkg/.omarchy/package.json" > "$digpkg/both.json"
cp "$digpkg/.omarchy/package.json" "$digpkg/good.json"
cp "$digpkg/both.json" "$digpkg/.omarchy/package.json"
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
check "provider rejects checksums and digests together" "1" "$rc"
jq '.upstream.digests = "true"' "$digpkg/good.json" > "$digpkg/.omarchy/package.json"
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
check "provider rejects a non-boolean digests" "1" "$rc"
cp "$digpkg/good.json" "$digpkg/.omarchy/package.json"
echo "Ordered GitHub assets with supplemental sources:"
local multipkg="$TEMP_DIR/selftest-multi-assets" multi_sum support_sum multi_vst
mkdir -p "$multipkg/.omarchy"
printf 'pkgver=1.0.0\npkgrel=1\n' > "$multipkg/PKGBUILD"
cat > "$multipkg/.omarchy/package.json" <<'EOF'
{
"source": "local",
"upstream": {
"github": "example/tool",
"digests": true,
"assets": {
"x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"],
"aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"]
},
"sources": {
"any": ["https://example.test/tool/{tag}/support.txt"]
}
}
}
EOF
local sum_x19_sig sum_a19_sig
sum_x19_sig=$(printf '1%.0s' {1..64})
sum_a19_sig=$(printf '2%.0s' {1..64})
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg xs "$sum_x19_sig" \
--arg a "$sum_a19" --arg as "$sum_a19_sig" '[
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
{name: "tool-1.9.0-x86_64", digest: ("sha256:" + $x)},
{name: "tool-1.9.0-x86_64.asc", digest: ("sha256:" + $xs)},
{name: "tool-1.9.0-aarch64", digest: ("sha256:" + $a)},
{name: "tool-1.9.0-aarch64.asc", digest: ("sha256:" + $as)}
]}
]')
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
multi_sum=$(github_upstream_release "$multipkg" 0 2>/dev/null) || multi_sum="<error>"
support_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/v1.9.0/support.txt' | sha256sum | cut -d' ' -f1)
check "ordered GitHub assets produce an ordered checksum array" "$sum_x19 $sum_x19_sig" \
"$(jq -r '.sha256sums.x86_64 | join(" ")' <<<"$multi_sum")"
check "GitHub supplemental source is downloaded and hashed" "$support_sum" \
"$(jq -r '.sha256sums.any[0]' <<<"$multi_sum")"
multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$?
check "GitHub asset lists and disjoint sources validate" "0" "$multi_vst"
jq '.upstream.sources.x86_64 = ["https://example.test/duplicate"]' \
"$multipkg/.omarchy/package.json" > "$multipkg/overlap.json"
cp "$multipkg/.omarchy/package.json" "$multipkg/good.json"
cp "$multipkg/overlap.json" "$multipkg/.omarchy/package.json"
multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$?
check "GitHub assets and sources cannot target the same checksum array" "1" "$multi_vst"
cp "$multipkg/good.json" "$multipkg/.omarchy/package.json"
echo "Debian Packages provider:"
local debpkg="$TEMP_DIR/selftest-debian" deb_sum deb_x_sum deb_a_sum deb_vst
mkdir -p "$debpkg/.omarchy"
printf 'pkgver=1.0.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD"
cat > "$debpkg/.omarchy/package.json" <<'EOF'
{
"source": "local",
"upstream": {
"debian": "https://packages.example.test/dists/stable/main/binary-amd64/Packages",
"package": "example-app",
"sources": {
"x86_64": ["https://downloads.example.test/app-{pkgver}-x64.tar.gz"],
"aarch64": ["https://downloads.example.test/app-{pkgver}-arm64.tar.gz"]
}
}
}
EOF
debian_fetch_packages() {
cat <<'EOF'
Package: unrelated
Version: 99.0.0
Package: example-app
Version: 1.9.0
Package: example-app
Version: 1.10.0
EOF
}
deb_sum=$(debian_upstream_release "$debpkg")
deb_x_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-x64.tar.gz' | sha256sum | cut -d' ' -f1)
deb_a_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-arm64.tar.gz' | sha256sum | cut -d' ' -f1)
check "Debian provider selects the newest exact package stanza" "1.10.0" "$(jq -r '.pkgver' <<<"$deb_sum")"
check "Debian x86_64 source is hashed" "$deb_x_sum" "$(jq -r '.sha256sums.x86_64[0]' <<<"$deb_sum")"
check "Debian aarch64 source is hashed" "$deb_a_sum" "$(jq -r '.sha256sums.aarch64[0]' <<<"$deb_sum")"
deb_vst=0; validate_package_metadata "$debpkg" >/dev/null || deb_vst=$?
check "Debian declaration validates" "0" "$deb_vst"
printf 'pkgver=1.10.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD"
check "checked-in Debian version avoids source downloads" "{}" "$(debian_upstream_release "$debpkg" | jq -c .)"
echo "End-to-end Debian sync with the checked-in 1password recipe:"
local one_root="$TEMP_DIR/e2e-1password" one_dir one_version=8.12.35
mkdir -p "$one_root"
cp -a "$BUILD_ROOT/pkgbuilds/1password" "$one_root/1password"
one_dir="$one_root/1password"
# Keep the real recipe shape, but make the fixture's starting version stable.
# Otherwise a routine package update can outrun the mocked release below.
sed -i -e 's/^pkgver=.*/pkgver=8.12.34/' -e 's/^pkgrel=.*/pkgrel=2/' "$one_dir/PKGBUILD"
debian_fetch_packages() {
printf 'Package: 1password\nVersion: %s\n' "$one_version"
}
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
local one_prev_updated=$UPDATED one_prev_failed=$FAILED
PKGBUILDS_DIR="$one_root" sync_package 1password >/dev/null 2>&1 || true
check "1password sync updates without failures" "updated=1 failed=0" \
"updated=$((UPDATED - one_prev_updated)) failed=$((FAILED - one_prev_failed))"
check "1password pkgver is the single download version source" "$one_version" \
"$(grep -m1 '^pkgver=' "$one_dir/PKGBUILD" | cut -d= -f2-)"
check "1password pkgrel resets to 1" "1" \
"$(grep -m1 '^pkgrel=' "$one_dir/PKGBUILD" | cut -d= -f2-)"
check "1password x86 URL follows the rewritten pkgver" "1password-${one_version}.x64.tar.gz" \
"$(CARCH=x86_64 bash -c 'source "$1"; basename "${source_x86_64[0]}"' _ "$one_dir/PKGBUILD")"
check "1password ARM URL follows the rewritten pkgver" "1password-${one_version}.arm64.tar.gz" \
"$(CARCH=aarch64 bash -c 'source "$1"; basename "${source_aarch64[0]}"' _ "$one_dir/PKGBUILD")"
echo "Quarantine backstop:" echo "Quarantine backstop:"
local rel st local rel st
rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}') rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}')
@@ -873,123 +614,10 @@ EOF
echo '{"source": "local", "upstream": {"github": "example/tool"}}' > "$agepkg/.omarchy/package.json" echo '{"source": "local", "upstream": {"github": "example/tool"}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "upstream without checksums/assets is rejected" "1" "$vst" check "upstream without checksums/assets is rejected" "1" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SHASUMS256.txt", "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "upstream with both checksums and digests is rejected" "1" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "digests": "yes", "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "non-boolean digests is rejected" "1" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": false, "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "checksums: false alongside digests is rejected" "1" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": null, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "digests: null is rejected" "1" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "digests: false beside a checksums manifest is accepted" "0" "$vst"
echo '{"source": "local", "upstream": {"github": "example/tool", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "digests: false alone is rejected" "1" "$vst"
cp "$digpkg/.omarchy/package.json" "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "the digests declaration shape is accepted" "0" "$vst"
echo '{"source":"local","upstream":{"github":"example/tool","git_tags":"https://example/tool.git","checksums":"sums","assets":{"any":"tool"}}}' > "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "multiple provider types are rejected" "1" "$vst"
cp "$pkg/.omarchy/package.json" "$agepkg/.omarchy/package.json" cp "$pkg/.omarchy/package.json" "$agepkg/.omarchy/package.json"
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$? vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
check "the real declaration shape is accepted" "0" "$vst" check "the real declaration shape is accepted" "0" "$vst"
echo "Git-tag provider:"
local tagpkg="$TEMP_DIR/selftest-tags" tag_sum remote_sum local_sum
mkdir -p "$tagpkg/.omarchy"
printf 'pkgver=1.0.0\npkgrel=4\nsha256sums=("old" "old")\n' > "$tagpkg/PKGBUILD"
printf 'local fixture\n' > "$tagpkg/local.patch"
cat > "$tagpkg/.omarchy/package.json" <<'EOF'
{
"source": "local",
"upstream": {
"git_tags": "https://example.test/tool.git",
"tag_pattern": "release/{pkgver}",
"sources": {
"any": ["https://downloads.example.test/tool-{pkgver}.tar.gz", "file:local.patch"]
}
}
}
EOF
git_tags_fetch_refs() {
printf '%s\n' \
'aaaa refs/tags/release/1.9.0' \
'bbbb refs/tags/release/1.10.0' \
'cccc refs/tags/not-a-release'
}
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
tag_sum=$(git_tags_upstream_release "$tagpkg")
remote_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/tool-1.10.0.tar.gz' | sha256sum | cut -d' ' -f1)
local_sum=$(sha256sum "$tagpkg/local.patch" | cut -d' ' -f1)
check "pacman ordering selects 1.10.0 over 1.9.0" "1.10.0" "$(jq -r '.pkgver' <<<"$tag_sum")"
check "remote source template is downloaded and hashed" "$remote_sum" "$(jq -r '.sha256sums.any[0]' <<<"$tag_sum")"
check "local source entry is hashed" "$local_sum" "$(jq -r '.sha256sums.any[1]' <<<"$tag_sum")"
vst=0; validate_package_metadata "$tagpkg" >/dev/null || vst=$?
check "git-tags declaration validates" "0" "$vst"
echo "npm provider:"
local npmpkg="$TEMP_DIR/selftest-npm" npm_sum npm_tar_sum npm_notes_sum
mkdir -p "$npmpkg/.omarchy"
printf 'pkgver=1.0.0\npkgrel=1\nsha256sums=("old" "old")\n' > "$npmpkg/PKGBUILD"
cat > "$npmpkg/.omarchy/package.json" <<'EOF'
{
"source": "local",
"upstream": {
"npm": "@example/tool",
"dist_tag": "latest",
"sources": {
"any": ["{npm_tarball}", "https://example.test/tool/{pkgver}/notes"]
}
}
}
EOF
npm_fetch_metadata() {
jq -n '{
"dist-tags": {latest: "2.0.0"},
versions: {"2.0.0": {dist: {tarball: "https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz"}}},
time: {"2.0.0": "2024-01-02T03:04:05.000Z"}
}'
}
npm_sum=$(npm_upstream_release "$npmpkg")
npm_tar_sum=$(printf 'remote fixture for %s\n' 'https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz' | sha256sum | cut -d' ' -f1)
npm_notes_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/2.0.0/notes' | sha256sum | cut -d' ' -f1)
check "npm dist-tag selects its version" "2.0.0" "$(jq -r '.pkgver' <<<"$npm_sum")"
check "npm tarball placeholder is hashed" "$npm_tar_sum" "$(jq -r '.sha256sums.any[0]' <<<"$npm_sum")"
check "npm pkgver template is hashed" "$npm_notes_sum" "$(jq -r '.sha256sums.any[1]' <<<"$npm_sum")"
check "npm publication time is preserved" "2024-01-02T03:04:05.000Z" "$(jq -r '.published_at' <<<"$npm_sum")"
vst=0; validate_package_metadata "$npmpkg" >/dev/null || vst=$?
check "npm declaration validates" "0" "$vst"
# A ")" in a comment inside a checksum array must not end the rewrite early;
# voxtype-bin annotates its arrays with "(new in v0.7.5)" and the like.
echo "Checksum array rewrite across commented lines:"
local commented="$TEMP_DIR/commented-pkgbuild" sum_c=$(printf 'c%.0s' {1..64})
cat > "$commented" <<'EOF'
sha256sums_x86_64=(
# Binaries
'aaaa' # tool
# Sidecar (new in v0.7.5)
'bbbb' # sidecar (x86_64)
)
sha256sums=( # support files (arch-independent)
'dddd'
)
package() { :; }
EOF
set_pkgbuild_array "$commented" sha256sums_x86_64 "$sum_c" "$sum_c"
set_pkgbuild_array "$commented" sha256sums "$sum_c"
check "commented arrays rewrite to valid shell" "0" \
"$(bash -n "$commented" 2>/dev/null; echo $?)"
check "commented arrays hold only the new checksums" "$sum_c $sum_c|$sum_c|package" \
"$(bash -c 'source "$1"; printf "%s|%s|%s" "${sha256sums_x86_64[*]}" "${sha256sums[*]}" "$(declare -F package)"' _ "$commented")"
# End to end over the real mise-bin package: its checked-in metadata and # End to end over the real mise-bin package: its checked-in metadata and
# PKGBUILD, the full sync_package path (selection, validation, backstop, # PKGBUILD, the full sync_package path (selection, validation, backstop,
# rewrite, read-back verification), with only the two network fetches # rewrite, read-back verification), with only the two network fetches
@@ -1041,13 +669,6 @@ EOF
check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))" check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))"
FAILED=0 FAILED=0
if ! bash "$BUILD_ROOT/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh"; then
failures=$((failures + 1))
fi
if ! bash "$BUILD_ROOT/pkgbuilds/tmog-bin/.omarchy/upstream-test.sh"; then
failures=$((failures + 1))
fi
echo "" echo ""
if [[ "$failures" -eq 0 ]]; then if [[ "$failures" -eq 0 ]]; then
print_success "Self-test passed" print_success "Self-test passed"
@@ -1062,100 +683,16 @@ if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 && "${SPECIFIC_PACKAGES[0]}" == "self-test"
exit 0 exit 0
fi fi
# Packages that pin the same upstream branch move together or not at all.
# The watch reads one shared clone per run, so they only disagree when one
# package's update failed after the tip was chosen (a checksum fetch, say).
# Leaving the other one advanced would ship omarchy-dev and
# omarchy-settings-dev from different commits, which is exactly the skew the
# release pair's lockstep guard exists to prevent. Restore the packages that
# moved and count the group as failed; the next run tries again.
declare -A BEFORE_SYNC=()
snapshot_package() {
local package="$1" pkgbuild="$PKGBUILDS_DIR/$1/PKGBUILD"
[[ -f "$pkgbuild" ]] || return 0
mkdir -p "$TEMP_DIR/before"
cp "$pkgbuild" "$TEMP_DIR/before/$package"
BEFORE_SYNC["$package"]=1
}
branch_watch_key() {
local package_dir="$1"
jq -r '
(.upstream.watch? | objects | select(has("git_branch")))
| "\(.git_branch)#\(.branch)"
' "$package_dir/.omarchy/package.json" 2>/dev/null
}
enforce_branch_lockstep() {
local package key
declare -A groups=()
for package in "${!BEFORE_SYNC[@]}"; do
key=$(branch_watch_key "$PKGBUILDS_DIR/$package")
[[ -n "$key" ]] || continue
groups["$key"]+="$package "
done
for key in "${!groups[@]}"; do
local members commits pin
read -r -a members <<<"${groups[$key]}"
(( ${#members[@]} > 1 )) || continue
commits=$(for package in "${members[@]}"; do
pin=$(grep -m1 -E '^_commit=' "$PKGBUILDS_DIR/$package/PKGBUILD" | cut -d= -f2- | tr -d "\"'" || true)
printf '%s\n' "${pin:-missing:$package}"
done | sort -u | grep -c .)
(( commits > 1 )) || continue
print_error "Lockstep violation on $key: ${members[*]} pin different commits; restoring all of them"
for package in "${members[@]}"; do
if ! cmp -s "$TEMP_DIR/before/$package" "$PKGBUILDS_DIR/$package/PKGBUILD"; then
cp "$TEMP_DIR/before/$package" "$PKGBUILDS_DIR/$package/PKGBUILD"
# Not ((--UPDATED)): an arithmetic command that evaluates to zero
# returns 1, and under errexit that would end the run right here.
UPDATED=$((UPDATED > 0 ? UPDATED - 1 : 0))
fi
done
((++FAILED))
done
}
sync_in_lane() {
local package="$1" package_dir="$PKGBUILDS_DIR/$1"
snapshot_package "$package"
if [[ -d "$package_dir" ]] && ! package_in_lane "$package_dir" "$LANE"; then
print_info "Skipping $package: not in the $LANE lane"
((++SKIPPED))
return 0
fi
sync_package "$package"
}
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
SPECIFIC_MODE=true SPECIFIC_MODE=true
# A targeted run is still a branch update: include every sibling watching
# the same branch, otherwise the lockstep check never sees the omitted one.
declare -A selected=() selected_branches=()
for package in "${SPECIFIC_PACKAGES[@]}"; do for package in "${SPECIFIC_PACKAGES[@]}"; do
selected["$package"]=1 sync_package "$package"
key=$(branch_watch_key "$PKGBUILDS_DIR/$package" || true)
[[ -z "$key" ]] || selected_branches["$key"]=1
done
for package_dir in "$PKGBUILDS_DIR"/*; do
[[ -f "$package_dir/PKGBUILD" ]] || continue
package=${package_dir##*/}
[[ -z "${selected[$package]:-}" ]] || continue
key=$(branch_watch_key "$package_dir" || true)
if [[ -n "$key" && -n "${selected_branches[$key]:-}" ]]; then
SPECIFIC_PACKAGES+=("$package")
fi
done
for package in "${SPECIFIC_PACKAGES[@]}"; do
sync_in_lane "$package"
done done
else else
while IFS= read -r package; do while IFS= read -r package; do
sync_in_lane "$package" sync_package "$package"
done < <(packages_for_upstream_sync) done < <(packages_for_upstream_sync)
fi fi
enforce_branch_lockstep
echo "" echo ""
if [[ $FAILED -gt 0 ]]; then if [[ $FAILED -gt 0 ]]; then
+32 -38
View File
@@ -84,18 +84,15 @@ echo ""
# --- queued work ------------------------------------------------------------- # --- queued work -------------------------------------------------------------
print_info "Queued builds (state files in $STATE_DIR; architectures: $(published_arches | tr '\n' ' '))" print_info "Queued builds (state files in $STATE_DIR)"
queued=false queued=false
for channel in edge rc stable; do for channel in edge rc stable; do
for arch in $(published_arches); do state_file="$STATE_DIR/.sync-needed-$channel"
state_file=$(sync_queue_file "$channel" "$arch") if [[ -f "$state_file" ]]; then
[[ "$arch" == "x86_64" && ! -f "$state_file" ]] && state_file=$(legacy_sync_queue_file "$channel") queued=true
if [[ -f "$state_file" ]]; then since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
queued=true printf ' • %-7s queued since %s\n' "$channel" "$since"
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown") fi
printf ' • %-7s %-8s queued since %s\n' "$channel" "$arch" "$since"
fi
done
done done
[[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)" [[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)"
echo "" echo ""
@@ -104,37 +101,34 @@ echo ""
# say so plainly: it is queued but deliberately not being retried yet. # say so plainly: it is queued but deliberately not being retried yet.
paused=false paused=false
for channel in edge rc stable; do for channel in edge rc stable; do
for arch in $(published_arches); do fail_file="$STATE_DIR/.build-failed-$channel"
fail_file=$(sync_fail_file "$channel" "$arch") [[ -f "$fail_file" ]] || continue
[[ "$arch" == "x86_64" && ! -f "$fail_file" ]] && fail_file=$(legacy_sync_fail_file "$channel") if [[ "$paused" == false ]]; then
[[ -f "$fail_file" ]] || continue print_error "Failing builds (backoff active)"
if [[ "$paused" == false ]]; then paused=true
print_error "Failing builds (backoff active)" fi
paused=true FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
fi # shellcheck disable=SC1090
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT="" source "$fail_file" 2>/dev/null || true
# shellcheck disable=SC1090 delay=600
source "$fail_file" 2>/dev/null || true for ((i = 1; i < FAILURE_COUNT; i++)); do
delay=600 delay=$((delay * 2))
for ((i = 1; i < FAILURE_COUNT; i++)); do ((delay >= 21600)) && { delay=21600; break; }
delay=$((delay * 2))
((delay >= 21600)) && { delay=21600; break; }
done
retry_at=$((FAILURE_AT + delay))
now=$(date +%s)
if ((now < retry_at)); then
when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
else
when="retries on the next tick"
fi
printf ' ✗ %-7s %-8s %s consecutive failure(s), %s\n' "$channel" "$arch" "$FAILURE_COUNT" "$when"
printf ' last attempt %s on commit %s\n' \
"$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \
"${FAILURE_FINGERPRINT:0:12}"
done done
retry_at=$((FAILURE_AT + delay))
now=$(date +%s)
if ((now < retry_at)); then
when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
else
when="retries on the next tick"
fi
printf ' ✗ %-7s %s consecutive failure(s), %s\n' "$channel" "$FAILURE_COUNT" "$when"
printf ' last attempt %s on commit %s\n' \
"$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \
"${FAILURE_FINGERPRINT:0:12}"
done done
if [[ "$paused" == true ]]; then if [[ "$paused" == true ]]; then
echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed-<channel>-<arch>" echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed-<channel>"
echo "" echo ""
fi fi
-80
View File
@@ -1,80 +0,0 @@
#!/bin/bash
# Retire packages from one channel of the remote repository.
#
# unpublish-packages --mirror <edge|rc|stable> --arch <arch> <pkgbase...>
#
# The counterpart of publish-artifact, with the same steps:
# 1. pull the channel's current database from the remote
# 2. find every entry built from the named pkgbases (so a package's split
# outputs and -debug go with it)
# 3. repo-remove them
# 4. upload the database
#
# Package files stay in the bucket. Published filenames are immutable and the
# R2 cache cannot recover from a rewrite; an unreferenced file costs nothing
# and pacman never sees it. Naming a package the channel does not hold is not
# an error, so a re-run is harmless.
#
# The remote is an rclone remote (REMOTE, default the production one);
# OMARCHY_PUBLISH_PREFIX can point a proof run at a scratch prefix.
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
REMOTE=${REMOTE:-pkgs.omarchy.org:omarchy-pkgs}
PREFIX=${OMARCHY_PUBLISH_PREFIX:-}
BASES=()
while [[ $# -gt 0 ]]; do
case $1 in
--mirror) MIRROR=$2; require_valid_mirror "$MIRROR"; shift 2 ;;
--arch) ARCH=$2; require_valid_arch "$ARCH"; shift 2 ;;
--remote) REMOTE=$2; shift 2 ;;
-h|--help) sed -n '2,19p' "$0"; exit 0 ;;
-*) print_error "Unknown option: $1"; exit 1 ;;
*) BASES+=("$1"); shift ;;
esac
done
(( ${#BASES[@]} )) || { print_error "No packages given"; exit 1; }
DEST="$REMOTE/${PREFIX:+$PREFIX/}$MIRROR/$ARCH"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
print_header "Unpublish from $DEST"
# --- 1. pull the current database -----------------------------------------
mkdir -p "$WORK/repo"
listing=$(rclone lsf "$DEST/" --s3-no-head 2>/dev/null || true)
if ! grep -q '^omarchy.db.tar.zst$' <<<"$listing"; then
print_info "No database at $DEST; nothing to remove"
exit 0
fi
rclone copy "$DEST/omarchy.db.tar.zst" "$WORK/repo" --s3-no-head
rclone copy "$DEST/omarchy.files.tar.zst" "$WORK/repo" --s3-no-head 2>/dev/null || true
before=$(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$')
print_info "Pulled current database ($before entries)"
# --- 2. entries built from the named pkgbases -----------------------------
names=$(bsdtar -xOf "$WORK/repo/omarchy.db.tar.zst" --include '*/desc' |
awk -v bases=" ${BASES[*]} " '
/^%NAME%$/ { getline name }
/^%BASE%$/ { getline base; if (index(bases, " " base " ")) print name }')
if [[ -z "$names" ]]; then
print_info "None of ${BASES[*]} is in $MIRROR/$ARCH; nothing to remove"
exit 0
fi
mapfile -t NAMES <<<"$names"
for n in "${NAMES[@]}"; do print_step "removing $n"; done
# --- 3. repo-remove ---------------------------------------------------------
( cd "$WORK/repo" && repo-remove --quiet omarchy.db.tar.zst "${NAMES[@]}" )
ln -sf omarchy.db.tar.zst "$WORK/repo/omarchy.db"
ln -sf omarchy.files.tar.zst "$WORK/repo/omarchy.files"
after=$(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$')
(( before - after == ${#NAMES[@]} )) || {
print_error "Expected to remove ${#NAMES[@]} entries, removed $((before - after))"; exit 1; }
print_info "Database now has $after entries"
# --- 4. upload the database -------------------------------------------------
rclone copy "$WORK/repo" "$DEST" --s3-no-head --checksum --copy-links --filter '+ omarchy.db*' --filter '+ omarchy.files*' --filter '- *'
print_success "Removed ${#NAMES[@]} package(s) from $DEST"
+12 -16
View File
@@ -11,11 +11,12 @@ source "$BUILD_ROOT/helpers/docker-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/lock-helpers.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh"
# Function to update repository database using the selected container engine # Function to update repository database using Docker
update_database() { update_database() {
print_info "Updating repository database in a container..." print_info "Updating repository database in Docker container..."
check_engine # Check Docker is available
check_docker
# Ensure output directory exists # Ensure output directory exists
if [[ ! -d "$REPO_DIR" ]]; then if [[ ! -d "$REPO_DIR" ]]; then
@@ -24,25 +25,20 @@ update_database() {
exit 1 exit 1
fi fi
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then # Make output directory writable for container
make_dir_writable "$REPO_DIR" make_dir_writable "$REPO_DIR"
fi
# repo-add is architecture- and mirror-independent. Use the host-native edge # repo-add is architecture- and mirror-independent, so always use the edge
# image, which also lets bootstrap-rc run before that channel exists remotely. # x86_64 image. This also lets bootstrap-rc build the rc database before the
local tool_arch # rc channel exists remotely (an rc image can only build after it does).
tool_arch=$(docker_native_arch) || { build_docker_image "$BUILD_DIR" "x86_64" "edge"
print_error "Unsupported host architecture: $(uname -m)"
exit 1
}
build_docker_image "$BUILD_DIR" "$tool_arch" "edge"
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$tool_arch")" \ docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \ -e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \ -e MIRROR="$MIRROR" \
-v "$REPO_ROOT:/output" \ -v "$REPO_ROOT:/output" \
-v "$BUILD_DIR:/build:ro" \ -v "$BUILD_DIR:/build:ro" \
"omarchy-pkg-builder:latest-$tool_arch-edge" /build/update-repo.sh omarchy-pkg-builder:latest-x86_64-edge /build/update-repo.sh
} }
# Main execution # Main execution
+6 -16
View File
@@ -45,7 +45,9 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then \
printf 'Server = https://mirror.omarchy.org/$repo/os/$arch\n' > /etc/pacman.d/mirrorlist; \ printf 'Server = https://mirror.omarchy.org/$repo/os/$arch\n' > /etc/pacman.d/mirrorlist; \
fi; \ fi; \
else \ else \
printf 'Server = https://fl.us.mirror.archlinuxarm.org/aarch64/$repo\n' > /etc/pacman.d/mirrorlist; \ curl -L "https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman-mirrorlist/mirrorlist" 2>/dev/null | \
sed -E 's/^\s*#\s*Server\s*=/Server =/g' > /etc/pacman.d/mirrorlist && \
sed -i 's/\$arch/aarch64/g' /etc/pacman.d/mirrorlist; \
fi fi
# Bootstrap keyrings (required before pacstrap can verify packages) # Bootstrap keyrings (required before pacstrap can verify packages)
@@ -106,16 +108,11 @@ RUN ln -sf /usr/lib/os-release /etc/os-release && \
# Setup Omarchy keyring manually before adding repo (avoids keyserver trust issues) # Setup Omarchy keyring manually before adding repo (avoids keyserver trust issues)
# Note: Repository is removed at the end since build scripts add it dynamically. # Note: Repository is removed at the end since build scripts add it dynamically.
# The keyring comes from this image's own channel (the bare /$arch path is a # The keyring comes from this image's own channel (the bare /$arch path is a
# stale legacy layout). It is always taken from the x86_64 tree, whatever the # stale legacy layout); %s keeps pacman's $arch literal while MIRROR expands.
# image's own architecture: omarchy-keyring is an arch=any package, and the
# x86_64 tree is the one that exists before a new architecture has published
# anything. Bootstrapping from the target's own tree would make the first
# aarch64 build depend on an aarch64 repository that only that build can
# create.
ARG MIRROR=edge ARG MIRROR=edge
RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \ RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 && \ pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 && \
printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/x86_64\n' "${MIRROR}" >> /etc/pacman.conf && \ printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/$arch\n' "${MIRROR}" >> /etc/pacman.conf && \
pacman -Sy --noconfirm && \ pacman -Sy --noconfirm && \
pacman -S --noconfirm omarchy-keyring && \ pacman -S --noconfirm omarchy-keyring && \
pacman-key --populate omarchy && \ pacman-key --populate omarchy && \
@@ -129,7 +126,6 @@ RUN pacman -Syu --noconfirm && \
wget \ wget \
curl \ curl \
jq \ jq \
rclone \
gnupg && \ gnupg && \
pacman -Scc --noconfirm && \ pacman -Scc --noconfirm && \
rm -rf /var/cache/pacman/pkg/* rm -rf /var/cache/pacman/pkg/*
@@ -141,14 +137,8 @@ RUN useradd -m -G wheel -s /bin/bash builder && \
chmod 700 /home/builder/.gnupg && \ chmod 700 /home/builder/.gnupg && \
chown -R builder:builder /home/builder chown -R builder:builder /home/builder
# Arch Linux ARM's makepkg.conf still defaults PKGEXT to .pkg.tar.xz; every
# tool downstream of the build (sign.sh, push-build, sync-rebuilds, the
# notifier) expects .pkg.tar.zst, so an aarch64 package would build and then
# be skipped at signing. Pin the extension so both architectures match.
RUN sed -i 's/^#MAKEFLAGS=.*/MAKEFLAGS="-j$(nproc)"/' /etc/makepkg.conf && \ RUN sed -i 's/^#MAKEFLAGS=.*/MAKEFLAGS="-j$(nproc)"/' /etc/makepkg.conf && \
sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf && \ sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf
sed -i "s|^PKGEXT=.*|PKGEXT='.pkg.tar.zst'|" /etc/makepkg.conf && \
sed -i 's|^#\?PACKAGER=.*|PACKAGER="Omarchy <pkgs@omarchy.org>"|' /etc/makepkg.conf
# Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust). # Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust).
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict # makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
+144 -249
View File
@@ -1,163 +1,75 @@
#!/bin/bash #!/bin/bash
# Plan a run or build one planned package in an isolated container. # Build script - builds packages based on package metadata
# Unscoped edge builds exclude skip_build packages. Stable also requires the fast release ring. # Unscoped edge builds exclude skip_build packages. Stable also requires the fast release ring.
# Explicit --package selections may build packages with skip_build=true. # Explicit --package selections may build packages with skip_build=true.
# Setup directories # Setup directories
ARCH=${ARCH:-x86_64} ARCH=${ARCH:-x86_64}
# ARCH selects the repository target for this script, but make and Kbuild also
# interpret an exported ARCH themselves (Linux calls this target "arm64").
# Keep the shell variable local to the orchestrator so PKGBUILDs see CARCH only.
export -n ARCH
MIRROR=${MIRROR:-edge} MIRROR=${MIRROR:-edge}
DRY_RUN=${DRY_RUN:-false} DRY_RUN=${DRY_RUN:-false}
# bin/build plans the whole run, then invokes this script once per package in
# a fresh container. PACKAGES retains the original request for validation.
BUILD_PACKAGE=${BUILD_PACKAGE:-}
BUILD_PLAN_DIR=${BUILD_PLAN_DIR:-}
PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds} PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds}
BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH} BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH} FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
HELPERS_DIR=${HELPERS_DIR:-/helpers} HELPERS_DIR=${HELPERS_DIR:-/helpers}
SRC_DIR=${SRC_DIR:-/src} SRC_DIR=${SRC_DIR:-/src}
# Set by bin/build from OMARCHY_DEFER_RUNTIME_DEPS after it has checked the
# request; re-checked here so the container never trusts a stray value.
DEFER_RUNTIME_DEPS=${DEFER_RUNTIME_DEPS:-false}
source "$HELPERS_DIR/package-metadata.sh" source "$HELPERS_DIR/package-metadata.sh"
# Where the channel's published database is read from for planning. On the
# repository host it is the published tree itself. Anywhere else (a CI runner,
# a fresh clone) that tree is absent, so the database is fetched from the
# public channel and the same URL serves as pacman's dependency repository.
# Set OMARCHY_PUBLISHED_REPO_URL= (empty) to disable the remote fallback.
PUBLISHED_REPO_URL=${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}
PUBLISHED_DB_DIR="$FINAL_OUTPUT_DIR"
PUBLISHED_REPO_SERVER=""
if [[ ! -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" && ! -f "$FINAL_OUTPUT_DIR/omarchy.db" && -n "$PUBLISHED_REPO_URL" ]]; then
remote_channel="$PUBLISHED_REPO_URL/$MIRROR/$ARCH"
remote_db_dir=$(mktemp -d /tmp/omarchy-published.XXXXXX) || exit 1
# Cache-bust: the channel sits behind a CDN that serves a stale database
# for a while after a sync.
if curl -fsSL "$remote_channel/omarchy.db.tar.zst?$(date +%s)" -o "$remote_db_dir/omarchy.db.tar.zst"; then
PUBLISHED_DB_DIR="$remote_db_dir"
PUBLISHED_REPO_SERVER="$remote_channel"
echo "==> No local published tree; planning against $remote_channel"
else
rm -rf "$remote_db_dir"
echo "==> No local published tree and $remote_channel is unavailable; treating the channel as empty"
fi
fi
if [[ $DEFER_RUNTIME_DEPS != "false" && $DEFER_RUNTIME_DEPS != "true" ]]; then
echo "DEFER_RUNTIME_DEPS must be true or false" >&2
exit 1
fi
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
deferred_runtime=0
deferred_settings=0
deferred_count=0
for package in $PACKAGES; do
((deferred_count += 1))
case $package in
omarchy|omarchy-dev) deferred_runtime=1 ;;
omarchy-settings|omarchy-settings-dev) deferred_settings=1 ;;
*)
echo "Runtime dependency deferral only applies to the omarchy pair, not $package" >&2
exit 1
;;
esac
done
if (( deferred_runtime != 1 || deferred_settings != 1 || deferred_count != 2 )); then
echo "Runtime dependency deferral requires exactly the omarchy pair" >&2
exit 1
fi
fi
if [[ "$DRY_RUN" != true ]]; then if [[ "$DRY_RUN" != true ]]; then
if [[ -z "$BUILD_PACKAGE" || -z "$BUILD_PLAN_DIR" ]]; then
echo "Use bin/build to plan and run isolated package builds" >&2
exit 1
fi
if ! grep -Fxq -- "$BUILD_PACKAGE" "$BUILD_PLAN_DIR/packages"; then
echo "Package is not in the build plan: $BUILD_PACKAGE" >&2
exit 1
fi
# Import GPG keys # Import GPG keys
/build/import-gpg-keys.sh || exit 1 /build/import-gpg-keys.sh || exit 1
mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR" mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
# The image drops pacman's private signing key. Keyring upgrades need a
# container-local key to sign newly trusted keys.
sudo pacman-key --init || exit 1
# Bring the container up to date before any makedepends are installed. The # Bring the container up to date before any makedepends are installed. The
# image is layer-cached, so its glibc drifts behind the mirror while makepkg # image is layer-cached, so its glibc drifts behind the mirror while makepkg
# -s pulls makedepends from the freshly synced database -- a partial upgrade # -s pulls makedepends from the freshly synced database -- a partial upgrade
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc). # that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
# Done before the Omarchy repos are added so only core/extra participate. # Done before the Omarchy repos are added so only core/extra participate.
echo "==> Updating build container packages..." echo "==> Updating build container packages..."
sudo pacman -Syu --noconfirm || exit 1 sudo pacman -Syu --noconfirm
# Configure Omarchy repositories for dependency resolution # Configure Omarchy repositories for dependency resolution
echo "==> Configuring Omarchy repositories for dependency resolution..." echo "==> Configuring Omarchy repositories for dependency resolution..."
# Always add omarchy-build repo first (for incremental builds). Repository # Always add omarchy-build repo (for incremental builds)
# order is pacman's priority order, so this must precede the official repos; # Packages in build-output are unsigned, so use SigLevel = Never
# otherwise pacman can select an older official package with the same name. sudo tee -a /etc/pacman.conf > /dev/null <<EOF
# Packages in build-output are unsigned, so use SigLevel = Never.
sudo sed -i "/^\[core\]$/i [omarchy-build]\nSigLevel = Never\nServer = file://$BUILD_OUTPUT_DIR\n" /etc/pacman.conf [omarchy-build]
SigLevel = Never
Server = file://$BUILD_OUTPUT_DIR
EOF
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR" echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
# Initialize empty build database if it doesn't exist # Initialize empty build database if it doesn't exist
cd "$BUILD_OUTPUT_DIR" || exit 1 cd "$BUILD_OUTPUT_DIR"
if [[ ! -f "omarchy-build.db.tar.zst" ]]; then if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
# Create an empty database # Create an empty database
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1 || exit 1 repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1 ln -sf omarchy-build.db.tar.zst omarchy-build.db
fi else
# Fold any packages already in the workspace into the database, whether # Database exists, check if we need to rebuild it from packages
# they came with an existing database or were dropped in by an earlier if ls *.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | grep -q .; then
# workflow job (OMARCHY_KEEP_BUILD_WORKSPACE). Without this a seeded
# workspace with no database would leave those packages invisible to
# dependency resolution.
staged_packages=()
for staged in *.pkg.tar.*; do
[[ -f "$staged" && "$staged" != *.sig ]] || continue
staged_packages+=("$staged")
done
if (( ${#staged_packages[@]} )); then
# Seed once per run. After that, only successful builds update the DB;
# rescanning in every container could reintroduce a failed build's partial
# outputs or overwrite the new version with an older kept artifact.
if [[ ! -e "$BUILD_PLAN_DIR/repository-initialized" ]]; then
echo "==> Rebuilding build database from existing packages..." echo "==> Rebuilding build database from existing packages..."
repo-add omarchy-build.db.tar.zst "${staged_packages[@]}" >/dev/null 2>&1 || exit 1 ls *.pkg.tar.* | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | xargs -r repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1 ln -sf omarchy-build.db.tar.zst omarchy-build.db
fi fi
# A resumed/repeated build can produce different bytes under the same
# filename. Never let the shared download cache substitute older bytes
# for the staged artifacts described by this run's database.
for staged in "${staged_packages[@]}"; do
sudo rm -f "/var/cache/pacman/pkg/$staged" "/var/cache/pacman/pkg/$staged.sig" || exit 1
done
fi fi
touch "$BUILD_PLAN_DIR/repository-initialized" || exit 1
# Add omarchy repo if it has a database (stable packages). The local tree # Add omarchy repo if it has a database (stable packages)
# is trusted as-is; the public channel is verified against the omarchy
# keyring the image already carries.
if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Optional TrustAll\nServer = file://$FINAL_OUTPUT_DIR\n" /etc/pacman.conf sudo tee -a /etc/pacman.conf > /dev/null <<EOF
[omarchy]
SigLevel = Optional TrustAll
Server = file://$FINAL_OUTPUT_DIR
EOF
echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR" echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR"
elif [[ -n "$PUBLISHED_REPO_SERVER" ]]; then
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Required DatabaseOptional\nServer = $PUBLISHED_REPO_SERVER\n" /etc/pacman.conf
echo " -> omarchy (priority 2): $PUBLISHED_REPO_SERVER"
fi fi
# Sync pacman database # Sync pacman database
sudo pacman -Sy || exit 1 sudo pacman -Sy
fi fi
echo "==> Package Builder" echo "==> Package Builder"
@@ -170,6 +82,8 @@ if [[ "$DRY_RUN" == true ]]; then
echo "==> Dry run: yes (plan only; makepkg will not run)" echo "==> Dry run: yes (plan only; makepkg will not run)"
fi fi
FAILED_PACKAGES=""
SUCCESSFUL_PACKAGES=""
SKIPPED_PACKAGES="" SKIPPED_PACKAGES=""
# Find package directory # Find package directory
@@ -191,10 +105,10 @@ LOCAL_VERSION_CACHE_LOADED=false
LOCAL_VERSION_CACHE_DB="" LOCAL_VERSION_CACHE_DB=""
load_local_versions() { load_local_versions() {
local db="$PUBLISHED_DB_DIR/omarchy.db.tar.zst" local db="$FINAL_OUTPUT_DIR/omarchy.db.tar.zst"
if [[ ! -f "$db" ]]; then if [[ ! -f "$db" ]]; then
db="$PUBLISHED_DB_DIR/omarchy.db" db="$FINAL_OUTPUT_DIR/omarchy.db"
fi fi
[[ -f "$db" ]] || return 0 [[ -f "$db" ]] || return 0
@@ -241,9 +155,26 @@ get_local_version() {
# Returns 0 (success) if should build, 1 if should skip # Returns 0 (success) if should build, 1 if should skip
should_build_for_arch() { should_build_for_arch() {
local pkg="$1" local pkg="$1"
local pkgdir local current_arch="$ARCH"
pkgdir=$(find_package_dir "$pkg") local pkgdir=$(find_package_dir "$pkg")
[[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH" local pkgbuild="$pkgdir/PKGBUILD"
[[ ! -f "$pkgbuild" ]] && return 1
# Check PKGBUILD arch=() array
local pkgbuild_archs=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${arch[@]}"')
# If arch=('any'), build for all architectures
if [[ "$pkgbuild_archs" == "any" ]]; then
return 0
fi
# Check if current arch is in PKGBUILD arch=()
if echo "$pkgbuild_archs" | grep -qw "$current_arch"; then
return 0 # Build
else
return 1 # Skip
fi
} }
# For VCS packages, makepkg recalculates pkgver() before the build. If the # For VCS packages, makepkg recalculates pkgver() before the build. If the
@@ -293,62 +224,22 @@ refresh_vcs_pkgver_preserving_local_pkgrel() {
fi fi
} }
# With runtime dependency checks deferred, makepkg runs --nodeps, so the
# build-time dependencies it would normally install with -s have to be
# installed explicitly: makedepends and checkdepends, including the
# architecture-suffixed variants for the current CARCH.
install_deferred_build_dependencies() {
local pkg="$1"
local -a build_deps=()
mapfile -t build_deps < <(
CARCH="$ARCH" bash -c '
source PKGBUILD
arch_makedepends="makedepends_${CARCH}[@]"
arch_checkdepends="checkdepends_${CARCH}[@]"
printf "%s\n" \
"${makedepends[@]}" "${!arch_makedepends}" \
"${checkdepends[@]}" "${!arch_checkdepends}"
' | awk 'NF && !seen[$0]++'
)
if (( ${#build_deps[@]} )); then
echo " Installing build-only dependencies for $pkg..."
sudo pacman -S --needed --noconfirm -- "${build_deps[@]}"
fi
}
# Build a package # Build a package
build_package() { build_package() {
local pkg="$1" local pkg="$1"
local pkgdir local pkgdir=$(find_package_dir "$pkg")
pkgdir=$(find_package_dir "$pkg") || return 1
echo "" echo ""
echo " -> Processing: $pkg" echo " -> Processing: $pkg"
# Install this consumer's freshly built prerequisites in its own container.
# Qualifying the repository also upgrades an older dependency baked into
# the base image, even if that version would satisfy makepkg's check.
if [[ "$DEFER_RUNTIME_DEPS" != true ]]; then
local consumer dependency
local -a built_deps=()
while read -r consumer dependency; do
[[ "$consumer" == "$pkg" ]] && built_deps+=("omarchy-build/$dependency")
done < "$BUILD_PLAN_DIR/dependencies"
if (( ${#built_deps[@]} )); then
echo " Installing freshly built dependencies for $pkg..."
sudo /usr/local/bin/pacman-for-makepkg -S --needed --noconfirm -- "${built_deps[@]}" || return 1
fi
fi
# Copy to build directory # Copy to build directory
cd /src || return 1 cd /src
rm -rf "$pkg" || return 1 rm -rf "$pkg"
cp -r "$pkgdir" "$pkg" || return 1 cp -r "$pkgdir" "$pkg"
cd "/src/$pkg" || return 1 cd "/src/$pkg" || return 1
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || { refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1 return 1
} }
@@ -357,6 +248,7 @@ build_package() {
if [[ -z "$pkgbuild_version" ]]; then if [[ -z "$pkgbuild_version" ]]; then
echo " Failed to read PKGBUILD version" echo " Failed to read PKGBUILD version"
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1 return 1
fi fi
@@ -388,72 +280,37 @@ build_package() {
# Build package without signing (signing is done separately) # Build package without signing (signing is done separately)
# PACMAN override uses a wrapper that adds --ask 4 to auto-resolve conflicts # PACMAN override uses a wrapper that adds --ask 4 to auto-resolve conflicts
# (e.g. rustup replacing rust) since --noconfirm defaults to 'N' on those prompts # (e.g. rustup replacing rust) since --noconfirm defaults to 'N' on those prompts
local -a makepkg_flags=(-scf --noconfirm) MAKEPKG_FLAGS="-scf --noconfirm"
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
# The pair's runtime dependencies (each other, and packages other jobs
# of the same pipeline build) are not resolvable here; the assembled set
# is installed in one verified transaction downstream. Only the
# build-time dependencies are installed, then makepkg skips the check.
install_deferred_build_dependencies "$pkg" || {
return 1
}
makepkg_flags=(-cf --noconfirm --nodeps)
fi
if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg "${makepkg_flags[@]}"; then if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg $MAKEPKG_FLAGS; then
# Ensure output directory exists # Ensure output directory exists
mkdir -p "$BUILD_OUTPUT_DIR" mkdir -p "$BUILD_OUTPUT_DIR"
# Copy only the artifacts makepkg declares as outputs. A PKGBUILD may use for pkg_file in *.pkg.tar.*; do
# another pacman package as a source (schist-bin does); a *.pkg.tar.* glob [[ -f "$pkg_file" ]] && cp "$pkg_file" "$BUILD_OUTPUT_DIR/"
# would mistake that source archive for one of our freshly built packages.
local -a package_files=()
mapfile -t package_files < <(makepkg --packagelist)
if [[ ${#package_files[@]} -eq 0 ]]; then
echo " Makepkg produced no package files for $pkg"
return 1
fi
local -a new_pkgs=()
local pkg_path pkg_file
for pkg_path in "${package_files[@]}"; do
pkg_file=${pkg_path##*/}
if [[ ! -f "$pkg_file" ]]; then
# makepkg predicts an automatic -debug output whenever debug is
# enabled, but data-only packages may contain no symbols and therefore
# legitimately produce no debug archive.
if [[ "$pkg_file" == *-debug-*.pkg.tar.* ]]; then
continue
fi
echo " Expected package file was not produced: $pkg_file"
return 1
fi
cp "$pkg_file" "$BUILD_OUTPUT_DIR/" || return 1
new_pkgs+=("$pkg_file")
done done
cd "$BUILD_OUTPUT_DIR" || return 1 cd "$BUILD_OUTPUT_DIR"
# Find ALL package files (handles split packages)
local new_pkgs=($(ls -t ${pkg}-*.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db'))
# Add every output from this build, including split packages.
if [[ ${#new_pkgs[@]} -gt 0 ]]; then if [[ ${#new_pkgs[@]} -gt 0 ]]; then
repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 || return 1 repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || return 1 ln -sf omarchy-build.db.tar.zst omarchy-build.db
sudo pacman -Sy >/dev/null 2>&1
fi fi
# A release may publish successful builds even when a peer fails. Record cd /src/$pkg
# outputs only after this package's entire split build has completed.
mkdir -p "$BUILD_PLAN_DIR/artifacts" || return 1
printf '%s\n' "${new_pkgs[@]}" > "$BUILD_PLAN_DIR/artifacts/$pkg" || return 1
echo " Successfully built $pkg" echo " Successfully built $pkg"
SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $pkg"
return 0 return 0
else else
echo " Makepkg failed for $pkg" echo " Makepkg failed for $pkg"
echo " DEBUG: Files in build directory:" echo " DEBUG: Files in build directory:"
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found" ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1 return 1
fi fi
} }
@@ -468,17 +325,11 @@ get_package_deps() {
return return
fi fi
# Include test dependencies and target-specific arrays: each container must # Extract depends and makedepends, filter for packages in our pkgbuilds/
# receive its prerequisites through the repository, not a previous build.
( (
CARCH="$ARCH"
source "$pkgbuild" 2>/dev/null source "$pkgbuild" 2>/dev/null
for kind in depends makedepends checkdepends; do echo "${depends[@]} ${makedepends[@]}"
generic="${kind}[@]" ) | tr ' ' '\n' | while read -r dep; do
specific="${kind}_${CARCH}[@]"
printf '%s\n' "${!generic}" "${!specific}"
done
) | awk 'NF && !seen[$0]++' | while read -r dep; do
# Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade') # Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade')
dep=$(echo "$dep" | sed 's/[<>=].*$//') dep=$(echo "$dep" | sed 's/[<>=].*$//')
# Check if this dependency exists in our pkgbuilds # Check if this dependency exists in our pkgbuilds
@@ -563,37 +414,27 @@ check_needs_build() {
if [[ "$local_version" == "$pkgbuild_version" ]]; then if [[ "$local_version" == "$pkgbuild_version" ]]; then
return 1 # Already up to date return 1 # Already up to date
else
return 0 # Needs building
fi fi
# Match check-versions: a retained archive is already published even when
# the DB now indexes a newer release (for example, 4.0.4rc1 vs 4.0.3).
# Rebuilding it would produce different bytes under an immutable filename.
if package_version_is_published "$FINAL_OUTPUT_DIR" "$pkg" "$pkgbuild_version" "$ARCH"; then
echo " + $pkg $pkgbuild_version - archive already published; skipping rebuild"
return 1
fi
return 0 # Needs building
} }
# Collect packages that should be built for the selected mirror # Collect packages that should be built for the selected mirror
collect_packages() { collect_packages() {
packages_for_unscoped_build "$MIRROR" "$ARCH" packages_for_unscoped_build "$MIRROR"
} }
# Main execution # Main execution
if [[ "$DRY_RUN" != true ]]; then if [[ "$DRY_RUN" != true ]]; then
cd "$SRC_DIR" || exit 1 cd "$SRC_DIR"
build_package "$BUILD_PACKAGE"
exit $?
fi fi
TOTAL_COUNT=0
echo "==> Checking which packages need building..." echo "==> Checking which packages need building..."
# First pass: determine which packages need building # First pass: determine which packages need building
PACKAGES_TO_BUILD=() PACKAGES_TO_BUILD=()
ORDERED_PACKAGES=()
PLANNED_DEPENDENCIES=()
# If PACKAGES is specified, only check those packages # If PACKAGES is specified, only check those packages
if [[ -n "$PACKAGES" ]]; then if [[ -n "$PACKAGES" ]]; then
@@ -632,6 +473,13 @@ if [[ -n "$PACKAGES" ]]; then
else else
# Build all packages that need updates from the relevant directories # Build all packages that need updates from the relevant directories
while IFS= read -r pkg; do while IFS= read -r pkg; do
# Check if package should be built for this architecture
if ! should_build_for_arch "$pkg"; then
echo " - $pkg - not built for $ARCH"
SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg"
continue
fi
if check_needs_build "$pkg"; then if check_needs_build "$pkg"; then
PACKAGES_TO_BUILD+=("$pkg") PACKAGES_TO_BUILD+=("$pkg")
else else
@@ -644,7 +492,7 @@ fi
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
echo "==> All packages are up to date!" echo "==> All packages are up to date!"
else else
echo "==> ${#PACKAGES_TO_BUILD[@]} package(s) need building: ${PACKAGES_TO_BUILD[*]}" echo "==> ${#PACKAGES_TO_BUILD[@]} package(s) need building: ${PACKAGES_TO_BUILD[@]}"
echo "==> Determining build order based on dependencies..." echo "==> Determining build order based on dependencies..."
# Second pass: order only the packages that need building # Second pass: order only the packages that need building
@@ -667,7 +515,6 @@ else
((unmet_deps_count[$pkg]++)) ((unmet_deps_count[$pkg]++))
# Track that dep blocks pkg from building # Track that dep blocks pkg from building
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg" blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
PLANNED_DEPENDENCIES+=("$pkg $dep")
fi fi
done done
done < <(get_package_deps "$pkg") done < <(get_package_deps "$pkg")
@@ -682,6 +529,7 @@ else
done done
# Build packages as dependencies become available # Build packages as dependencies become available
ORDERED_PACKAGES=()
while [[ ${#ready_to_build[@]} -gt 0 ]]; do while [[ ${#ready_to_build[@]} -gt 0 ]]; do
# Take the first ready package # Take the first ready package
current="${ready_to_build[0]}" current="${ready_to_build[0]}"
@@ -703,16 +551,63 @@ else
exit 1 exit 1
fi fi
echo "==> Build order: ${ORDERED_PACKAGES[*]}" echo "==> Build order: ${ORDERED_PACKAGES[@]}"
fi
# The host consumes plain data, never shell code or parsed human log output. if [[ "$DRY_RUN" == true ]]; then
if [[ -n "$BUILD_PLAN_DIR" ]]; then echo ""
mkdir -p "$BUILD_PLAN_DIR" || exit 1 echo "==> Dry run complete. Packages that would build: ${ORDERED_PACKAGES[@]}"
printf '%s\n' "${ORDERED_PACKAGES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/packages" || exit 1 exit 0
printf '%s\n' "${PLANNED_DEPENDENCIES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/dependencies" || exit 1 fi
printf '%s\n' $SKIPPED_PACKAGES | sed '/^$/d' > "$BUILD_PLAN_DIR/skipped" || exit 1
# Determine which packages need to be installed for other packages being built
declare -A INSTALL_PACKAGES
for pkg in "${ORDERED_PACKAGES[@]}"; do
while IFS= read -r dep; do
[[ -z "$dep" ]] && continue
# Only install if it's being built in this run
for build_pkg in "${ORDERED_PACKAGES[@]}"; do
[[ "$dep" == "$build_pkg" ]] && INSTALL_PACKAGES["$dep"]=1
done
done < <(get_package_deps "$pkg")
done
if [[ ${#INSTALL_PACKAGES[@]} -gt 0 ]]; then
echo "==> Packages needed as dependencies: ${!INSTALL_PACKAGES[@]}"
fi
# Build packages in dependency order
for pkg in "${ORDERED_PACKAGES[@]}"; do
((TOTAL_COUNT++))
build_package "$pkg"
done
fi fi
echo "" echo ""
echo "==> Plan complete. Packages that would build: ${ORDERED_PACKAGES[*]}" echo "========================================"
echo "==> Build Summary"
echo "========================================"
# Count results
SUCCESS_COUNT=$(echo $SUCCESSFUL_PACKAGES | wc -w)
SKIPPED_COUNT=$(echo $SKIPPED_PACKAGES | wc -w)
FAILED_COUNT=$(echo $FAILED_PACKAGES | wc -w)
echo " Total packages: $TOTAL_COUNT"
echo " Built: $SUCCESS_COUNT"
echo " Skipped: $SKIPPED_COUNT (already up-to-date)"
echo " Failed: $FAILED_COUNT"
# List failures if any
if [[ -n "$FAILED_PACKAGES" ]]; then
echo ""
echo "Failed packages:"
for pkg in $FAILED_PACKAGES; do
echo " - $pkg"
done
echo ""
echo "==> Some packages failed to build"
exit 1
fi
echo ""
echo "==> All packages processed successfully!"
-118
View File
@@ -1,118 +0,0 @@
# The x86_64 builder pool
x86_64 package builds run on DigitalOcean droplets that exist for one job
each. aarch64 builds run on GitHub's `ubuntu-24.04-arm` runners and need
nothing here.
## How it works
- **Jobs ask for a runner labelled `omarchy-builder`.** `build-pr.yml` builds
x86_64 packages there, and so does the `rebuild` job in `publish.yml`.
- **A controller creates one droplet per queued job.** `controller.sh` runs
from a systemd timer every minute on a small always-on droplet (tag
`omarchy-controller`). It polls GitHub for queued jobs with the label and
creates droplets up to `MAX_DROPLETS`. It has no inbound endpoint.
- **A droplet runs one job and powers off.** `runner-cloud-init.yaml` installs
Docker and the GitHub runner, registers it `--ephemeral` with a
registration token that expires in an hour, runs the job, and powers off, also when the runner download or
registration fails. The controller removes any other failed droplet at
`MAX_AGE_MINUTES`.
- **The controller deletes droplets** that are powered off, older than
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`, and
replaces a stuck one in the same tick.
- **Sizes and regions fall back.** Each tick tries every size in `SIZES`, in
every region DigitalOcean lists it in stock. `REGIONS` only sets which
regions go first. A refused create is logged with DigitalOcean's message.
No secret reaches a builder. Signing and upload happen in the `publish` job on
a GitHub-hosted runner.
## Operate it
SSH to the controller droplet as root. It is the droplet tagged
`omarchy-controller` in the DigitalOcean account that pays for the builders;
the operators' keys were installed when it was created.
```bash
journalctl -u omarchy-controller -n 50 --no-pager # What it is doing
journalctl -u omarchy-controller -f # Follow it
systemctl list-timers omarchy-controller.timer # Is it ticking
```
| Log line | Meaning |
|---|---|
| `creating <name> (<size> in <region>)` | A queued job is getting a droplet |
| `<size> in <region> refused: <message>` | DigitalOcean refused; the next region or size is tried |
| `no size in '<sizes>' can be created in any region` | Every create was refused. Read the `refused:` lines above it for why: out of stock (add a size to `SIZES`), or the account's droplet limit |
| `at cap (<live>/<max>, <busy> busy) with <queued> queued` | `MAX_DROPLETS` is reached. Raise it if the queue is long |
| `deleting droplet <id> (status=<status> age=<n>m)` | Reaping a finished, old or stuck droplet |
Check the pool from anywhere:
```bash
gh api repos/omacom/omarchy-pkgs/actions/runners --jq \
'"online \([.runners[]|select(.status=="online")]|length), busy \([.runners[]|select(.busy)]|length)"'
```
Jobs queued with no runner online for several minutes means the controller is
not creating droplets: read its journal. See what is waiting:
```bash
gh run list -R omacom/omarchy-pkgs --workflow build-pr.yml --status queued
```
Only x86_64 jobs use this pool. A queued aarch64 job is waiting on GitHub's
own runners.
### Change settings
Edit `/etc/omarchy-controller.env` on the box. The next tick reads it.
| Setting | Default in `controller-box/controller.env.example` | |
|---|---|---|
| `MAX_DROPLETS` | 6 | Builders alive at once. The DigitalOcean account's droplet limit also applies |
| `SIZES` | `g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb` | Tried in order |
| `REGIONS` | empty | Regions to try first; empty means any |
| `MAX_AGE_MINUTES` | 200 | Delete a droplet older than this |
| `MAX_BOOT_MINUTES` | 10 | Delete a droplet still provisioning after this |
| `DO_SSH_KEYS` | Ryan, DHH and Emir's account key IDs | Attached to every builder. Without one, DigitalOcean emails a root password per builder |
### Change the controller
Merge the change to `master`. The unit pulls `/opt/omarchy-pkgs` before every
tick, so it is live within a minute. The unit and timer files are copies made
when the box was created; after changing them, on the box:
```bash
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
systemctl daemon-reload
```
`tests/controller.sh` exercises every controller decision against canned API
responses. Run it before merging a change.
### Reach a builder
Operators can SSH to a builder as root while it lives. It powers off after its
one job.
### Clean up by hand
The controller reaps on its own. To do it manually, delete droplets tagged
`omarchy-builder` in the DigitalOcean account.
## Stand up a controller
```bash
DIGITALOCEAN_TOKEN=<account that pays for builders> GITHUB_TOKEN=<fine-grained PAT> \
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh
```
- The GitHub PAT is fine-grained and scoped to this repository: Actions read,
Administration read and write (for runner registration tokens).
- The DigitalOcean token is written into the box's env file, so it is the
account that pays. It needs ssh_key:read to attach `DO_SSH_KEYS`; without
it every create is refused with 403.
- `ADMIN_GITHUB_USERS` names whose GitHub SSH keys get root on the box and
the builders. Set it; the default is a fixed list in `create.sh`.
- The script refuses to create a second controller.
-45
View File
@@ -1,45 +0,0 @@
#cloud-config
# The always-on controller droplet (smallest size is fine). Clones the repo
# for ci/controller.sh, installs the unit and timer, and starts polling.
#
# Substitute before use:
# __REPO_URL__ https://github.com/omacom/omarchy-pkgs.git
# __BRANCH__ branch carrying ci/ (master once merged)
# __ENV_B64__ base64 of a filled-in controller.env.example
# __SSH_KEYS_JSON__ JSON array of public keys authorized for root
package_update: true
packages: [curl, jq, git]
# Root stays reachable by key so the journal can be read. Two things stand
# in the way on DO images: disable_root rewrites root's keys into a stub, and
# with no account ssh key attached DO expires root's password, which makes
# sshd refuse every non-interactive session with "password change required".
disable_root: false
chpasswd:
expire: false
ssh_authorized_keys: __SSH_KEYS_JSON__
users:
- name: controller
shell: /bin/bash
write_files:
# defer: write after the users module has created the controller group,
# otherwise chown to root:controller fails and the unit cannot read this.
- path: /etc/omarchy-controller.env
permissions: "0640"
owner: root:controller
encoding: b64
defer: true
content: __ENV_B64__
runcmd:
- chage -d "$(date +%F)" -M -1 root
- chown root:controller /etc/omarchy-controller.env && chmod 0640 /etc/omarchy-controller.env
- git clone --depth 1 --branch __BRANCH__ __REPO_URL__ /opt/omarchy-pkgs
- mkdir -p /run/omarchy-controller && chown controller:controller /run/omarchy-controller
- echo "d /run/omarchy-controller 0755 controller controller -" > /etc/tmpfiles.d/omarchy-controller.conf
# runcmd is executed by /bin/sh: no brace expansion.
- cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.service /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.timer /etc/systemd/system/
- systemctl daemon-reload
- systemctl enable --now omarchy-controller.timer
-24
View File
@@ -1,24 +0,0 @@
# /etc/omarchy-controller.env — mode 0600, owned by root, read by systemd.
DIGITALOCEAN_TOKEN=dop_v1_...
# Fine-grained PAT scoped to the repo: Actions: read, Administration: read+write
GITHUB_TOKEN=github_pat_...
REPO=omacom/omarchy-pkgs
LABEL=omarchy-builder
TAG=omarchy-builder
# Builder sizes, tried in order in any region that has them in stock.
SIZES="g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb"
# Optional: regions to try first, e.g. "ric1". Empty means any.
REGIONS=
MAX_DROPLETS=6
MAX_AGE_MINUTES=200
# Delete a droplet DigitalOcean still reports as provisioning after this long.
MAX_BOOT_MINUTES=10
LOCK=/run/omarchy-controller/lock
# Operator public keys for root on every builder droplet (JSON array).
# create.sh fills this from the operators' GitHub keys.
SSH_KEYS_JSON=[]
# DigitalOcean account SSH key IDs attached to every builder (JSON array), so
# DigitalOcean does not email a root password for each one. Needs ssh_key:read
# on DIGITALOCEAN_TOKEN. List them with: doctl compute ssh-key list
# Ryan R. Hughes, DHH, Emir.
DO_SSH_KEYS=[59143572,59143588,59779105]
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
# Create the controller droplet with plain curl. Run from a laptop, once.
#
# DIGITALOCEAN_TOKEN=... GITHUB_TOKEN=... ci/controller-box/create.sh [branch]
#
# The DO token given here is baked into the box's env file, so it must be the
# token for the account that should pay for builder droplets.
set -euo pipefail
here=$(dirname "$0")
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
REPO=${REPO:-omacom/omarchy-pkgs}
BRANCH=${1:-master}
REGION=${REGION:-ric1}
NAME=${NAME:-omarchy-controller}
# Optional DO ssh key ids as a JSON array, e.g. SSH_KEYS='[123]', for reading
# the journal while bringing the box up. Not needed once it works.
SSH_KEYS=${SSH_KEYS:-[]}
# Public keys authorized for root: the operators' GitHub keys, fetched at
# creation so the box never depends on an ssh_key API scope. Override with
# ADMIN_GITHUB_USERS.
ADMIN_GITHUB_USERS=${ADMIN_GITHUB_USERS:-ryanrhughes dhh emirb}
ssh_keys_json=$(for u in $ADMIN_GITHUB_USERS; do curl -fsS "https://github.com/$u.keys"; done | jq -R . | jq -sc .)
[[ $(jq length <<<"$ssh_keys_json") -gt 0 ]] || { echo "no ssh keys fetched for $ADMIN_GITHUB_USERS" >&2; exit 1; }
env_file=$(sed -e "s|^DIGITALOCEAN_TOKEN=.*|DIGITALOCEAN_TOKEN=$DIGITALOCEAN_TOKEN|" \
-e "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$GITHUB_TOKEN|" \
-e "s|^REPO=.*|REPO=$REPO|" \
-e "s|^SSH_KEYS_JSON=.*|SSH_KEYS_JSON=$ssh_keys_json|" "$here/controller.env.example")
userdata=$(sed -e "s|__REPO_URL__|https://github.com/$REPO.git|" -e "s|__BRANCH__|$BRANCH|" \
-e "s|__ENV_B64__|$(printf '%s\n' "$env_file" | base64 -w0)|" \
-e "s|__SSH_KEYS_JSON__|$ssh_keys_json|" "$here/cloud-init.yaml")
body=$(jq -n --arg name "$NAME" --arg region "$REGION" --arg ud "$userdata" --argjson keys "$SSH_KEYS" \
'{name:$name, region:$region, size:"s-1vcpu-1gb", image:"ubuntu-24-04-x64", tags:["omarchy-controller"], user_data:$ud, ssh_keys:$keys}')
# Refuse to create a second one.
existing=$(curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/droplets?tag_name=omarchy-controller" | jq '.droplets | length')
if (( existing > 0 )); then echo "a controller droplet already exists" >&2; exit 1; fi
curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" -H "Content-Type: application/json" \
-X POST -d "$body" https://api.digitalocean.com/v2/droplets | jq -r '"created \(.droplet.name) id=\(.droplet.id)"'
@@ -1,15 +0,0 @@
[Unit]
Description=Provision ephemeral omarchy-builder runner droplets for queued jobs
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=controller
EnvironmentFile=/etc/omarchy-controller.env
# Run the branch's current controller, not the one cloned when the box was
# built. As root (the checkout's owner); a failed pull keeps the last one.
ExecStartPre=-+/usr/bin/git -C /opt/omarchy-pkgs pull --ff-only --quiet
ExecStart=/opt/omarchy-pkgs/ci/controller.sh
# The reaper's safety net is time, not state; a hung tick must not hold the lock.
TimeoutStartSec=240
@@ -1,10 +0,0 @@
[Unit]
Description=Run the omarchy-builder controller every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=5s
[Install]
WantedBy=timers.target
-201
View File
@@ -1,201 +0,0 @@
#!/bin/bash
# Droplet-per-job controller for the omarchy-builder runner pool.
#
# Run from a systemd timer every minute on a small always-on droplet. No
# inbound endpoint: it polls GitHub for queued jobs wanting our label, creates
# one ephemeral droplet per job (up to MAX_DROPLETS), and deletes droplets
# that have powered off, exceeded MAX_AGE_MINUTES, or are still provisioning
# after MAX_BOOT_MINUTES. The reaper does not
# trust its own bookkeeping: it lists by tag and acts on what DigitalOcean
# reports.
#
# Talks to both APIs with curl. No doctl: its saved contexts silently choose
# an account; a token in the environment cannot. Needs curl and jq.
#
# Environment:
# DIGITALOCEAN_TOKEN DO API token for the account that pays for droplets
# GITHUB_TOKEN fine-grained PAT: Actions read, Administration write
# REPO owner/name
set -euo pipefail
REPO=${REPO:?owner/name}
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
LABEL=${LABEL:-omarchy-builder}
TAG=${TAG:-omarchy-builder}
# Sizes to try, in order, in any region DigitalOcean lists them in stock. A
# size can sell out in a region for hours; the create is then refused with
# 422 and the next region, then the next size, is tried. REGIONS only orders
# the regions tried first. SIZE and REGION, if set, are one-item lists.
SIZES=${SIZES:-${SIZE:-g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb}}
REGIONS=${REGIONS:-${REGION:-}}
IMAGE=${IMAGE:-ubuntu-24-04-x64}
MAX_DROPLETS=${MAX_DROPLETS:-4}
MAX_AGE_MINUTES=${MAX_AGE_MINUTES:-200}
# A droplet DigitalOcean still reports as "new" this long after creation is
# stuck provisioning. Left alone it counts as a runner booting, and holds a
# queued job until MAX_AGE_MINUTES.
MAX_BOOT_MINUTES=${MAX_BOOT_MINUTES:-10}
RUNNER_VERSION=${RUNNER_VERSION:-2.337.0}
CLOUD_INIT=${CLOUD_INIT:-$(dirname "$0")/runner-cloud-init.yaml}
# Operator public keys authorized on every builder (JSON array of strings).
# The box's env file carries them; empty means no root login.
SSH_KEYS_JSON=${SSH_KEYS_JSON:-[]}
# DigitalOcean account SSH key IDs or fingerprints attached at creation (JSON
# array). A droplet created with none gets a root password that DigitalOcean
# emails to the token's owner, once per builder. Attaching one needs the
# ssh_key:read scope on DIGITALOCEAN_TOKEN; without it every create is
# refused with 403.
DO_SSH_KEYS=${DO_SSH_KEYS:-[]}
LOCK=${LOCK:-/tmp/omarchy-controller.lock}
log() { echo "$(date '+%F %T') $*"; }
# The only two places the outside world is touched. The self-test overrides
# both, so every decision below is exercised against canned responses.
do_api() { # do_api <path> [curl args...]
local path=$1; shift
# --fail-with-body: a refused create still prints why.
curl -sS --fail-with-body -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-H "Content-Type: application/json" "https://api.digitalocean.com/v2/$path" "$@"
}
gh_api() { # gh_api <path> [curl args...]
local path=$1; shift
curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" "https://api.github.com/$path" "$@"
}
# --- reap ------------------------------------------------------------------
reap() {
local now id status created age
now=$(date +%s)
while read -r id status created; do
[[ -n "$id" ]] || continue
age=$(( (now - $(date -d "$created" +%s)) / 60 ))
if [[ $status == off ]] || (( age > MAX_AGE_MINUTES )) ||
{ [[ $status == new ]] && (( age > MAX_BOOT_MINUTES )); }; then
log "deleting droplet $id (status=$status age=${age}m)"
do_api "droplets/$id" -X DELETE
fi
done < <(do_api "droplets?tag_name=$TAG&per_page=200" |
jq -r '.droplets[] | "\(.id) \(.status) \(.created_at)"')
}
# --- demand ----------------------------------------------------------------
# Emit every item, including later pages of large build matrices. Keep API
# failures fatal so a failed query cannot look like an empty queue.
gh_items() {
local path=$1 key=$2 page=1 response count separator="?"
[[ $path != *"?"* ]] || separator="&"
while :; do
response=$(gh_api "${path}${separator}per_page=100&page=$page") || return 1
count=$(jq -er --arg key "$key" '.[$key] | arrays | length' <<< "$response") || return 1
jq -c --arg key "$key" '.[$key][]' <<< "$response" || return 1
(( count == 100 )) || break
((page += 1))
done
}
queued_jobs() {
local status runs run
# A workflow can be in progress while most of its matrix is still queued.
runs=$(
for status in queued in_progress; do
gh_items "repos/$REPO/actions/runs?status=$status" workflow_runs || exit 1
done
) || return 1
jq -r '.id' <<< "$runs" | sort -u |
while read -r run; do
gh_items "repos/$REPO/actions/runs/$run/jobs" jobs |
jq -r --arg l "$LABEL" 'select(.status=="queued") | select(.labels | index($l)) | .id' || return 1
done | sort -u | wc -l
}
live_droplets() {
# Not the ones reap() just deleted: DigitalOcean can list them for a while.
do_api "droplets?tag_name=$TAG&per_page=200" | jq --argjson boot "$MAX_BOOT_MINUTES" '
[.droplets[] | select(.status != "off")
| select(.status != "new" or (now - (.created_at | fromdateiso8601)) / 60 <= $boot)] | length'
}
busy_runners() {
gh_api "repos/$REPO/actions/runners?per_page=100" \
| jq --arg l "$LABEL" '[.runners[] | select(.busy) | select(any(.labels[]; .name == $l))] | length'
}
# --- capacity --------------------------------------------------------------
# "size region" lines to try, best first: SIZES order, then REGIONS order,
# then every other region where DigitalOcean lists the size in stock.
candidates() {
local page=1 response count catalog=""
while :; do
response=$(do_api "sizes?per_page=200&page=$page") || return 1
count=$(jq -er '.sizes | arrays | length' <<< "$response") || return 1
catalog+=$(jq -c '.sizes[]' <<< "$response")$'\n'
(( count == 200 )) || break
((page += 1))
done
jq -rs --arg sizes "$SIZES" --arg regions "$REGIONS" '
($regions | split(" ") | map(select(length > 0))) as $pref
| INDEX(.slug) as $by
| $sizes | split(" ") | map(select(length > 0)) | .[]
| . as $size | $by[$size] // {} | select(.available == true)
| .regions as $in
| (($pref | map(select(. as $r | $in | index($r)))) + ($in - $pref))[]
| "\($size) \(.)"' <<< "$catalog"
}
# --- create ----------------------------------------------------------------
# Built once per tick by the first create; a refused pair is dropped from it.
CANDIDATES=""
create_droplet() {
local token userdata name size region body response
[[ -n $CANDIDATES ]] || CANDIDATES=$(candidates) || return 1
token=$(gh_api "repos/$REPO/actions/runners/registration-token" -X POST | jq -r .token)
userdata=$(sed -e "s|__REPO__|$REPO|g" -e "s|__RUNNER_TOKEN__|$token|g" \
-e "s|__RUNNER_LABELS__|$LABEL|g" -e "s|__RUNNER_VERSION__|$RUNNER_VERSION|g" \
-e "s|__SSH_KEYS_JSON__|$SSH_KEYS_JSON|" "$CLOUD_INIT")
name="$TAG-$(date +%s)-$RANDOM"
while read -r size region; do
[[ -n $size ]] || continue
body=$(jq -n --arg name "$name" --arg region "$region" --arg size "$size" --arg image "$IMAGE" \
--arg tag "$TAG" --arg ud "$userdata" --argjson keys "$DO_SSH_KEYS" \
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, ssh_keys:$keys, monitoring:false}')
log "creating $name ($size in $region)"
if response=$(do_api droplets -X POST -d "$body"); then
jq -r '"created droplet \(.droplet.id)"' <<< "$response"
return 0
fi
log "$size in $region refused: $(jq -r .message <<< "$response" 2>/dev/null || echo "$response")"
CANDIDATES=$(grep -Fvx "$size $region" <<< "$CANDIDATES" || true)
done <<< "$CANDIDATES"
# Every size refused everywhere: the rest of this tick's creates would be too.
log "no size in '$SIZES' can be created in any region"
return 1
}
controller_tick() {
CANDIDATES=""
reap
local queued live busy available need room
queued=$(queued_jobs)
live=$(live_droplets)
busy=$(busy_runners)
# A live droplet whose runner is busy is spoken for. Only droplets still
# booting or listening can absorb a queued job.
available=$(( live - busy )); (( available < 0 )) && available=0
need=$(( queued - available ))
(( need > 0 )) || return 0
room=$(( MAX_DROPLETS - live ))
(( need > room )) && need=$room
if (( need <= 0 )); then
log "at cap ($live/$MAX_DROPLETS, $busy busy) with $queued queued"
return 0
fi
local i
for (( i = 0; i < need; i++ )); do create_droplet; done
}
if [[ "${CONTROLLER_LIBRARY_ONLY:-}" != 1 ]]; then
exec 9>"$LOCK"; flock -n 9 || exit 0
controller_tick
fi
-86
View File
@@ -1,86 +0,0 @@
#cloud-config
# Ephemeral GitHub Actions runner for omarchy-pkgs package builds.
#
# Boots an Ubuntu droplet, installs docker and the runner, registers ONCE with
# --ephemeral, runs exactly one job, then powers off. The controller (or the
# reaper) deletes the powered-off droplet. Nothing here holds a long-lived
# credential: the registration token is single-use and expires in an hour.
#
# Substitute before use:
# __REPO__ owner/name
# __RUNNER_TOKEN__ registration token (gh api -X POST repos/O/R/actions/runners/registration-token)
# __RUNNER_LABELS__ e.g. omarchy-builder
# __RUNNER_VERSION__ e.g. 2.329.0
# Operators can reach a builder by key while it lives; it powers off after
# one job anyway. Keys are substituted by the controller (__SSH_KEYS_JSON__).
# Never by password: DigitalOcean sets and emails one when no account key is
# attached, and its vendor data turns password logins on to match.
disable_root: false
ssh_pwauth: false
chpasswd:
expire: false
ssh_authorized_keys: __SSH_KEYS_JSON__
package_update: true
packages:
- docker.io
- docker-buildx
- unzip
- git
- curl
- jq
- rsync
users:
- name: runner
groups: [docker]
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
write_files:
# defer: write after users/groups exist, so /home/runner is created by
# useradd (owned by runner) rather than by this module as root.
- path: /home/runner/start.sh
permissions: "0755"
owner: runner:runner
defer: true
content: |
#!/bin/bash
set -euo pipefail
# Power off after the one job, and also when the download or the
# registration fails: the controller deletes powered-off droplets, but
# counts a running one as a runner still booting until MAX_AGE_MINUTES.
trap 'sudo poweroff' EXIT
cd /home/runner
mkdir -p actions-runner && cd actions-runner
arch=$(uname -m); [[ $arch == x86_64 ]] && arch=x64
curl -fsSL -o runner.tgz \
"https://github.com/actions/runner/releases/download/v__RUNNER_VERSION__/actions-runner-linux-${arch}-__RUNNER_VERSION__.tar.gz"
tar xzf runner.tgz && rm runner.tgz
./config.sh --unattended --ephemeral \
--url "https://github.com/__REPO__" \
--token "__RUNNER_TOKEN__" \
--name "do-$(hostname)" \
--labels "__RUNNER_LABELS__" \
--replace
./run.sh
runcmd:
# With no account ssh key attached, DO expires root's password, and sshd
# then refuses every non-interactive session. Clear it first so operators
# can read the logs of a builder that never registers.
- chage -d "$(date +%F)" -M -1 root
- systemctl enable --now docker
# aarch64 builds run under user-mode emulation (DO has no arm droplets).
# Register QEMU with the F and C flags via tonistiigi/binfmt, exactly as
# helpers/docker-helpers.sh setup_qemu does. Ubuntu's qemu-user-static
# registers without C, so sudo inside the emulated container fails with
# "effective uid is not 0"; multiarch/qemu-user-static is abandoned at QEMU
# 7.2, under which qmake's compiler probe returns nothing on current gcc
# ("failed to parse default include paths", PR #517). Pin the emulator
# version: the tag is the only thing that decides what every aarch64 build
# runs under. Best-effort: an x86-only job never needs it.
- docker run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall qemu-aarch64 --install arm64 || true
- chown -R runner:runner /home/runner
- sudo -u runner /home/runner/start.sh > /home/runner/runner.log 2>&1
-80
View File
@@ -1,80 +0,0 @@
# How a package builds
`bin/build` is the one builder: PR builds, rebuilds at merge and local builds
all call it.
## Dependency resolution
Within one `bin/build` run, the build system handles dependencies between the
packages it was asked to build. CI builds each package in its own job, so a
PR's packages do not see each other's fresh builds; they resolve from `edge`.
1. Plans dependency order once, including `depends`, `makedepends`,
`checkdepends`, and their architecture-specific arrays.
2. Builds each package in a fresh container. Installed packages and changes
to the container's system files cannot carry over to the next build.
3. Shares successful artifacts through the temporary `[omarchy-build]` repo,
installing newly built prerequisites in each consumer's container.
4. Blocks consumers of a failed prerequisite while continuing independent
builds.
Example: If `aether` depends on `hyprshade`, `hyprshade` is built first.
Isolation also lets the release and dev Omarchy pairs build in the same run:
Flea can install `omarchy` without preventing `omarchy-dev` from installing
its conflicting settings package in a different container.
Pacman downloads are cached under `cache/pacman/<channel>/<arch>/` across
containers and runs. The installed package database is never shared. Each
container updates its base system before resolving build dependencies, so a
cached builder image cannot cause a partial system upgrade. The existing
`OMARCHY_KEEP_BUILD_WORKSPACE`, `OMARCHY_SKIP_BUILDER_IMAGE`, and
`OMARCHY_DEFER_RUNTIME_DEPS` flags retain their behavior.
`tests/build-isolation.sh` exercises conflicting package pairs, failed
prerequisites, resumed builds, cache replacement, and deferred dependencies
using real containers and pacman transactions. It uses the prepared builder
image, or an image named by `TEST_BUILDER_IMAGE`; CI builds the small fixture
image in `tests/build-isolation.Dockerfile`.
## Daily builder images
`Refresh builder images` builds fresh `edge` environments daily at 04:23 UTC,
when their inputs change on `master`, and on manual dispatch. x86_64 and
aarch64 build on native GitHub-hosted runners, without occupying the DO
package-builder pool. Each candidate must pass `tests/build-isolation.sh`,
including real package builds, before publication to
`ghcr.io/omacom/omarchy-pkg-builder`. Only `master` in this repository can
publish; PR workflows cannot replace the shared images.
PRs that change image inputs also build and test both candidates on native
runners, with a read-only token and no registry publication.
The compatibility tag contains the architecture, mirror, and a hash of the
entire `build/` context, including executable bits and symlink targets but
excluding checkout timestamps and ownership. This deliberately invalidates
images when mounted build scripts change too. `v1` identifies the image build
contract; change it if the invocation or compatibility rules change. Each
successful refresh also gets a run-specific tag for diagnosis and rollback.
A failed build, isolation test, or push leaves the previous compatible image
selected. Scheduled builds use `--pull --no-cache` so unchanged Dockerfiles
still pick up fresh Arch packages.
To build and test a candidate locally:
```bash
bin/builder-image key --arch x86_64 --mirror edge
bin/builder-image build --arch x86_64 --mirror edge --tag builder-candidate:test --fresh
CONTAINER_ENGINE=docker TEST_BUILDER_IMAGE=builder-candidate:test tests/build-isolation.sh
```
The workflow uses its repository `GITHUB_TOKEN` with `packages: write`; no
registry PAT is needed. **One-time setup when the image package is first
created:** GHCR
creates the package private. In the `omacom/omarchy-pkg-builder` package
settings, change visibility to **Public**, then rerun the failed refresh job.
The workflow checks anonymous registry access before advancing the compatible
tag, so fork PRs will not be directed to an image they cannot pull. Subsequent
refreshes preserve that package visibility.
`publish.yml` and `unpublish.yml` pull the compatible image to sign and update
databases. Package build jobs still build their own image on the runner.
-44
View File
@@ -1,44 +0,0 @@
# Rebuild triggers
```bash
bin/sync-rebuilds # Bump every package whose dependencies moved
bin/sync-rebuilds quickshell-git # Update specific packages
bin/sync-rebuilds --self-test # Run the regression tests
```
Some packages have to be rebuilt when something they link against changes, even though nothing in their own source moved. A Qt private-API consumer is the usual case: `Qt_6_PRIVATE_API` symbols are not covered by the soname, so a qt6-base point release can leave an installed binary unable to resolve a symbol at startup, and pacman upgrades Qt out from under it because the dependency is unversioned. The package still builds from the same git commit, so its version alone would not trigger a rebuild.
A package names those dependencies in `.omarchy/package.json`:
```json
{ "source": "local", "rebuild_on": ["qt6-base", "qt6-declarative", "qt6-wayland"] }
```
`bin/sync-rebuilds` reads each named package's version from the official
repositories for every architecture a merge builds (`CI_ARCHES` in
`helpers/paths.sh`, both by default) that the package supports and compares
it to `rebuilt_against`. Records are kept per architecture because Arch and
Arch Linux ARM can carry different dependency versions. pkgrel is bumped once
when any recorded version moves; that one source revision is then built for
each architecture by the PR that carries the bump.
The bump is the point of the command, and it has to land in git rather than in the builder. A rebuild that reuses the published version string produces a package pacman will never offer anyone, so rebuilding without a version change would ship nothing. Bumping pkgrel needs no other change: the PR that carries the bump builds the package, and the merge publishes it.
The bumped version is checked against the published one as well as the checked-in one, and refused when pacman would not order it higher. The checked-in version is not the floor; what a user already has is, and a checkout that has fallen behind the repository can otherwise be bumped to something that loses to the package it means to replace. That check is skipped with a warning when the published database cannot be read.
x86_64 versions are read from the local pacman database, so the workflow runs
in an Arch container pointed at `mirror.omarchy.org`, the same mirror as the
x86_64 builder. aarch64 versions are read directly from the live Arch Linux ARM
repository database, which is also what the ARM builder uses. Testing and
staging repositories do not count. A legacy flat `rebuilt_against` record is
read as x86_64 and is migrated naturally the next time a rebuild is needed.
A dependency this repository carries for an architecture (a recipe here that builds for edge on it, such as aquamarine on aarch64) shadows the distribution's, because the builder lists `[omarchy]` first. Its version is the recipe's, and it counts only once edge publishes that version: until then the builder still links against the previous one, so dependents are left alone for that run.
## The workflow
`sync-rebuilds.yml` runs `bin/sync-rebuilds` every 6 hours and opens one PR per
package on `auto/sync-rebuilds-<package>` with auto-merge enabled. Each PR lands
once `result`, `self-tests` and `build-isolation` pass, and the merge publishes.
A rebuild that fails stays an open red PR for a maintainer without holding back
the others.
-119
View File
@@ -1,119 +0,0 @@
# Cutting an Omarchy release
The release train runs from `bin/omarchy-release`. Nothing in the GitHub
workflows promotes a channel: its channel moves and RC builds were written to
execute on the old repository host, which is abandoned (see
[repository-host.md](repository-host.md)), against a local copy of the
published tree.
> `rc` and `ship` build the release pair with `bin/repo release` and move
> channels with `bin/repo advance`, on the machine that holds the published
> tree. No workflow replaces them yet. That machine's tree has to match the
> bucket first: `advance` reads the source channel from local disk.
`bin/omarchy-release` is the front door. A release train has three human
moments, each one command — and bare `omarchy-release` observes reality
(branches, pins, published channels, tags) and walks you to the next one:
```bash
bin/omarchy-release # shepherd: status + guided next step
bin/omarchy-release start 4.0.2 # open the train: branch v4-0-2 + staging PR
bin/omarchy-release pick # choose merged PRs to cherry-pick (multi-select)
bin/omarchy-release rc # publish the next 4.0.2rcN to the rc channel
bin/omarchy-release ship # tag, final pins, promote rc → stable,
# draft GitHub release, ISO, website — one swoop
bin/omarchy-release doctor # verify credentials/connections up front
```
Versions are inferred from branch names (`v4-0-2` ⇒ `4.0.2rcN` ⇒ tag
`v4.0.2`); `start` is the only place a version is typed. Every command is
idempotent — re-runs skip whatever is already done. `ship` refuses to promote
a commit no RC was cut from. One exception: `ship` carries on past a failed
ISO or website step, and a later `ship` exits once stable holds the version
and the GitHub release exists. Finish a failed ISO or website step by hand.
## Promoting a channel
Ship the tested rc channel to stable (copies packages + signatures, then
cleans, rebuilds the database, and syncs):
```
bin/repo advance --from rc --to stable --arch all
```
Open a release train that ships new edge packages by carrying edge into rc
first:
```
bin/repo advance --from edge --to rc --arch all
```
`advance` moves x86_64 alone unless given `--arch`. `--arch all` covers the
architectures in `PUBLISHED_ARCHES` (`helpers/paths.sh`), x86_64 only by
default; set `OMARCHY_ARCHES="x86_64 aarch64"` on the machine that runs it to
promote both. The variable is not carried over when `bin/repo` forwards a
command over ssh, and `omarchy-release ship` promotes through the same path. It refuses to
carry a package into a channel its `channels` metadata excludes.
## The pin engine
The release pair is marked `"pinned": true`. It is built for `rc` only from
the `rc` branch worktree, with `OMARCHY_RC_PINS=1` set (`omarchy-release rc`
sets it), and never built for `stable`: stable gets it by promotion. A merge to
`master` therefore cannot overwrite an RC in flight.
The `omarchy` and `omarchy-settings` packages are released as a pair, always
built from the same upstream commit of basecamp/omarchy.
**Use `bin/omarchy-release`** — it drives the whole train across all four repositories and
calls the pin engine below for you.
`bin/omarchy-pkgs` is that engine, available directly for one-off pins and
debugging. It rewrites both PKGBUILDs in lockstep (same `_tag`/`_commit`/
`pkgver`/`sha256sums`), validates ordering with `vercmp`, commits, and pushes
the current branch. Driven by `omarchy-release` it pins to the release branch
on the `rc` branch and orders against the rc channel; invoked directly it
targets the current branch and edge.
```bash
bin/omarchy-pkgs release v4.0.0 # Final release from the upstream v4.0.0 tag
bin/omarchy-pkgs release rc v4.0.0 # Newest upstream v4.0.0-rcN tag -> 4.0.0rcN
bin/omarchy-pkgs release beta v4.0.0 # Same for beta (alpha also supported)
bin/omarchy-pkgs release latest # Newest upstream tag, rc/beta included (prompts)
bin/omarchy-pkgs release rc # Untagged RC from the quattro tip, auto-numbered
bin/omarchy-pkgs release --commit abc123 --base 4.1.0 # Untagged RC from a commit
bin/omarchy-pkgs release ... --dry-run # Show the plan; write nothing
bin/omarchy-pkgs release ... --no-push # Full flow, local commit only (testing)
bin/omarchy-pkgs self-test # Version normalization + ordering tests
```
## Versioning rules
- Finals are `X.Y.Z`; pre-releases are `X.Y.ZalphaN` / `X.Y.ZbetaN` /
`X.Y.ZrcN` in the **attached** form only. pacman's vercmp orders
`4.0.0alpha1 < 4.0.0beta1 < 4.0.0rc1 < 4.0.0`, but separator forms
(`4.0.0.rc1`, `4.0.0_rc1`) sort **after** `4.0.0` and would strand users on
the pre-release — the tooling normalizes upstream tags (`v4.0.0-rc1`,
`v4.0.0-rc.1`, ...) to the attached form and refuses anything it cannot
normalize. Upstream tags are cut on the quattro branch.
- `pkgrel` resets to 1 on every version change. Bump `pkgrel` by hand only to
repackage the same source.
- `epoch` is never set by tooling. It is sticky forever; adding one is a
human decision of last resort.
## Where releases land
- **RCs build for the rc channel.** RC testers and RC ISO installs run the real
`omarchy` package against the package set stable users will actually get.
Stable never sees an rc version; rc testers upgrade rc1 → rc2 → final
naturally.
- **Finals build into rc, then the whole channel promotes.** After testing, the
ship step promotes the exact tested artifacts (packages and signatures)
forward:
```bash
bin/repo advance --from rc --to stable --arch all
```
Neither package is on the `fast` ring, and `bin/omarchy-pkgs` never touches
stable — promotion is always this explicit step.
-586
View File
@@ -1,586 +0,0 @@
# The repository host (abandoned)
**This pipeline is abandoned.** Packages are built and published by GitHub
workflows: see the [README](../README.md). Don't follow this page to publish
anything.
The package pipeline used to run on one machine that built, signed and
published every channel on a timer. This page is kept as the reference for
the tooling that was written for it and is still in the tree: `bin/repo`, the
units in `systemd/`, and the host side of the release train
([releases.md](releases.md)).
**That tooling writes to the same bucket as CI.** If you run any of it
(`release`, `sync`, `promote`, `push`, `deploy`, `advance`, `remove`), these
rules apply:
- Do not run `bin/repo release`, `sync`, `push` or `deploy` for a package CI
publishes. Merge a PR, or dispatch `publish.yml`, instead.
- Keep the `omarchy-auto-release-*` and `omarchy-check-versions` timers
disabled while CI publishes the same channels. Two writers on one channel
database overwrite each other.
- Never pass `--prune` to `bin/repo sync`. The host's tree does not hold what
CI published, and `--prune` deletes remote files the local tree lacks.
- Never rebuild a version that is already published. Bump `pkgrel`.
## Setting up a host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### aarch64 under emulation
The repository host builds every architecture it publishes on the same
machine. A foreign architecture runs under QEMU user emulation, which
`bin/build` checks by actually running a container for the target platform.
Rootful Docker registers QEMU on first use. Rootless Podman uses the host's
registration and prints the one-time Arch setup commands when it is missing or
lacks the credential flag required by `sudo` inside the builder:
```bash
# Verify
podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m
# Should output: aarch64
```
**Note**: emulated builds are much slower than native ones.
## Published architectures
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
`helpers/paths.sh` names the architectures the host's scheduled pipeline and
release train work on. CI does not read it:
```bash
PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}"
```
That list drives the whole scheduled pipeline. `check-versions` compares
PKGBUILDs against each architecture's channel databases and writes one queue
file per channel and architecture (`.sync-needed-<channel>-<arch>`);
`auto-release <channel>` works through the queues one architecture at a
time, each with its own backoff (`.build-failed-<channel>-<arch>`), so a
failing build on one architecture never holds up the other; and the release
train advances channels with `--arch all`: it takes one host-wide lock and
verifies every architecture's source database before moving any of them. The
first entry is the reference architecture the release train observes channels
through. Rerunning the same advance after a failed remote sync completes it
safely.
Adding an architecture to the scheduled pipeline is therefore one checked-in
change to that list: the next `check-versions` tick queues everything the new
architecture lacks, and the next `auto-release` tick starts building it. CI
does not read this list: it builds the architectures in `CI_ARCHES`, both by
default. For a one-off run, override it directly:
```bash
OMARCHY_ARCHES=x86_64 bin/check-versions
OMARCHY_ARCHES=aarch64 bin/check-versions
OMARCHY_ARCHES="x86_64 aarch64" bin/check-versions
```
The builder image bootstraps
`omarchy-keyring` from the x86_64 tree for every architecture, so the first
build of a new architecture does not depend on a repository that only it can
create.
## Release in one command
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
The release command is smart and **incremental** - it only builds packages that have changed or are missing. You generally don't need to specify a package manually unless you are debugging a specific failure.
When a package fails, a completed build run still signs and publishes the packages
that succeeded. Failed packages and their blocked dependents remain queued with
failure backoff; retries compare against the updated repository and skip the
published versions. Only artifacts recorded by fully completed package builds
are eligible for a partial release. An interrupted build, a failed publication
step, or an incomplete pair using deferred runtime dependencies still stops the
release. Reports distinguish partial publication from complete success.
```bash
# Build changed/new packages, sign, promote, clean, update, and sync
bin/repo release
# Stable Mirror
bin/repo release --mirror stable
# ARM64
bin/repo release --arch aarch64
# Build one package (still skipped when its version is already published)
bin/repo release --package omarchy-nvim
# Show what would build without signing/promoting/syncing
bin/repo release --dry-run
```
### Step-by-Step
```bash
bin/repo build # Build (unsigned)
bin/repo sign # Sign packages
bin/repo promote # Copy to production
bin/repo clean # Remove old versions
bin/repo update # Update database
bin/repo sync # Sync to remote
```
## Building Heavy Packages Locally
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Large packages build faster on a local machine than on the server. Build them
here, then hand the artifacts to the repository host, which signs and publishes
them:
```bash
bin/repo deploy --package nvidia-580xx-utils # Build here, publish from the host
```
`deploy` is `build` followed by `push`. The two steps are also available
separately when a build needs inspecting before it ships:
```bash
bin/build --package nvidia-580xx-utils # Build on the fast machine
bin/repo push --package nvidia-580xx-utils # Upload + publish on the host
```
`push` uploads to the host's `build-output/`, verifies checksums, and runs
`bin/upload-prebuilt` there. Do not publish from a local checkout instead: a
local machine holds neither the complete repository nor a signing key.
**Name the package.** `build` plans against the local repository database, or
against the public channel's when there is none. `deploy` refuses to run
unscoped when the local database is missing. Unscoped builds belong on
the repository host, where `bin/repo release` does the same job against a real
database.
Every other `bin/repo` command — `sign`, `promote`, `update`, `clean`,
`advance`, `remove`, `sync`, `release` — works on the published tree. Run them
from anywhere: with a repository host configured `bin/repo` forwards them over
ssh to run there (see [Build trigger and the build host](#build-trigger-and-the-build-host)),
and `--local` forces execution on the current machine. `bin/repo build`
forwards too; call `bin/build` directly, or pass `--local`, to build on this
machine. `push` and `deploy` always run locally.
## Commands
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### Global Flags
These flags can be used with all commands:
- `--mirror <edge|rc|stable>`: Selects the channel (default: `edge`).
- `--local`: Run on this machine instead of forwarding to the repository host.
- `--arch <x86_64|aarch64>`: Selects the target architecture (default: `x86_64`).
### Build
```bash
bin/repo build # All packages (x86_64, edge)
bin/repo build --arch aarch64 # ARM64
bin/repo build --mirror rc # Release-candidate channel
bin/repo build --mirror stable # Stable channel
bin/repo build --package yay cursor-bin # Specific packages
bin/repo build --dry-run # Show what would build
```
**Output**: Unsigned `.pkg.tar.zst` in `build-output/`. Only builds packages whose version differs from what the repository holds.
Use `--dry-run` to show the build plan without running `makepkg`.
### Sign
```bash
bin/repo sign
```
Reads the GPG key and passphrase from the environment and signs all packages in `build-output/`. Supports `--arch` and `--mirror`.
### Promote
```bash
bin/repo promote # Copy to production
bin/repo promote --arch aarch64 # ARM64
bin/repo promote --dry-run # Preview
```
Copies signed packages from `build-output/` → `pkgs.omarchy.org/`.
### Clean
```bash
bin/repo clean # Keep 2 versions
bin/repo clean --keep 3 # Keep 3 versions
bin/repo clean --dry-run # Preview
```
Removes old package versions from the file system. **Does not update the database.**
### Update
```bash
bin/repo update # Update database
```
Updates the repository database (adding the newest version of each package). Run this after `promote` or `clean`.
### Sync Repository
```bash
bin/repo sync # Sync current arch/mirror
bin/repo sync --mirror stable # Sync stable
bin/repo sync --arch aarch64 # Sync ARM64
bin/repo sync --skip-prod-check # No confirmation
```
Syncs package repositories to the remote server using rclone based on the configured mirror and architecture.
**Uploads are additive.** A local tree is not authoritative about what belongs on
the remote — `pkgs.omarchy.org/` is gitignored, and packages built on another
machine exist only there — so sync never deletes by default. To take a package
out of the channels, use the `unpublish.yml` workflow.
For the same reason sync refuses to publish a repository database built from a
tree holding fewer packages than the remote database already lists. The database
is what pacman resolves against, so a partial one hides every package it does not
know about even though the files are still on the mirror. Use `bin/repo push` to
publish packages built on another machine.
### Deploy
```bash
bin/repo deploy --package nvidia-580xx-utils # Build locally, publish from the host
bin/repo deploy --host root@example.com # Point at a specific repo host
bin/repo deploy --dry-run # Show the plan, change nothing
```
Runs `build` then `push` in one command. The repository host is resolved before
the build starts, so a missing `--host` fails immediately rather than after a long
compile.
### Push to the Repository Host
```bash
bin/repo push # Push everything in build-output
bin/repo push --package nvidia-580xx-utils # Push one package
bin/repo push --mirror stable --arch aarch64 # Pick mirror and architecture
bin/repo push --host root@example.com # Override the repo host
bin/repo push --dry-run # Show the plan, transfer nothing
```
Uploads packages from `build-output/` to the repository host and publishes them there
with `bin/upload-prebuilt` (sign → promote → update → sync). Use it when a package
is quicker to build on a local machine than on the server.
Publishing happens on the host rather than locally for two reasons: the host
has a GPG signing key (CI has its own copy in the `publish` environment), and
it holds the repository tree that its database and sync are built from. Local
machines therefore need no secrets.
The host comes from `--host`, `$OMARCHY_REPO_HOST`, then `.repo-host`. The
setting is named for the repository rather than for building, which happens
wherever you like. The same setting tells `bin/omarchy-pkgs release` which host
to poke after a release push.
`--package` means the same thing as it does to `build`: a pkgbase, whose every
output ships together. Pushing `nvidia-580xx-utils` carries `nvidia-580xx-dkms`
and `opencl-nvidia-580xx` with it, because that is what the build produced. An
output's own name still selects just that one, for publishing a single package
on purpose. Omit `--package` to push everything built.
Publishing signs and promotes everything staged on the host, not just what this
push uploaded, so `push` stops when it finds packages already staged there —
usually leftovers from a failed run. Remove them on the host, or pass
`--include-staged` to publish them too.
### Other host commands
```bash
bin/repo advance --from rc --to stable # Ship the tested rc channel to stable
bin/repo advance --from edge --to rc # Open a train: carry edge forward into rc
bin/repo advance --from edge --to rc --package x # Deliberate single-package advance
bin/repo advance --from rc --to stable --dry-run # Preview without changing files
bin/repo bootstrap-rc # One-time initial rc seed from stable
bin/repo timers # Timer schedule, last runs, queues, backoff, lock
bin/repo list # List package metadata
bin/repo deploy # Build locally, then publish from the host
bin/repo push # Upload local builds to the host and publish
bin/repo remove <package> # Remove package (host workflow; CI uses unpublish.yml)
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
```
## Build trigger and the build host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Release commands run from anywhere. `bin/repo` is a **remote control**: with a
repository host configured, every command that operates on the published tree
(`release`, `build`, `sign`, `promote`, `update`, `clean`, `advance`,
`bootstrap-rc`, `remove`, `sync`, `migrate`) executes ON the host over ssh —
the exact same code, run where the tree lives, so ssh'ing in and running the
same commands by hand behaves identically. Pass `--local` to force execution
on the current machine. `list`, `push`, `deploy`, and `setup` never forward
(`push`/`deploy` exist precisely to move local builds *to* the host).
Without a configured host, commands run locally — which on the build host
itself (no `.repo-host` there) is exactly right, and elsewhere the exact
commands to run are printed by the orchestrator, with the 5-minute
auto-release timer as the backstop.
The host setting is any destination `ssh` accepts, resolved in this order:
1. `--host <dest>`, for `push`, `deploy` and `bin/omarchy-release`
2. `OMARCHY_REPO_HOST` environment variable
3. the git-ignored `.repo-host` file (one line; `#` comments allowed)
The server layout lives under `/root`, so the value is `root@<ip>`,
`root@<hostname>`, or — nicest — a `Host` alias from `~/.ssh/config` that
carries the user, key, and port:
```
# .repo-host
root@pkgs.example.com
```
All connections are plain ssh; nothing else is used to reach the host.
**How "this machine is the build host" is detected — and its one caveat.**
The marker is the published database living in this checkout
(`pkgs.omarchy.org/<channel>/<arch>/omarchy.db*`), the same convention the
rest of `bin/` uses. A workstation that once ran a full local
`bin/repo release` carries that marker too and would infer it is the host.
An explicitly configured destination always outranks the inference — `--host`,
`OMARCHY_REPO_HOST`, and `.repo-host` are checked first, and only when none is
set does local detection apply. So if a machine ever misidentifies itself,
writing the real host into `.repo-host` is the fix.
## Directory Structure
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```
omarchy-pkgs/
├── pkgbuilds/ # Source PKGBUILDs
│ └── package-name/
│ ├── PKGBUILD
│ └── .omarchy/
│ ├── package.json # Source/sync/release metadata
│ └── upstream.sh # Optional custom vendor release feed hook
├── build/
├── build-output/ # Unsigned packages (temporary)
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
│ ├── rc/
│ └── stable/
├── pkgs.omarchy.org/ # Signed packages (production)
│ ├── .release.lock # Host-wide lock: one channel mutation at a time
│ ├── edge/ # Each channel: x86_64/ and aarch64/
│ ├── rc/
│ └── stable/
└── bin/ # CLI tools (on host)
```
## Architecture-Specific Notes
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
### x86_64
- Native builds (fast)
- Mirrors: mirror.omarchy.org, rackspace, pkgbuild.com
### aarch64
- Built on the repository host like x86_64; under QEMU when the host is x86_64
- On an ARM host, package builds and the signing/database utility containers
run natively; only an explicitly requested x86_64 package build is emulated
- Uses Arch Linux ARM repositories through the same HTTPS mirror for every
channel (Arch Linux ARM publishes no dated snapshots to pin a channel's base)
- Additional repos: `[alarm]`, `[aur]`
- Same workflow, just add `--arch aarch64`; the scheduled pipeline runs it
automatically once `aarch64` is in `PUBLISHED_ARCHES`
- Packages whose `arch=()` lacks `aarch64` are skipped, not failed
### Building for Both Architectures
```bash
# Build x86_64
bin/repo release --package myapp
# Build aarch64
bin/repo release --arch aarch64 --package myapp
# Sync both
bin/repo sync
bin/repo sync --arch aarch64
```
## Version Management
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Packages are only rebuilt if:
- PKGBUILD version differs from the repository version
- Package doesn't exist in production
A package that has to be rebuilt because something underneath it changed is handled by turning that into a version change: `bin/sync-rebuilds` bumps pkgrel when a dependency named in `rebuild_on` moves.
## Scheduled releases on the host
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Four systemd units in `systemd/` drive unattended releases from the host.
All four units run **every 5 minutes**, staggered by a minute each, so a push
reaches the mirror in minutes rather than hours:
1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions for every published architecture, creates one state file per channel and architecture if builds are needed
2. **auto-release-edge** (`*:1/5`): For each published architecture with a state file, builds all edge packages that need updates
3. **auto-release-rc** (`*:2/5`): Builds fast-ring packages for rc, from the main checkout like the other two — natively in the rc image, not copied from another channel. The pinned release pair is built separately by `omarchy-release rc` in the `rc` branch worktree
4. **auto-release-stable** (`*:3/5`): If a state file exists, builds `release_ring=fast` packages for stable
That cadence is only safe because of three guards:
- **No overlap.** Every channel-mutating run takes a host-wide lock
(`pkgs.omarchy.org/.release.lock`). Scheduled runs take it
**non-blocking**: if a build is already going, the tick exits immediately
instead of queuing. Waiting would stack one stalled process per tick behind
a long build and stampede when it finished. Manual commands still wait, as
an operator expects. `check-versions` takes it too — its `git pull` would
otherwise swap PKGBUILDs out from under a running build.
- **Backoff on failure.** A failed release records the attempt in
`.build-failed-<channel>-<arch>` and backs off exponentially — 10m, 20m, 40m, up to
a 6h ceiling — instead of rebuilding the same broken tree every 5 minutes.
**Any new commit clears the backoff immediately**, since a push is the most
likely fix. Clear it by hand with
`rm /root/.state/.build-failed-<channel>-<arch>`.
- **Quiet when idle.** With nothing queued a tick exits without output, so the
journal shows the runs that mattered rather than 288 no-ops a day.
`bin/repo timers` reports all of this: schedules, last results, what is
queued, what is failing and when it will retry, and whether the lock is held.
## Build reports
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
Every release run reports to Basecamp Campfire — not just failures, so a push
can be followed all the way to the mirror without watching the host:
| Event | Message |
|---|---|
| 🔨 Started | channel, arch, host, the commit being built, and which packages are queued |
| ✅ Published | the packages and versions that went out, duration, and the channel URL |
| 📦 Promoted | what `advance` moved between channels (including the rc bootstrap) |
| 📦 Nothing to publish | a queued run built nothing — see below |
| 🔴 Failed | which step failed, the commit, and the last 25 log lines |
Reports are tied to release *runs*, not timer ticks: a tick with nothing
queued exits silently, so a quiet day is a quiet chat. That is what makes the
"nothing to publish" report meaningful rather than routine — a release only
runs when the version check queued work, so building nothing means the check
and the builder disagree about what is out of date. It names the packages that
were queued but not built, which is where to start looking.
By default everything posts to `BASECAMP_CHATBOT_URL`, the same chat the
sync workflows use — one variable, nothing extra to configure.
If release traffic starts drowning that chat, give it its own: create a second
Basecamp chat, add a chatbot integration to it, and export its lines URL
alongside the existing one in `/root/.omarchy/build-credentials`:
```bash
export OMARCHY_RELEASE_CHATBOT_URL="https://3.basecamp.com/<account>/integrations/<key>/buckets/<project>/chats/<chat>/lines"
```
Release reports then go there while the sync workflows keep posting to
`BASECAMP_CHATBOT_URL`. With neither set, reports are silently skipped.
`bin/setup` reports which of the three cases applies.
Check on all of it with `bin/repo timers` — schedule, each unit's last run and
whether it succeeded, what is queued, whether a release is running right now,
and any failed units. Like the other host commands it forwards over ssh, so
the build box's state is one command away from any machine:
```bash
bin/repo timers # runs on the repository host when one is configured
bin/repo timers --local # inspect this machine instead
```
State files are stored in `/root/.state/`:
- `.sync-needed-<channel>-<arch>` — the packages queued for that channel and
architecture, one per line; the release run reads them to name what it is
building
- `.build-failed-<channel>-<arch>` — consecutive failure count, timestamp, and
the commit it failed on (drives the backoff; removing it forces a retry)
Legacy files without the architecture suffix are consumed once as x86_64
state, so upgrading the host does not lose an in-flight build.
## Schedule (America/New_York)
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
| Minute of every hour | Action |
|------|--------|
| :00, :05, :10, … | check-versions (git pull + creates state files) |
| :01, :06, :11, … | auto-release-edge |
| :02, :07, :12, … | auto-release-rc |
| :03, :08, :13, … | auto-release-stable |
Each release unit is a no-op when its channel has nothing queued, another run
holds the lock, or the channel is in failure backoff.
## Installation
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```bash
ssh root@<host> 'cd /root/omarchy-pkgs && bin/setup --skip-timers'
```
`bin/setup` installs the dependencies, ensures Docker is running and creates
the state directory. Without `--skip-timers` it also installs and enables the
release timers, which must stay off while CI publishes. It works on
Debian/Ubuntu and on Arch, and is idempotent, so run it again whenever a
dependency is added.
The host does not need to be Arch: makepkg, repo-add and package signing all
run inside containers, so it needs only Docker, rclone, bsdtar, jq, git and
rsync. Docker is left alone when it already works, rather than replacing a
working installation from Docker's own repository with the distribution's.
```bash
bin/repo setup --check # Report what is missing, change nothing
bin/repo setup --skip-timers # Prepare the host without the release timers
```
Signing credentials (`/root/.omarchy/build-credentials`) and the rclone remote
hold secrets, so setup reports on them rather than creating them.
## Management
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
```bash
# Everything at a glance: schedules, last runs, queues, backoff, lock
bin/repo timers # forwards to the host when one is configured
# Manual trigger (edge, rc, stable)
systemctl start omarchy-check-versions.service
systemctl start omarchy-auto-release-edge.service
# View logs
journalctl -u omarchy-auto-release-edge.service -n 50
# Clear a channel stuck in failure backoff (a new commit also clears it)
rm /root/.state/.build-failed-edge-x86_64
# Release lock left behind by a killed run (bin/repo timers shows if it is live)
rm /root/omarchy-pkgs/pkgs.omarchy.org/.release.lock
```
With the timers enabled, `check-versions` pulls the main checkout on its
5-minute tick. With them disabled, nothing does: `bin/repo` pulls before a
command it forwards, and a command run on the host itself needs a `git pull`
first. The checkout must be on `master`.
-410
View File
@@ -1,410 +0,0 @@
# Direct upstream watches
Omarchy owns the recipes in `pkgbuilds/`. `bin/sync-upstream` discovers new
releases directly from project/vendor feeds and updates versions, declared
release variables, and the source checksums already used by the recipe. It never
imports upstream PKGBUILDs or runs downloaded build scripts. Architecture support,
root install hooks, dependencies, and build functions remain ours to maintain.
`upstream.watch` complements the existing declarative providers and custom hooks:
```json
{
"source": "local",
"upstream": {
"watch": {
"github": "abenz1267/walker",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
}
}
```
## Watch fields
Choose exactly one provider: `github` (owner/repository), `git_tags` (repository
HTTPS URL), `git_branch` (repository URL plus explicit `branch`), `npm` or `pypi`
(package name), `debian` (Packages index plus exact `package`), `json` (URL plus
version `path`), `regex` (text URL), `redirect` (final HTTPS download URL), or
`archive` (inspect archive metadata without extracting/executing code).
Tag, text, redirect and archive watches use an explicit `pattern` with a named
`version` capture. Tag patterns match the entire tag. `version` optionally formats
those captures into an Arch pkgver; e.g. Sublime uses `4.{version}`. JSON feeds can
expose additional capture values through `fields`, a name-to-JSON-path map.
`variables` maps recipe scalars such as `_commit` or `_build` to capture templates.
Only explicitly declared underscore-prefixed variables can change. GitHub
`{commit}` resolves the selected tag, not a moving target_commitish branch.
`submodules` can map a recipe variable to a gitlink in the selected GitHub tag;
RustDesk uses this for hbb_common. Downloaded repository code is never evaluated.
For upstreams that rebuild a release, declare a numeric `revision` template and
its `revision_variable`. With unchanged pkgver, only an increasing revision can
advance that variable, and the downstream pkgrel increments instead of resetting.
Cursor CLI uses `sequence` to preserve its date/counter/hash version convention
when the vendor publishes a second hash on the same day. A new pkgver resets
pkgrel to 1, but the complete epoch:pkgver-pkgrel must still increase.
GitHub releases exclude drafts and prereleases unless `allow_prerelease` is true.
Existing `min_release_age` policies apply: a feed without a verifiable publication
time cannot bypass a configured hold.
## Branch watches
A `git_branch` watch treats every commit on a branch as a release and writes an
immutable pin (`"_commit": "{commit}"`) so the recipe never carries a moving
`#branch=` source; `tests/pinned-sources.sh` enforces that. The clone is bare,
blobless and single-branch, read only with git, and shared by every package
that watches the same branch in one run, so two recipes pinned from it always
see the same commit. Values available to `version`:
- `{date}` (default), `{count}` (commits on the branch), `{commit}`
(`{commit:.7}` for the short form)
- with `tag_pattern` (a regular expression with a named `version` group,
matched against whole tags): `{tag}`, `{version}` from that tag, and
`{distance}`, the number of commits past it. Only tags in the pinned
commit's own history count, so a release cut on another branch is ignored.
`{version}.r{distance}.g{commit:.7}` gives `1.21.0.r15.gabc1234`, which pacman
orders above the `1.21.0` release it follows and below `1.21.1`; `omasnap-git`
uses it. The Omarchy dev pair uses `{version}.r{count}.g{commit:.7}` instead
because its published history counted every commit and the number must never
go down.
`min_release_age` holds a branch tip until its commit timestamp is old enough.
A fresh tip falls back to the newest commit on the branch's first-parent
history that has cleared the window, so a busy branch still advances; it never
selects a commit from a merged side branch. This uses Git's committer date, not
the time a commit was pushed, measured from when the run cloned the branch so
packages pinned from it agree. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
Packages marked `"auto_merge": true` ride the unattended lane
(`track-branches.yml`), checked hourly instead of every 6 hours. Every lane
opens its PRs through `package-prs.yml` and auto-merges them as soon as the
build checks pass. Each package gets its own
PR on `auto/track-branches-<package>`, and packages that pin the same branch
share one, so a package whose build fails holds back only itself. The lane works with every
provider, not only branch watches, so it also carries release feeds trusted to
ship without review: the browsers, large vendors' apps, Omacom's own projects,
and vendor binaries that hold fresh releases with `min_release_age`. `bin/sync-upstream --lane
reviewed|auto-merge|all` selects a lane; the scheduled workflows each pass their
own. Packages that pin the same branch move in lockstep: if one of them fails
to update, the run restores the others and reports the group as failed. A
targeted sync includes the other packages watching that branch, so requesting
only `omarchy-dev` also updates `omarchy-settings-dev`.
### Enable unattended branch updates
The schedule already runs in GitHub Actions; no server cron job is needed.
It uses a personal access token so its PRs trigger builds and its merges trigger
publishing without manual approval. No GitHub App is required.
1. Use a fine-grained PAT with access to **omacom/omarchy-pkgs** and repository
**Contents: Read and write** and **Pull requests: Read and write** permissions.
Its owner must be trusted by the build workflow (for example, a collaborator).
The existing controller PAT can be reused when it has these permissions.
2. In the repository's
[Actions secrets](https://github.com/omacom/omarchy-pkgs/settings/secrets/actions),
save the PAT as `PKGS_BOT_TOKEN`. Update this secret when the token is rotated
or expires. The built-in Actions `GITHUB_TOKEN` cannot run this unattended chain.
3. Keep **Allow auto-merge** enabled and require `result`, `self-tests`, and
`build-isolation` on `master`; the tracker does not request a protection bypass.
4. After merging the tracker, run **Track upstream branches** once from Actions
to verify that its PR builds, auto-merges, and starts **Publish merged packages**.
Subsequent runs happen every two hours.
Checksums retain their algorithms (SHA256, SHA512, BLAKE2, etc.) and source order.
Changed git sources are hashed with makepkg's git-archive convention. Unchanged
sources retain their hashes; `mutable_sources` explicitly names entries such as
`source:0` that must be fetched again for a new version despite a stable URL.
Existing `SKIP` entries remain unchanged (including signed metadata verified by
the recipe); new skips are never introduced. Changed URLs are still fetched.
A matching GitHub release asset SHA256 digest avoids downloading large assets.
Missing architecture artifacts or malformed metadata fail the package atomically.
Every declared architecture must read back the same release and checksum values.
Archive watches use `member` to select a text member, or `filenames: true` to read
versions from archive member names. Debian archives are read through their control
metadata. `unescape_json` handles JSON strings embedded in a vendor's HTML page.
## Maintenance and validation
Running watches locally requires Python 3.11+, Bash, curl, git, jq, Arch's
`vercmp`, and `bsdtar`. CI installs these in its Arch container.
- Edit packaging and architecture changes directly in PKGBUILD. The old AUR
overlays have been folded into these recipes and removed.
- Keep source-code patches and install hooks checked in as ordinary package files.
- Bump pkgrel when changing a recipe at the same version. Removing a dotted AUR
suffix must never lower the complete version.
- Add a watch with each new package. `bin/add-package --source aur` is a one-time
import; it records historical `origin` metadata and leaves an owned recipe.
- `python helpers/upstream-watch.py check pkgbuilds/NAME` checks release discovery
without rewriting the recipe. `bin/sync-upstream NAME` performs the update.
- `python tests/upstream-watch.py` tests update atomicity, architecture coverage,
version ordering, source hashes and hostile metadata using offline fixtures.
The scheduled workflow continues reviewing completed updates if another package
fails. The failing recipe stays unchanged and the run still reports failure.
## Migrated package watches
68 active AUR packages now use direct watches. The nine previously disabled
packages retain manual maintenance holds. Historical AUR provenance is recorded
in `origin` and has no effect on release selection.
| Package | Provider | Upstream |
|---|---|---|
| `1password-beta` | debian | [https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages](https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages) |
| `1password-cli` | json | [https://app-updates.agilebits.com/check/1/0/CLI2/en/0](https://app-updates.agilebits.com/check/1/0/CLI2/en/0) |
| `aether` | github | [omacom/aether](https://github.com/omacom/aether) |
| `basecamp-cli` | github | [basecamp/basecamp-cli](https://github.com/basecamp/basecamp-cli) |
| `bun-bin` | github | [oven-sh/bun](https://github.com/oven-sh/bun) |
| `claude-code` | regex | [https://downloads.claude.ai/claude-code-releases/latest](https://downloads.claude.ai/claude-code-releases/latest) |
| `cliamp` | github | [bjarneo/cliamp](https://github.com/bjarneo/cliamp) |
| `crush-bin` | github | [charmbracelet/crush](https://github.com/charmbracelet/crush) |
| `cursor-bin` | json | [https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable](https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable) |
| `cursor-cli` | regex | [https://cursor.com/install](https://cursor.com/install) |
| `dbxcli-bin` | github | [dropbox/dbxcli](https://github.com/dropbox/dbxcli) |
| `dropbox` | redirect | [https://www.dropbox.com/download?plat=lnx.x86_64](https://www.dropbox.com/download?plat=lnx.x86_64) |
| `dropbox-cli` | regex | [https://linux.dropbox.com/packages/](https://linux.dropbox.com/packages/) |
| `heroic-games-launcher-bin` | github | [Heroic-Games-Launcher/HeroicGamesLauncher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) |
| `hyprshade` | pypi | [hyprshade](https://pypi.org/project/hyprshade/) |
| `lib32-nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
| `limine-mkinitcpio-hook` | git_tags | [https://gitlab.com/Zesko/limine-entry-tool.git](https://gitlab.com/Zesko/limine-entry-tool.git) |
| `limine-snapper-sync` | git_tags | [https://gitlab.com/Zesko/limine-snapper-sync.git](https://gitlab.com/Zesko/limine-snapper-sync.git) |
| `lmstudio-bin` | regex | [https://lmstudio.ai/download](https://lmstudio.ai/download) |
| `localsend` | github | [localsend/localsend](https://github.com/localsend/localsend) |
| `localsend-bin` | github | [localsend/localsend](https://github.com/localsend/localsend) |
| `macbook12-spi-driver-dkms` | git_branch | [https://github.com/marc-git/macbook12-spi-driver.git](https://github.com/marc-git/macbook12-spi-driver.git) |
| `omarchy-dev`, `omarchy-settings-dev` | git_branch (auto-merge) | [https://github.com/basecamp/omarchy.git](https://github.com/basecamp/omarchy.git) `quattro` |
| `omasnap-git` | git_branch (auto-merge) | [https://github.com/omacom/omasnap.git](https://github.com/omacom/omasnap.git) `main` |
| `makima-bin` | github | [cyber-sushi/makima](https://github.com/cyber-sushi/makima) |
| `minecraft-launcher` | archive | [https://launcher.mojang.com/download/Minecraft.deb](https://launcher.mojang.com/download/Minecraft.deb) |
| `nautilus-dropbox` | github | [dropbox/nautilus-dropbox](https://github.com/dropbox/nautilus-dropbox) |
| `nautilus-open-any-terminal` | git_tags | [https://github.com/Stunkymonkey/nautilus-open-any-terminal.git](https://github.com/Stunkymonkey/nautilus-open-any-terminal.git) |
| `nordvpn-bin` | debian | [https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages](https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages) |
| `nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
| `omarchy-chromium-bin` | github | [omacom/omarchy-chromium](https://github.com/omacom/omarchy-chromium) |
| `omarchy-emacs` | git_tags | [https://github.com/scottjones/omarchy-emacs.git](https://github.com/scottjones/omarchy-emacs.git) |
| `omazed` | git_tags | [https://github.com/aps6/omazed.git](https://github.com/aps6/omazed.git) |
| `once-bin` | github | [basecamp/once](https://github.com/basecamp/once) |
| `openai-codex-bin` | github | [openai/codex](https://github.com/openai/codex) |
| `python-mediapipe` | github | [google-ai-edge/mediapipe](https://github.com/google-ai-edge/mediapipe) |
| `python-sounddevice` | pypi | [sounddevice](https://pypi.org/project/sounddevice/) |
| `python-terminaltexteffects` | pypi | [terminaltexteffects](https://pypi.org/project/terminaltexteffects/) |
| `rustdesk` | github | [rustdesk/rustdesk](https://github.com/rustdesk/rustdesk) |
| `spotify` | debian | [https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages](https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages) |
| `sublime-text-4` | json | [https://www.sublimetext.com/updates/4/stable_update_check](https://www.sublimetext.com/updates/4/stable_update_check) |
| `sunshine` | github | [LizardByte/Sunshine](https://github.com/LizardByte/Sunshine) |
| `ttf-ia-writer` | git_branch | [https://github.com/iaolo/iA-Fonts.git](https://github.com/iaolo/iA-Fonts.git) |
| `tuxedo-drivers-nocompatcheck-dkms` | git_tags | [https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git](https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git) |
| `typora` | debian | [https://downloads.typora.io/linux/Packages](https://downloads.typora.io/linux/Packages) |
| `ufw-docker` | git_tags | [https://github.com/chaifeng/ufw-docker.git](https://github.com/chaifeng/ufw-docker.git) |
| `vi` | regex | [https://sources.archlinux.org/other/vi/](https://sources.archlinux.org/other/vi/) |
| `visual-studio-code-bin` | json | [https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest](https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest) |
| `walker` | github | [abenz1267/walker](https://github.com/abenz1267/walker) |
| `xpadneo-dkms` | github | [atar-axis/xpadneo](https://github.com/atar-axis/xpadneo) |
| `yaru-icon-theme` | git_tags | [https://github.com/ubuntu/yaru.git](https://github.com/ubuntu/yaru.git) |
| `yay` | github | [Jguer/yay](https://github.com/Jguer/yay) |
| `yt6801-dkms` | archive | [https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817](https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817) |
## Existing manual holds
`libfprint-git`, `libretro-cap32-git`, `libretro-database-git`, `libretro-fbneo-git`, `libretro-uae-git`, `libretro-vice-git`, `quickshell-git`, `supergfxctl`.
These packages were already excluded from automatic AUR updates. The migration preserves that policy.
`m1n1-aurora` and `uboot-asahi` are deliberate holds: Apple Silicon boot code, pinned by hand like `linux-aurora`, and bumped only after a cold boot on the qualification Macs. `m1n1-aurora` pins an aurora-silicon/m1n1 commit plus a local patch. `uboot-asahi` follows asahi-alarm's recipe and patch set (asahi-alarm/PKGBUILDs), which a tag watch on AsahiLinux/u-boot cannot carry.
`cua-driver-bin` is a deliberate hold: Omarchy bumps it by hand, so a Cua release ships only when a maintainer has verified it. It keeps its `.omarchy/upstream.sh` hook and `min_release_age`, so lifting the hold means removing `"sync": false`. `cua-hyprland-plugin` declares no upstream source, so no automation updates it either.
## Package-specific boundaries
- NVIDIA watches remain on the 580 driver branch.
- Hardware-specific packages keep their declared architectures; this migration does not invent ARM binaries for x86-only upstreams.
- iA Duospace was deleted upstream. Its four legacy font files retain their original immutable pin while the other families track the current repository.
- RustDesk reads hbb_common from the release gitlink; its existing build-time dependency/toolchain checks remain in force.
- Spotify uses HTTPS and retains its signed Release/Packages verification.
- Source and build compatibility still need review when upstream code changes. Direct watches remove AUR recipe churn, not the need to maintain packaging.
## How the sync PRs build
`sync-upstream.yml`, `sync-rebuilds.yml` and `track-branches.yml` each run their
sync once, split the changed recipes into one PR per package with
`.github/scripts/package-pr-groups.sh`, and hand them to `package-prs.yml`. That
pushes each PR with the PAT in `PKGS_BOT_TOKEN`, so its builds start without an
approval hold, and arms auto-merge. A push to an `auto/sync-*` or
`auto/track-branches-*` branch does not cancel the PR's build in flight: the new
build waits, then reuses every artifact the finished one uploaded.
A red PR stays open for a maintainer and holds back only its own package. A
later run updates it if upstream moves again, and closes it once it conflicts
with `master`. A run started by hand with `packages` updates the same
per-package PRs a scheduled run would.
## Declarative providers and hooks
The older provider form, still in use beside `upstream.watch`. Give a new
package an `upstream.watch`; `helpers/upstream-watch.py check` validates that
form only.
```bash
bin/sync-upstream # Update every package with an upstream hook
bin/sync-upstream openai-codex-desktop # Update specific packages
```
A package declares where its releases come from either as data or, for an
unusual feed, a small hook.
A vendor shipping tagged GitHub releases is pure data, declared as `upstream`
in `.omarchy/package.json` with no code at all:
```json
"upstream": {
"github": "jdx/mise",
"checksums": "SHASUMS256.txt",
"assets": {
"x86_64": "mise-{tag}-linux-x64.tar.xz",
"aarch64": "mise-{tag}-linux-arm64.tar.xz"
}
}
```
`checksums` names the manifest asset the vendor publishes. A vendor publishing
none sets `"digests": true` instead, and the checksums come from the SHA-256
digest GitHub's release API reports for every asset — see
`pkgbuilds/schist-bin/.omarchy/package.json`. Either way the artifacts
themselves are never downloaded.
An architecture may map to an ordered array when its PKGBUILD downloads more
than one release asset. Small versioned files outside the release assets can be
listed under `sources` and are downloaded and hashed when a new version appears:
```json
"upstream": {
"github": "owner/project",
"digests": true,
"assets": {
"x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"],
"aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"]
},
"sources": {
"any": ["https://raw.githubusercontent.com/owner/project/{tag}/LICENSE"]
}
}
```
Asset and source keys must be disjoint because each key maps to one PKGBUILD
checksum array (`any` means the unsuffixed `sha256sums`).
Repositories whose historical releases use incompatible tag schemes may set
`"latest_only": true`. The provider then considers only the newest stable
GitHub release, while retaining all validation for that release. A quarantine
will wait for that release to age instead of falling back to an older one.
`{tag}` and `{pkgver}` interpolate into asset names; a leading `v` on the tag is
stripped for `pkgver`; drafts and prereleases are ignored. Only the 100 most
recent releases are considered. The provider fails closed on anything it cannot
read — an unusable tag, timestamp, or checksum stops the sync rather than being
skipped.
Projects that publish version tags but no checksum manifest can declare the
tag repository, the exact tag shape, and every source that should be hashed:
```json
"upstream": {
"git_tags": "https://github.com/owner/project.git",
"tag_pattern": "v{pkgver}",
"sources": {
"any": ["https://github.com/owner/project/archive/refs/tags/{tag}.tar.gz"]
}
}
```
The newest matching tag is selected with pacman's `vercmp`; unrelated tags are
ignored. `tag_pattern` must contain exactly one `{pkgver}`. Source templates may
use `{tag}` and `{pkgver}`. Each expanded URL must be HTTPS and is downloaded
only when the discovered version is newer. A checked-in patch or other local
source can be included as `file:patch-name.patch`; it is hashed from the package
directory. Keys such as `any`, `x86_64`, and `aarch64` select the corresponding
`sha256sums` array.
npm packages use the same source mapping, with `{npm_tarball}` available for
the tarball named by the selected dist-tag:
```json
"upstream": {
"npm": "@scope/package",
"dist_tag": "latest",
"sources": {
"any": ["{npm_tarball}", "https://example.com/v{pkgver}/CHANGELOG.md"]
}
}
```
`dist_tag` defaults to `latest`. The registry's publication timestamp is
carried into the provider result, so `min_release_age` works for npm packages.
A vendor with a plain-text Debian `Packages` index can use it to discover the
newest exact package version, then hash immutable source URLs:
```json
"upstream": {
"debian": "https://example.com/debian/dists/stable/main/binary-amd64/Packages",
"package": "example-app",
"sources": {
"x86_64": ["https://example.com/tool-{pkgver}-x64.tar.gz"],
"aarch64": ["https://example.com/tool-{pkgver}-arm64.tar.gz"]
}
}
```
This deliberately accepts only Debian versions that are already valid Arch
`pkgver` values. Feeds needing epoch, revision, or filename translation retain
a hook. Exactly one of `github`, `git_tags`, `npm`, or `debian` may appear in a
declaration.
A timestamped provider may also declare `"min_release_age": "24h"`
(`s`/`m`/`h`/`d` suffix or bare seconds) to quarantine fresh releases until
maintainers have had time to pull a bad or compromised one. GitHub Releases and
npm provide publication times; raw git tags and Debian Packages indexes do not,
so combining either with this policy fails closed. The newest release that has
cleared the window ships, so a fast release cadence cannot starve updates. The
window is enforced
centrally: whatever reports the release must prove its age via `published_at`,
or the sync fails. A maintainer deliberately shipping inside the window runs
`BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>` locally and merges the
result through a normal PR; scheduled automation never sets the bypass.
A vendor whose feed fits no convention (a Debian package index, a bare
version.txt) instead provides `.omarchy/upstream.sh`, a hook that reports the
newest upstream release as JSON on stdout — declaring both an `upstream` block
and a hook is an error:
```json
{
"pkgver": "1.2.3",
"sha256sums": { "x86_64": ["<sha256>"], "aarch64": ["<sha256>"] }
}
```
Architecture keys become `sha256sums_<arch>` in the PKGBUILD; the key `any` means
the unsuffixed `sha256sums` array, and only the arrays a hook names are touched.
An empty object (`{}`) reports no update, which is how a hook waits out a release
that has landed for one architecture but not yet the other.
When the reported version is newer than the checked-in one, `bin/sync-upstream`
rewrites `pkgver` and those checksum arrays and resets `pkgrel` to 1. A version
that is equal or older leaves the package alone, so a vendor rolling a release
back cannot walk the repository backwards.
Hooks should read checksums from whatever manifest the vendor publishes rather
than downloading the artifacts — see `pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh`,
which reads OpenAI's Debian package index and never fetches the 750 MB of debs
it describes. Hooks honoring `min_release_age` receive the window as
`MIN_RELEASE_AGE_SECONDS` and report `published_at` alongside `pkgver`.
`bin/sync-upstream self-test` runs offline fixture tests over the release
selection, quarantine backstop, duration parsing, and manifest validation.
-44
View File
@@ -1,44 +0,0 @@
#!/bin/bash
# Package files cross from a PR build to publish.yml as one GitHub Actions
# artifact. actions/upload-artifact rejects any path containing ':', and a
# package with an epoch is named `name-1:ver-rel-arch.pkg.tar.zst` by
# makepkg. So the files ride inside a tar with a plain name and keep their
# own names untouched: pacman clients and bin/publish-artifact both rely on
# the filename matching PKGINFO.
#
# Both functions run under the workflow's `bash -e`: nothing in them may
# return non-zero except the final failure.
# package_files <dir>: the *.pkg.tar.zst directly in <dir>, one per line.
# Signatures and the scratch database next to them are not packages.
package_files() {
local f
for f in "$1"/*.pkg.tar.zst; do
[[ -e "$f" ]] && printf '%s\n' "$f"
done
return 0
}
# pack_packages <dir> <tar>: every package in <dir> into <tar>.
pack_packages() {
local dir=$1 out=$2 files=()
mapfile -t files < <(package_files "$dir")
(( ${#files[@]} )) || { echo "pack_packages: no *.pkg.tar.zst in $dir" >&2; return 1; }
tar -cf "$out" -C "$dir" -- "${files[@]##*/}"
}
# unpack_packages <artifact dir> <dest>: the packages an unzipped artifact
# carried, into <dest>. Packed artifacts hold packages.tar; artifacts from
# builds before packing hold the bare files. The bare form can go once
# those artifacts have expired (7-day retention).
unpack_packages() {
local src=$1 dest=$2 files=()
mkdir -p "$dest"
if [[ -f "$src/packages.tar" ]]; then
tar -xf "$src/packages.tar" -C "$dest"
return 0
fi
mapfile -t files < <(package_files "$src")
(( ${#files[@]} )) || { echo "unpack_packages: nothing to unpack in $src" >&2; return 1; }
cp -- "${files[@]}" "$dest/"
}
+24 -126
View File
@@ -1,113 +1,25 @@
# Container engine helpers for Omarchy package build system # Docker helper functions for Omarchy package build system
container_engine_supported() { check_docker() {
[[ "$CONTAINER_ENGINE" == "docker" || "$CONTAINER_ENGINE" == "podman" ]] if ! command -v docker &>/dev/null; then
} print_error "Docker is not installed"
if [[ -z "${CONTAINER_ENGINE:-}" ]]; then
for engine in docker podman; do
if command -v "$engine" >/dev/null 2>&1 && "$engine" info >/dev/null 2>&1; then
CONTAINER_ENGINE="$engine"
break
fi
done
fi
export CONTAINER_ENGINE
# Rootless Podman otherwise maps the image's builder uid 1000 to a subordinate
# host uid. keep-id makes files written through bind mounts belong to the user
# who invoked the build.
CONTAINER_RUN_ARGS=()
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
CONTAINER_RUN_ARGS+=("--userns=keep-id:uid=1000,gid=1000")
fi
check_engine() {
if [[ -n "$CONTAINER_ENGINE" ]] && ! container_engine_supported; then
print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)"
exit 1 exit 1
fi fi
if [[ -z "$CONTAINER_ENGINE" ]]; then if ! docker info &>/dev/null; then
print_error "No working container engine found (tried Docker, then Podman)" print_error "Docker daemon is not running"
if command -v docker >/dev/null 2>&1; then print_warning "Start Docker with: sudo systemctl start docker"
print_warning "Docker is installed but unavailable. Start it with: sudo systemctl start docker"
elif command -v podman >/dev/null 2>&1; then
print_warning "Podman is installed but 'podman info' failed"
else
print_warning "Install Docker or Podman"
fi
exit 1 exit 1
fi fi
if ! command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
print_error "$CONTAINER_ENGINE is not installed"
exit 1
fi
if ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
print_error "Docker daemon is not running or is not accessible"
print_warning "Start Docker with: sudo systemctl start docker"
else
print_error "Podman is not available to the current user"
fi
exit 1
fi
}
docker_native_arch() {
case "$(uname -m)" in
x86_64) echo x86_64 ;;
aarch64 | arm64) echo aarch64 ;;
*) return 1 ;;
esac
} }
setup_qemu() { setup_qemu() {
local target_arch="${1:-aarch64}" # Setup QEMU for building ARM64 packages on x86_64 hosts
if ! docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes >/dev/null 2>&1; then
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then print_error "Failed to setup QEMU for ARM64 emulation"
local registration="/proc/sys/fs/binfmt_misc/qemu-$target_arch"
local packaged_registration="/usr/lib/binfmt.d/qemu-$target_arch-static.conf"
local flags=""
if [[ -r "$registration" ]]; then
flags=$(sed -n 's/^flags: //p' "$registration")
fi
if [[ "$flags" == *F* && "$flags" == *C* ]]; then
print_success "QEMU $target_arch emulation is registered"
return 0
fi
print_error "Rootless Podman requires QEMU binfmt registration with the F and C flags"
print_info "F keeps the emulator available inside containers; C lets container sudo preserve credentials."
print_info "Configure it once with:"
echo " sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt"
echo " sudo mkdir -p /etc/binfmt.d"
echo " sed 's/:FP$/:FPC/' $packaged_registration | sudo tee /etc/binfmt.d/qemu-$target_arch-static.conf >/dev/null"
echo " sudo systemctl restart systemd-binfmt"
exit 1 exit 1
fi fi
print_success "QEMU ARM64 emulation enabled"
# Register emulators for builds whose target differs from the host, with
# the F and C flags (tonistiigi/binfmt always sets both). The image tag pins
# the QEMU version every emulated build runs under; multiarch/qemu-user-static
# stopped at QEMU 7.2, which breaks qmake's compiler probe on current gcc.
# Keep ci/runner-cloud-init.yaml on the same tag. Uninstall first: install
# leaves an existing registration (an older emulator) in place and exits 0.
local platform_arch
case "$target_arch" in
aarch64) platform_arch=arm64 ;;
x86_64) platform_arch=amd64 ;;
*) platform_arch="$target_arch" ;;
esac
if ! "$CONTAINER_ENGINE" run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall "qemu-$target_arch" --install "$platform_arch" >/dev/null 2>&1; then
print_error "Failed to set up QEMU emulation"
exit 1
fi
print_success "QEMU emulation enabled"
} }
build_docker_image() { build_docker_image() {
@@ -127,45 +39,31 @@ build_docker_image() {
;; ;;
esac esac
print_info "Building container image for $arch ($platform) using $mirror mirror..." print_info "Building Docker image for $arch ($platform) using $mirror mirror..."
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then docker buildx build \
"$CONTAINER_ENGINE" buildx build \ --platform "$platform" \
--platform "$platform" \ --build-arg MIRROR="$mirror" \
--build-arg MIRROR="$mirror" \ --load \
--load \ -t "$image_tag" \
-t "$image_tag" \ -f "$build_dir/Dockerfile" \
-f "$build_dir/Dockerfile" \ "$build_dir"
"$build_dir"
else
"$CONTAINER_ENGINE" build \
--platform "$platform" \
--build-arg MIRROR="$mirror" \
-t "$image_tag" \
-f "$build_dir/Dockerfile" \
"$build_dir"
fi
} }
get_platform_arg() { get_platform_arg() {
local arch="$1" local arch="$1"
case "$arch" in case "$arch" in
x86_64) echo "--platform=linux/amd64" ;; x86_64) echo "--platform linux/amd64" ;;
aarch64) echo "--platform=linux/arm64" ;; aarch64) echo "--platform linux/arm64" ;;
*) return 1 ;; *) echo "" ;;
esac esac
} }
make_dir_writable() { make_dir_writable() {
local dir="$1" local dir="$1"
if (( EUID == 0 )); then if [ "$(id -u)" -eq 0 ]; then
chmod -R 777 "$dir" chmod -R 777 "$dir"
else else
# chown can succeed on part of the tree and fail on the rest (files a sudo chown -R $(id -u):$(id -g) "$dir" 2>/dev/null || chmod -R 777 "$dir"
# previous container left behind as another uid); the old `|| chmod`
# fallback only ran when chown failed outright, leaving those files
# unwritable. Always follow with chmod so the whole tree is usable.
sudo chown -R "$(id -u):$(id -g)" "$dir" 2>/dev/null || true
chmod -R 777 "$dir"
fi fi
} }
+20 -181
View File
@@ -3,24 +3,21 @@
# Expects package directories in $PKGBUILDS_DIR, each with: # Expects package directories in $PKGBUILDS_DIR, each with:
# .omarchy/package.json # .omarchy/package.json
# #
# Minimal schema (legacy source:aur remains readable for initial imports): # Minimal schema:
# { "source": "aur" }
# { "source": "aur", "sync": false }
# { "source": "aur", "aur": "different-aur-name" }
# { "source": "aur", "release_ring": "fast" }
# { "source": "aur", "skip_build": true }
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
# { "source": "aur", "rebuild_on": ["qt6-base"] }
# { "source": "local" } # { "source": "local" }
# { "source": "local", "sync": false }
# { "source": "local", "release_ring": "fast" }
# { "source": "local", "skip_build": true }
# { "source": "local", "rebuild_on": ["qt6-base"] }
# { "source": "local", "upstream": { "watch": { "github": "owner/repo", "pattern": "v(?P<version>[0-9.]+)" } } }
# { "source": "local", "channels": ["edge"] } # { "source": "local", "channels": ["edge"] }
# { "source": "local", "channels": ["edge", "rc", "stable"] } # { "source": "local", "channels": ["edge", "rc", "stable"] }
# { "source": "local", "min_release_age": "24h" } # { "source": "local", "min_release_age": "24h" }
# { "source": "local", "auto_merge": true, "upstream": { "watch": { "git_branch": "...", "branch": "main" } } } # { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": ["name-{tag}-x64.tar.xz"] } } }
# { "source": "local", "upstream": { "github": "owner/repo", "digests": true, "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
# { "source": "local", "upstream": { "git_tags": "https://example/repo.git", "tag_pattern": "v{pkgver}", "sources": { "any": ["https://example/archive/{tag}.tar.gz"] } } }
# { "source": "local", "upstream": { "npm": "@scope/package", "sources": { "any": ["{npm_tarball}"] } } }
# { "source": "local", "upstream": { "debian": "https://example/debian/dists/stable/main/binary-amd64/Packages", "package": "example", "sources": { "any": ["https://example/releases/{pkgver}.tar.gz"] } } }
# #
# bin/import-aur records historical origin.aur and origin.commit; # bin/sync-aur also writes upstream_commit for AUR-backed packages, and
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on. # bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
@@ -143,67 +140,6 @@ package_has_pkgbuild() {
[[ -f "$pkgdir/PKGBUILD" ]] [[ -f "$pkgdir/PKGBUILD" ]]
} }
# Read one variable from a PKGBUILD the way makepkg would see it.
#
# makepkg always exports CARCH, so PKGBUILDs may branch on it at file scope
# (per-architecture sources, tarball suffixes, even `return` for an
# unsupported architecture). Sourcing without CARCH takes the wrong branch or
# aborts partway, which leaves pkgver and pkgrel empty — and an empty version
# never equals the published one, so the package is queued for a rebuild that
# promotion then refuses. Every read of a PKGBUILD goes through here.
#
# Prints the value; exit status is that of `source PKGBUILD` itself, so a
# caller can tell "variable empty" from "PKGBUILD could not be read".
package_pkgbuild_var() {
local pkgdir="$1"
local var="$2"
local arch="${3:-${ARCH:-x86_64}}"
(cd "$pkgdir" && env -u OMARCHY_SRC CARCH="$arch" bash -c '
source PKGBUILD >/dev/null 2>&1
rc=$?
printf "%s\n" "${!1:-}"
exit "$rc"
' _ "$var")
}
# The architectures declared by a PKGBUILD. Set CARCH while reading it so a
# conditional arch=() assignment is evaluated for the architecture we are
# actually checking, even when the repository host is a different one.
package_arches() {
local pkgdir="$1"
local arch="${2:-${ARCH:-x86_64}}"
(cd "$pkgdir" && env -u OMARCHY_SRC CARCH="$arch" bash -c '
source PKGBUILD >/dev/null 2>&1
printf "%s\n" "${arch[*]}"
')
}
package_supports_arch() {
local pkgdir="$1"
local target="${2:-${ARCH:-x86_64}}"
local arches
arches=$(package_arches "$pkgdir" "$target") || return 1
case " $arches " in
*" any "* | *" $target "*) return 0 ;;
*) return 1 ;;
esac
}
# The channel DB indexes only its newest version, but older published archives
# remain immutable. Both the scheduler and build planner must skip an existing
# filename even when the checkout differs from the version currently indexed.
package_version_is_published() {
local repo_dir="$1" package="$2" version="$3" target="$4" path
for path in "$repo_dir/$package-$version-$target.pkg.tar."* \
"$repo_dir/$package-$version-any.pkg.tar."*; do
[[ -f "$path" && "$path" != *.sig ]] && return 0
done
return 1
}
# Channel membership: where a package may be published. Packages without a # Channel membership: where a package may be published. Packages without a
# `channels` key are members of every channel (they flow edge -> rc -> stable). # `channels` key are members of every channel (they flow edge -> rc -> stable).
package_has_channels() { package_has_channels() {
@@ -280,11 +216,6 @@ package_builds_for_mirror() {
package_moves_to_channel() { package_moves_to_channel() {
local pkgdir="$1" channel="$2" local pkgdir="$1" channel="$2"
package_in_channel "$pkgdir" "$channel" || return 1 package_in_channel "$pkgdir" "$channel" || return 1
# Pinned packages build natively in rc from the release pin. That the
# advancing environment lacks OMARCHY_RC_PINS (so *it* may not build them)
# does not make the edge copy movable over the pin's artifact — edge's
# version can be ahead of the in-flight RC.
[[ "$channel" == "rc" ]] && package_is_pinned "$pkgdir" && return 1
! package_builds_for_mirror "$pkgdir" "$channel" ! package_builds_for_mirror "$pkgdir" "$channel"
} }
@@ -328,32 +259,6 @@ packages_for_upstream_sync() {
done done
} }
# Upstream updates travel in one of two lanes. The reviewed lane is the
# 6-hourly sync PR a maintainer reads before merging. A package that marks
# "auto_merge": true rides the unattended lane instead: its bump PR is opened
# and auto-merged by the branch tracker as soon as CI is green, which is how a
# package that follows a moving branch (omarchy-dev, omasnap-git) gets rebuilt
# without anyone clicking, and how trusted release feeds (browsers, large
# vendors, Omacom's own projects) ship without waiting for review. The lanes are disjoint so a branch tip can never
# hold up a reviewed vendor release, or the other way round.
package_auto_merge() {
local pkgdir="$1" metadata
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
[[ "$(jq -r 'if has("auto_merge") then .auto_merge else false end' "$metadata")" == "true" ]]
}
# package_in_lane <pkgdir> <reviewed|auto-merge|all>
package_in_lane() {
local pkgdir="$1" lane="$2"
case "$lane" in
all | "") return 0 ;;
auto-merge) package_auto_merge "$pkgdir" ;;
reviewed) ! package_auto_merge "$pkgdir" ;;
*) echo "invalid lane: $lane (expected reviewed, auto-merge, or all)" >&2; return 2 ;;
esac
}
# Packages that must be rebuilt when a dependency they link against changes, # Packages that must be rebuilt when a dependency they link against changes,
# even though nothing in their own source moved. `rebuild_on` names those # even though nothing in their own source moved. `rebuild_on` names those
# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was # dependencies; `rebuilt_against` records the versions the checked-in pkgrel was
@@ -393,12 +298,9 @@ packages_for_mirror() {
packages_for_unscoped_build() { packages_for_unscoped_build() {
local mirror="$1" local mirror="$1"
local arch="${2:-${ARCH:-x86_64}}"
package_dirs | while IFS= read -r pkgdir; do package_dirs | while IFS= read -r pkgdir; do
if package_builds_for_mirror "$pkgdir" "$mirror" && if package_builds_for_mirror "$pkgdir" "$mirror" && ! package_build_skipped "$pkgdir"; then
! package_build_skipped "$pkgdir" &&
package_supports_arch "$pkgdir" "$arch"; then
basename "$pkgdir" basename "$pkgdir"
fi fi
done done
@@ -541,70 +443,23 @@ validate_package_metadata() {
return 1 return 1
fi fi
if ! jq -e 'if has("auto_merge") | not then true else (.auto_merge | type) == "boolean" end' "$metadata" >/dev/null; then
echo "invalid auto_merge for $(basename "$pkgdir"): must be boolean"
return 1
fi
if package_auto_merge "$pkgdir" && ! package_has_upstream_provider "$pkgdir" && ! package_has_upstream_hook "$pkgdir"; then
echo "invalid auto_merge for $(basename "$pkgdir"): only an upstream watch, provider, or hook can be auto-merged"
return 1
fi
# `has` rather than `// {}`: jq's // treats false as absent, which would # `has` rather than `// {}`: jq's // treats false as absent, which would
# let "upstream": false slip through as an empty declaration. # let "upstream": false slip through as an empty declaration.
if ! jq -e ' if ! jq -e '
def valid_sources:
type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
));
def valid_assets:
type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value |
(type == "string" and length > 0)
or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length)
)
));
if has("upstream") | not then true if has("upstream") | not then true
elif (.upstream | type) != "object" then false elif (.upstream | type) != "object" then false
else .upstream | else .upstream |
([has("github"), has("git_tags"), has("npm"), has("debian"), has("watch")] | map(select(.)) | length) == 1 ((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
and if has("watch") then (.watch | type == "object") and ((.checksums // "") | type == "string" and length > 0)
elif has("github") then and ((.assets // {}) | type == "object" and length > 0 and (to_entries | all(
(.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z")) (.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0)
and (if has("checksums") then (.checksums | type == "string" and length > 0) else true end) )))
and (if has("digests") then (.digests | type == "boolean") else true end)
and (if has("latest_only") then (.latest_only | type == "boolean") else true end)
and (has("checksums") != (has("digests") and .digests == true))
and (.assets | valid_assets)
and (if has("sources") then
(.sources | valid_sources)
and ((.assets | keys) as $assets | (.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length))
else true end)
elif has("git_tags") then
(.git_tags | type == "string" and test("\\Ahttps://[^[:space:]]+\\.git\\z"))
and (.tag_pattern | type == "string" and (split("{pkgver}") | length) == 2)
and (.sources | valid_sources)
elif has("npm") then
(.npm | type == "string" and test("\\A(@[a-z0-9_.-]+/)?[a-z0-9_.-]+\\z"))
and ((.dist_tag // "latest") | type == "string" and test("\\A[a-z0-9_.-]+\\z"))
and (.sources | valid_sources)
else
(.debian | type == "string" and test("\\Ahttps://[^[:space:]]+\\z"))
and (.package | type == "string" and test("\\A[a-z0-9][a-z0-9+.-]*\\z"))
and (.sources | valid_sources)
end
end end
' "$metadata" >/dev/null; then ' "$metadata" >/dev/null; then
echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, npm, debian, or watch provider" echo "invalid upstream for $(basename "$pkgdir"): needs github owner/repo, checksums asset name, and an assets arch->name map"
return 1 return 1
fi fi
if jq -e '.upstream? | objects | has("watch")' "$metadata" >/dev/null; then
python3 "${BASH_SOURCE[0]%/*}/upstream-watch.py" validate "$pkgdir" || return 1
fi
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata") pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
case "$pkgrel_type" in case "$pkgrel_type" in
object|missing) ;; object|missing) ;;
@@ -626,28 +481,12 @@ validate_package_metadata() {
return 1 return 1
fi fi
if ! jq -e ' if ! jq -e '(.rebuilt_against // {}) | type == "object" and (to_entries | all(.value | type == "string" and length > 0))' "$metadata" >/dev/null; then
def version_map: echo "invalid rebuilt_against for $(basename "$pkgdir"): must be an object mapping package names to versions"
type == "object" and (to_entries | all(.value | type == "string" and length > 0));
(.rebuilt_against // {}) as $record |
($record | version_map) or
(($record | type) == "object"
and ((($record | keys) - ["x86_64", "aarch64"]) | length == 0)
and ($record | to_entries | all(.value | version_map)))
' "$metadata" >/dev/null; then
echo "invalid rebuilt_against for $(basename "$pkgdir"): must map architectures to package-version maps"
return 1 return 1
fi fi
if ! jq -e ' if ! jq -e '((.rebuilt_against // {}) | keys) - (.rebuild_on // []) | length == 0' "$metadata" >/dev/null; then
(.rebuild_on // []) as $triggers |
(.rebuilt_against // {}) as $record |
if ($record | to_entries | all(.value | type == "string")) then
((($record | keys) - $triggers) | length == 0)
else
($record | to_entries | all((((.value | keys) - $triggers) | length) == 0))
end
' "$metadata" >/dev/null; then
echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name" echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name"
return 1 return 1
fi fi
-70
View File
@@ -5,76 +5,6 @@
ARCH=${ARCH:-x86_64} ARCH=${ARCH:-x86_64}
MIRROR=${MIRROR:-edge} MIRROR=${MIRROR:-edge}
# Architectures the tooling knows how to build.
VALID_ARCHES="x86_64 aarch64"
# Architectures this repository PUBLISHES. The scheduled pipeline (version
# check, auto-release, channel advance with --arch all) runs once per entry,
# in this order; the first entry is the reference architecture that the
# release train observes channels through. Adding an architecture here is the
# enablement step: the next check-versions tick queues its packages and the
# next auto-release tick builds them. OMARCHY_ARCHES overrides it for a
# one-off run.
PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}"
validate_arch() {
case " $VALID_ARCHES " in
*" $1 "*) return 0 ;;
*) return 1 ;;
esac
}
require_valid_arch() {
if ! validate_arch "$1"; then
echo "Invalid architecture: $1 (must be one of: $VALID_ARCHES)" >&2
exit 1
fi
}
published_arches() {
local arch
for arch in $PUBLISHED_ARCHES; do
require_valid_arch "$arch"
echo "$arch"
done
}
reference_arch() {
published_arches | head -1
}
# Architectures a merge to master builds and publishes: bin/build-matrix plans
# a PR build for each one a package supports, and publish.yml ships them. This
# is independent of PUBLISHED_ARCHES, so anything deciding what a merge has to
# rebuild (bin/sync-rebuilds) follows this list. CI_ARCHES overrides it.
CI_ARCHES="${CI_ARCHES:-x86_64 aarch64}"
ci_arches() {
local arch
for arch in $CI_ARCHES; do
require_valid_arch "$arch"
echo "$arch"
done
}
# Scheduled-pipeline state, one file per channel and architecture, so one
# architecture's queue or backoff never gates another's.
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
sync_queue_file() { # sync_queue_file <mirror> <arch>
echo "$STATE_DIR/.sync-needed-$1-$2"
}
sync_fail_file() { # sync_fail_file <mirror> <arch>
echo "$STATE_DIR/.build-failed-$1-$2"
}
# The pre-architecture names, .sync-needed-<mirror> and .build-failed-<mirror>,
# meant x86_64. A host upgraded mid-cycle may still hold one; readers treat
# it as the x86_64 file until it is consumed.
legacy_sync_queue_file() { echo "$STATE_DIR/.sync-needed-$1"; }
legacy_sync_fail_file() { echo "$STATE_DIR/.build-failed-$1"; }
# Valid package channels, in pipeline order: packages move edge -> rc -> stable # Valid package channels, in pipeline order: packages move edge -> rc -> stable
VALID_MIRRORS="edge rc stable" VALID_MIRRORS="edge rc stable"
+32 -321
View File
@@ -1,7 +1,8 @@
# Declarative upstream providers for bin/sync-upstream. # GitHub-releases upstream provider for bin/sync-upstream.
# #
# A package whose upstream ships tagged GitHub releases needs no upstream.sh # A package whose upstream ships tagged GitHub releases with a checksum
# hook: the whole feed is data, declared in .omarchy/package.json -- # manifest asset needs no upstream.sh hook: the whole feed is data, declared
# in .omarchy/package.json --
# #
# "upstream": { # "upstream": {
# "github": "jdx/mise", # "github": "jdx/mise",
@@ -12,28 +13,17 @@
# } # }
# } # }
# #
# "checksums" names the vendor's manifest asset. A vendor publishing none can
# set "digests": true instead, which reads the SHA-256 digest GitHub's release
# API reports for every asset, so the sync never downloads the artifacts.
#
# {tag} and {pkgver} interpolate into asset names; tags may carry a leading # {tag} and {pkgver} interpolate into asset names; tags may carry a leading
# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The # "v", which is stripped for pkgver. Drafts and prereleases are ignored. The
# provider emits the same JSON contract as an upstream.sh hook, so # provider emits the same JSON contract as an upstream.sh hook, so
# bin/sync-upstream's validation and min_release_age backstop apply # bin/sync-upstream's validation and min_release_age backstop apply
# unchanged. Git-tag, npm, and Debian Packages providers below cover projects # unchanged; a feed that fits no convention keeps a bespoke upstream.sh.
# without GitHub releases; a feed that fits no convention keeps a bespoke hook.
# Return the single declarative provider selected by a package. An empty package_upstream_github_repo() {
# result means either no provider or an invalid/ambiguous declaration; the local pkgdir="$1"
# caller distinguishes those through package_has_upstream_provider(). # `objects` drops a non-object upstream value (validation rejects those
package_upstream_provider() { # separately) instead of erroring the jq pipeline.
local pkgdir="$1" metadata package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' ""
metadata=$(metadata_file_for_dir "$pkgdir")
jq -r '
(.upstream? | objects) as $u
| [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm" or . == "debian" or . == "watch")]
| if length == 1 then .[0] else "" end
' "$metadata"
} }
# Fetches sit behind functions so the self-test can replace them with fixture # Fetches sit behind functions so the self-test can replace them with fixture
@@ -53,221 +43,6 @@ github_fetch_checksums() {
curl -fsSL "https://github.com/$repo/releases/download/$tag/$asset" curl -fsSL "https://github.com/$repo/releases/download/$tag/$asset"
} }
git_tags_fetch_refs() {
local repo="$1"
git ls-remote --tags "$repo"
}
npm_fetch_metadata() {
local package="$1" encoded
encoded=$(jq -rn --arg package "$package" '$package | @uri')
curl -fsSL "https://registry.npmjs.org/$encoded"
}
debian_fetch_packages() {
local url="$1"
curl --proto '=https' --proto-redir '=https' -fsSL "$url"
}
upstream_fetch_source() {
local url="$1" output="$2"
curl --proto '=https' --proto-redir '=https' -fsSL -o "$output" "$url"
}
# Hash every URL template in upstream.sources and emit hook-contract JSON.
# Templates may use {pkgver}, {tag}, and (for npm) {npm_tarball}. Downloads
# happen only after discovery reports a version newer than the PKGBUILD.
upstream_hash_sources() {
local package_dir="$1" pkgver="$2" tag="${3:-}" npm_tarball="${4:-}"
local metadata sources work result arch template url file sum sums index=0
metadata=$(metadata_file_for_dir "$package_dir")
sources=$(jq -c '.upstream.sources' "$metadata")
work=$(mktemp -d)
result=$(jq -n --arg pkgver "$pkgver" '{pkgver: $pkgver, sha256sums: {}}')
while IFS= read -r arch; do
sums='[]'
while IFS= read -r template; do
if [[ "$template" == file:* ]]; then
file=${template#file:}
if [[ ! "$file" =~ ^[A-Za-z0-9._+-]+$ || ! -f "$package_dir/$file" ]]; then
echo "upstream source names an unsafe or missing local file: '$file'" >&2
rm -rf "$work"
return 1
fi
sum=$(sha256sum "$package_dir/$file" | cut -d' ' -f1)
sums=$(jq -c --arg sum "$sum" '. + [$sum]' <<<"$sums")
continue
fi
url=${template//\{pkgver\}/$pkgver}
url=${url//\{tag\}/$tag}
url=${url//\{npm_tarball\}/$npm_tarball}
if [[ ! "$url" =~ ^https://[^[:space:]{}]+$ ]]; then
echo "upstream source template produced an unsafe URL: '$url'" >&2
rm -rf "$work"
return 1
fi
file="$work/source-$((index += 1))"
if ! upstream_fetch_source "$url" "$file"; then
echo "could not fetch upstream source: $url" >&2
rm -rf "$work"
return 1
fi
sum=$(sha256sum "$file" | cut -d' ' -f1)
sums=$(jq -c --arg sum "$sum" '. + [$sum]' <<<"$sums")
done < <(jq -r --arg arch "$arch" '.[$arch][]' <<<"$sources")
result=$(jq -c --arg arch "$arch" --argjson sums "$sums" '.sha256sums[$arch] = $sums' <<<"$result")
done < <(jq -r 'keys[]' <<<"$sources")
rm -rf "$work"
printf '%s\n' "$result"
}
git_tags_upstream_release() {
local package_dir="$1" metadata repo pattern prefix suffix refs
metadata=$(metadata_file_for_dir "$package_dir")
repo=$(jq -r '.upstream.git_tags // ""' "$metadata")
pattern=$(jq -r '.upstream.tag_pattern // ""' "$metadata")
prefix=${pattern%%\{pkgver\}*}
suffix=${pattern#*\{pkgver\}}
if [[ ! "$repo" =~ ^https://[^[:space:]]+\.git$ || "$pattern" != *'{pkgver}'* || "$suffix" == *'{pkgver}'* ]]; then
echo "invalid git_tags provider configuration" >&2
return 1
fi
if ! refs=$(git_tags_fetch_refs "$repo"); then
echo "could not fetch tags from $repo" >&2
return 1
fi
local best_pkgver="" best_tag="" tag candidate
declare -A version_tags=()
while read -r tag; do
tag=${tag%\^\{\}}
[[ "$tag" == "$prefix"*"$suffix" ]] || continue
candidate=${tag#"$prefix"}
[[ -z "$suffix" ]] || candidate=${candidate%"$suffix"}
[[ "$candidate" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ ]] || continue
if [[ -n "${version_tags[$candidate]:-}" && "${version_tags[$candidate]}" != "$tag" ]]; then
echo "multiple tags map to pkgver $candidate: ${version_tags[$candidate]} and $tag" >&2
return 1
fi
version_tags[$candidate]="$tag"
if [[ -z "$best_pkgver" || $(vercmp "$candidate" "$best_pkgver") -gt 0 ]]; then
best_pkgver="$candidate"
best_tag="$tag"
fi
done < <(sed -n 's#^.*refs/tags/##p' <<<"$refs")
[[ -n "$best_pkgver" ]] || { echo "no usable tags found at $repo" >&2; return 1; }
local current_pkgver
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
if [[ $(vercmp "$best_pkgver" "$current_pkgver") -le 0 ]]; then
echo '{}'
return 0
fi
upstream_hash_sources "$package_dir" "$best_pkgver" "$best_tag"
}
npm_upstream_release() {
local package_dir="$1" metadata package dist_tag npm_metadata pkgver tarball published_at release
metadata=$(metadata_file_for_dir "$package_dir")
package=$(jq -r '.upstream.npm // ""' "$metadata")
dist_tag=$(jq -r '.upstream.dist_tag // "latest"' "$metadata")
if [[ ! "$package" =~ ^(@[a-z0-9_.-]+/)?[a-z0-9_.-]+$ || ! "$dist_tag" =~ ^[a-z0-9_.-]+$ ]]; then
echo "invalid npm provider configuration" >&2
return 1
fi
if ! npm_metadata=$(npm_fetch_metadata "$package"); then
echo "could not fetch npm metadata for $package" >&2
return 1
fi
pkgver=$(jq -r --arg tag "$dist_tag" '."dist-tags"[$tag] // ""' <<<"$npm_metadata")
tarball=$(jq -r --arg version "$pkgver" '.versions[$version].dist.tarball // ""' <<<"$npm_metadata")
published_at=$(jq -r --arg version "$pkgver" '.time[$version] // ""' <<<"$npm_metadata")
if [[ ! "$pkgver" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ || ! "$tarball" =~ ^https://registry\.npmjs\.org/ ]]; then
echo "npm returned an unusable $package release" >&2
return 1
fi
local current_pkgver
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
if [[ $(vercmp "$pkgver" "$current_pkgver") -le 0 ]]; then
echo '{}'
return 0
fi
release=$(upstream_hash_sources "$package_dir" "$pkgver" "$pkgver" "$tarball") || return 1
if [[ -n "$published_at" ]]; then
release=$(jq -c --arg published_at "$published_at" '.published_at = $published_at' <<<"$release")
fi
printf '%s\n' "$release"
}
# Discover a package version from a plain-text Debian Packages index, then
# hash the declared immutable sources. This intentionally supports only
# versions that are already valid Arch pkgver values; feeds needing Debian
# epoch/revision translation keep a package-specific hook.
debian_upstream_release() {
local package_dir="$1" metadata index_url package packages
metadata=$(metadata_file_for_dir "$package_dir")
index_url=$(jq -r '.upstream.debian // ""' "$metadata")
package=$(jq -r '.upstream.package // ""' "$metadata")
if [[ ! "$index_url" =~ ^https://[^[:space:]]+$ || ! "$package" =~ ^[a-z0-9][a-z0-9+.-]*$ ]]; then
echo "invalid Debian Packages provider configuration" >&2
return 1
fi
if ! jq -e '
.upstream.sources | type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
))
' "$metadata" >/dev/null; then
echo "invalid Debian Packages source mapping" >&2
return 1
fi
if ! packages=$(debian_fetch_packages "$index_url"); then
echo "could not fetch Debian Packages index: $index_url" >&2
return 1
fi
packages=${packages//$'\r'/}
local best_pkgver="" candidate
while IFS= read -r candidate; do
if [[ ! "$candidate" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ ]]; then
echo "$package has an unusable Debian version: ${candidate:-<empty>}" >&2
return 1
fi
if [[ -z "$best_pkgver" || $(vercmp "$candidate" "$best_pkgver") -gt 0 ]]; then
best_pkgver="$candidate"
fi
done < <(awk -v target="$package" '
BEGIN { RS = ""; FS = "\n" }
{
name = version = ""
for (i = 1; i <= NF; i++) {
if ($i ~ /^Package: /) name = substr($i, 10)
if ($i ~ /^Version: /) version = substr($i, 10)
}
if (name == target) print version
}
' <<<"$packages")
[[ -n "$best_pkgver" ]] || {
echo "no usable $package release found in $index_url" >&2
return 1
}
local current_pkgver
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
if [[ $(vercmp "$best_pkgver" "$current_pkgver") -le 0 ]]; then
echo '{}'
return 0
fi
upstream_hash_sources "$package_dir" "$best_pkgver" "$best_pkgver"
}
# Emits the newest qualifying release as hook-contract JSON. min_release_age # Emits the newest qualifying release as hook-contract JSON. min_release_age
# is honored during selection (newest release older than the window wins, # is honored during selection (newest release older than the window wins,
# even when a younger one exists) and BYPASS_MIN_RELEASE_AGE=1 lifts it. # even when a younger one exists) and BYPASS_MIN_RELEASE_AGE=1 lifts it.
@@ -276,7 +51,7 @@ debian_upstream_release() {
# not silently choose from. # not silently choose from.
github_upstream_release() { github_upstream_release() {
local package_dir="$1" min_age="${2:-0}" local package_dir="$1" min_age="${2:-0}"
local metadata repo checksums_name use_digests latest_only local metadata repo checksums_name
metadata=$(metadata_file_for_dir "$package_dir") metadata=$(metadata_file_for_dir "$package_dir")
repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata") repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata")
@@ -284,46 +59,9 @@ github_upstream_release() {
echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2 echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2
return 1 return 1
fi fi
# Enforced here as well as in validate_package_metadata: the scheduled sync checksums_name=$(jq -r '(.upstream? | objects | .checksums) // ""' "$metadata")
# reaches this provider without running the validator first. if [[ -z "$checksums_name" ]]; then
checksums_name=$(jq -r '(.upstream? | objects | .checksums) | strings' "$metadata") echo "upstream.checksums names the checksum manifest asset and is required" >&2
use_digests=$(jq -r '(.upstream? | objects | .digests) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata")
latest_only=$(jq -r '(.upstream? | objects | .latest_only) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata")
if [[ "$use_digests" == "invalid" ]]; then
echo "upstream.digests must be true or false" >&2
return 1
fi
if [[ "$latest_only" == "invalid" ]]; then
echo "upstream.latest_only must be true or false" >&2
return 1
fi
if [[ -n "$checksums_name" && "$use_digests" == "true" ]]; then
echo "upstream sets both checksums and digests; keep exactly one" >&2
return 1
fi
if [[ -z "$checksums_name" && "$use_digests" != "true" ]]; then
echo "upstream needs either checksums (a manifest asset name) or digests: true" >&2
return 1
fi
if ! jq -e '
def valid_sources:
type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
));
(.upstream.assets | type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value |
(type == "string" and length > 0)
or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length)
)
)))
and (if .upstream | has("sources") then
(.upstream.sources | valid_sources)
and ((.upstream.assets | keys) as $assets | (.upstream.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length))
else true end)
' "$metadata" >/dev/null; then
echo "invalid upstream.assets or upstream.sources mapping" >&2
return 1 return 1
fi fi
local arches local arches
@@ -339,9 +77,6 @@ github_upstream_release() {
echo "could not fetch the release feed for $repo" >&2 echo "could not fetch the release feed for $repo" >&2
return 1 return 1
fi fi
if [[ "$latest_only" == "true" ]]; then
releases=$(jq '[.[] | select((.draft or .prerelease) | not)][0:1]' <<<"$releases")
fi
local candidates=0 best_tag="" best_pkgver="" best_published_at="" local candidates=0 best_tag="" best_pkgver="" best_published_at=""
local tag published_at pkgver published_epoch local tag published_at pkgver published_epoch
@@ -394,57 +129,33 @@ github_upstream_release() {
return 0 return 0
fi fi
local checksums="" local checksums
if [[ "$use_digests" != "true" ]] \ if ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
&& ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
echo "could not fetch $checksums_name for $repo $best_tag" >&2 echo "could not fetch $checksums_name for $repo $best_tag" >&2
return 1 return 1
fi fi
local result local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at")
result=$(jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published_at" \ local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}'
'{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}') local arch template filename checksum
local arch template filename checksum checksum_source sums
for arch in "${arches[@]}"; do for arch in "${arches[@]}"; do
if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then
echo "invalid architecture key in upstream.assets: '$arch'" >&2 echo "invalid architecture key in upstream.assets: '$arch'" >&2
return 1 return 1
fi fi
sums='[]' template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata")
while IFS= read -r template; do filename=${template//\{pkgver\}/$best_pkgver}
filename=${template//\{pkgver\}/$best_pkgver} filename=${filename//\{tag\}/$best_tag}
filename=${filename//\{tag\}/$best_tag} # Manifest lines are "<sha256> <name>", with the name sometimes prefixed
if [[ "$use_digests" == "true" ]]; then # "./" (sha256sum of a local path) or "*" (binary-mode marker).
# Only a "sha256:<hex>" digest is stripped to its hex; any other shape checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums")
# falls through empty and fails the check below. if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" ' echo "no valid checksum for $filename in $repo $best_tag $checksums_name" >&2
first(.[] | select(.tag_name == $tag)) | (.assets // [])[] return 1
| select(.name == $name) | (.digest // "") fi
| if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end jq_args+=(--arg "sum_$arch" "$checksum")
' <<<"$releases") jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]"
checksum_source="the release API digest"
else
# Manifest lines are "<sha256> <name>", with the name sometimes prefixed
# "./" (sha256sum of a local path) or "*" (binary-mode marker).
checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums")
checksum_source="$checksums_name"
fi
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
echo "no valid checksum for $filename in $repo $best_tag $checksum_source" >&2
return 1
fi
sums=$(jq -c --arg checksum "$checksum" '. + [$checksum]' <<<"$sums")
done < <(jq -r --arg arch "$arch" '
.upstream.assets[$arch] | if type == "array" then .[] else . end
' "$metadata")
result=$(jq -c --arg arch "$arch" --argjson sums "$sums" '.sha256sums[$arch] = $sums' <<<"$result")
done done
if jq -e '.upstream | has("sources")' "$metadata" >/dev/null; then jq -n "${jq_args[@]}" "$jq_filter"
local source_release
source_release=$(upstream_hash_sources "$package_dir" "$best_pkgver" "$best_tag") || return 1
result=$(jq -c --argjson source "$source_release" '.sha256sums += $source.sha256sums' <<<"$result")
fi
printf '%s\n' "$result"
} }
-644
View File
@@ -1,644 +0,0 @@
#!/usr/bin/env python3
"""Discover releases and update our own recipes; never import upstream build code.
The watch selects release metadata. Sources, supported architectures, integrity
algorithms and packaging behavior stay in the checked-in PKGBUILD. Only release
scalars and checksum arrays are replaced, atomically, after every source passes.
"""
import argparse
import datetime as dt
import gzip
import hashlib
import io
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tarfile
import tempfile
from urllib.parse import quote, urlsplit
import zipfile
PROVIDERS = {"github", "git_tags", "git_branch", "npm", "pypi", "debian", "json", "regex", "archive", "redirect"}
VERSION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._+]*\Z")
SCALAR = re.compile(r"[A-Za-z0-9._+/-]+\Z")
SUM = re.compile(r"(md5|sha1|sha224|sha256|sha384|sha512|b2)sums(_[a-z0-9_]+)?\Z")
HASHES = {"b2": "blake2b"}
# How many times a redirect watch probes before it calls the feed unmatched.
REDIRECT_PROBES = 5
def run(args, **kwargs):
return subprocess.check_output(args, **kwargs)
def vercmp(a, b):
return int(run(["vercmp", a, b], text=True).strip())
def https(url):
parts = urlsplit(url)
if parts.scheme != "https" or not parts.hostname or parts.username or parts.password or re.search(r"[\s\x00-\x1f]", url):
raise ValueError(f"expected an HTTPS upstream URL: {url!r}")
return url
class Fetcher:
def __init__(self, cache):
self.cache = Path(cache)
self.cache.mkdir(parents=True, exist_ok=True)
def file(self, url):
https(url)
dest = self.cache / hashlib.sha256(url.encode()).hexdigest()
if not dest.exists():
scratch = dest.with_suffix(f".{os.getpid()}.tmp")
command = ["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSL",
"--connect-timeout", "20", "--max-time", "300", "--retry", "2", "-o", str(scratch), url]
# Credentials only go to GitHub's API, never to release assets or vendors.
token = os.environ.get("UPSTREAM_GITHUB_TOKEN")
if token and urlsplit(url).hostname == "api.github.com":
command[1:1] = ["--config", "-"]
subprocess.run(command, input=f'header = "Authorization: Bearer {token}"\n', text=True, check=True)
else:
subprocess.run(command, check=True)
scratch.replace(dest)
return dest
def text(self, url):
data = self.file(url).read_bytes()
if data.startswith(b"\x1f\x8b"):
data = gzip.decompress(data)
return data.decode()
def json(self, url):
return json.loads(self.text(url))
def validate(watch):
if not isinstance(watch, dict) or len(PROVIDERS & watch.keys()) != 1:
raise ValueError("watch must select exactly one release provider")
provider = next(iter(PROVIDERS & watch.keys()))
allowed = PROVIDERS | {"pattern", "path", "package", "branch", "variables", "fields",
"submodules", "allow_prerelease", "unescape_json", "filenames",
"sequence", "version", "revision", "revision_variable",
"mutable_sources", "member", "dist_tag", "tag_pattern"}
if watch.keys() - allowed:
raise ValueError(f"unknown watch fields: {sorted(watch.keys() - allowed)}")
value = watch[provider]
if not isinstance(value, str) or not value:
raise ValueError(f"invalid watch.{provider}")
if provider == "github":
if not re.fullmatch(r"[\w.-]+/[\w.-]+", value):
raise ValueError("invalid GitHub repository")
elif provider in {"npm", "pypi"}:
if not re.fullmatch(r"(?:@[\w.-]+/)?[\w.-]+", value):
raise ValueError("invalid registry package")
else:
https(value)
if "pattern" in watch or provider in {"github", "git_tags", "regex", "archive", "redirect"}:
if not isinstance(watch.get("pattern"), str):
raise ValueError("watch needs an explicit release pattern")
pattern = re.compile(watch["pattern"])
if "version" not in pattern.groupindex:
raise ValueError("release pattern needs a named version group")
if provider == "json" and (not isinstance(watch.get("path"), str) or not watch["path"]):
raise ValueError("JSON watch needs a version path")
if provider == "debian":
name = watch.get("package", "")
if not isinstance(name, str) or not re.fullmatch(r"[a-z0-9][a-z0-9+.-]*", name):
raise ValueError("Debian watch needs an exact package name")
if provider == "git_branch":
branch = watch.get("branch", "")
if not isinstance(branch, str) or not branch or branch.startswith("-"):
raise ValueError("git branch watch needs an explicit branch")
run(["git", "check-ref-format", "refs/heads/" + branch])
# A branch watch whose version template names a tag needs to know
# which tags count as releases; anything else is an untagged branch.
if "tag_pattern" in watch:
if not isinstance(watch["tag_pattern"], str) or not watch["tag_pattern"]:
raise ValueError("watch.tag_pattern must be a regular expression string")
if "version" not in re.compile(watch["tag_pattern"]).groupindex:
raise ValueError("tag_pattern needs a named version group")
template = watch.get("version", "{version}")
if any(field in template for field in ("{tag", "{distance")) and "tag_pattern" not in watch:
raise ValueError("a version built from {tag}/{distance} needs a tag_pattern")
elif "tag_pattern" in watch:
raise ValueError("tag_pattern only applies to git_branch watches")
for field in ("variables", "submodules", "fields"):
mapping = watch.get(field, {})
if not isinstance(mapping, dict):
raise ValueError(f"watch.{field} must be a string mapping")
for name, value in mapping.items():
pattern = r"[a-z][a-z0-9_]*" if field == "fields" else r"_[a-z][a-z0-9_]*"
if not re.fullmatch(pattern, name) or not isinstance(value, str) or not value:
raise ValueError(f"invalid watch.{field} mapping")
if "submodules" in watch and provider != "github":
raise ValueError("submodules require a GitHub watch")
if watch.get("variables", {}).keys() & watch.get("submodules", {}).keys():
raise ValueError("a release variable cannot also be a submodule")
for path in watch.get("submodules", {}).values():
if path.startswith("/") or any(part in {"", ".", ".."} for part in path.split("/")):
raise ValueError("submodule path must be relative to the release repository")
for field in ("allow_prerelease", "unescape_json", "filenames", "sequence"):
if field in watch and not isinstance(watch[field], bool):
raise ValueError(f"watch.{field} must be boolean")
for field in ("version", "revision", "member", "dist_tag"):
if field in watch and (not isinstance(watch[field], str) or not watch[field]):
raise ValueError(f"watch.{field} must be a string template")
if "revision_variable" in watch:
name = watch["revision_variable"]
if not isinstance(name, str) or name not in watch.get("variables", {}) or not watch.get("revision"):
raise ValueError("revision_variable requires a declared variable and revision template")
for field in ("mutable_sources",):
entries = watch.get(field, [])
if not isinstance(entries, list) or any(not isinstance(v, str) or not re.fullmatch(r"source(?:_[a-z0-9_]+)?:[0-9]+", v) for v in entries):
raise ValueError(f"watch.{field} must name source-array:index entries")
return provider
def json_path(data, path):
for key in path.split("."):
data = data[int(key)] if isinstance(data, list) else data[key]
return data
def candidate(watch, values):
values = {k: str(v) for k, v in values.items() if v is not None}
version = watch.get("version", "{version}").format_map(values)
if not VERSION.fullmatch(version):
raise ValueError(f"unusable upstream version: {version!r}")
revision = watch.get("revision", "").format_map(values)
if revision and not re.fullmatch(r"[0-9]+", revision):
raise ValueError("upstream release revision must be numeric")
return {"pkgver": version, "values": values,
"published_at": values.get("published_at"),
"revision": revision}
def matches(watch, text, extra=None, full=False):
pattern = re.compile(watch["pattern"])
found = [pattern.fullmatch(text)] if full else pattern.finditer(text)
for match in found:
if match:
yield candidate(watch, {**(extra or {}), **match.groupdict()})
def git_branch_tip(url, branch, tag_pattern, cache, min_age=0):
"""Describe the newest commit on an upstream branch that has cleared
min_age: commit, total count, date, and with a tag_pattern
the newest release tag reachable from it plus the distance from that tag,
so a branch build can be versioned <tag>.r<n>.g<sha>, above the release it
follows and below the next one, the way a pkgver() function would.
One blobless single-branch clone per (url, branch) per run, shared by
every package that tracks it, so two recipes pinned from one clone always
see the same commit. The clone is read with git only; nothing in it runs.
A tip younger than min_age falls back along first-parent history, the
commits that were themselves the branch tip, never into a merged side
branch. Holding the whole branch instead stalled a busy branch for as
long as it kept getting commits. Age is measured from when the clone was
made, so siblings synced seconds apart cannot straddle the window.
"""
https(url)
key = hashlib.sha256(f"{url}#{branch}".encode()).hexdigest()
work = Path(cache) / f"{key}.branch.git"
cloned_at = work.with_name(f"{key}.branch.time")
if not work.exists():
scratch = work.with_name(f"{work.name}.{os.getpid()}.tmp")
started = dt.datetime.now(dt.timezone.utc).isoformat()
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", branch, url, str(scratch)], check=True)
cloned_at.write_text(started)
scratch.replace(work)
git = ["git", "-C", str(work)]
commit = run([*git, "rev-parse", "HEAD"], text=True).strip()
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError("branch tip is not a commit")
if min_age:
now = dt.datetime.fromisoformat(cloned_at.read_text().strip())
for line in run([*git, "log", "--first-parent", "--format=%H %cI", commit], text=True).splitlines():
sha, committed = line.split()
if (now - dt.datetime.fromisoformat(committed)).total_seconds() >= min_age:
commit = sha
break
count = run([*git, "rev-list", "--count", commit], text=True).strip()
date = run([*git, "show", "-s", "--format=%cs", commit], text=True).strip().replace("-", "")
timestamp = run([*git, "show", "-s", "--format=%cI", commit], text=True).strip()
values = {"version": date, "date": date, "count": count, "commit": commit, "published_at": timestamp}
if tag_pattern:
pattern = re.compile(tag_pattern)
best = None
# Only tags in this commit's history count; a release cut on another
# branch is not something this branch is "past".
for tag in run([*git, "tag", "--merged", commit], text=True).split():
match = pattern.fullmatch(tag)
if not match:
continue
version = match.group("version")
if best is None or vercmp(version, best[0]) > 0:
best = (version, tag)
if best is None:
raise ValueError(f"no tag on {branch} matches {tag_pattern}")
distance = run([*git, "rev-list", "--count", f"{best[1]}..{commit}"], text=True).strip()
values.update({"tag": best[1], "version": best[0], "distance": distance})
return values
def discover(watch, fetch, min_age=0):
provider = validate(watch)
feed = watch[provider]
results = []
if provider == "github":
releases = fetch.json(f"https://api.github.com/repos/{feed}/releases?per_page=100")
if not isinstance(releases, list):
raise ValueError("GitHub did not return a release list")
for release in releases:
if release.get("draft") or (release.get("prerelease") and not watch.get("allow_prerelease")):
continue
for item in matches(watch, release["tag_name"], {"tag": release["tag_name"], "published_at": release["published_at"]}, full=True):
item["assets"] = release.get("assets", [])
results.append(item)
elif provider == "git_tags":
refs = run(["git", "ls-remote", "--tags", feed], text=True)
tags = {}
for line in refs.splitlines():
commit, ref = line.split()
tag = ref.removeprefix("refs/tags/")
if tag.endswith("^{}"):
tags[tag[:-3]] = commit
else:
tags.setdefault(tag, commit)
for tag, commit in tags.items():
results.extend(matches(watch, tag, {"tag": tag, "commit": commit}, full=True))
elif provider == "git_branch":
tip = git_branch_tip(feed, watch["branch"], watch.get("tag_pattern"), fetch.cache, min_age)
results.append(candidate(watch, tip))
elif provider == "npm":
data = fetch.json("https://registry.npmjs.org/" + quote(feed, safe=""))
version = data["dist-tags"][watch.get("dist_tag", "latest")]
results.append(candidate(watch, {"version": version, "published_at": data.get("time", {}).get(version)}))
elif provider == "pypi":
data = fetch.json(f"https://pypi.org/pypi/{feed}/json")
version = data["info"]["version"]
dates = [r["upload_time_iso_8601"] for r in data["releases"].get(version, []) if not r.get("yanked")]
if not dates:
raise ValueError("PyPI release has no unyanked files")
results.append(candidate(watch, {"version": version, "published_at": max(dates)}))
elif provider == "debian":
for stanza in re.split(r"\n\s*\n", fetch.text(feed).replace("\r", "")):
fields = dict(re.findall(r"^([A-Za-z0-9-]+): (.*)$", stanza, re.M))
if fields.get("Package") != watch["package"]:
continue
if "pattern" in watch:
results.extend(matches(watch, fields["Version"], full=True))
else:
results.append(candidate(watch, {"version": fields["Version"]}))
elif provider == "json":
data = fetch.json(feed)
values = {"version": json_path(data, watch["path"])}
values.update({name: json_path(data, path) for name, path in watch.get("fields", {}).items()})
results.append(candidate(watch, values))
elif provider == "redirect":
# A download redirect can be a staged rollout: some requests land on a
# newer build the pattern deliberately rejects (Dropbox, 2026-10-04:
# 4 of 100 HEADs ended at 274.3.4801 instead of 272.4.3798), so one
# probe that misses is not an answer. Keep the first one that matches.
for _ in range(REDIRECT_PROBES):
final_url = run(["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSLI", "--max-time", "60", "--retry", "2",
"-o", "/dev/null", "-w", "%{url_effective}", feed], text=True)
results.extend(matches(watch, final_url))
if results:
break
elif provider == "regex":
text = fetch.text(feed)
if watch.get("unescape_json"):
text = text.replace('\\"', '"')
results.extend(matches(watch, text))
elif provider == "archive":
file = fetch.file(feed)
if zipfile.is_zipfile(file):
with zipfile.ZipFile(file) as archive:
names = archive.namelist()
if watch.get("filenames"):
results.extend(matches(watch, "\n".join(names)))
for name in ([] if watch.get("filenames") else names):
if re.fullmatch(watch.get("member", ".*"), name):
results.extend(matches(watch, archive.read(name).decode()))
elif file.read_bytes()[:8] == b"!<arch>\n":
names = run(["bsdtar", "-tf", str(file)], text=True).splitlines()
controls = [name for name in names if name.startswith("control.tar")]
if len(controls) != 1:
raise ValueError("deb does not contain exactly one control archive")
data = run(["bsdtar", "-xOf", str(file), controls[0]])
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
members = [m for m in archive if m.name.removeprefix("./") == "control"]
if len(members) != 1:
raise ValueError("deb control file is missing or ambiguous")
results.extend(matches(watch, archive.extractfile(members[0]).read().decode()))
else:
with tarfile.open(file) as archive:
for member in archive:
if member.isfile() and re.fullmatch(watch.get("member", ".*"), member.name):
results.extend(matches(watch, archive.extractfile(member).read().decode()))
if not results:
raise ValueError(f"no matching releases in {feed}")
return results
def select_release(releases, min_age=0, now=None, bypass=False):
now = now or dt.datetime.now(dt.timezone.utc)
best = None
for release in releases:
if min_age and not bypass:
value = release.get("published_at")
if not value or not re.fullmatch(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:?\d\d)", value):
raise ValueError("release age cannot be established")
if (now - dt.datetime.fromisoformat(value.replace("Z", "+00:00"))).total_seconds() < min_age:
continue
order = vercmp(release["pkgver"], best["pkgver"]) if best else 1
if best and order == 0:
order = vercmp(release["revision"] or "0", best["revision"] or "0")
if order > 0:
best = release
return best
DUMP = r'''
source "$1" >/dev/null || exit 1
set +u
for __watch_name in pkgver pkgrel epoch arch $(compgen -A variable | LC_ALL=C sort); do
case "$__watch_name" in
pkgver|pkgrel|epoch|arch|source|source_*|md5sums*|sha1sums*|sha224sums*|sha256sums*|sha384sums*|sha512sums*|b2sums*|_*)
[[ $__watch_name == __watch_* ]] && continue
declare -n __watch_value="$__watch_name"
printf '%s\0' "$__watch_name" "${#__watch_value[@]}" "${__watch_value[@]}"
unset -n __watch_value
;;
esac
done
'''
def read_recipe(path, arch="x86_64"):
with tempfile.TemporaryDirectory(prefix="recipe-read-") as work:
env = {**os.environ, "CARCH": arch, "SRCDEST": work, "srcdir": work, "pkgdir": work}
data = run(["bash", "-c", DUMP, "_", str(path.resolve())], cwd=path.parent, env=env).decode().split("\0")
result = {}
index = 0
while index < len(data) - 1:
name, size = data[index:index + 2]
index += 2
size = int(size)
result[name] = data[index:index + size]
index += size
return result
def scalar(recipe, name, default=""):
return recipe.get(name, [default])[0] if recipe.get(name) else default
def replace_scalar(text, name, value):
if not SCALAR.fullmatch(value):
raise ValueError(f"unsafe {name} value")
pattern = re.compile(r"^" + re.escape(name) + r"=.*$", re.M)
if len(pattern.findall(text)) != 1:
raise ValueError(f"expected one top-level {name}= assignment")
return pattern.sub(lambda _: f"{name}={value}", text)
def replace_array(text, name, values):
starts = list(re.finditer(r"^" + re.escape(name) + r"=\(", text, re.M))
if len(starts) != 1:
raise ValueError(f"expected one top-level {name}= array")
start = starts[0]
depth, quote_char, escaped, comment = 1, None, False, False
for index in range(start.end(), len(text)):
char = text[index]
if comment:
if char == "\n": comment = False
elif escaped:
escaped = False
elif char == "\\" and quote_char != "'":
escaped = True
elif quote_char:
if char == quote_char: quote_char = None
elif char in "\"'": quote_char = char
elif char == "#" and (index == 0 or text[index - 1].isspace()): comment = True
elif char == "(": depth += 1
elif char == ")":
depth -= 1
if depth == 0:
replacement = name + "=(" + " ".join("'" + value + "'" for value in values) + ")"
return text[:start.start()] + replacement + text[index + 1:]
raise ValueError(f"unclosed {name} array")
def bump_pkgrel(value):
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value):
raise ValueError(f"invalid pkgrel: {value}")
components = value.split(".")
components[-1] = str(int(components[-1]) + 1)
return ".".join(components)
def complete_version(recipe):
return f"{scalar(recipe, 'epoch', '0')}:{scalar(recipe, 'pkgver')}-{scalar(recipe, 'pkgrel')}"
def hash_file(path, algorithm):
with path.open("rb") as stream:
return hashlib.file_digest(stream, HASHES.get(algorithm, algorithm)).hexdigest()
def source_url(source):
return source.split("::", 1)[-1]
def git_source_file(url, cache):
base, fragment = url.removeprefix("git+").split("#", 1)
kind, ref = fragment.split("=", 1)
https(base)
if kind not in {"tag", "commit"} or (kind == "commit" and not re.fullmatch(r"[0-9a-f]{40}", ref)):
raise ValueError("VCS sources must name an immutable commit or a checksummed tag")
if kind == "tag":
run(["git", "check-ref-format", "refs/tags/" + ref])
dest = cache / (hashlib.sha256(url.encode()).hexdigest() + ".git.tar")
if not dest.exists():
with tempfile.TemporaryDirectory(prefix="upstream-source-", dir=cache) as work:
subprocess.run(["git", "init", "--quiet", "--bare", work], check=True)
subprocess.run(["git", "-C", work, "fetch", "--quiet", "--depth=1", base, "refs/tags/" + ref if kind == "tag" else ref], check=True)
scratch = Path(work) / "source.tar"
with scratch.open("wb") as output:
subprocess.run(["git", "-c", "core.abbrev=no", "-C", work, "archive", "--format", "tar", "FETCH_HEAD"], stdout=output, check=True)
# The cache must never retain partial archives after a git failure.
scratch.replace(dest)
return dest
def updated_checksums(before, after, package, fetch, release, watch):
arrays = {}
source_names = {key for key in before if key == "source" or key.startswith("source_")}
if source_names != {key for key in after if key == "source" or key.startswith("source_")}:
raise ValueError("release changed the set of source architectures")
for source_name in sorted(source_names):
old_sources, sources = before[source_name], after[source_name]
suffix = source_name.removeprefix("source")
names = [name for name in before if SUM.fullmatch(name) and (SUM.fullmatch(name)[2] or "") == suffix]
if not sources:
continue
if len(sources) != len(old_sources) or not names:
raise ValueError(f"{source_name}: sources changed shape or have no checksums")
for name in names:
if len(before[name]) != len(sources):
raise ValueError(f"{name}: source/checksum count mismatch")
values = []
algorithm = SUM.fullmatch(name)[1]
for index, source in enumerate(sources):
old = before[name][index]
if source == old_sources[index] and f"{source_name}:{index}" not in watch.get("mutable_sources", []):
values.append(old)
continue
url = source_url(source)
# A release API digest can supply SHA256 without downloading a
# large asset, but only when its exact declared URL matches.
assets = [a for a in release.get("assets", []) if a.get("browser_download_url") == url]
if algorithm == "sha256" and len(assets) == 1 and re.fullmatch(r"sha256:[0-9a-f]{64}", assets[0].get("digest") or ""):
values.append("SKIP" if old == "SKIP" else assets[0]["digest"][7:])
continue
if url.startswith("git+https://"):
file = git_source_file(url, fetch.cache)
elif url.startswith("https://"):
file = fetch.file(url)
elif "://" not in url:
file = (package / url).resolve()
if not file.is_relative_to(package.resolve()) or not file.is_file():
raise ValueError(f"unsafe local source: {url}")
else:
raise ValueError(f"unsupported source transport: {url}")
# Preserve existing signature/prepare()-verified sources. Never
# introduce SKIP; still fetch changed URLs to verify availability.
values.append("SKIP" if old == "SKIP" else hash_file(file, algorithm))
if values != before[name]:
arrays[name] = values
return arrays
def resolve_release_fields(watch, release, fetch):
values = release["values"].copy()
if "github" in watch and any("{commit}" in value for value in watch.get("variables", {}).values()):
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/ref/tags/{quote(values['tag'], safe='')}")['object']
if ref['type'] == 'tag':
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/tags/{ref['sha']}")['object']
if ref['type'] != 'commit' or not re.fullmatch(r"[0-9a-f]{40}", ref['sha']):
raise ValueError("release tag does not resolve to a commit")
values['commit'] = ref['sha']
variables = {k: template.format_map(values) for k, template in watch.get('variables', {}).items()}
for name, path in watch.get('submodules', {}).items():
entry = fetch.json(f"https://api.github.com/repos/{watch['github']}/contents/{quote(path, safe='/')}?ref={quote(values['tag'], safe='')}")
if not entry.get('submodule_git_url') or not re.fullmatch(r"[0-9a-f]{40}", entry.get('sha', '')):
raise ValueError(f"release does not contain submodule {path}")
variables[name] = entry['sha']
if any(not SCALAR.fullmatch(value) for value in variables.values()):
raise ValueError("unsafe release variable value")
release['variables'] = variables
return release
def sync(package, fetch, min_age=0, check=False):
metadata = json.loads((package / ".omarchy/package.json").read_text())
if metadata.get("sync") is False:
return {"status": "skipped", "reason": "upstream updates held by sync=false"}
watch = metadata["upstream"]["watch"]
validate(watch)
path = package / "PKGBUILD"
original = path.read_text()
before = read_recipe(path)
bypass = os.environ.get("BYPASS_MIN_RELEASE_AGE") == "1"
release = select_release(discover(watch, fetch, 0 if bypass else min_age), min_age, bypass=bypass)
if release is None:
return {"status": "skipped", "reason": "minimum release age"}
current = scalar(before, "pkgver")
if watch.get('sequence'):
prefix, counter, identity = current.rsplit('.', 2)
new_prefix, new_identity = release['values']['version'], release['values']['hash']
if new_prefix == prefix:
release['pkgver'] = current if new_identity == identity else f"{prefix}.{int(counter) + 1}.{new_identity}"
order = vercmp(release["pkgver"], current)
if order < 0:
return {"status": "skipped", "current": current, "available": release["pkgver"], "reason": "upstream is older"}
if order == 0 and not watch.get("revision_variable"):
return {"status": "skipped", "current": current, "reason": "already current"}
release = resolve_release_fields(watch, release, fetch)
changed_variables = {k: v for k, v in release["variables"].items() if scalar(before, k) != v}
if order == 0 and not changed_variables:
return {"status": "skipped", "current": current, "reason": "already current"}
if order == 0 and changed_variables:
# Only a declared, forward-moving release revision can rebuild the same
# version. A changed hash/commit alone is an immutable-release violation.
revision_field = watch.get("revision_variable")
if revision_field not in changed_variables or vercmp(changed_variables[revision_field], scalar(before, revision_field, "0")) <= 0:
raise ValueError("release metadata changed without a newer version/revision")
new_pkgrel = "1" if order > 0 else bump_pkgrel(scalar(before, "pkgrel"))
text = replace_scalar(original, "pkgver", release["pkgver"])
text = replace_scalar(text, "pkgrel", new_pkgrel)
for name, value in release["variables"].items():
text = replace_scalar(text, name, value)
if check:
return {"status": "available", "current": current, "release": release}
scratch = path.with_name("PKGBUILD.sync-upstream")
try:
scratch.write_text(text)
after = read_recipe(scratch)
if scalar(after, "pkgver") != release["pkgver"] or scalar(after, "pkgrel") != new_pkgrel:
raise ValueError("recipe did not retain the release version")
if vercmp(complete_version(after), complete_version(before)) <= 0:
raise ValueError("complete package version must increase")
if before["arch"] != after["arch"]:
raise ValueError("release changed supported architectures")
arrays = updated_checksums(before, after, package, fetch, release, watch)
for name, values in arrays.items():
text = replace_array(text, name, values)
scratch.write_text(text)
subprocess.run(["bash", "-n", str(scratch)], check=True)
for arch in before["arch"]:
result = read_recipe(scratch, "x86_64" if arch == "any" else arch)
if complete_version(result) != complete_version(after):
raise ValueError(f"{arch}: inconsistent release version")
for name, values in arrays.items():
if result.get(name) != values:
raise ValueError(f"{arch}: rewritten {name} differs from the checked source hashes")
for name in after:
if name == "source" or name.startswith("source_"):
if result.get(name) != after[name]:
raise ValueError(f"{arch}: conditional {name} differs from the checked sources; use source_<arch> arrays")
scratch.chmod(path.stat().st_mode)
scratch.replace(path)
return {"status": "updated", "before": complete_version(before), "after": complete_version(after)}
finally:
scratch.unlink(missing_ok=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["sync", "check", "validate"])
parser.add_argument("package", type=Path)
parser.add_argument("--min-age", type=int, default=0)
args = parser.parse_args()
package = args.package.resolve()
if args.command == "validate":
validate(json.loads((package / ".omarchy/package.json").read_text())["upstream"]["watch"])
return
with tempfile.TemporaryDirectory(prefix="upstream-watch-") as cache:
fetch = Fetcher(os.environ.get("UPSTREAM_CACHE_DIR", cache))
print(json.dumps(sync(package, fetch, args.min_age, check=args.command == "check")))
if __name__ == "__main__":
try:
main()
except (ValueError, KeyError, TypeError, IndexError, re.error, OSError, subprocess.CalledProcessError) as error:
print(f"upstream watch failed: {error}", file=sys.stderr)
sys.exit(1)
+2 -16
View File
@@ -1,19 +1,5 @@
{ {
"source": "local", "source": "aur",
"release_ring": "fast", "release_ring": "fast",
"upstream": { "upstream_commit": "a0f4262f94eb8a5b604146e71d42a3bb522feedf"
"watch": {
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages",
"package": "1password",
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)~(?P<build>[0-9]+)\\.BETA",
"version": "{version}_{build}.BETA",
"variables": {
"_tarver": "{version}-{build}.BETA"
}
}
},
"origin": {
"aur": "1password-beta",
"commit": "18d2de51dc01c9a58c1e8e96262f7afadcbf0648"
}
} }
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
set -euo pipefail
# Keep the AUR x86_64 checksums and add aarch64 sources. The aarch64
# artifacts are signed by 1Password's validpgpkeys, so we skip checksums there
# instead of baking version-specific hashes into Omarchy metadata.
set +u
CARCH=x86_64 source PKGBUILD
set -u
if declare -p sha256sums >/dev/null 2>&1 && [[ ${#sha256sums[@]} -ge 2 ]]; then
x86_sums=("${sha256sums[@]}")
elif declare -p sha256sums_x86_64 >/dev/null 2>&1 && [[ ${#sha256sums_x86_64[@]} -ge 2 ]]; then
x86_sums=("${sha256sums_x86_64[@]}")
else
echo "Unable to read x86_64 checksums from PKGBUILD" >&2
exit 1
fi
sha256_from_url() {
curl -fsSL "$1" | sha256sum | awk '{ print $1 }'
}
arm_url="https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz"
arm_tar_sum=$(sha256_from_url "$arm_url")
arm_sig_sum=$(sha256_from_url "$arm_url.sig")
emit_archdir() {
cat <<'EOF'
case "${CARCH}" in
x86_64)
_archdir="x64"
;;
aarch64)
_archdir="arm64"
;;
esac
EOF
}
emit_sources() {
cat <<EOF
source=()
sha256sums=()
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-\${_tarver}.x64.tar.gz{,.sig})
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-\${_tarver}.arm64.tar.gz{,.sig})
sha256sums_x86_64=('${x86_sums[0]}'
'${x86_sums[1]}')
sha256sums_aarch64=('$arm_tar_sum'
'$arm_sig_sum')
EOF
}
tmpfile=$(mktemp)
skip_checksums=false
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$skip_checksums" == true ]]; then
[[ "$line" == ")" ]] && skip_checksums=false
continue
fi
case "$line" in
'_tar="1password-${_tarver}.x64.tar.gz"')
emit_archdir >> "$tmpfile"
;;
"arch=('x86_64')")
echo "arch=('x86_64' 'aarch64')" >> "$tmpfile"
;;
source=\(*)
emit_sources >> "$tmpfile"
;;
sha256sums=\(*)
[[ "$line" == *")" ]] || skip_checksums=true
;;
*)
line=${line//1password-\$\{_tarver\}.x64/1password-\$\{_tarver\}.\$\{_archdir\}}
printf '%s\n' "$line" >> "$tmpfile"
;;
esac
done < PKGBUILD
mv "$tmpfile" PKGBUILD
+10 -7
View File
@@ -1,6 +1,6 @@
pkgname=1password-beta pkgname=1password-beta
_tarver=8.12.42-32.BETA _tarver=8.12.34-29.BETA
case "${CARCH}" in case "${CARCH}" in
x86_64) x86_64)
_archdir="x64" _archdir="x64"
@@ -9,8 +9,8 @@ case "${CARCH}" in
_archdir="arm64" _archdir="arm64"
;; ;;
esac esac
pkgver=8.12.42_32.BETA pkgver=${_tarver//-/_}
pkgrel=1 pkgrel=29.1
conflicts=('1password' '1password-beta-bin') conflicts=('1password' '1password-beta-bin')
pkgdesc="Password manager and secure wallet" pkgdesc="Password manager and secure wallet"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -22,8 +22,10 @@ source=()
sha256sums=() sha256sums=()
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-${_tarver}.x64.tar.gz{,.sig}) source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-${_tarver}.x64.tar.gz{,.sig})
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz{,.sig}) source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz{,.sig})
sha256sums_x86_64=('55fe8dbc80e65337e657c926948dff356b09c3d0860f3b381f1df5af2b99741a' '62494cae0f90d553cade1eeda82cec4d5b26e4719f2233085ce47a70cc80da63') sha256sums_x86_64=('6894b283a534cf94b07903fb38966a0aab2e37f75ee3848ce340308819aadddb'
sha256sums_aarch64=('53a63a857e9219da410839915216c4f9cc017cb17bbff9095caf01365e28c5d8' 'd910aedf72c4ff31745b0bbfc12d03c03b177df85f73a381a35227439320ad5c') '8d4df4d0a80d2be7aad7d91ad964a750c8f32db5007261045003c191690c8246')
sha256sums_aarch64=('c77ce6ddf36dbd6054c64d91b7f644274d3218f465fd60e211d0296f6443124a'
'91c7249a1cf5e7924ef2810cb0cb1b893d8a9a424b373b433f45d7aaa5a8369b')
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22') validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
package() { package() {
@@ -40,7 +42,7 @@ package() {
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png" "${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
done done
# Install desktop file # Install desktop file
install -Dm0644 resources/com.onepassword.OnePassword.desktop -t "${pkgdir}"/usr/share/applications/ install -Dm0644 resources/1password.desktop -t "${pkgdir}"/usr/share/applications/
# Fill in policy kit file with a list of (the first 10) human users of the system. # Fill in policy kit file with a list of (the first 10) human users of the system.
export POLICY_OWNERS export POLICY_OWNERS
@@ -63,7 +65,8 @@ EOF" > ./com.1password.1Password.policy
# Cleanup un-needed files # Cleanup un-needed files
rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh
rm -r "${pkgdir}"/opt/1Password/resources/icons/ rm -r "${pkgdir}"/opt/1Password/resources/icons/
rm "${pkgdir}"/opt/1Password/resources/com.onepassword.OnePassword.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers rm "${pkgdir}"/opt/1Password/resources/1password.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
# Symlink /usr/bin executable to opt # Symlink /usr/bin executable to opt
install -dm0755 "${pkgdir}"/usr/bin install -dm0755 "${pkgdir}"/usr/bin
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
+2 -12
View File
@@ -1,15 +1,5 @@
{ {
"source": "local", "source": "aur",
"auto_merge": true,
"release_ring": "fast", "release_ring": "fast",
"upstream": { "upstream_commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
"watch": {
"json": "https://app-updates.agilebits.com/check/1/0/CLI2/en/0",
"path": "version"
}
},
"origin": {
"aur": "1password-cli",
"commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
}
} }
+7 -7
View File
@@ -2,8 +2,8 @@
# Contributors: Felix Seidel, Claudia Pellegrino, Liu Yuxuan # Contributors: Felix Seidel, Claudia Pellegrino, Liu Yuxuan
pkgname=1password-cli pkgname=1password-cli
pkgver=2.40.0 pkgver=2.39.0
pkgrel=2 pkgrel=1
pkgdesc="1Password command line tool" pkgdesc="1Password command line tool"
arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64') arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64')
url="https://app-updates.agilebits.com/product_history/CLI2" url="https://app-updates.agilebits.com/product_history/CLI2"
@@ -18,11 +18,11 @@ source_arm=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm
source_armv6h=("${source_arm}") source_armv6h=("${source_arm}")
source_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip") source_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip")
sha256sums_x86_64=('74277219e8da60958c00f9aee9d2023225e98fdda8bfd2156a5d9e85e0edaab3') sha256sums_x86_64=('6fba7f376b6c6dec49f41b06408930a43ad064cce103c6a2ce5b3d0413a86434')
sha256sums_i686=('adb1da45c0a40bb97c1e1ad62119d0a1aeb4ed9d979c06a7b1f95a48d9142b3d') sha256sums_i686=('387d95f046ec9334e9de63514f96767fdd5dacbae292eb086fdfa0b0da310ec4')
sha256sums_arm=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354') sha256sums_arm=('673913f24ff57ce43e9d25ee451121d4733d188f45f6ab0468983fad85f8cc42')
sha256sums_armv6h=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354') sha256sums_armv6h=("${sha256sums_arm}")
sha256sums_aarch64=('0e8ac99ee93d661aa725dc24a5ef8bf344741d224064a5dfb469dc689faec86a') sha256sums_aarch64=('829baeff1c07e055cfa132031b1d9f2282ccdf5076258e482caf2fda70aea5d0')
check() { check() {
if (( ! SKIPPGPCHECK )); then if (( ! SKIPPGPCHECK )); then
+2 -16
View File
@@ -1,19 +1,5 @@
{ {
"source": "local", "source": "aur",
"auto_merge": true,
"release_ring": "fast", "release_ring": "fast",
"upstream": { "upstream_commit": "e323d0d1f8dea6b75bb651ce14acc73904cd0326"
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/stable/main/binary-amd64/Packages",
"package": "1password",
"sources": {
"x86_64": [
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz",
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz.sig"
],
"aarch64": [
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz",
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz.sig"
]
}
}
} }
+13 -40
View File
@@ -1,47 +1,27 @@
pkgname=1password pkgname=1password
pkgver=8.12.40
pkgrel=2 _tarver=8.12.34
_tar="1password-${_tarver}.x64.tar.gz"
pkgver=${_tarver//-/_}
pkgrel=34
conflicts=('1password-beta' '1password-beta-bin') conflicts=('1password-beta' '1password-beta-bin')
pkgdesc="Password manager and secure wallet" pkgdesc="Password manager and secure wallet"
arch=('x86_64' 'aarch64') arch=('x86_64')
url='https://1password.com' url='https://1password.com'
license=('LicenseRef-1Password-Proprietary') license=('LicenseRef-1Password-Proprietary')
options=(!strip) options=(!strip)
install="1password.install" install="1password.install"
source_x86_64=( source=(https://downloads.1password.com/linux/tar/stable/${CARCH}/${_tar}{,.sig})
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz" sha256sums=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13'
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz.sig" 'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7'
)
source_aarch64=(
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz"
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz.sig"
)
sha256sums_x86_64=(
'0ae9645d31be78a8fb57d15f49e5fa4f0b67a0b3608797a410e8fd36f0206266'
'2f777820dc2eb6e7b7b38f4557a897f97fb3259443261fdfb2008127bd975817'
)
sha256sums_aarch64=(
'7476c0fc8215338cd9f304b14822688010fd8ed54d5ea4367c0bbbf72845be1e'
'80412176560a3f76180f7c05ae7ebafe21fac764349cfeed71fe498ee25564ee'
) )
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22') validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
package() { package() {
depends=('hicolor-icon-theme' 'libgtk-3.so=0' 'nss' 'xdg-utils') depends=('hicolor-icon-theme' 'libgtk-3.so=0' 'nss' 'xdg-utils')
# 1Password names its tarballs by a vendor architecture suffix. arch=()
# above already limits which architectures makepkg builds, so no fallback
# branch is needed here; keeping this inside package() means sourcing the
# PKGBUILD without CARCH (as the version check does) still reads the
# version correctly.
local _tararch
case "$CARCH" in
x86_64) _tararch=x64 ;;
aarch64) _tararch=arm64 ;;
esac
# Go to source directory # Go to source directory
cd "1password-${pkgver}.${_tararch}" cd "1password-${_tarver}.x64"
# Install icons # Install icons
resolutions=(32x32 64x64 256x256 512x512) resolutions=(32x32 64x64 256x256 512x512)
@@ -51,14 +31,7 @@ package() {
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png" "${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
done done
# Install desktop file # Install desktop file
install -Dm0644 resources/com.onepassword.OnePassword.desktop \ install -Dm0644 resources/1password.desktop -t "${pkgdir}"/usr/share/applications/
"${pkgdir}/usr/share/applications/1password.desktop"
# 1Password reads the display scale itself, the way Electron apps do, and
# comes up oversized next to every other window on a scaled monitor. Pin it
# and let the compositor do the scaling.
sed -i 's|^Exec=.*|Exec=/opt/1Password/1password --force-device-scale-factor=1 %U|' \
"${pkgdir}/usr/share/applications/1password.desktop"
# Fill in policy kit file with a list of (the first 10) human users of the system. # Fill in policy kit file with a list of (the first 10) human users of the system.
export POLICY_OWNERS export POLICY_OWNERS
@@ -76,12 +49,12 @@ EOF" > ./com.1password.1Password.policy
# Move package contents to /opt/1Password # Move package contents to /opt/1Password
cd "${srcdir}" cd "${srcdir}"
install -dm0755 "${pkgdir}"/opt install -dm0755 "${pkgdir}"/opt
mv "1password-${pkgver}.${_tararch}" "${pkgdir}/opt/1Password" mv "1password-${_tarver}.x64" "${pkgdir}/opt/1Password"
# Cleanup un-needed files # Cleanup un-needed files
rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh
rm -r "${pkgdir}"/opt/1Password/resources/icons/ rm -r "${pkgdir}"/opt/1Password/resources/icons/
rm "${pkgdir}"/opt/1Password/resources/com.onepassword.OnePassword.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers rm "${pkgdir}"/opt/1Password/resources/1password.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
# Symlink /usr/bin executable to opt # Symlink /usr/bin executable to opt
install -dm0755 "${pkgdir}"/usr/bin install -dm0755 "${pkgdir}"/usr/bin
+2 -12
View File
@@ -1,15 +1,5 @@
{ {
"source": "local", "source": "aur",
"auto_merge": true,
"release_ring": "fast", "release_ring": "fast",
"upstream": { "upstream_commit": "04592d5713f9c09ea2d81cf4b8476714d80014bc"
"watch": {
"github": "omacom/aether",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "aether",
"commit": "0f047f40a200121075ca3cedce59ddf226f2a0f1"
}
} }
+9 -9
View File
@@ -1,18 +1,18 @@
# Maintainer: Bjarne Øverli <bjarne@oever.li> # Maintainer: Bjarne Øverli <bjarne@oever.li>
pkgname=aether pkgname=aether
pkgver=4.32.0 pkgver=4.29.6
pkgrel=2 pkgrel=1
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes' pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
url='https://github.com/omacom/aether' url='https://github.com/omacom-io/aether'
license=('MIT') license=('MIT')
depends=('webkit2gtk-4.1' 'gtk3') depends=('webkit2gtk-4.1' 'gtk3')
source=("aether-${pkgver}.tar.gz::https://github.com/omacom/aether/archive/refs/tags/v${pkgver}.tar.gz") source=("aether-${pkgver}.tar.gz::https://github.com/omacom-io/aether/archive/refs/tags/v${pkgver}.tar.gz")
source_x86_64=("aether-linux-amd64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-amd64") source_x86_64=("aether-linux-amd64-${pkgver}::https://github.com/omacom-io/aether/releases/download/v${pkgver}/aether-linux-amd64")
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-arm64") source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom-io/aether/releases/download/v${pkgver}/aether-linux-arm64")
sha256sums=('3234e5138a46699f8f47330ba582244d5063c8356ffaae1458323e4dda02be82') sha256sums=('8b2e97283007c9eefda879e17c5fecb9d59fc90bf2160af93eeba57f11fcdb25')
sha256sums_x86_64=('c705a4abef734b17ae37cdbff58a9a796517e6aa5f93ef18b68e3ee99d547ff9') sha256sums_x86_64=('47b5afa4144b3a3cd7755524956ffe6b074ed5f2f90dadda23e424efeaf88790')
sha256sums_aarch64=('f70195deba008204d58661c3ec1442a16e63f3358b4579969facb0e55dbf63c4') sha256sums_aarch64=('52d1ffb201970ddb6205882a8e1c583cae63470d4180f4bc2fc6298dfb71ddd9')
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}") noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
package() { package() {
@@ -1,6 +0,0 @@
{
"source": "local",
"channels": [
"edge"
]
}
@@ -1,36 +0,0 @@
From 375e80ec3826b54618fdd5f2db708c0f2bb936ae Mon Sep 17 00:00:00 2001
From: Marcelo Alcantara <maralc@gmail.com>
Date: Wed, 16 Sep 2026 00:39:04 +1000
Subject: [PATCH] drm: re-read possible CRTCs when rescanning connectors
A driver can change a connector's possible CRTCs at runtime. Apple's DCP
driver narrows a Type-C port's encoder to the display pipeline the fabric
routed it to and relies on the following hotplug for userspace to re-read
it. possibleCrtcs was only read when the connector was first created, so a
rerouted port kept a stale mask and was never assigned its now-free CRTC
until the compositor restarted.
---
src/backend/drm/DRM.cpp | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/backend/drm/DRM.cpp b/src/backend/drm/DRM.cpp
index 6618a26..b492dac 100644
--- a/src/backend/drm/DRM.cpp
+++ b/src/backend/drm/DRM.cpp
@@ -1287,6 +1287,13 @@ void Aquamarine::CDRMBackend::scanConnectors() {
} else {
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector id {} already initialized", connectorID));
conn = *it;
+
+ // drivers may narrow or widen these on hotplug, e.g. when a Type-C port is rerouted to another pipeline
+ const auto possibleCrtcs = drmModeConnectorGetPossibleCrtcs(gpu->fd, drmConn);
+ if (possibleCrtcs && possibleCrtcs != conn->possibleCrtcs) {
+ backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} possible CRTCs changed {:#x} -> {:#x}", conn->szName, conn->possibleCrtcs, possibleCrtcs));
+ conn->possibleCrtcs = possibleCrtcs;
+ }
}
conn->status = drmConn->connection;
--
2.50.1 (Apple Git-155)
-63
View File
@@ -1,63 +0,0 @@
# Maintainer: Caleb Maclennan <caleb@alerque.com>
# Contributor: Aaron Blasko <blaskoazzolaaaron [at] gmail.com>
#
# Arch's aquamarine 0.15.1-1 plus one patch, carried on edge for Apple Silicon:
# re-read a connector's possible CRTCs when rescanning, so a Type-C port the
# DCP fabric reroutes to another display pipeline gets a CRTC without
# restarting the compositor. Drop the carry once hyprwm releases the fix.
pkgname=aquamarine
pkgver=0.15.1
pkgrel=1.1
pkgdesc='a very light linux rendering backend library'
arch=(aarch64)
url="https://github.com/hyprwm/$pkgname"
license=(BSD-3-Clause)
depends=(libgcc
libstdc++
glibc # libc.so libm.so
hyprutils libhyprutils.so
libdisplay-info libdisplay-info.so
libdrm # libdrm.so
libglvnd libEGL.so
libinput # libinput.so
mesa # libgbm.so
opengl-driver
pixman
seatd libseat.so
systemd-libs libudev.so
wayland libwayland-client.so
wayland-protocols)
makedepends=(cmake
hyprwayland-scanner)
provides=("lib$pkgname.so")
_archive="$pkgname-$pkgver"
source=("$url/archive/v$pkgver/$_archive.tar.gz"
0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch)
sha256sums=('2f9de98c0bd1b7b1b09c576e390a2fef436449762fb334163c414f0c300296f2'
'50e9e38ff915b18074dc9b5dd1d07d7f24e340e99f254476d6abe578eece7864')
prepare() {
cd "$_archive"
patch -Np1 -i ../0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch
}
build() {
cd "$_archive"
# cc1plus needs more than 8 MiB of stack for DRM.cpp. GCC raises its own
# limit natively, but under QEMU user-mode emulation (the aarch64 builder)
# the guest stack is fixed at exec and setrlimit is ignored, so the compiler
# segfaults. QEMU reads this at exec; native builds ignore it. The object
# code is identical either way.
export QEMU_STACK_SIZE=64M
cmake -B build \
-D CMAKE_INSTALL_PREFIX=/usr \
-D CMAKE_BUILD_TYPE=Release
cmake --build build
}
package() {
cd "$_archive"
DESTDIR="$pkgdir" cmake --install build
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname" LICENSE
}
+1 -1
View File
@@ -4,7 +4,7 @@ epoch=1
pkgver=0.6.0 pkgver=0.6.0
pkgrel=2 pkgrel=2
pkgdesc="Control brightness on Apple Displays connected via USB-C" pkgdesc="Control brightness on Apple Displays connected via USB-C"
arch=('x86_64' 'aarch64') arch=('x86_64')
url="https://github.com/omakasui/asdcontrol" url="https://github.com/omakasui/asdcontrol"
license=('GPL2') license=('GPL2')
options=('!debug') options=('!debug')
+4
View File
@@ -0,0 +1,4 @@
[asusctl]
source = "git"
git = "https://gitlab.com/asus-linux/asusctl.git"
+4
View File
@@ -0,0 +1,4 @@
{
"source": "aur",
"upstream_commit": "ed336f7dbd5e8bee333aeaa6246da89241f1c8e6"
}
@@ -0,0 +1,18 @@
diff --git a/PKGBUILD b/PKGBUILD
index 55d0c22..8897aec 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -6,7 +6,7 @@ pkgname=(
asusctl
rog-control-center
)
-pkgver=6.3.8
+pkgver=6.4.0
pkgrel=1
pkgdesc="A control daemon, tools, and a collection of crates for interacting with ASUS ROG laptops"
arch=('x86_64')
@@ -28,2 +28,2 @@
-source=("git+https://gitlab.com/asus-linux/asusctl.git#tag=$pkgver")
-b2sums=('01269bc9fe63c1ce37568f9085c25dbcaaaa5cd9f51339d4c6904600d179ec826dace289d6e4e3501988fce5800da98e9cfa18b21c40da152d91c4f7fe821ffe')
+source=("git+https://github.com/opengamingcollective/asusctl.git#tag=$pkgver")
+b2sums=('0cfdc7f792fc6499cde9e4c9c6d82e484605e1b2878bb9f3a0e0696f5910b9ba7898f124fecd5fb3f39a0787f81e5163721606290099d757365dc3d6715a1bcf')
File renamed without changes.
File renamed without changes.
+108
View File
@@ -0,0 +1,108 @@
# Maintainer: Fabian Bornschein <fabiscafe@archlinux.org>
# Contributor: Static_Rocket
pkgbase=asusctl
pkgname=(
asusctl
rog-control-center
)
pkgver=6.4.0
pkgrel=1.1
pkgdesc="A control daemon, tools, and a collection of crates for interacting with ASUS ROG laptops"
arch=('x86_64')
url="https://asus-linux.org"
license=('MPL-2.0')
makedepends=(
clang
cmake
fontconfig
git
hicolor-icon-theme
libayatana-appindicator
libinput
libusb
rust
seatd
systemd
)
source=("git+https://github.com/opengamingcollective/asusctl.git#tag=$pkgver")
b2sums=('0cfdc7f792fc6499cde9e4c9c6d82e484605e1b2878bb9f3a0e0696f5910b9ba7898f124fecd5fb3f39a0787f81e5163721606290099d757365dc3d6715a1bcf')
prepare() {
cd "${pkgbase}"
# Keep rust/cargo build-dependency management inside the build directory
export CARGO_HOME="${srcdir}/cargo"
# Follow Rust package guidelines
## https://wiki.archlinux.org/title/Rust_package_guidelines
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
}
build() {
cd "${pkgbase}"
# Keep rust/cargo build-dependency management inside the build directory
export CARGO_HOME="${srcdir}/cargo"
# Follow Rust package guidelines
## https://wiki.archlinux.org/title/Rust_package_guidelines
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
make build
}
package_asusctl() {
pkgdesc="${pkgdesc/tools/CLI tools}"
depends=(
glibc
libgcc
libusb
systemd
systemd-libs
)
conflicts=(gnome-shell-extension-asusctl-gnome)
install=asusctl.install
optdepends=(
'acpi_call: fan control'
'asusctltray: tray profile switcher'
'rog-control-center: app to control asusctl'
'supergfxctl: hybrid GPU control'
)
cd "${pkgbase}"
export CARGO_HOME="${srcdir}/cargo"
make DESTDIR="${pkgdir}" \
install-asusctl \
install-asusd \
install-asusd_user \
install-asus-shutdown \
install-data-asusd \
install-data-asusd_user
}
package_rog-control-center() {
depends=(
asusctl
fontconfig
freetype2
glibc
hicolor-icon-theme
libayatana-appindicator
libgcc
libinput
libxkbcommon
mesa
seatd
systemd-libs
)
pkgdesc="App to control asusctl"
cd "${pkgbase}"
export CARGO_HOME="${srcdir}/cargo"
make DESTDIR="${pkgdir}" \
install-data-rog_gui \
install-rog_gui
}
@@ -20,11 +20,3 @@ path = [
] ]
SPDX-FileCopyrightText = "Arch Linux contributors" SPDX-FileCopyrightText = "Arch Linux contributors"
SPDX-License-Identifier = "0BSD" SPDX-License-Identifier = "0BSD"
[[annotations]]
path = [
"0001-Lower-severity-of-XID-collision-warnings.patch",
"0002-Stop-looking-for-modules-in-cwd.patch",
]
SPDX-FileCopyrightText = "gtk2 contributors"
SPDX-License-Identifier = "LGPL-2.1-or-later"
+19
View File
@@ -0,0 +1,19 @@
post_install() {
printf ":: asusd provides a service that is activated by an udev rule on\n"
printf ":: startup. Please reboot the system or run\n"
printf ":: # systemctl start asusd.service\n"
printf ":: to make it work.\n"
printf ":: See https://gitlab.com/asus-linux/asusctl#kernel-support.\n"
printf ":: for latest required kernel patches/versions\n"
}
post_upgrade() {
if systemctl is-active asusd.service --quiet
then
printf ":: asusd service will be restarted…\n"
systemctl daemon-reload
systemctl restart asusd.service
fi
printf ":: See https://gitlab.com/asus-linux/asusctl#kernel-support.\n"
printf ":: for latest required kernel patches/versions\n"
}
-9
View File
@@ -1,9 +0,0 @@
{
"source": "local",
"upstream": {
"watch": {
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)",
"git_tags": "https://github.com/AsahiLinux/avd-fw.git"
}
}
}
-51
View File
@@ -1,51 +0,0 @@
# Open replacement firmware for the Apple Video Decoder.
# Built from AsahiLinux sources, without extracting proprietary firmware.
pkgname=avd-fw
pkgver=0.1
pkgrel=1
pkgdesc='Open replacement firmware for the Apple Video Decoder (AVD) on Apple Silicon'
# This recipe uses the native ARM linker and serves Apple Silicon systems.
# Restrict both builds and publication to aarch64.
arch=('aarch64')
url='https://github.com/AsahiLinux/avd-fw'
license=('MIT')
# clang cross-compiles to arm-none-eabi out of the box, so no arm-none-eabi
# toolchain is required; llvm supplies llvm-objcopy, which meson.build looks
# up by name to turn each linked ELF into a padded raw image.
makedepends=('meson' 'clang' 'llvm')
# !buildflags is load-bearing: makepkg's CFLAGS/LDFLAGS target the aarch64 host
# and would be injected into a bare-metal Cortex-M3 build. !strip keeps makepkg
# from running the host strip over raw firmware images.
options=('!strip' '!debug' '!buildflags' '!lto')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('2e131244275cb15c94243e41eec8a2a665ec895cfe3a818181549db991e62c67')
build() {
# The cross file selects clang, pins the host machine to cortex-m3 and sets
# libdir=lib, which lands the images in /usr/lib/firmware/apple.
#
# Optimization is pinned to match upstream's Makefile (-O2). Meson's default
# buildtype is "debug" (-O0 -g), which would ship unoptimized firmware; the
# MMIO accessors in src/util.c go through a volatile typedef, so -O2 cannot
# elide register reads or writes.
meson setup \
--cross-file "$pkgname-$pkgver/llvm.ini" \
--prefix=/usr \
-Doptimization=2 \
-Ddebug=false \
"$pkgname-$pkgver" build
meson compile -C build
}
package() {
meson install -C build --destdir "$pkgdir"
# meson installs custom_target outputs 0755; these are firmware blobs, not
# programs, and every other firmware file on the system is 0644.
chmod 644 "$pkgdir"/usr/lib/firmware/apple/*.bin
install -Dm644 "$pkgname-$pkgver/LICENSE" \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
@@ -1,17 +0,0 @@
{
"source": "local",
"release_ring": "fast",
"origin": {
"aur": "bambustudio-bin",
"commit": "b962c12d14873f94669e0e256a444f957b1843cd"
},
"upstream": {
"watch": {
"regex": "https://api.github.com/repos/bambulab/BambuStudio/releases/latest",
"pattern": "\"name\"\\s*:\\s*\"BambuStudio_ubuntu24\\.04-v(?P<version>[0-9]+(?:\\.[0-9]+)*)-(?P<build>[0-9]+)\\.AppImage\"",
"variables": {
"_build": "{build}"
}
}
}
}
@@ -1,12 +0,0 @@
[Desktop Entry]
Name=Bambu Studio
GenericName=3D Printing Software
Comment=Slicer for Bambu Lab and other 3D printers
Exec=/usr/bin/bambu-studio %U
Icon=BambuStudio
Terminal=false
Type=Application
Categories=Graphics;3DGraphics;Engineering;
MimeType=x-scheme-handler/bambustudio;x-scheme-handler/bambustudioopen;model/stl;model/3mf;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;
Keywords=3D;Printing;Slicer;gcode;stl;3mf;
StartupWMClass=bambu-studio
-48
View File
@@ -1,48 +0,0 @@
# Maintainer: goll <adrian.goll+aur[at]gmail>
# Contributor: George Woodall <georgewoodall82@gmail.com>
pkgname=bambustudio-bin
pkgver=02.08.02.61
pkgrel=1
pkgdesc="PC Software for BambuLab's 3D printers"
arch=("x86_64")
url="https://github.com/bambulab/BambuStudio"
license=('AGPL-3.0-only')
conflicts=('bambustudio' 'bambustudio-git')
depends=('cairo' 'dbus' 'fontconfig' 'gcc-libs' 'glib2' 'glibc'
'gst-libav' 'gst-plugins-base-libs' 'gstreamer' 'gtk3' 'libglvnd'
'libx11' 'mesa' 'pango' 'wayland' 'webkit2gtk-4.1')
makedepends=('7zip')
options=('!strip' '!debug')
# The upstream watch updates the timestamp together with pkgver.
_build=20260820225108
source=("bambustudio-${pkgver}.AppImage::https://github.com/bambulab/BambuStudio/releases/download/v${pkgver}/BambuStudio_ubuntu24.04-v${pkgver}-${_build}.AppImage"
"BambuStudio.desktop"
"bambu-studio")
noextract=("bambustudio-${pkgver}.AppImage")
sha256sums=(
'd501b103fac5424513ec0e8d6bc145fb30719de2c7d94d7320d723740c81a7fd'
'f10718a8b201cad64800746fe8167ccc032c545d05f7ad8caa99eb5fb975f2a1'
'a3a5c8f6a8b287e42b93957e9602621923c766e0b6a3f10c14eca10b023b15f2'
)
prepare() {
# Read the embedded SquashFS without executing or modifying the AppImage.
rm -rf "$srcdir/squashfs-root"
7z x "$srcdir/bambustudio-${pkgver}.AppImage" -o"$srcdir/squashfs-root" >/dev/null
}
package() {
cd "$srcdir/squashfs-root"
install -Dm755 AppRun "$pkgdir/opt/$pkgname/AppRun"
cp -a bin resources "$pkgdir/opt/$pkgname/"
local icon size
for icon in usr/share/icons/hicolor/*/apps/BambuStudio.png; do
size="${icon#usr/share/icons/hicolor/}"
install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size"
done
install -Dm755 "$srcdir/bambu-studio" "$pkgdir/usr/bin/bambu-studio"
install -Dm644 "$srcdir/BambuStudio.desktop" \
"$pkgdir/usr/share/applications/BambuStudio.desktop"
}
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
exec "/opt/bambustudio-bin/AppRun" "$@"
+2 -12
View File
@@ -1,15 +1,5 @@
{ {
"source": "local", "source": "aur",
"auto_merge": true,
"release_ring": "fast", "release_ring": "fast",
"upstream": { "upstream_commit": "fe2a6345e271f618dc3adf31e48557a717866395"
"watch": {
"github": "basecamp/basecamp-cli",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "basecamp-cli",
"commit": "89c4eb5a0ac9ffe98aecd4ea8b789cf1fee42bf1"
}
} }
+5 -5
View File
@@ -1,6 +1,6 @@
# Maintainer: Basecamp <support@basecamp.com> # Maintainer: Basecamp <support@basecamp.com>
pkgname=basecamp-cli pkgname=basecamp-cli
pkgver=0.13.0 pkgver=0.9.1
pkgrel=1 pkgrel=1
pkgdesc="CLI for Basecamp project management" pkgdesc="CLI for Basecamp project management"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -13,10 +13,10 @@ optdepends=(
'zsh: for zsh shell completions' 'zsh: for zsh shell completions'
'fish: for fish shell completions' 'fish: for fish shell completions'
) )
source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_amd64.tar.gz") source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.9.1/basecamp_${pkgver}_linux_amd64.tar.gz")
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_arm64.tar.gz") source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.9.1/basecamp_${pkgver}_linux_arm64.tar.gz")
sha256sums_x86_64=('8b33a93018c165f82ac7050d83cad8cde509f9cb7cfbccbc48fb9d98c0649931') sha256sums_x86_64=('bcb1fa3a03e702bbf81a3004ded4bc7808605e41106cbc768ba4006b89e0db2f')
sha256sums_aarch64=('bef69d622d66685899b4f955b255951bb588cf1aac54f990b6bd818ae551104c') sha256sums_aarch64=('ff50313024b6ca14e40146e30e1c9c9c00a4e4103fe55615eeee3e7c359d88d1')
package() { package() {
install -Dm755 "basecamp" "${pkgdir}/usr/bin/basecamp" install -Dm755 "basecamp" "${pkgdir}/usr/bin/basecamp"
-48
View File
@@ -1,48 +0,0 @@
# Maintainer: Ryan Hughes <ryan@omarchy.org>
# Contributor: Bart De Vries <bart at mogwai dot be>
# Contributor: Dan Johansen <strit@manjaro.org>
#
# Runs x86_64-only Linux programs (dropbox) on AArch64. Pinned past v0.4.5-1:
# that release crashes in its glib wrapper as soon as Dropbox starts its tray
# icon. Move to a release tag once one includes the pinned commit.
pkgname=box64
pkgver=0.4.5.1.r309.gd58d619
pkgrel=1
_commit=d58d619e84e03282326e8a26c2cbfe749931b5f8
pkgdesc='Linux userspace x86_64 emulator with native library wrapping'
arch=('aarch64')
url='https://github.com/ptitSeb/box64'
license=('MIT')
depends=('gcc-libs' 'glibc')
makedepends=('cmake' 'python')
backup=('etc/box64.box64rc')
options=('!strip' '!emptydirs')
source=("$pkgname-$_commit.tar.gz::$url/archive/$_commit.tar.gz")
sha256sums=('f7615a3c4ac5412a983a3fd26be79311e6e2702884d8bdfde954b23bad39fff8')
build() {
# ARM64 is box64's generic AArch64 profile, so the binary does not depend
# on the CPU of the machine that built it.
cmake -S "$pkgname-$_commit" -B build \
-DARM64=ON \
-DARM_DYNAREC=ON \
-DNOGIT=ON \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr
cmake --build build --parallel
}
package() {
DESTDIR="$pkgdir" cmake --install build
# Packages call box64 explicitly. A binfmt handler would compete with
# qemu-user-static-binfmt for every x86_64 executable on the system.
rm -r "$pkgdir/etc/binfmt.d"
# The Qt rcfile editor needs PySide, and its launcher entry would show up
# on every system that installs box64 only as a dependency.
rm "$pkgdir/usr/bin/box64-configurator" \
"$pkgdir/usr/share/applications/box64-configurator.desktop"
install -Dm644 "$pkgname-$_commit/LICENSE" -t "$pkgdir/usr/share/licenses/$pkgname"
}
-16
View File
@@ -1,16 +0,0 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
"debian": "https://brave-browser-apt-release.s3.brave.com/dists/stable/main/binary-amd64/Packages",
"package": "brave-browser",
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "brave-bin",
"commit": "d86e5479e163dac19d7ace3de472710e6eb06eea"
}
}
-62
View File
@@ -1,62 +0,0 @@
# Maintainer: brave <aur-release@brave.com>
# Contributor: Caleb Maclennan <caleb@alerque.com>
# Contributor: José Miguel Sarasola <jmsaraur@gmail.com>
# Contributor: Như Bảo Trương <28810481+nhubaotruong@users.noreply.github.com>
# Contributor: Andrés Rodríguez <hello@andres.codes>
# Contributor: Jacob Mischka <jacob@mischka.me>
# Contributor: Manuel Mazzuola <origin.of@gmail.com>
# Contributor: Simón Oroño <simonorono@protonmail.com>
# Contributor: now-im <now im 627 @ gmail . com>
# Contributor: Giusy Digital <kurmikon at libero dot it>
pkgname=brave-bin
pkgver=1.97.56
pkgrel=1
epoch=1
pkgdesc='Web browser that blocks ads and trackers by default (binary release)'
arch=(x86_64 aarch64)
url=https://brave.com
license=(MPL2 BSD custom:chromium)
depends=(alsa-lib
gtk3
libxss
nss
ttf-font)
optdepends=('cups: Printer support'
'libgnome-keyring: Enable GNOME keyring support'
'libnotify: Native notification support')
provides=("${pkgname%-bin}=$pkgver" 'brave-browser')
conflicts=("${pkgname%-bin}")
options=(!strip)
source=($pkgname.sh brave-browser.desktop)
source_x86_64=(${pkgname}-${pkgver}-x86_64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-browser-${pkgver}-linux-amd64.zip)
source_aarch64=(${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-browser-${pkgver}-linux-arm64.zip)
noextract=(${pkgname}-${pkgver}-x86_64.zip ${pkgname}-${pkgver}-aarch64.zip)
sha256sums=('75a87dd17b42fcc6f27adfd16c82bed1c08e9251b07d2012f8d49f7412fa1d00'
'c07276b69c7304981525ecb022f92daf7ae125a4fb05ac3442157b50826e257a')
sha256sums_x86_64=('8acac76eae35942e30cc63e05adc772f632ad87be8a5ffb3d8a6e280380584ec')
sha256sums_aarch64=('f18823bfe8bf626a7412a2cc142ba851f991665ca31af8d750f80dbb8371f984')
prepare() {
mkdir -p brave
bsdtar -xf "$pkgname-$pkgver-$CARCH.zip" -C brave
chmod +x brave/brave
}
package() {
install -dm0755 "$pkgdir/opt"
cp -a brave "$pkgdir/opt/$pkgname"
# allow firejail users to get the suid sandbox working
chmod 4755 "$pkgdir/opt/brave-bin/chrome-sandbox"
install -Dm0755 "$pkgname.sh" "$pkgdir/usr/bin/brave"
install -Dm0644 -t "$pkgdir/usr/share/applications/" "brave-browser.desktop"
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" brave/LICENSE
pushd "$pkgdir/usr/"
for size in 16x16 24x24 32x32 48x48 64x64 128x128 256x256; do
install -Dm0644 "$pkgdir/opt/$pkgname/product_logo_${size/x*/}.png" \
"share/icons/hicolor/$size/apps/brave-desktop.png"
done
}
-25
View File
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-"${HOME}/.config"}"
CONF_FILE="${XDG_CONFIG_HOME}/brave-flags.conf"
if
test -f "${CONF_FILE}"
then
mapfile -t CONF_LIST < "${CONF_FILE}"
fi
for CONF_LINE in "${CONF_LIST[@]}"
do
if ! [[
"${CONF_LINE}" =~ ^[[:space:]]*(#|$)
]]
then
FLAG_LIST+=("${CONF_LINE}")
fi
done
export CHROME_VERSION_EXTRA='stable'
exec /opt/brave-bin/brave "${FLAG_LIST[@]}" "${@}"
-224
View File
@@ -1,224 +0,0 @@
[Desktop Entry]
Version=1.0
Name=Brave
# Only KDE 4 seems to use GenericName, so we reuse the KDE strings.
# From Ubuntu's language-pack-kde-XX-base packages, version 9.04-20090413.
GenericName=Web Browser
GenericName[ar]=متصفح الشبكة
GenericName[bg]=Уеб браузър
GenericName[ca]=Navegador web
GenericName[cs]=WWW prohlížeč
GenericName[da]=Browser
GenericName[de]=Web-Browser
GenericName[el]=Περιηγητής ιστού
GenericName[en_GB]=Web Browser
GenericName[es]=Navegador web
GenericName[et]=Veebibrauser
GenericName[fi]=WWW-selain
GenericName[fr]=Navigateur Web
GenericName[gu]=વેબ બ્રાઉઝર
GenericName[he]=דפדפן אינטרנט
GenericName[hi]=वेब ब्राउज़र
GenericName[hu]=Webböngésző
GenericName[it]=Browser Web
GenericName[ja]=ウェブブラウザ
GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ
GenericName[ko]=웹 브라우저
GenericName[lt]=Žiniatinklio naršyklė
GenericName[lv]=Tīmekļa pārlūks
GenericName[ml]=വെബ് ബ്രൌസര്‍
GenericName[mr]=वेब ब्राऊजर
GenericName[nb]=Nettleser
GenericName[nl]=Webbrowser
GenericName[pl]=Przeglądarka WWW
GenericName[pt]=Navegador Web
GenericName[pt_BR]=Navegador da Internet
GenericName[ro]=Navigator de Internet
GenericName[ru]=Веб-браузер
GenericName[sl]=Spletni brskalnik
GenericName[sv]=Webbläsare
GenericName[ta]=இணைய உலாவி
GenericName[th]=เว็บเบราว์เซอร์
GenericName[tr]=Web Tarayıcı
GenericName[uk]=Навігатор Тенет
GenericName[zh_CN]=网页浏览器
GenericName[zh_HK]=網頁瀏覽器
GenericName[zh_TW]=網頁瀏覽器
# Not translated in KDE, from Epiphany 2.26.1-0ubuntu1.
GenericName[bn]=ওয়েব ব্রাউজার
GenericName[fil]=Web Browser
GenericName[hr]=Web preglednik
GenericName[id]=Browser Web
GenericName[or]=ଓ୍ବେବ ବ୍ରାଉଜର
GenericName[sk]=WWW prehliadač
GenericName[sr]=Интернет прегледник
GenericName[te]=మహాతల అన్వేషి
GenericName[vi]=Bộ duyệt Web
# Gnome and KDE 3 uses Comment.
Comment=Access the Internet
Comment[ar]=الدخول إلى الإنترنت
Comment[bg]=Достъп до интернет
Comment[bn]=ইন্টারনেটটি অ্যাক্সেস করুন
Comment[ca]=Accedeix a Internet
Comment[cs]=Přístup k internetu
Comment[da]=Få adgang til internettet
Comment[de]=Internetzugriff
Comment[el]=Πρόσβαση στο Διαδίκτυο
Comment[en_GB]=Access the Internet
Comment[es]=Accede a Internet.
Comment[et]=Pääs Internetti
Comment[fi]=Käytä internetiä
Comment[fil]=I-access ang Internet
Comment[fr]=Accéder à Internet
Comment[gu]=ઇંટરનેટ ઍક્સેસ કરો
Comment[he]=גישה אל האינטרנט
Comment[hi]=इंटरनेट तक पहुंच स्थापित करें
Comment[hr]=Pristup Internetu
Comment[hu]=Internetelérés
Comment[id]=Akses Internet
Comment[it]=Accesso a Internet
Comment[ja]=インターネットにアクセス
Comment[kn]=ಇಂಟರ್ನೆಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಿ
Comment[ko]=인터넷 연결
Comment[lt]=Interneto prieiga
Comment[lv]=Piekļūt internetam
Comment[ml]=ഇന്റര്‍‌നെറ്റ് ആക്‌സസ് ചെയ്യുക
Comment[mr]=इंटरनेटमध्ये प्रवेश करा
Comment[nb]=Gå til Internett
Comment[nl]=Verbinding maken met internet
Comment[or]=ଇଣ୍ଟର୍ନେଟ୍ ପ୍ରବେଶ କରନ୍ତୁ
Comment[pl]=Skorzystaj z internetu
Comment[pt]=Aceder à Internet
Comment[pt_BR]=Acessar a internet
Comment[ro]=Accesaţi Internetul
Comment[ru]=Доступ в Интернет
Comment[sk]=Prístup do siete Internet
Comment[sl]=Dostop do interneta
Comment[sr]=Приступите Интернету
Comment[sv]=Gå ut på Internet
Comment[ta]=இணையத்தை அணுகுதல்
Comment[te]=ఇంటర్నెట్‌ను ఆక్సెస్ చెయ్యండి
Comment[th]=เข้าถึงอินเทอร์เน็ต
Comment[tr]=İnternet'e erişin
Comment[uk]=Доступ до Інтернету
Comment[vi]=Truy cập Internet
Comment[zh_CN]=访问互联网
Comment[zh_HK]=連線到網際網路
Comment[zh_TW]=連線到網際網路
StartupNotify=true
StartupWMClass=brave-browser
TryExec=brave
Exec=brave %U
Terminal=false
Icon=brave-desktop
Type=Application
Categories=Network;WebBrowser;
MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ipfs;x-scheme-handler/ipns;
Actions=new-window;new-private-window;
[Desktop Action new-window]
Name=New Window
Name[am]=አዲስ መስኮት
Name[ar]=نافذة جديدة
Name[bg]=Нов прозорец
Name[bn]=নতুন উইন্ডো
Name[ca]=Finestra nova
Name[cs]=Nové okno
Name[da]=Nyt vindue
Name[de]=Neues Fenster
Name[el]=Νέο Παράθυρο
Name[en_GB]=New Window
Name[es]=Nueva ventana
Name[et]=Uus aken
Name[fa]=پنجره جدید
Name[fi]=Uusi ikkuna
Name[fil]=New Window
Name[fr]=Nouvelle fenêtre
Name[gu]=નવી વિંડો
Name[hi]=नई विंडो
Name[hr]=Novi prozor
Name[hu]=Új ablak
Name[id]=Jendela Baru
Name[it]=Nuova finestra
Name[iw]=חלון חדש
Name[ja]=新規ウインドウ
Name[kn]=ಹೊಸ ವಿಂಡೊ
Name[ko]=새 창
Name[lt]=Naujas langas
Name[lv]=Jauns logs
Name[ml]=പുതിയ വിന്‍ഡോ
Name[mr]=नवीन विंडो
Name[nl]=Nieuw venster
Name[no]=Nytt vindu
Name[pl]=Nowe okno
Name[pt]=Nova janela
Name[pt_BR]=Nova janela
Name[ro]=Fereastră nouă
Name[ru]=Новое окно
Name[sk]=Nové okno
Name[sl]=Novo okno
Name[sr]=Нови прозор
Name[sv]=Nytt fönster
Name[sw]=Dirisha Jipya
Name[ta]=புதிய சாளரம்
Name[te]=క్రొత్త విండో
Name[th]=หน้าต่างใหม่
Name[tr]=Yeni Pencere
Name[uk]=Нове вікно
Name[vi]=Cửa sổ Mới
Name[zh_CN]=新建窗口
Name[zh_TW]=開新視窗
Exec=brave
[Desktop Action new-private-window]
Name=New Incognito Window
Name[ar]=نافذة جديدة للتصفح المتخفي
Name[bg]=Нов прозорец „инкогнито“
Name[bn]=নতুন ছদ্মবেশী উইন্ডো
Name[ca]=Finestra d'incògnit nova
Name[cs]=Nové anonymní okno
Name[da]=Nyt inkognitovindue
Name[de]=Neues Inkognito-Fenster
Name[el]=Νέο παράθυρο για ανώνυμη περιήγηση
Name[en_GB]=New Incognito window
Name[es]=Nueva ventana de incógnito
Name[et]=Uus inkognito aken
Name[fa]=پنجره جدید حالت ناشناس
Name[fi]=Uusi incognito-ikkuna
Name[fil]=Bagong Incognito window
Name[fr]=Nouvelle fenêtre de navigation privée
Name[gu]=નવી છુપી વિંડો
Name[hi]=नई गुप्त विंडो
Name[hr]=Novi anoniman prozor
Name[hu]=Új Inkognitóablak
Name[id]=Jendela Penyamaran baru
Name[it]=Nuova finestra di navigazione in incognito
Name[iw]=חלון חדש לגלישה בסתר
Name[ja]=新しいシークレット ウィンドウ
Name[kn]=ಹೊಸ ಅಜ್ಞಾತ ವಿಂಡೋ
Name[ko]=새 시크릿 창
Name[lt]=Naujas inkognito langas
Name[lv]=Jauns inkognito režīma logs
Name[ml]=പുതിയ വേഷ പ്രച്ഛന്ന വിന്‍ഡോ
Name[mr]=नवीन गुप्त विंडो
Name[nl]=Nieuw incognitovenster
Name[no]=Nytt inkognitovindu
Name[pl]=Nowe okno incognito
Name[pt]=Nova janela de navegação anónima
Name[pt_BR]=Nova janela anônima
Name[ro]=Fereastră nouă incognito
Name[ru]=Новое окно в режиме инкогнито
Name[sk]=Nové okno inkognito
Name[sl]=Novo okno brez beleženja zgodovine
Name[sr]=Нови прозор за прегледање без архивирања
Name[sv]=Nytt inkognitofönster
Name[ta]=புதிய மறைநிலைச் சாளரம்
Name[te]=క్రొత్త అజ్ఞాత విండో
Name[th]=หน้าต่างใหม่ที่ไม่ระบุตัวตน
Name[tr]=Yeni Gizli pencere
Name[uk]=Нове вікно в режимі анонімного перегляду
Name[vi]=Cửa sổ ẩn danh mới
Name[zh_CN]=新建隐身窗口
Name[zh_TW]=新增無痕式視窗
Exec=brave --incognito
MimeType=x-scheme-handler/unknown;x-scheme-handler/about;text/html;text/xml;application/xhtml_xml;image/webp;x-scheme-handler/http;x-scheme-handler/https;
@@ -1,16 +0,0 @@
{
"source": "local",
"auto_merge": true,
"release_ring": "fast",
"upstream": {
"watch": {
"debian": "https://brave-browser-apt-release.s3.brave.com/dists/stable/main/binary-amd64/Packages",
"package": "brave-origin",
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "brave-origin-bin",
"commit": "f2daecf7b7576cd445094ed09c3c783619ca0eb2"
}
}
Loaded 100 of 1592 files, more files were not shown because too many files have changed in this diff. Show more