Compare commits

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
1560 changed files with 3615 additions and 439455 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.');
};
-34
View File
@@ -1,34 +0,0 @@
const approvePrWorkflows = require('./approve-pr-workflows.cjs');
const BOT = 'github-actions[bot]';
// A sync workflow pushes its branch with GITHUB_TOKEN. GitHub holds the
// resulting pull_request runs for approval and, unlike a person's push,
// creates no pull_request_target run, so approve-pr.yml never sees it. The
// sync workflow therefore releases the runs for the commit it just pushed,
// under the same rule approve-pr.yml applies: only while build-approved is
// on the PR. The sync applies that label itself, so its pushes build without
// a maintainer. It acts only on its own bot-authored, same-repository PR for
// the branch and commit it pushed.
module.exports = async function approveSyncPush({ github, context, core,
number, branch, headSha, since, approve = approvePrWorkflows, ...options }) {
if (!Number.isInteger(number) || !branch || !headSha || !since) {
throw new Error('Missing sync PR number, branch, head SHA or push time.');
}
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
const repository = `${context.repo.owner}/${context.repo.repo}`;
if (pr.user?.login !== BOT || pr.head.repo?.full_name !== repository ||
pr.base.repo?.full_name !== repository || pr.head.ref !== branch) {
throw new Error(`PR #${number} is not ${BOT}'s ${branch} PR in ${repository}; refusing to approve.`);
}
if (pr.state !== 'open' || pr.head.sha !== headSha) {
core.info(`PR #${number} is closed or has moved past ${headSha}; nothing to approve.`);
return;
}
if (!pr.labels.some(label => label.name === 'build-approved')) {
core.info(`PR #${number} has no build-approved label; its runs stay held.`);
return;
}
await approve({ github, context, core, vouchStatus: 'bot', pullRequest: pr,
action: 'synchronize', since, ...options });
};
-84
View File
@@ -1,84 +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.
// - Not the upstream sync. It labels its own PR build-approved to release
// GitHub's hold on its pushes, which is no one's approval; it stays on the
// reviewed lane.
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;
}
const REVIEWED_BRANCHES = /^auto\/sync-upstream(\/|$)/;
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 (pr.head.repo?.full_name === repository && REVIEWED_BRANCHES.test(pr.head.ref)) {
return { enable: false, reason: `${pr.head.ref} stays on the reviewed lane` };
}
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;
-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
-319
View File
@@ -1,319 +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 sync bots'
# branches (auto/sync-*) 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.
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-')) }}
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"
-455
View File
@@ -1,455 +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
concurrency:
group: publish
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Every matrix entry, as a file the shell steps can loop over:
# package arch channels publish_arches
- name: Plan
run: |
jq -r '.include[] | "\(.package) \(.arch) \(.channels|gsub(" ";",")) \(.publish_arches|gsub(" ";","))"' \
<<'EOF_MATRIX' > plan.txt
${{ needs.changes.outputs.matrix }}
EOF_MATRIX
cat plan.txt
# Fetch each package's artifact into build-output/edge/<arch>/: the PR's,
# or the rebuild job's when the PR's had expired. An artifact
# carries its package files inside packages.tar (see build-pr.yml and
# helpers/artifact-helpers.sh: the upload action rejects the colon in
# an epoch filename).
- name: Collect artifacts
env:
GH_TOKEN: ${{ github.token }}
CONTAINER_ENGINE: docker
run: |
set -uo pipefail
source helpers/artifact-helpers.sh
# sources.jsonl: where each package's files came from, or that the
# build failed. A failed build ends the run before any publish, and
# the record says so instead of the report job finding nothing.
: > sources.jsonl
failed=0
while read -r package arch channels publish_arches; do
hash=$(git rev-parse "HEAD:pkgbuilds/$package")
label="$package-$arch-$hash"
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
| jq -r '[.artifacts[] | select(.expired|not)] | sort_by(.created_at) | last | select(. != null) | "\(.archive_download_url) \(.workflow_run.id)"')
read -r found from_run <<<"$found" || true
mkdir -p "build-output/edge/$arch"
if [[ -n "$found" ]]; then
if [[ $from_run == "${{ github.run_id }}" ]]; then
kind=rebuild
echo "==> $label: artifact from this run's $arch rebuild"
else
kind=pr-artifact
echo "==> $label: reusing the build artifact from run $from_run"
fi
rm -rf /tmp/artifact; mkdir -p /tmp/artifact
if curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/artifact.zip "$found" \
&& unzip -oq /tmp/artifact.zip -d /tmp/artifact \
&& unpack_packages /tmp/artifact "build-output/edge/$arch"; then
jq -nc --arg p "$package" --arg a "$arch" --arg s "$kind" '{package:$p, arch:$a, source:$s}' >> sources.jsonl
else
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"artifact-download-failed"}' >> sources.jsonl; failed=1; break
fi
else
# bin/build plans against the public channel first. If the
# channel already holds master's version there is nothing to
# build and nothing to publish: a re-run for a package that
# turned out to be fine. Record it and move on.
plan=$(CONTAINER_ENGINE=docker bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
# "Packages that would build:" followed by nothing means none.
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
echo "==> $label: already published at master's version, nothing to do"
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"already-published"}' >> sources.jsonl
continue
fi
# Nothing builds here. No artifact means the rebuild failed (see
# the rebuild job), or an artifact expired between planning and
# now (re-run all jobs).
echo "::error::$label: no artifact from the rebuild job"
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"build-failed"}' >> sources.jsonl; failed=1; break
fi
done < plan.txt
ls -1 build-output/edge/*/*.pkg.tar.zst 2>/dev/null || true
if (( failed )); then
# Write the record now; the publish step will not run.
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
> publish-record.json
cat publish-record.json
exit 1
fi
- name: Publish
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
# The token is scoped to the bucket; it may not CreateBucket, and
# rclone's existence check is a CreateBucket in disguise.
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }}
# repo-add, gpg and bsdtar are Arch tools; run the publish inside the
# builder image (host-native, edge) with the workspace mounted.
run: |
set -euo pipefail
if ! compgen -G "build-output/edge/*/*.pkg.tar.zst" >/dev/null; then
echo "Nothing to publish: every requested package is already published at master's version."
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
> publish-record.json
cat publish-record.json
exit 0
fi
# A fresh runner has no images, and building this one here cost
# every publish about 100 s (and 20 s more to start a container
# from it) for the 14 s of signing and upload it is needed for.
# builder-images.yml already publishes the tested image for exactly
# these build inputs under their key; pull that. Build only when no
# image carries the key: a merge that changed build/ publishes
# before the refresh it triggered has finished.
builder=omarchy-pkg-builder:latest-x86_64-edge
if ! docker image inspect "$builder" >/dev/null 2>&1; then
key=$(bin/builder-image key --arch x86_64 --mirror edge)
published="ghcr.io/omacom/omarchy-pkg-builder:$key"
if docker pull --quiet "$published" &&
[[ $(docker image inspect "$published" --format '{{index .Config.Labels "org.omarchy.builder.key"}}') == "$key" ]]; then
docker tag "$published" "$builder"
echo "==> Builder image: pulled $published"
else
echo "==> Builder image: none published for $key, building it"
docker buildx build --load -t "$builder" --build-arg MIRROR=edge build
fi
fi
# Group the merge's files by the (channel, architecture) slot each
# belongs to. A package's files live under build-output/edge/<built
# arch>/ and are named <pkgname>-<ver>-<arch|any>.pkg.tar.zst; a
# split package's outputs share the pkgbase's directory, so match
# on the artifact list rather than the name.
# pkgbase is read inside the builder image: the Ubuntu host has no
# bsdtar. One container call maps every file to its pkgbase.
docker run --rm -v "$PWD:/w:ro" -w /w omarchy-pkg-builder:latest-x86_64-edge bash -c '
for f in build-output/edge/*/*.pkg.tar.zst; do
printf "%s %s\n" "$f" "$(bsdtar -xOf "$f" .PKGINFO | awk -F" = " "\$1==\"pkgbase\"{print \$2}")"
done' > pkgbase.txt
declare -A slot_files=()
while read -r package arch channels publish_arches; do
for f in build-output/edge/"$arch"/*.pkg.tar.zst; do
# Only files this package produced (its PKGINFO pkgbase).
[[ $(awk -v f="$f" '$1==f{print $2}' pkgbase.txt) == "$package" ]] || continue
for mirror in ${channels//,/ }; do
for parch in ${publish_arches//,/ }; do
slot_files["$mirror/$parch"]+="$f "
done
done
done
done < plan.txt
# Deterministic slot order: edge before rc before stable, x86_64
# before aarch64, so a failure leaves the earlier rings consistent.
# Every slot's outcome goes into publish-record.json for the report
# job: what was published, where, from which artifact, and whether
# the slot succeeded. A failing slot stops the loop (set -e) but the
# record still shows everything before it landed.
: > slots.jsonl
record_slot() { jq -nc --arg m "$1" --arg a "$2" --arg st "$3" --arg files "$4" \
'{mirror:$m, arch:$a, status:$st, packages:[$files | split(" ") | .[] | select(length>0) | sub(".*/";"") | sub("\\.pkg\\.tar\\.zst$";"")]}' >> slots.jsonl; }
status=0
for mirror in edge rc stable; do
for parch in x86_64 aarch64; do
files=${slot_files["$mirror/$parch"]:-}
[[ -n "$files" ]] || continue
echo "==> $mirror/$parch: $files"
if docker run --rm \
-e GPG_PRIVATE_KEY -e GPG_PASSPHRASE -e OMARCHY_PUBLISH_PREFIX \
-e RCLONE_CONFIG_R2_TYPE -e RCLONE_CONFIG_R2_PROVIDER -e RCLONE_CONFIG_R2_ENDPOINT -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET \
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY \
-v "$PWD:/w:ro" -w /w \
omarchy-pkg-builder:latest-x86_64-edge \
bin/publish-artifact --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$parch" $files; then
record_slot "$mirror" "$parch" published "$files"
else
record_slot "$mirror" "$parch" failed "$files"
status=1
break 2
fi
done
done
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
--slurpfile slots slots.jsonl --slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:$slots}' \
> publish-record.json
cat publish-record.json
exit $status
- name: Keep the publish record
if: always()
uses: actions/upload-artifact@v4
with:
name: publish-record-${{ github.run_id }}
path: publish-record.json
retention-days: 90
# Tell people what happened. A comment on the merged PR (found by the
# merge commit, so squash and rebase merges work too) and a line appended
# to a running JSON log in the bucket, next to the packages it describes,
# so the history is public and can be rendered later.
report:
needs: [changes, publish]
if: always() && needs.publish.result != 'skipped'
runs-on: ubuntu-latest
environment: publish
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: publish-record-${{ github.run_id }}
- name: Render
id: render
run: |
jq -r --arg outcome "${{ needs.publish.result }}" '
def pkgs: [.packages[] | select(test("-debug-")|not)] | join(", ");
def src: if .source=="pr-artifact" then "PR artifact" elif .source=="rebuild" or .source=="native-rebuild" then "rebuilt at merge" elif .source=="built" then "built here" elif .source=="already-published" then "already published, skipped" else "**" + .source + "**" end;
"### Publish " + (if $outcome=="success" then "succeeded" else "FAILED" end) +
" → **" + .target + "**" + (if .target!="live" then " (proof prefix, not live)" else "" end),
"",
"Packages: " + ([.sources[] | .package + " (" + .arch + ", " + src + ")"] | join("; ")),
"",
(if (.slots|length) > 0 then (.slots[] | "- `" + .mirror + "/" + .arch + "`: " + (if .status=="published" then "" else "**" + .status + "** " end) + pkgs)
elif ([.sources[] | select(.source=="already-published")] | length) == (.sources|length) then "_Nothing to publish: already at master'"'"'s version everywhere._"
else "_Nothing was published._" end),
"",
(if ([.sources[] | select(.source|test("failed"))] | length) > 0 then "_A build failed, so no channel was touched._\n"
elif (.plan|length) > (.slots|length) then "_Some planned slots did not run because an earlier slot failed._\n" else "" end),
"Commit " + .commit[0:7] + " · [run](" + .run + ")"
' publish-record.json > comment.md
cat comment.md
- name: Append to the publish log in the bucket
env:
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
curl -fsSL https://downloads.rclone.org/rclone-current-linux-amd64.zip -o rclone.zip && unzip -jq rclone.zip '*/rclone' && chmod +x rclone
# One JSON object per line, newest last. Served at
# https://pkgs.omarchy.org/publish-log.jsonl
./rclone copy R2:omarchy-pkgs/publish-log.jsonl . --s3-no-head 2>/dev/null || : > publish-log.jsonl
jq -c . publish-record.json >> publish-log.jsonl
./rclone copyto publish-log.jsonl R2:omarchy-pkgs/publish-log.jsonl --s3-no-head
echo "log now has $(wc -l < publish-log.jsonl) entries"
- name: Comment on the merged PR
# Only for a push: the merge commit names its PR. A dispatch runs
# from master's head, whose PR merged something else entirely, so
# commenting there would attach this run's report to the wrong PR.
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
run: |
pr=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty')
if [[ -n "$pr" ]]; then
gh pr comment "$pr" -R "${{ github.repository }}" --body-file comment.md
echo "commented on #$pr"
else
echo "no PR for ${{ github.sha }} (manual dispatch?); skipping PR comment"
fi
result:
needs: [changes, publish]
if: always()
runs-on: ubuntu-latest
steps:
- run: |
echo "changes result: ${{ needs.changes.result }}, publish result: ${{ needs.publish.result }}"
[[ "${{ needs.changes.result }}" == "success" ]]
[[ "${{ needs.publish.result }}" == "success" || "${{ needs.publish.result }}" == "skipped" ]]
+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"
+4 -51
View File
@@ -1,14 +1,5 @@
name: Sync Rebuild Triggers
# Rebuilds ride the unattended lane, like track-branches.yml: the pkgrel bump
# PR 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.
#
# 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:
schedule:
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
@@ -28,31 +19,11 @@ jobs:
pull-requests: write
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
uses: actions/checkout@v4
with:
persist-credentials: false
# A scoped dispatch regenerates only the named packages. Pushed to the
# shared branch, that would replace every other pending update in its
# PR, so it gets a branch and PR of its own.
- name: Choose the PR branch
id: branch
env:
PACKAGES: ${{ github.event.inputs.packages }}
run: |
read -r -a package_args <<< "${PACKAGES:-}"
.github/scripts/sync-pr-branch.sh auto/sync-rebuilds "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
# Runs in an Arch container against the mirror the x86_64 builder itself
# uses, because the question being asked is what that builder will link
# against and a different mirror can be hours ahead of it. Recording a
@@ -99,12 +70,11 @@ jobs:
- name: Create Pull Request
if: steps.changes.outputs.has_changes == 'true'
id: cpr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PKGS_BOT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: rebuild against updated dependencies'
title: "chore: rebuild against updated dependencies${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
title: 'chore: rebuild against updated dependencies'
body: |
Automated pkgrel bump for packages that link against a dependency
which has moved in the official repositories.
@@ -114,27 +84,10 @@ jobs:
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.
branch: ${{ steps.branch.outputs.branch }}
branch: auto/sync-rebuilds
delete-branch: true
labels: automated
# Auto-merge, not a direct merge: branch protection still has to see
# the build checks green before the rebuild lands.
- name: Enable auto-merge
if: steps.cpr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.cpr.outputs.pull-request-number }}
run: |
# Idempotent across re-runs of an updated PR: enabling twice errors.
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
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
reviewers: ryanrhughes
- name: Notify Basecamp on failure
if: failure() && env.BASECAMP_CHATBOT_URL != ''
+9 -80
View File
@@ -17,12 +17,6 @@ jobs:
permissions:
contents: write
pull-requests: write
outputs:
branch: ${{ steps.branch.outputs.branch }}
pushed_at: ${{ steps.pushed.outputs.at }}
number: ${{ steps.cpr.outputs.pull-request-number }}
operation: ${{ steps.cpr.outputs.pull-request-operation }}
head_sha: ${{ steps.cpr.outputs.pull-request-head-sha }}
steps:
- name: Checkout repository
@@ -30,25 +24,12 @@ jobs:
with:
persist-credentials: false
# A scoped dispatch regenerates only the named packages. Pushed to the
# shared branch, that would replace every other pending update in its
# PR, so it gets a branch and PR of its own.
- name: Choose the PR branch
id: branch
env:
PACKAGES: ${{ github.event.inputs.packages }}
run: |
read -r -a package_args <<< "${PACKAGES:-}"
.github/scripts/sync-pr-branch.sh auto/sync-upstream "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
# 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.
- name: Update packages from upstream release feeds
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" \
@@ -58,29 +39,23 @@ jobs:
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive
pacman -Syu --noconfirm jq
groupadd -g "$HOST_GID" runner
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
chown -R runner:runner /workspace/pkgbuilds
# The reviewed lane only: packages marked auto_merge ride
# track-branches.yml, which merges without a human.
if [[ -n "${PACKAGES:-}" ]]; then
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
runuser -u runner -- ./bin/sync-upstream --lane reviewed
runuser -u runner -- ./bin/sync-upstream
fi
'
env:
PACKAGES: ${{ github.event.inputs.packages }}
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Failed feeds leave their recipes untouched; completed updates still
# reach review. The failed sync step keeps the workflow red and notifies.
- name: Check for changes
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: changes
run: |
if [ -z "$(git status --porcelain)" ]; then
@@ -89,35 +64,22 @@ jobs:
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
# Runs created by this push are newer than this; the approve job
# waits for them. A minute's slack absorbs runner clock skew.
- name: Record push time
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: pushed
run: echo "at=$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: cpr
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync upstream releases'
title: "chore: sync upstream releases${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
title: 'chore: sync upstream releases'
body: |
Automated update of packages that track 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`. Failed package updates
are left untouched; check the workflow result for outstanding failures.
branch: ${{ steps.branch.outputs.branch }}
Each package reports its newest release through
`.omarchy/upstream.sh`.
branch: auto/sync-upstream
delete-branch: true
# The bot is trusted; build-approved lets the approve job below
# release GitHub's hold on its pushes without a maintainer.
labels: |
automated
build-approved
labels: automated
reviewers: ryanrhughes
- name: Notify Basecamp on failure
@@ -131,36 +93,3 @@ jobs:
"🔴 <strong>Upstream sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
'{content: $content}')" \
"$BASECAMP_CHATBOT_URL"
# GitHub holds pull_request runs from a GITHUB_TOKEN push for approval and
# creates no pull_request_target run for it, so approve-pr.yml never sees
# the sync's own pushes. The sync labels its PR build-approved, so release
# the held runs for the commit just pushed, whether it opened the PR or
# updated it. A separate job, so the sync container's token never holds
# actions: write.
approve:
needs: sync
if: ${{ !cancelled() && (needs.sync.outputs.operation == 'created' || needs.sync.outputs.operation == 'updated') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
actions: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Release held build and test runs
uses: actions/github-script@v7
env:
NUMBER: ${{ needs.sync.outputs.number }}
BRANCH: ${{ needs.sync.outputs.branch }}
HEAD_SHA: ${{ needs.sync.outputs.head_sha }}
SINCE: ${{ needs.sync.outputs.pushed_at }}
with:
script: |
const approve = require('./.github/scripts/approve-sync-push.cjs');
const { NUMBER, BRANCH, HEAD_SHA, SINCE } = process.env;
await approve({ github, context, core, number: Number(NUMBER),
branch: BRANCH, headSha: HEAD_SHA, since: SINCE });
+3 -44
View File
@@ -1,28 +1,12 @@
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:
pull_request:
push:
branches: [master]
workflow_dispatch:
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:
runs-on: ubuntu-latest
@@ -32,12 +16,6 @@ jobs:
with:
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
# the same comparator pacman uses on users' machines.
- name: Run self-tests
@@ -47,27 +25,8 @@ jobs:
-w /workspace \
archlinux:base-devel bash -lc '
set -euo pipefail
pacman -Syu --noconfirm git jq python libarchive neovim tmux
python tests/oma-service-removal.py
python tests/upstream-watch.py
bash tests/ipu7-install.sh
bash tests/ipu7-headers.sh
pacman -Syu --noconfirm jq
./bin/sync-upstream self-test
./bin/sync-rebuilds --self-test
./bin/omarchy-pkgs 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/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
'
-161
View File
@@ -1,161 +0,0 @@
name: Track upstream branches
# The unattended lane. Packages marked "auto_merge": true 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. 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.
#
# 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:
# Every 2 hours, off the hour to dodge the scheduling backlog at :00
- cron: '35 */2 * * *'
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 auto/track-branches 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
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 the reviewed sync: 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 \
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: Check for changes
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
id: changes
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git status --porcelain
{
echo "### Pinned"
git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' | grep -E '^\+(pkgver|_commit)=' | sed 's/^+/- /'
} >> "$GITHUB_STEP_SUMMARY"
fi
# The PR title names what moved, so the merged history reads like a
# changelog: "Track upstream branches: omarchy-dev 4.0.0.r6520.g1a2b3c4, ...".
- name: Describe the pins
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: describe
run: |
title=$(git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' \
| awk '/^\+\+\+ b\/pkgbuilds\//{split($2,p,"/"); pkg=p[3]} /^\+pkgver=/{sub(/^\+pkgver=/,""); printf "%s %s, ", pkg, $0}' \
| sed 's/, $//')
echo "title=Track upstream branches: ${title}" >> "$GITHUB_OUTPUT"
- name: Open or update the tracking PR
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
id: pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.PKGS_BOT_TOKEN }}
commit-message: ${{ steps.describe.outputs.title }}
title: ${{ steps.describe.outputs.title }}
body: |
Automated pin of packages that follow a moving upstream branch
(`"auto_merge": true` in `.omarchy/package.json`). Each package's
`_commit` now points at the branch tip. Fresh tips wait until
their commit timestamp is at least `min_release_age` old.
This PR auto-merges once the build checks pass. A failing build
leaves it open; the next tracker run replaces it with the newer tip.
branch: auto/track-branches
delete-branch: true
labels: automated
# Auto-merge, not a direct merge: branch protection still has to see
# `result`, `self-tests` and `build-isolation` green, and this lane
# inherits every rule the reviewed lane has except the human.
- name: Enable auto-merge
if: ${{ !cancelled() && steps.pr.outputs.pull-request-number != '' }}
env:
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
PR: ${{ steps.pr.outputs.pull-request-number }}
run: |
# Idempotent across re-runs of an updated PR: enabling twice errors.
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
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
- 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>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"
-79
View File
@@ -1,79 +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.
concurrency:
group: publish
cancel-in-progress: false
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/
logs/
build-output/
cache/
pkgs.omarchy.org/
pkgbuilds/*/.SRCINFO
pkgbuilds/*/.gitignore
@@ -37,7 +35,3 @@ pkgbuilds/yay/yay/
.srcdest/
.repo-host
.worktrees/
# Python helpers and offline tests
__pycache__/
.release-verification/
+137 -365
View File
@@ -23,72 +23,25 @@ The filesystem no longer encodes release policy. Instead:
(`OMARCHY_RC_PINS=1`, which `omarchy-release rc` sets) may build it for rc — master's
shipped pins can never overwrite an in-flight RC. The dev pair
(`omarchy-dev`, `omarchy-settings-dev`) is pinned to `edge`
- packages that follow a moving upstream branch (the dev pair on `quattro`,
`omasnap-git` on `main`) still pin an exact commit in their PKGBUILD. A
`git_branch` upstream watch moves that pin, and `"auto_merge": true` puts the
package on the unattended lane: `track-branches.yml` opens the bump PR every
two hours and auto-merges it once the build checks pass, so a branch tip
reaches the edge channel without anyone clicking. No PKGBUILD may carry an
unpinned git source (`tests/pinned-sources.sh`); a branch that has to be
followed gets a watch, not a `#branch=` fragment
- Omarchy owns every checked-in recipe; upstream watches update release metadata without replacing packaging or architecture support
- AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/`
- packages can opt out of unscoped builds with `skip_build`; explicit `--package` builds remain available
- packages follow direct upstream watches/providers in `.omarchy/package.json`, or a custom `.omarchy/upstream.sh` hook
- packages that follow a vendor release feed instead of the AUR carry an `.omarchy/upstream.sh` hook
## Prerequisites
### aarch64 Builds (Optional)
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:
To build ARM64 packages on x86_64, enable QEMU emulation:
```bash
# Run after each reboot
docker run --privileged --rm tonistiigi/binfmt --install arm64
# Verify
podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m
docker run --rm --platform linux/arm64 alpine:latest uname -m
# Should output: aarch64
```
**Note**: emulated builds are much slower than native ones.
### Published architectures
`helpers/paths.sh` names the architectures this repository publishes:
```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. A remote sync failure can still leave a promotion temporarily partial;
rerunning the same advance 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. A
checked-in list also means the rebuild workflow and release host cannot drift
onto different architecture sets. 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.
**Note**: aarch64 builds use QEMU and slower than native x86_64 builds.
## Quick Start
@@ -133,14 +86,6 @@ bin/repo advance --from edge --to rc
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
@@ -337,16 +282,14 @@ 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.
### Import an initial AUR recipe
### Sync AUR PKGBUILDs
```bash
bin/add-package package-name --source aur
bin/sync-aur # Sync all AUR packages with sync enabled
bin/sync-aur yay v4l2-relayd # Sync specific packages
```
AUR is an optional source for an initial recipe. Imported packages become
Omarchy-owned immediately; subsequent updates use direct upstream releases.
There is no scheduled AUR sync. Edit the checked-in PKGBUILD to maintain
architecture support and packaging behavior.
AUR sync is metadata-driven. It preserves `.omarchy/`, replaces the package root with AUR contents, applies `.omarchy/patches/*.patch`, runs `.omarchy/post-sync.sh` when present, applies pkgrel metadata, removes AUR-only `.SRCINFO` and `.gitignore` files, and records `upstream_commit`.
### Sync Upstream Releases
@@ -357,11 +300,10 @@ bin/sync-upstream openai-codex-desktop # Update specific packages
Some vendors publish a release feed of their own that is faster and more precise
than the AUR packaging of it. Those packages are `source: local` — Omarchy owns
the PKGBUILD — and declare where releases come from either as data or, for an
unusual feed, a small hook.
the PKGBUILD — and declare where releases come from in one of two ways.
A vendor shipping tagged GitHub releases is pure data, declared as `upstream`
in `.omarchy/package.json` with no code at all:
A vendor shipping tagged GitHub releases with a checksum manifest asset is pure
data, declared as `upstream` in `.omarchy/package.json` with no code at all:
```json
"upstream": {
@@ -374,107 +316,16 @@ in `.omarchy/package.json` with no code at all:
}
```
`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
A package 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. 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
@@ -527,13 +378,7 @@ A package names those dependencies in `.omarchy/package.json`:
{ "source": "aur", "sync": false, "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 rebuilt by
each architecture's normal queue.
`bin/sync-rebuilds` reads each named package's version from the official repositories and compares it to `rebuilt_against`, the record of what the checked-in pkgrel was last bumped for. pkgrel is bumped unless every name in `rebuild_on` is recorded and still matches, so a name the record does not carry reads as changed rather than going unexamined forever. Opting a package in therefore buys one rebuild: what its published build actually linked against is not knowable from here, and a record written without a rebuild would certify a build nobody checked.
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 merely unlocking the build gate would ship nothing. Bumping pkgrel needs no other change: `bin/check-versions` and the builder both already rebuild when pkgrel moves.
@@ -541,14 +386,9 @@ For an AUR-synced package the bump is expressed as the dotted Omarchy pkgrel suf
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.
Versions are read from the local pacman database, so this runs on Arch or in an Arch container against a synced database. Only `core`, `extra` and `multilib` count: a Qt release sitting in testing or kde-unstable is not what the builder will link against, and rebuilding for it would ship a package built against the wrong ABI. The workflow points that database at `mirror.omarchy.org`, the mirror the x86_64 builder itself uses, because a mirror running ahead of the builder would record a version the build never linked against and nothing re-fires once the record matches.
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.
aarch64 is not covered. Those builds resolve Qt from Arch Linux ARM, which can lag Arch, so one record cannot describe both architectures. Only x86_64 is published today, so nothing currently ships from the untracked side; if ARM publishing starts, `rebuilt_against` has to become per-architecture before this can be trusted there.
### Other
@@ -563,28 +403,14 @@ bin/omarchy-release # Release front door (start / pick / rc / s
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/add-package <package> # Add an Omarchy-owned package with metadata
bin/package-worktree <package> # Inspect historical AUR provenance in a scratch workspace
bin/repo remove <package> # Remove package (host workflow; CI uses unpublish.yml)
bin/add-package <package> # Add an AUR/local package with metadata
bin/package-worktree <package> # Create upstream/patched/current scratch workspace
bin/repo remove <package> # Remove package
bin/sync-upstream # Update packages that track a vendor release feed
bin/sync-rebuilds # Bump pkgrel for packages whose dependencies moved
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
```
### Retiring a package
Delete its recipe directory in a PR. Once that merges, take it out of the
channel databases with the **Unpublish retired packages** workflow:
```
gh workflow run unpublish.yml -f packages="<pkgbase> ..." -f channels="edge rc stable"
```
It removes every entry built from those pkgbases (split outputs and `-debug`
included) from both architectures' databases, and refuses any package that
still has a recipe on master. Package files stay in the bucket: published
filenames are immutable, and nothing references them once the entries are gone.
### Package Metadata Tools
```bash
@@ -597,7 +423,7 @@ bin/repo list # Table view of source package metadata
bin/repo list --json # Agent/script-friendly JSON
bin/repo list --repo --mirror stable # List packages in a published repo database
bin/package-worktree yay # Compare with the original imported AUR recipe
bin/package-worktree v4l2-relayd # Create upstream/patched/current scratch workspace
```
## Cutting an Omarchy Release
@@ -711,7 +537,9 @@ omarchy-pkgs/
│ ├── PKGBUILD
│ └── .omarchy/
│ ├── package.json # Source/sync/release metadata
│ └── upstream.sh # Optional custom vendor release feed hook
│ ├── patches/ # Omarchy patches reapplied after AUR sync
│ ├── post-sync.sh # Optional dynamic post-sync customization hook
│ └── upstream.sh # Optional vendor release feed hook (non-AUR packages)
├── build/
├── build-output/ # Unsigned packages (temporary)
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
@@ -731,29 +559,45 @@ Each source package has Omarchy metadata at `pkgbuilds/<package>/.omarchy/packag
Minimal examples:
```json
{ "source": "aur" }
```
```json
{ "source": "aur", "sync": false }
```
```json
{ "source": "aur", "release_ring": "fast" }
```
```json
{ "source": "local" }
{ "source": "local", "release_ring": "fast" }
```
```json
{ "source": "local", "skip_build": true }
{ "source": "local", "upstream": { "watch": { "github": "abenz1267/walker", "pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)" } } }
```
```json
{ "source": "aur", "pkgrel": { "suffix": 1 } }
```
Fields:
- `source`: `local` for maintained packages. The legacy `aur` value is used only during an initial import. A local recipe can follow an upstream watch, provider, or `.omarchy/upstream.sh` hook.
- `upstream`: optional direct release watch (see [Upstream watches](docs/upstream-sources.md)), or an existing GitHub, git-tag, npm, or Debian provider. GitHub architecture assets may be a string or an ordered array, and can be combined with disjoint versioned `sources` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
- `source`: `aur` or `local`. A `local` package can still follow an upstream release, either declaratively via `upstream` or with an `.omarchy/upstream.sh` hook.
- `upstream`: optional for `local` packages whose vendor ships tagged GitHub releases with a checksum manifest asset. `{ "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "<arch>": "name-{tag}.tar.xz" } }` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
- `min_release_age`: optional quarantine for upstream releases (`"24h"`, `"2d"`, or bare seconds). The newest release older than the window ships; anything younger waits, and a release whose age cannot be proven fails the sync. Bypass deliberately with `BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>`.
- `sync`: `false` records an existing manual maintenance hold. Held packages have no upstream watch/provider/hook and are excluded from automatic updates.
- `auto_merge`: optional boolean; defaults to `false`. `true` moves the package's upstream updates from the reviewed 6-hourly sync PR to the unattended lane: `track-branches.yml` opens its bump PR and auto-merges it when CI is green. Meant for packages that follow a moving branch through a `git_branch` watch, where every tip is a release and there is nothing for a reviewer to read. Requires an upstream watch, provider, or hook.
- `origin`: optional historical import provenance, with `aur` (package name) and `commit`. It does not control updates.
- `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually.
- `aur`: optional AUR package name when it differs from the local package directory, usually for split packages.
- `release_ring`: optional. `fast` means the package is built directly for stable as well as edge, with the artifacts replicated into rc for parity. Packages without a ring build in edge and reach stable through the pipeline (`bin/repo advance`).
- `channels`: optional array bounding where the package may be built (`edge`, `rc`, `stable`). Without the key a package is a member of every channel and follows the default build rules above; `bin/repo advance` refuses to carry a package anywhere it isn't a member.
- `pinned`: optional boolean. A pinned package's version is set per release by `omarchy-release` on the `rc` branch, so it is never built for stable (promotion only) and is built for rc only from that branch's worktree (`OMARCHY_RC_PINS=1`). Used by `omarchy` and `omarchy-settings`.
- `skip_build`: optional boolean; defaults to `false`. Set `true` to exclude a package from scheduled version checks and unscoped builds. The package can still be built explicitly with `bin/repo release --package <name>`.
- `pkgrel`: legacy import customization metadata. Maintained recipes keep their complete package release directly in PKGBUILD; rebuilds increment it there.
- `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `<aur pkgrel>.<suffix>` instead of replacing AUR's pkgrel. `offset` can be used only when preserving monotonic upgrades from old absolute pkgrel bumps. The metadata is removed automatically when AUR sync changes `pkgver`; the current package version is read from the checked-in PKGBUILD, so the version is not duplicated in JSON.
- `rebuild_on`: optional array of package names this package links against closely enough that it must be rebuilt when they change, independent of its own source. Read by `bin/sync-rebuilds`.
- `rebuilt_against`: written by `bin/sync-rebuilds`. Maps each built architecture to the versions of its `rebuild_on` packages that the current pkgrel was bumped for.
- `upstream_commit`: legacy AUR metadata, superseded by `origin.commit`. `bin/package-worktree` can use historical provenance to inspect the original recipe.
- `rebuilt_against`: written by `bin/sync-rebuilds`. Records the version of each `rebuild_on` package that the current pkgrel was bumped for.
- `upstream_commit`: set by `bin/sync-aur` for AUR packages. Used by `bin/package-worktree` to recreate the exact raw AUR package that Omarchy last synced.
### Build Matrix
@@ -765,26 +609,78 @@ Fields:
## Adding Packages
### Start from an existing recipe
### From AUR
```bash
bin/add-package package-name --source aur --fast
# Review the imported files, own any architecture/packaging changes directly,
# and declare an upstream watch/provider or hook in .omarchy/.
bin/sync-upstream package-name
bin/add-package package-name
bin/repo release --package package-name
```
The import records historical provenance in `origin`. It does not opt a package
into future AUR imports. Upstream watches update only release scalars and source
checksums; downstream build behavior stays in the recipe. Ordinary source-code
patches still belong beside PKGBUILD and are applied by `prepare()` as needed.
### Custom package
### From AUR, fast release ring
```bash
bin/add-package my-package --scaffold
# Fill in PKGBUILD, package files, and upstream metadata
bin/add-package package-name --fast
bin/repo release --package package-name
bin/repo release --mirror stable --package package-name
```
### AUR-origin, manually maintained by Omarchy
```bash
bin/add-package package-name --no-sync
```
### Local Customizations for AUR Packages
For static changes, create `pkgbuilds/package-name/.omarchy/patches/*.patch` to maintain modifications across AUR syncs.
The recommended workflow is to use a scratch workspace:
```bash
bin/package-worktree package-name --dir /tmp/package-name-worktree
```
This creates:
```text
upstream/ # raw AUR package at upstream_commit
patched/ # AUR + existing Omarchy .omarchy customizations
current/ # current checked-in package directory
```
Patch-authoring flow:
```bash
# 1. Make the intended change in pkgbuilds/package-name/
# 2. Recreate the scratch workspace
bin/package-worktree package-name --dir /tmp/package-name-worktree
# 3. Inspect drift from patched -> current
# For multi-file changes, inspect this and split into focused patches.
diff -ruN /tmp/package-name-worktree/patched /tmp/package-name-worktree/current
# For a single PKGBUILD change, write a patch like this:
mkdir -p pkgbuilds/package-name/.omarchy/patches
(
cd /tmp/package-name-worktree/patched
diff -u --label a/PKGBUILD --label b/PKGBUILD \
PKGBUILD /tmp/package-name-worktree/current/PKGBUILD || true
) > pkgbuilds/package-name/.omarchy/patches/my-fix.patch
# 4. Verify the package is reproducible from AUR + .omarchy
bin/sync-aur package-name
bin/package-worktree package-name --dir /tmp/package-name-check
diff -ruN /tmp/package-name-check/patched /tmp/package-name-check/current
```
For dynamic changes that depend on the current upstream version, add `pkgbuilds/package-name/.omarchy/post-sync.sh`. The hook runs after the AUR package is copied into a temporary worktree and before the Omarchy pkgrel suffix is applied. After patches/hooks/metadata pkgrel overrides, `bin/sync-aur` removes AUR-only `.SRCINFO` and `.gitignore` files before writing the package back.
### Custom Package
```bash
bin/add-package my-package --local --scaffold
# Fill in PKGBUILD and package files
bin/repo release --package my-package
```
@@ -795,15 +691,10 @@ bin/repo release --package my-package
- 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)
- QEMU emulation required on x86_64 hosts (slower)
- Uses Arch Linux ARM repositories
- 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
- Same workflow, just add `--arch aarch64`
### Building for Both Architectures
@@ -823,73 +714,12 @@ bin/repo sync --arch aarch64
The build system automatically handles inter-package dependencies:
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. Any failure still prevents the release from publishing.
1. Parses `depends=()` and `makedepends=()` from PKGBUILDs
2. Builds in correct order
3. Makes newly-built packages available via temporary `[omarchy-build]` repo
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. **First publication needs one package setting:** 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. This change only produces images;
package jobs keep their existing behavior until image consumption is enabled.
## Version Management
Packages are only rebuilt if:
@@ -906,70 +736,17 @@ The repository includes GitHub workflows and systemd services for automated rele
#### GitHub Workflows
1. **sync-upstream.yml** (Every 6 hours): Watches direct upstream feeds and updates owned recipes on the reviewed lane. Successful package updates reach a PR even if another feed fails; failed recipes stay untouched and the workflow remains red.
2. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories, opens a PR, and enables auto-merge. The PR lands once its build checks pass; a rebuild that fails stays an open red PR for a maintainer.
3. **track-branches.yml** (Every 2 hours): The unattended lane. Pins every `"auto_merge": true` package to the tip of its watched branch once its commit timestamp clears `min_release_age`, opens one PR for all of them, and enables auto-merge. Packages pinned from the same branch move together or not at all, including targeted syncs. The PR builds like any other; a tip that fails to build stays an open red PR until the next tick supersedes it.
The tracking and rebuild PRs and their auto-merge use the PAT stored in `PKGS_BOT_TOKEN`, with
Contents and Pull requests write access to this repository and an owner trusted
to trigger builds. The existing controller PAT can be reused. No GitHub App is
required. The built-in Actions `GITHUB_TOKEN` cannot drive the unattended
build-and-publish chain, so both workflows require this secret before they run.
The reviewed upstream sync continues to use `GITHUB_TOKEN` and requires
maintainer approval as before. See [setup instructions](docs/upstream-sources.md#enable-unattended-branch-updates).
Scheduled runs regenerate one shared PR (`auto/sync-upstream`, `auto/sync-rebuilds`)
from master. A manual run with the `packages` input only regenerates those
packages, so it opens its own PR on `auto/sync-upstream-<packages>` (or
`auto/sync-rebuilds-<packages>`) rather than replacing the shared PR's other
pending updates. The next scheduled run still picks the same update up in the
shared PR if it has not merged by then; identical package trees reuse the same
build artifacts.
Upstream sync PRs are pushed with `GITHUB_TOKEN`, so GitHub holds their build and test
runs for approval on every push and starts no `pull_request_target` workflow
for them. The upstream sync labels its own PRs **`build-approved`**, and
its `approve` job releases the held runs for each commit they push, including
the push that opens the PR. A push to an `auto/sync-*` branch does not cancel
the PR's in-flight build: the new build waits for it and then reuses its
artifacts, so a long aarch64 build is not restarted by every sync.
Package PRs that are trusted to build also merge themselves.
`auto-merge-pr.yml` enables auto-merge (with `PKGS_BOT_TOKEN`, so the merge
publishes) on any open, non-draft PR whose author is a collaborator, vouched,
or a bot, or that carries **`build-approved`**, and that changes only
packages: anything under `pkgbuilds/`, plus the test a package PR brings with
it (a new file under `tests/`, and `test.yml` lines that only run new
`./tests/*.sh`). The PR lands once `result`, `self-tests` and
`build-isolation` pass; a red build stays open. PRs that also touch
workflows, scripts, build tooling or existing tests still need a maintainer, as does
the upstream sync (`auto/sync-upstream`), which labels itself. Removing
`build-approved` withdraws the auto-merge it armed. For a PR opened before
the workflow existed, run it by hand: `gh workflow run auto-merge-pr.yml -f pr=<number>`.
To approve builds for an unvouched contributor's PR, apply **`build-approved`**.
Until approval, the PR shows **Awaiting build approval** and its required
`result` check stays pending, keeping the PR blocked from merging without
reporting a failed build. Actual build failures and denouncements still fail.
Applying the label triggers a package build and automatically releases GitHub's
pending build and test workflows for that PR's current commit. The approval workflow
runs only trusted default-branch code; package builds and tests stay in the
ordinary PR workflows. It may take a few minutes for GitHub to register and
release all the runs.
The label stays effective for that PR while attached, including later commits;
it does not vouch for the author's other PRs. Removing it stops further label
approvals, but does not cancel runs already released. An explicit denouncement
in `.github/VOUCHED.td` still blocks builds. If the approval workflow times out,
remove and reapply the label to retry.
1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found.
2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out.
3. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR.
#### Systemd Services
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
1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions, creates state files if builds are needed
2. **auto-release-edge** (`*:1/5`): If a state file exists, 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 and replicates them to rc
@@ -983,11 +760,10 @@ That cadence is only safe because of three guards:
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
`.build-failed-<channel>` 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>`.
likely fix. Clear it by hand with `rm /root/.state/.build-failed-<channel>`.
- **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.
@@ -1040,14 +816,10 @@ 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.
- `.sync-needed-<channel>` — the packages queued for that channel, one per
line; the release run reads them to name what it is building
- `.build-failed-<channel>` — consecutive failure count, timestamp, and the
commit it failed on (drives the backoff; removing it forces a retry)
### Schedule (America/New_York)
+20 -19
View File
@@ -6,7 +6,7 @@ source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
PACKAGE=""
SOURCE="local"
SOURCE="aur"
SYNC="true"
RELEASE_RING=""
AUR_PACKAGE=""
@@ -17,14 +17,14 @@ usage() {
cat <<EOF
Usage: $0 <package> [OPTIONS]
Create an Omarchy-owned package. --source aur imports a starting recipe once;
future releases must use an upstream watch, provider, or hook.
Create pkgbuilds/<package> with Omarchy metadata. AUR packages are synced
immediately after metadata is written.
Options:
--source <aur|local> Initial recipe source (default: local)
--source <aur|local> Package source (default: aur)
--local Shortcut for --source local
--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
--release-ring <ring> Release ring (currently: fast)
--scaffold For local packages, create a starter PKGBUILD
@@ -32,9 +32,9 @@ Options:
-h, --help Show this help message
Examples:
$0 yay --source aur
$0 spotify --source aur --fast
$0 signal-desktop --source aur --no-sync
$0 yay
$0 spotify --fast
$0 signal-desktop --no-sync
$0 omarchy-zsh --local --scaffold
EOF
}
@@ -97,10 +97,6 @@ if [[ -z "$PACKAGE" ]]; then
usage
exit 1
fi
if [[ ! $PACKAGE =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
print_error "Invalid package name: $PACKAGE"
exit 1
fi
case "$SOURCE" in
aur|local) ;;
@@ -116,11 +112,6 @@ PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
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
print_error "Package metadata already exists: $METADATA_FILE"
print_info "Use --force to overwrite it"
@@ -155,8 +146,18 @@ jq -n "${jq_args[@]}" "$jq_filter" > "$METADATA_FILE"
print_success "Wrote $METADATA_FILE"
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
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
+10 -57
View File
@@ -25,22 +25,6 @@ SKIP_PROD_CHECK=false
FAST_RING_ONLY=false
BOOTSTRAP=false
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() {
echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]"
@@ -52,8 +36,7 @@ usage() {
echo " or --bootstrap (one-time initial seed)"
echo ""
echo "Options:"
echo " --arch <arch>|all Target architecture (x86_64 or aarch64, default: x86_64);"
echo " all = every published architecture, one after another"
echo " --arch <arch> Target architecture (x86_64 or aarch64, default: x86_64)"
echo " --package <names...> Advance only the named package(s)"
echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)"
echo " --bootstrap One-time stable -> rc seed before forward-only enforcement"
@@ -82,14 +65,8 @@ while [[ $# -gt 0 ]]; do
shift 2
;;
--arch)
if [[ "$2" == "all" ]]; then
ALL_ARCHES=true
ARCH=all
else
require_valid_arch "$2"
ARCH="$2"
update_arch_paths
fi
ARCH="$2"
update_arch_paths
shift 2
;;
--package)
@@ -157,31 +134,6 @@ case "$FROM->$TO" in
;;
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"
TARGET_DIR="$REPO_ROOT/$TO/$ARCH"
SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst"
@@ -326,7 +278,7 @@ while IFS=$'\t' read -r name base filename; do
done < <(read_manifest)
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
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
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[@]}"
echo "Published filenames are never rewritten, so the $TO copy stays authoritative."
echo "A same-name collision means this version reached $TO through another lineage"
echo "(native build, bootstrap seed, or a same-version rebuild) and the package has"
echo "not moved in $FROM since; it advances under a new filename when it does."
echo "Published filenames are never rewritten, and two artifacts fighting over"
echo "one name means something built twice from different inputs. Resolve it"
echo "deliberately: bump pkgrel and rebuild so the new artifact gets a new"
echo "filename, or remove the source copy if the destination is correct."
exit 1
fi
if [[ "$DRY_RUN" == true ]]; then
+56 -103
View File
@@ -1,10 +1,6 @@
#!/bin/bash
# Run the release workflow for a channel when work is queued.
# Usage: auto-release <edge|rc|stable> [<arch>|all]
#
# 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.
# Usage: auto-release <edge|rc|stable>
#
# 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"
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
# (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}"
if [[ -z "$MIRROR" ]]; then
print_error "Usage: $0 <mirror> [<arch>|all]"
print_error "Usage: $0 <mirror>"
echo " mirror: edge, rc, or stable"
echo " arch: one of $VALID_ARCHES, or all (default) for every published architecture"
exit 1
fi
@@ -44,13 +39,17 @@ if [[ "$MIRROR" != "edge" && "$MIRROR" != "rc" && "$MIRROR" != "stable" ]]; then
exit 1
fi
if [[ "$ARCH_ARG" == "all" ]]; then
ARCHES=$(published_arches)
else
require_valid_arch "$ARCH_ARG"
ARCHES="$ARCH_ARG"
STATE_FILE="$STATE_DIR/.sync-needed-$MIRROR"
FAIL_FILE="$STATE_DIR/.build-failed-$MIRROR"
# Nothing queued: stay quiet. At a 5-minute cadence this is most invocations,
# and a header for each would bury the runs that matter in the journal.
if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi
print_header "Processing Sync for $MIRROR"
# The build inputs are this checkout's contents; its HEAD identifies them.
current_fingerprint() {
git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown"
@@ -66,103 +65,57 @@ backoff_seconds() {
echo "$delay"
}
# Returns 0 when this architecture's queue was processed (or was empty), 1 when
# the release failed. Backoff and "someone else holds the lock" are not
# failures: they are this tick deciding to do nothing.
release_arch() {
local arch="$1"
local state_file fail_file
state_file=$(sync_queue_file "$MIRROR" "$arch")
fail_file=$(sync_fail_file "$MIRROR" "$arch")
FAIL_COUNT=0
if [[ -f "$FAIL_FILE" ]]; then
# shellcheck disable=SC1090
source "$FAIL_FILE" 2>/dev/null || true
FAIL_COUNT="${FAILURE_COUNT:-0}"
failed_at="${FAILURE_AT:-0}"
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
# A queue written under the pre-architecture name belongs to x86_64.
if [[ "$arch" == "x86_64" && ! -f "$state_file" && -f "$(legacy_sync_queue_file "$MIRROR")" ]]; then
state_file=$(legacy_sync_queue_file "$MIRROR")
fi
if [[ "$arch" == "x86_64" && ! -f "$fail_file" && -f "$(legacy_sync_fail_file "$MIRROR")" ]]; then
fail_file=$(legacy_sync_fail_file "$MIRROR")
fi
# Nothing queued: stay quiet. At a 5-minute cadence this is most
# invocations, and a header for each would bury the runs that matter in
# the journal.
[[ -f "$state_file" ]] || return 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)"
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 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"
exit 0
fi
print_info "Backoff elapsed — retrying $MIRROR (failure #$((FAIL_COUNT + 1)) if this fails)"
fi
fi
# Non-blocking: a build in progress means this tick has nothing to do. The
# lock is reentrant, so once this run holds it the remaining architectures
# run under the same acquisition.
if ! try_release_lock; then
local holder
holder=$(release_lock_holder)
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
return 0
fi
# Non-blocking: a build in progress means this tick has nothing to do.
if ! try_release_lock; then
holder=$(release_lock_holder)
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
exit 0
fi
print_info "State file found: $state_file"
print_info "Starting release workflow for $MIRROR ($arch)..."
print_info "State file found: $STATE_FILE"
print_info "Starting release workflow for $MIRROR..."
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --arch "$arch" --skip-prod-check; then
print_success "Release completed successfully for $MIRROR ($arch)"
local completed_state=("$state_file" "$fail_file")
if [[ "$arch" == "x86_64" ]]; then
completed_state+=("$(legacy_sync_queue_file "$MIRROR")" "$(legacy_sync_fail_file "$MIRROR")")
fi
if ! rm -f "${completed_state[@]}"; then
print_error "Release succeeded, but its queue state could not be cleared"
return 1
fi
print_success "State file removed: $state_file"
return 0
fi
fail_count=$((fail_count + 1))
if ! cat >"$fail_file" <<EOF
FAILURE_COUNT=$fail_count
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --skip-prod-check; then
print_success "Release completed successfully for $MIRROR"
rm -f "$STATE_FILE"
rm -f "$FAIL_FILE"
print_success "State file removed: $STATE_FILE"
else
status=$?
FAIL_COUNT=$((FAIL_COUNT + 1))
cat >"$FAIL_FILE" <<EOF
FAILURE_COUNT=$FAIL_COUNT
FAILURE_AT=$(date +%s)
FAILURE_FINGERPRINT=$(current_fingerprint)
EOF
then
print_error "Could not record failure state: $fail_file"
return 1
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"
next=$(backoff_seconds "$FAIL_COUNT")
print_error "Release failed for $MIRROR (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"
return 1
}
status=0
for arch in $ARCHES; do
release_arch "$arch" || status=1
done
exit "$status"
exit "$status"
fi
+29 -218
View File
@@ -13,27 +13,6 @@ print_header "Omarchy Package Builder"
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
while [[ $# -gt 0 ]]; do
case $1 in
@@ -86,14 +65,7 @@ while [[ $# -gt 0 ]]; do
echo " $0 --arch aarch64"
echo " $0 --mirror stable"
echo " $0 --package yay"
echo " $0 --package yay walker 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 " $0 --package yay elephant cursor-bin"
echo ""
exit 0
;;
@@ -104,55 +76,18 @@ while [[ $# -gt 0 ]]; do
esac
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
print_info "Target architecture: $ARCH"
print_info "Mirror: $MIRROR"
print_info "Build workspace: $BUILD_OUTPUT_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
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" \
MIRROR="$MIRROR" \
PACKAGES="$PACKAGES" \
DRY_RUN=true \
DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS" \
PKGBUILDS_DIR="$PKGBUILDS_DIR" \
BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
FINAL_OUTPUT_DIR="$REPO_DIR" \
@@ -165,45 +100,21 @@ fi
# Create directories if they don't exist
mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
# Check the selected container engine is available
check_engine
# Check Docker is available
check_docker
# A foreign target architecture runs under QEMU user emulation. Probe by
# actually running a container for the target platform: that is the only
# test that covers both "binfmt not registered" and "registered but broken".
HOST_ARCH=$(uname -m)
[[ "$HOST_ARCH" == "arm64" ]] && HOST_ARCH=aarch64
if [[ "$HOST_ARCH" != "$ARCH" ]]; then
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
# Setup QEMU for aarch64 builds on x86_64 hosts
if [[ "$(uname -m)" == "x86_64" && "$ARCH" == "aarch64" ]]; then
# Check if QEMU is already working
if ! docker run --rm --platform linux/arm64 alpine:3.21 /bin/true >/dev/null 2>&1; then
print_info "Setting up QEMU for ARM64 emulation..."
setup_qemu
fi
fi
# Clean build-output directory to start fresh, unless the caller seeded it
# with packages from an earlier job or is resuming an interrupted run.
if [[ $KEEP_BUILD_WORKSPACE == "1" ]]; then
print_info "Keeping existing build workspace..."
else
print_info "Cleaning build workspace..."
rm -rf "${BUILD_OUTPUT_DIR:?}"/*
fi
# Clean build-output directory to start fresh
print_info "Cleaning build workspace..."
rm -rf "$BUILD_OUTPUT_DIR"/*
mkdir -p "$BUILD_OUTPUT_DIR"
# Show package info
@@ -213,43 +124,18 @@ else
print_info "Building unscoped packages for $MIRROR mirror"
fi
# Build/update the Docker image, unless the caller prepared the exact image
# already (a workflow building it once with an external BuildKit cache). A
# 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
# Build/update the Docker image
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
print_info "Planning isolated package builds..."
print_info "Running package build..."
# Create output directories if they don't exist
mkdir -p "$BUILD_OUTPUT_DIR"
mkdir -p "$REPO_DIR"
# Share downloaded archives, never /var/lib/pacman or an installed root.
# Channel/architecture separation preserves each mirror's dependency set.
PACKAGE_CACHE_DIR="$BUILD_ROOT/cache/pacman/$MIRROR/$ARCH"
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
# Ensure output directories are writable by container user
make_dir_writable "$BUILD_OUTPUT_DIR"
make_dir_writable "$REPO_DIR"
# Build Docker arguments
DOCKER_ARGS=(
@@ -258,101 +144,26 @@ DOCKER_ARGS=(
-e MIRROR="$MIRROR"
-e PACKAGES="$PACKAGES"
-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 "$REPO_ROOT:/pkgs.omarchy.org:ro"
-v "$REPO_ROOT:/pkgs.omarchy.org"
-v "$BUILD_DIR:/build:ro"
-v "$BUILD_ROOT/helpers:/helpers:ro"
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
)
# Podman-created images can leave WORKDIR owned by a remapped uid. Mount the
# existing host-user-owned workspace so the builder can write there.
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.
# Run the builder with assembled args
IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
PLATFORM_ARG=$(get_platform_arg "$ARCH")
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \
-e DRY_RUN=true "$IMAGE_TAG" /build/build.sh
docker run $PLATFORM_ARG "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh
mapfile -t ORDERED_PACKAGES < "$PLAN_DIR/packages"
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
BUILD_RESULT=$?
# Summary
echo ""
print_header "Build Summary"
echo " Total packages: ${#ORDERED_PACKAGES[@]}"
echo " Built: ${#SUCCESSFUL_PACKAGES[@]}"
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
if [[ $BUILD_RESULT -eq 0 ]]; then
print_success "Build completed successfully!"
else
print_warning "Some packages failed (see details above)"
# Reserved for a completed run with unsuccessful packages. Other failures
# must not let release publish arbitrary files left in the workspace.
exit 2
exit $BUILD_RESULT
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
# Check PKGBUILD versions against published repo versions
# Creates a state file per channel and architecture when 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
# Creates state files for edge and/or stable if any packages need building
set -e
@@ -14,27 +10,11 @@ source "$BUILD_ROOT/helpers/paths.sh"
source "$BUILD_ROOT/helpers/package-metadata.sh"
source "$BUILD_ROOT/helpers/lock-helpers.sh"
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
ARCH="${ARCH:-x86_64}"
PULL=false
ARCHES=""
while [[ $# -gt 0 ]]; do
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)
[[ "${1:-}" == "--pull" ]] && PULL=true
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() {
local pkgdir="$1"
local epoch pkgver pkgrel
[[ -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}"
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')
fi
}
@@ -113,8 +77,8 @@ check_vcs_unchanged() {
# If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged.
local pkgbuild_epoch pkgbuild_pkgrel
pkgbuild_epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH")
pkgbuild_pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH")
pkgbuild_epoch=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${epoch:-}"')
pkgbuild_pkgrel=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel}"')
local repo_pkgrel="${repo_version##*-}"
local repo_no_pkgrel="${repo_version%-*}"
@@ -142,11 +106,12 @@ check_package() {
local mirror="$3"
local pkgbuild_version repo_version
if ! pkgbuild_version=$(get_pkgbuild_version "$pkgdir") || [[ -z "$pkgbuild_version" ]]; then
print_warning "$pkg: could not read pkgver/pkgrel from its PKGBUILD for $ARCH — not queueing"
pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
repo_version=$(get_repo_version "$pkg" "$mirror")
if [[ -z "$pkgbuild_version" ]]; then
return 1
fi
repo_version=$(get_repo_version "$pkg" "$mirror")
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; 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.
# If the artifact for this version already exists in the channel, there is
# nothing to build regardless of which direction the versions differ.
if package_version_is_published "$REPO_ROOT/$mirror/$ARCH" "$pkg" "$pkgbuild_version" "$ARCH"; then
print_warning "$pkg $pkgbuild_version is already published; not queueing"
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 — this checkout is behind the channel; not queueing"
return 1
fi
@@ -182,12 +147,11 @@ check_package() {
check_mirror() {
local mirror="$1"
local state_file
state_file=$(sync_queue_file "$mirror" "$ARCH")
local state_file="$STATE_DIR/.sync-needed-$mirror"
local needs_build=false
local packages=()
print_info "Checking $mirror packages for $ARCH..."
print_info "Checking $mirror packages..."
while IFS= read -r pkg; do
local pkgdir="$PKGBUILDS_DIR/$pkg"
@@ -195,7 +159,7 @@ check_mirror() {
needs_build=true
packages+=("$pkg")
fi
done < <(packages_for_unscoped_build "$mirror" "$ARCH")
done < <(packages_for_unscoped_build "$mirror")
echo ""
@@ -204,23 +168,18 @@ check_mirror() {
# 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".
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 "State file created: $state_file"
else
print_info "${mirror^} ($ARCH) is up to date"
print_info "${mirror^} is up to date"
fi
echo ""
}
# One pass per published architecture: the version comparison reads that
# architecture's channel databases, and each queue is its own file.
for ARCH in $ARCHES; do
update_arch_paths
check_mirror edge
check_mirror rc
check_mirror stable
done
check_mirror edge
check_mirror rc
check_mirror stable
print_success "Version check complete!"
+7 -13
View File
@@ -1,27 +1,21 @@
#!/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
set -e
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
check_engine
print_header "Cleaning Container Builder Images"
print_header "Cleaning Docker Builder Images"
# Remove all 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.
print_info "Clearing build cache..."
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
"$CONTAINER_ENGINE" buildx prune -a -f
else
"$CONTAINER_ENGINE" image prune --build-cache -f
fi
# Clear buildx cache for these builds
print_info "Clearing buildx cache..."
docker buildx prune -a -f
print_success "Container builder cache cleared"
print_success "Docker builder cache cleared"
print_info "Next build will create fresh images"
+3 -7
View File
@@ -31,13 +31,9 @@ clean_packages() {
# Skip signature files
[[ "$pkg" == *.sig ]] && continue
# Format: name-version-release-arch.pkg.tar.*. Work from the right because
# package names can themselves contain version-like pieces (qt6-5compat,
# nvidia-580xx-utils), while pkgver and pkgrel cannot contain hyphens.
local stem="${pkg%%.pkg.tar.*}"
stem="${stem%-*}" # architecture
stem="${stem%-*}" # pkgrel
local pkgname="${stem%-*}" # pkgver
# Extract package name (remove version and architecture)
# Format: name-version-release-arch.pkg.tar.*
local pkgname=$(echo "$pkg" | sed -E 's/-[0-9]+.*-(any|x86_64|i686)\.pkg\.tar\..*//')
# Add to array
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/paths.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}"
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)
DEFAULT_RC_REF="quattro"
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
@@ -93,7 +92,7 @@ version_is_rc() { version_is_prerelease "$1"; }
pkgbuild_var() {
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
@@ -328,19 +327,13 @@ regenerate_checksums() {
trigger_build_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
# 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
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"
else
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
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:"
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
fi
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"
else
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]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
source "$BUILD_ROOT/helpers/paths.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}"
PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}"
# The first published architecture supplies the version-ordering floor when
# 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"
RC_DB_URL="$PKGS_DB_BASE/rc/x86_64/omarchy.db.tar.zst"
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
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
# the database cannot be read (callers must not mistake an outage for absence).
published_version() {
local channel="$1" arch="${2:-$OBSERVED_ARCH}" tmp descs
local channel="$1" tmp descs
tmp=$(mktemp) || return 2
# 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
# 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"
return 2
fi
@@ -212,14 +207,6 @@ published_version() {
' <<<"$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
# touching the working tree.
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 reset --hard origin/rc
cd /root/omarchy-pkgs-rc
# The pair is built once per published architecture (helpers/paths.sh on the
# host decides which), so every architecture'"'"'s rc channel carries the pins.
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
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check
'
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.
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
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
fi
# --arch all: the host advances every architecture it publishes, so a train
# never moves a channel for one architecture and not another.
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --arch all --skip-prod-check "$@"
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --skip-prod-check "$@"
}
wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds]
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 arch got pending
print_info "Waiting for $want in $channel for: $(published_arches | tr '\n' ' ')"
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got
print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..."
while ((waited < timeout)); do
pending=""
for arch in $(published_arches); do
got=$(published_version "$channel" "$arch" 2>/dev/null) || got=""
[[ "${got%-*}" == "$want" ]] || pending+=" $arch=${got:-unreachable}"
done
if [[ -z "$pending" ]]; then
print_success "$channel now serves omarchy $want on every published architecture"
got=$(published_version "$channel" 2>/dev/null) || got=""
if [[ "${got%-*}" == "$want" ]]; then
print_success "$channel now serves omarchy $got"
return 0
fi
sleep 60
@@ -321,7 +298,7 @@ wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds
printf '.' >&2
done
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."
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]
local version="$1" rc_flag="${2:-}" dir
dir=$(iso_checkout) || return 1
if [[ -z "$CONTAINER_ENGINE" ]] || ! "$CONTAINER_ENGINE" 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:"
if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
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"
return 1
fi
@@ -685,13 +662,15 @@ cmd_rc() {
if [[ -n "$pin" ]]; then
local pin_ver="${pin%% *}" pin_commit="${pin##* }"
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
if all_arches_at_version rc "$pin_ver"; then
print_success "$pin_ver is already cut from this head and published to rc on every architecture"
local pub
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"
return 0
fi
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"
maybe_iso "$pin_ver" rc "$iso_mode"
return 0
@@ -707,7 +686,7 @@ cmd_rc() {
new_ver="${new_pin%% *}"
print_success "Pinned $new_ver (rc branch pushed)"
trigger_rc_build || return 1
trigger_rc_build || true
if [[ "$wait" == true ]]; then
wait_for_published rc "$new_ver" || return 1
fi
@@ -729,7 +708,9 @@ cmd_ship() {
exit 1
fi
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
print_success "Nothing to ship — $version is tagged, released, and live on stable"
exit 0
@@ -771,8 +752,10 @@ cmd_ship() {
echo " omarchy-release rc"
exit 1
fi
if ! all_arches_at_version rc "$pin_ver"; then
print_error "$pin_ver is pinned but is not published for every architecture"
local pub
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."
exit 1
fi
@@ -816,9 +799,10 @@ cmd_ship() {
# 2. Final pins into rc. Resolve the tag we just established so the final
# PKGBUILDs record both its provenance and its exact commit.
local stable_pub
if all_arches_at_version rc "$version"; then
print_success "2/7 Final $version already published to rc on every architecture"
local rc_pub stable_pub
rc_pub=$(published_version rc 2>/dev/null) || rc_pub=""
if [[ "${rc_pub%-*}" == "$version" ]]; then
print_success "2/7 Final $version already published to rc"
else
if [[ "$pin_ver" != "$version" ]]; then
print_info "2/7 Pinning final $version from tag v$version..."
@@ -826,21 +810,22 @@ cmd_ship() {
else
print_info "2/7 Final $version pinned — re-triggering build"
fi
trigger_rc_build || exit 1
trigger_rc_build || true
wait_for_published rc "$version" || exit 1
fi
# 3. Promote rc -> stable
if all_arches_at_version stable "$version"; then
print_success "3/7 Stable already serves $version on every architecture"
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
if [[ "${stable_pub%-*}" == "$version" ]]; then
print_success "3/7 Stable already serves $version"
else
host_advance rc stable || exit 1
if ! all_arches_at_version stable "$version"; then
print_error "Promotion ran but stable does not serve $version on every architecture"
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
if [[ "${stable_pub%-*}" != "$version" ]]; then
print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing"
exit 1
fi
stable_pub=$(published_version stable 2>/dev/null) || stable_pub="$version"
print_success "3/7 Promoted to stable: omarchy $stable_pub on every architecture"
print_success "3/7 Promoted to stable: omarchy $stable_pub"
fi
# 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=""
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
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
echo "ship|$last_ver is tagged but not fully shipped — resume ship"
return
@@ -939,7 +924,7 @@ next_step() { # prints "<command>|<description>"
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
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"
else
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 "curl available" command -v curl
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
local ch arch
local ch
for ch in edge stable; do
for arch in $(published_arches); do
if published_version "$ch" "$arch" >/dev/null 2>&1; then
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"
if published_version "$ch" >/dev/null 2>&1; then
print_success "$ch channel db readable"
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
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
if host=$(repo_host); then
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
@@ -1035,10 +1016,10 @@ cmd_doctor() {
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"
fi
if [[ -n "$CONTAINER_ENGINE" ]] && "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
print_success "$CONTAINER_ENGINE available (ISO builds possible here)"
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
print_success "docker available (ISO builds possible here)"
else
print_warning "container engine unavailable — ISO builds will print instructions instead"
print_warning "docker unavailable — ISO builds will print instructions instead"
fi
echo ""
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 "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"
# 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 ""
if ((failures == 0)); then
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.
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
current/ Current checked-in package directory
Options:
--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
Examples:
@@ -75,13 +75,13 @@ if [[ ! -f "$METADATA" ]]; then
exit 1
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"
exit 1
fi
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.origin.aur // .aur // $package' "$METADATA")
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.origin.commit // .upstream_commit // ""' "$METADATA")}
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.aur // $package' "$METADATA")
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.upstream_commit // ""' "$METADATA")}
if [[ -z "$DEST_DIR" ]]; then
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
@@ -224,7 +224,7 @@ print_info "AUR package: $AUR_PACKAGE"
if [[ -n "$UPSTREAM_COMMIT" ]]; then
print_info "Upstream commit: $UPSTREAM_COMMIT"
else
print_warning "No origin.commit recorded; using AUR HEAD"
print_warning "No upstream_commit recorded; using AUR HEAD"
fi
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
# still matches, for pushing just one of them on purpose.
#
# Package identity comes from .PKGINFO rather than the filename or PKGBUILD: it
# is what makepkg actually recorded, and it handles epochs and split packages.
package_identity_of() {
# pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg
# actually recorded, and it needs no guessing about which directory built what.
pkgbase_of() {
bsdtar -xOf "$1" .PKGINFO 2>/dev/null |
awk -F ' = ' '
$1 == "pkgname" { name = $2 }
$1 == "pkgbase" { base = $2 }
$1 == "pkgver" { version = $2 }
END { print name "\t" base "\t" version }
'
awk -F ' = ' '$1 == "pkgbase" { print $2; exit }'
}
FILES=()
@@ -166,18 +161,14 @@ if [[ -z "$PACKAGES" ]]; then
FILES=("${ALL_FILES[@]}")
else
declare -A MATCHED=()
declare -A LATEST_FILE=()
declare -A LATEST_VERSION=()
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
if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then
FILES+=("$file")
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
fi
done
@@ -190,8 +181,6 @@ else
exit 1
fi
done
mapfile -t FILES < <(printf '%s\n' "${LATEST_FILE[@]}" | sort)
fi
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
# this run. Absent for a manual run, which is fine — the report just omits it.
QUEUED_PACKAGES=""
QUEUE_FILE=$(sync_queue_file "$MIRROR" "$ARCH")
# 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
QUEUE_FILE="${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-$MIRROR"
[[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE")
if [[ "$DRY_RUN" != true ]]; then
@@ -138,43 +134,11 @@ if [[ "$DRY_RUN" == true ]]; then
else
print_info "Step 1/6: Building packages..."
fi
BUILD_RESULT_DIR=$(mktemp -d)
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
"$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || {
print_error "Build failed"
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
exit 1
fi
}
if [[ "$DRY_RUN" == true ]]; then
echo ""
@@ -187,12 +151,6 @@ BUILT_COUNT=$(grep -c '' <<<"$BUILT_FILES")
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
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
echo ""
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+="$(format_package_list_html "$BUILT_FILES")"
summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/"
if [[ "$partial" == true ]]; then
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
notify_success "Release published: $MIRROR" "$summary"
else
# Rare by construction: a release only runs when the version check queued
# work, so publishing nothing means the check and the builder disagreed
@@ -275,10 +224,4 @@ else
fi
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!"
+10 -15
View File
@@ -62,8 +62,8 @@ if [[ ! -d "$REPO_DIR" ]]; then
exit 1
fi
# Check the selected container engine is available
check_engine
# Check Docker is available
check_docker
# Find package files to confirm before running Docker
cd "$REPO_DIR"
@@ -88,29 +88,24 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
# repo-remove is architecture-independent, so use a host-native edge image.
TOOL_ARCH=$(docker_native_arch) || {
print_error "Unsupported host architecture: $(uname -m)"
exit 1
}
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "edge"
# Build/update the Docker image (always use x86_64 for removal - it's architecture independent)
# repo-remove is mirror-independent — always use the edge x86_64 image
build_docker_image "$BUILD_DIR" "x86_64" "edge"
acquire_release_lock || exit 1
print_info "Removing package..."
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
make_dir_writable "$REPO_DIR"
fi
# Ensure directory is writable by container user
make_dir_writable "$REPO_DIR"
# Run the removal script in the host-native image.
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
# Run the removal script in Docker (always use x86_64 image)
docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \
-v "$REPO_ROOT:/pkgs.omarchy.org" \
-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=$?
+21 -58
View File
@@ -15,8 +15,6 @@ set -e
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
source "$BUILD_ROOT/helpers/message-helpers.sh"
source "$BUILD_ROOT/helpers/docker-helpers.sh"
source "$BUILD_ROOT/helpers/paths.sh"
CHECK_ONLY=false
SKIP_TIMERS=false
@@ -63,12 +61,10 @@ if command -v apt-get >/dev/null 2>&1; then
DISTRO="debian"
PKG_BSDTAR="libarchive-tools"
PKG_DOCKER="docker.io"
PKG_PODMAN="podman"
elif command -v pacman >/dev/null 2>&1; then
DISTRO="arch"
PKG_BSDTAR="libarchive"
PKG_DOCKER="docker"
PKG_PODMAN="podman"
else
print_error "Unsupported distribution — need apt-get or pacman"
exit 1
@@ -76,36 +72,18 @@ fi
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
print_error "Run as root (installing packages and systemd units)"
exit 1
fi
# The release timers are systemd units. Say so plainly rather than failing
# later on a missing command — a container is the usual way to end up here,
# and it cannot be a repository host.
# Docker and the release timers are both systemd units. Say so plainly rather
# than failing later on a missing command — a container is the usual way to end
# up here, and it cannot be a repository host.
if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then
print_error "systemctl not found — the repository host must run systemd"
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."
exit 1
fi
@@ -161,27 +139,24 @@ else
fi
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
# replacing that underneath a working builder would be a poor trade for
# tidiness.
print_info "Checking $ENGINE_NAME..."
print_info "Checking Docker..."
if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
print_step "$CONTAINER_ENGINE present: $("$CONTAINER_ENGINE" --version 2>/dev/null | head -1)"
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
print_success "$ENGINE_NAME is installed and available — leaving it alone"
if command -v docker >/dev/null 2>&1; then
print_step "docker present: $(docker --version 2>/dev/null | head -1)"
if docker info >/dev/null 2>&1; then
print_success "Docker is installed and running — leaving it alone"
elif [[ "$CHECK_ONLY" == true ]]; then
print_warning "$ENGINE_NAME is installed but unavailable"
elif [[ "$CONTAINER_ENGINE" == "podman" ]]; then
print_error "Podman is installed but unavailable to the current user"
exit 1
print_warning "Docker is installed but not running; would start it"
else
print_info "Docker is installed but not running — starting it"
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"
else
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
elif [[ "$CHECK_ONLY" == true ]]; then
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
print_warning "Would install $PKG_ENGINE and enable it"
else
print_warning "Would install $PKG_ENGINE"
fi
print_warning "Would install $PKG_DOCKER and enable it"
else
print_info "Installing $PKG_ENGINE..."
install_packages "$PKG_ENGINE"
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
systemctl enable --now docker.service
fi
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
print_success "$ENGINE_NAME installed and available"
print_info "Installing $PKG_DOCKER..."
install_packages "$PKG_DOCKER"
systemctl enable --now docker.service
if docker info >/dev/null 2>&1; then
print_success "Docker installed and running"
else
print_error "$ENGINE_NAME installed but not responding"
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
echo " Check 'systemctl status docker' — builds cannot run without it."
fi
print_error "Docker installed but not responding"
echo " Check 'systemctl status docker' — builds cannot run without it."
exit 1
fi
fi
@@ -270,10 +237,6 @@ echo ""
# --- 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)
if [[ "$SKIP_TIMERS" == true ]]; then
+9 -16
View File
@@ -52,8 +52,8 @@ if [[ ! -d "$BUILD_OUTPUT_DIR" ]]; then
exit 1
fi
# Check the selected container engine is available
check_engine
# Check Docker is available
check_docker
# Check GPG credentials are in environment
if [[ -z "$GPG_PRIVATE_KEY" ]]; then
@@ -66,30 +66,23 @@ if [[ -z "$GPG_PASSPHRASE" ]]; then
exit 1
fi
# Signing is architecture-independent, so run its utility container natively
# on either an x86_64 or ARM host.
TOOL_ARCH=$(docker_native_arch) || {
print_error "Unsupported host architecture: $(uname -m)"
exit 1
}
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "$MIRROR"
# Build/update the Docker image (always use x86_64 for signing - it's architecture independent)
build_docker_image "$BUILD_DIR" "x86_64" "$MIRROR"
print_info "Running package signing..."
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
make_dir_writable "$BUILD_OUTPUT_DIR"
fi
# Ensure output directory is writable by container user
make_dir_writable "$BUILD_OUTPUT_DIR"
# Run the signing script in the host-native image.
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
# Run the signing script in Docker (always use x86_64 image)
docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \
-e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \
-e GPG_PASSPHRASE="$GPG_PASSPHRASE" \
-v "$BUILD_ROOT/build-output:/build-output" \
-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=$?
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 -416
View File
@@ -17,14 +17,11 @@ SELF_TEST=false
# rebuilding for it would ship a package built against the wrong ABI.
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_MIRRORS=(edge stable)
# 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)
PUBLISHED_ARCH=x86_64
usage() {
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"] }
The versions the current pkgrel was bumped for are recorded per published
architecture in rebuilt_against, and written by this command:
The versions the current pkgrel was bumped for are recorded alongside, in
rebuilt_against, and written by this command:
{ "rebuild_on": ["qt6-base"], "rebuilt_against": {
"x86_64": { "qt6-base": "6.11.2-3" },
"aarch64": { "qt6-base": "6.11.2-2" }
} }
{ "rebuild_on": ["qt6-base"], "rebuilt_against": { "qt6-base": "6.11.2-2" } }
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
@@ -66,11 +60,8 @@ Examples:
$0 # Update every package that declares rebuild_on
$0 quickshell-git # Update specific packages
x86_64 trigger versions come from the local pacman database; aarch64 versions
come from the live Arch Linux ARM repository database. A trigger this
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.
Trigger versions are read from the local pacman database, so sync it first
(pacman -Sy) or this reports whatever that database last saw.
EOF
}
@@ -103,7 +94,7 @@ SPECIFIC_MODE=false
# 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
# gets to say which package was left alone instead of the run dying here.
native_repo_version() {
repo_version() {
local package="$1"
local info
@@ -117,61 +108,9 @@ native_repo_version() {
' <<<"$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 EDGE_VERSION=()
declare -A EDGE_READ=()
PUBLISHED_LOADED=false
PUBLISHED_AVAILABLE=true
remember_published() {
local name="$1"
@@ -192,44 +131,34 @@ load_published_versions() {
[[ "$PUBLISHED_LOADED" == true ]] && return 0
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
db="$TEMP_DIR/published-$mirror-$arch.db.tar.zst"
for mirror in "${PUBLISHED_MIRRORS[@]}"; do
db="$TEMP_DIR/published-$mirror.db.tar.zst"
if ! curl -fsSL --max-time 120 -o "$db" \
"$PUBLISHED_BASE_URL/$mirror/$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"
continue
fi
if ! tar -tf "$db" >/dev/null 2>&1; then
print_warning "Unreadable published $mirror/$arch database; bumps are not checked against it this run"
continue
fi
[[ "$mirror" == edge ]] && EDGE_READ["$arch"]=1
if ! curl -fsSL --max-time 120 -o "$db" \
"$PUBLISHED_BASE_URL/$mirror/$PUBLISHED_ARCH/omarchy.db.tar.zst" 2>/dev/null; then
print_warning "Could not read the published $mirror database; bumps are not checked against it this run"
PUBLISHED_AVAILABLE=false
continue
fi
while IFS=$'\t' read -r name base version; do
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
if [[ "$mirror" == edge && -n "$version" ]]; then
[[ -z "$name" ]] || EDGE_VERSION["$arch/$name"]="$version"
[[ -z "$base" ]] || EDGE_VERSION["$arch/$base"]="$version"
fi
done < <(
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
function emit() {
if (name != "" && version != "") print name "\t" 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() }
'
)
done
while IFS=$'\t' read -r name base version; do
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
done < <(
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
function emit() {
if (name != "" && version != "") print name "\t" 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() }
'
)
done
}
@@ -240,29 +169,6 @@ published_version() {
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.
version_pkgver() {
local version="${1#*:}"
@@ -273,7 +179,7 @@ pkgbuild_field() {
local package_dir="$1"
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
@@ -346,17 +252,7 @@ record_triggers() {
local package_dir="$1"
local current="$2"
# A flat record is the legacy x86_64 shape. Preserve records for
# 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"
write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current"
}
# Metadata that does not parse would otherwise drop its package out of the run
@@ -404,61 +300,21 @@ sync_package() {
print_info "Checking $package against ${triggers[*]}..."
local current="{}" arch arch_current trigger version carried considered=0
for arch in $(ci_arches); do
package_supports_arch "$package_dir" "$arch" || continue
considered=$((considered + 1))
arch_current="{}"
for trigger in "${triggers[@]}"; do
if carried=$(carried_trigger_dir "$arch" "$trigger"); then
if ! version=$(carried_version "$arch" "$carried"); then
print_error " Could not read the $arch version of $trigger from its recipe; leaving $package alone"
((++FAILED))
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"
local current="{}" trigger version
for trigger in "${triggers[@]}"; do
version=$(repo_version "$trigger")
if [[ -z "$version" ]]; then
print_error " $trigger is in no official repository; leaving $package alone"
((++FAILED))
return 0
fi
if ! current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$current"); then
print_error " Could not record $trigger $version for $package"
((++FAILED))
return 0
fi
done
if ((considered == 0)); then
print_info " Skipping: not built for any published architecture"
((++SKIPPED))
return 0
fi
local recorded
if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then
print_error " Could not read .omarchy/package.json for $package"
@@ -467,20 +323,13 @@ sync_package() {
fi
[[ -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
# not carry reads as changed instead of going unexamined forever.
local moved
if ! moved=$(jq -r --argjson recorded "$recorded" '
[to_entries[] as $arch
| $arch.value | to_entries[] as $trigger
| select($recorded[$arch.key][$trigger.key] != $trigger.value)
| "\($arch.key)/\($trigger.key) \($recorded[$arch.key][$trigger.key] // "unrecorded") -> \($trigger.value)"]
to_entries
| map(select($recorded[.key] != .value)
| "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)")
| join(", ")
' <<<"$current"); then
print_error " Could not compare recorded trigger versions for $package"
@@ -489,7 +338,7 @@ sync_package() {
fi
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))
return 0
fi
@@ -535,7 +384,6 @@ sync_package() {
# checkout that has fallen behind the repository can otherwise be bumped to
# something pacman orders below what it would replace.
local floor
load_published_versions
floor=$(published_version "$package")
if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then
if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then
@@ -618,11 +466,11 @@ selftest_root() {
}
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"
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"
}
@@ -641,97 +489,44 @@ STUB
chmod +x "$root/stub/pacman"
}
# A repository database assembled by hand from name=version pairs.
selftest_db() {
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
# Serves a repository database assembled by hand from name=version pairs. With
# none given the stub fails, which is how the unreachable-repository path is
# exercised.
selftest_published() {
local root="$1"
shift
local staging="$root/stub/db"
local entry name version
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
cat > "$root/stub/curl" <<'STUB'
#!/bin/bash
out=""
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) out="$2"; shift 2 ;;
*) url="$1"; shift ;;
*) shift ;;
esac
done
case "$url" in
*/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
db="$(dirname "$0")/omarchy.db.tar.zst"
[[ -f "$db" && -n "$out" ]] || exit 22
cp "$db" "$out"
STUB
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() {
local failures=0
local root
@@ -746,15 +541,11 @@ cmd_self_test() {
fi
}
# SELFTEST_ARCHES stands in for CI_ARCHES; "default" leaves it unset, as the
# scheduled workflow does.
run_case() {
local root="$1"
shift
local status=0 arches=(CI_ARCHES="${SELFTEST_ARCHES:-x86_64}")
[[ "${SELFTEST_ARCHES:-}" != default ]] || arches=(-u CI_ARCHES)
env "${arches[@]}" PATH="$root/stub:$PATH" \
"$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
local status=0
PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
echo "$status"
}
@@ -772,7 +563,7 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")"
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:"
root=$(selftest_root fresh)
@@ -782,7 +573,7 @@ cmd_self_test() {
check "run succeeds" 0 "$(run_case "$root")"
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")"
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:"
root=$(selftest_root current)
@@ -821,138 +612,6 @@ cmd_self_test() {
check "suffix recorded for the next AUR sync" 1 \
"$(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 ""
if [[ "$failures" -eq 0 ]]; then
print_success "Self-test passed"
@@ -967,9 +626,9 @@ if [[ "$SELF_TEST" == true ]]; then
exit $?
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
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
fi
done
+23 -486
View File
@@ -9,10 +9,8 @@ source "$BUILD_ROOT/helpers/upstream-github.sh"
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache"
SPECIFIC_PACKAGES=()
LANE=all
usage() {
cat <<EOF
@@ -20,11 +18,11 @@ Usage: $0 [PACKAGE...]
Update packages that track an upstream vendor release feed instead of the AUR.
Packages opt in declaratively through "upstream" in .omarchy/package.json.
Providers cover GitHub Releases with checksum manifests or API asset digests,
semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See
README.md for the schemas. Anything outside those conventions may provide
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout:
A package whose upstream ships tagged GitHub releases with a checksum manifest
opts in declaratively, via "upstream" in .omarchy/package.json (see
helpers/upstream-github.sh for the schema); no code needed. Anything with a
bespoke feed provides pkgbuilds/<package>/.omarchy/upstream.sh instead, a hook
that reports the newest upstream release as JSON on stdout:
{
"pkgver": "1.2.3",
@@ -50,14 +48,6 @@ the bypass, so the resulting change still goes through a reviewed PR.
Arguments:
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:
self-test Run the offline fixture tests for release selection, the
quarantine backstop, and metadata parsing
@@ -74,14 +64,6 @@ while [[ $# -gt 0 ]]; do
usage
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"
exit 1
@@ -158,22 +140,17 @@ set_pkgbuild_array() {
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
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 {
while ((getline line < block) > 0) print line
close(block)
replaced = 1
# 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
# every line up to the next ")". A ")" inside a comment -- "# sidecar
# (new in v0.7.5)" -- closes nothing, and ending the skip there would
# leave the rest of the old array behind a stray ")".
if (index(code(substr($0, length(prefix) + 1)), ")") == 0) skipping = 1
# every line up to the next ")".
if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
next
}
skipping { if (code($0) ~ /\)/) skipping = 0; next }
skipping { if ($0 ~ /\)/) skipping = 0; next }
{ print }
' "$pkgbuild" > "$rewritten"; then
print_error "Failed to rewrite ${name} in $pkgbuild"
@@ -355,33 +332,27 @@ sync_package() {
return 0
fi
if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then
print_info "Skipping $package: upstream updates held by sync=false"
((++SKIPPED))
return 0
fi
local provider has_upstream=false
provider=$(package_upstream_provider "$package_dir")
local github_repo has_upstream=false
github_repo=$(package_upstream_github_repo "$package_dir")
if package_has_upstream_provider "$package_dir"; then
has_upstream=true
fi
# A present-but-unusable declaration fails loudly; treating it like "no
# upstream source" would silently drop the package from scheduled runs.
if [[ "$has_upstream" == true && -z "$provider" ]]; then
print_error "Package $package has an unusable or ambiguous upstream declaration"
if [[ "$has_upstream" == true && -z "$github_repo" ]]; then
print_error "Package $package has an unusable upstream declaration (needs a github owner/repo)"
((++FAILED))
return 0
fi
if [[ -n "$provider" && -f "$hook" ]]; then
print_error "Package $package declares both an upstream provider and an upstream.sh hook; keep exactly one"
if [[ -n "$github_repo" && -f "$hook" ]]; then
print_error "Package $package declares both upstream.github and an upstream.sh hook; keep exactly one"
((++FAILED))
return 0
fi
if [[ -z "$provider" && ! -f "$hook" ]]; then
if [[ -z "$github_repo" && ! -f "$hook" ]]; then
if [[ "$SPECIFIC_MODE" == true ]]; then
print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh"
((++FAILED))
@@ -401,31 +372,10 @@ sync_package() {
print_info "Checking $package for upstream releases..."
if [[ "$provider" == watch ]]; then
local result
if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then
print_error "Upstream watch 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"
local release
if [[ -n "$github_repo" ]]; then
if ! release=$(github_upstream_release "$package_dir" "$min_age"); then
print_error "GitHub release provider failed for $package"
((++FAILED))
return 0
fi
@@ -511,8 +461,8 @@ sync_package() {
# paths. Covers release selection (fallback past quarantined releases,
# draft/prerelease filtering, bypass, unchanged version), failure paths
# (unusable tags/timestamps, missing checksums), checksum template mapping
# for both architectures, release API digests, the min_release_age backstop,
# the duration parser, and manifest validation.
# for both architectures, the min_release_age backstop, the duration parser,
# and manifest validation.
cmd_self_test() {
local failures=0
@@ -589,31 +539,6 @@ EOF
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")"
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>"
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=$?
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:"
local rel st
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"
vst=0; validate_package_metadata "$agepkg" >/dev/null || 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"
vst=0; validate_package_metadata "$agepkg" >/dev/null || 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
# PKGBUILD, the full sync_package path (selection, validation, backstop,
# 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))"
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 ""
if [[ "$failures" -eq 0 ]]; then
print_success "Self-test passed"
@@ -1062,100 +683,16 @@ if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 && "${SPECIFIC_PACKAGES[0]}" == "self-test"
exit 0
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
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
selected["$package"]=1
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"
sync_package "$package"
done
else
while IFS= read -r package; do
sync_in_lane "$package"
sync_package "$package"
done < <(packages_for_upstream_sync)
fi
enforce_branch_lockstep
echo ""
if [[ $FAILED -gt 0 ]]; then
+32 -38
View File
@@ -84,18 +84,15 @@ echo ""
# --- 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
for channel in edge rc stable; do
for arch in $(published_arches); do
state_file=$(sync_queue_file "$channel" "$arch")
[[ "$arch" == "x86_64" && ! -f "$state_file" ]] && state_file=$(legacy_sync_queue_file "$channel")
if [[ -f "$state_file" ]]; then
queued=true
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
printf ' • %-7s %-8s queued since %s\n' "$channel" "$arch" "$since"
fi
done
state_file="$STATE_DIR/.sync-needed-$channel"
if [[ -f "$state_file" ]]; then
queued=true
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
printf ' • %-7s queued since %s\n' "$channel" "$since"
fi
done
[[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)"
echo ""
@@ -104,37 +101,34 @@ echo ""
# say so plainly: it is queued but deliberately not being retried yet.
paused=false
for channel in edge rc stable; do
for arch in $(published_arches); do
fail_file=$(sync_fail_file "$channel" "$arch")
[[ "$arch" == "x86_64" && ! -f "$fail_file" ]] && fail_file=$(legacy_sync_fail_file "$channel")
[[ -f "$fail_file" ]] || continue
if [[ "$paused" == false ]]; then
print_error "Failing builds (backoff active)"
paused=true
fi
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
# shellcheck disable=SC1090
source "$fail_file" 2>/dev/null || true
delay=600
for ((i = 1; i < FAILURE_COUNT; i++)); do
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}"
fail_file="$STATE_DIR/.build-failed-$channel"
[[ -f "$fail_file" ]] || continue
if [[ "$paused" == false ]]; then
print_error "Failing builds (backoff active)"
paused=true
fi
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
# shellcheck disable=SC1090
source "$fail_file" 2>/dev/null || true
delay=600
for ((i = 1; i < FAILURE_COUNT; i++)); do
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 %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
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 ""
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/lock-helpers.sh"
# Function to update repository database using the selected container engine
# Function to update repository database using Docker
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
if [[ ! -d "$REPO_DIR" ]]; then
@@ -24,25 +25,20 @@ update_database() {
exit 1
fi
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
make_dir_writable "$REPO_DIR"
fi
# Make output directory writable for container
make_dir_writable "$REPO_DIR"
# repo-add is architecture- and mirror-independent. Use the host-native edge
# image, which also lets bootstrap-rc run before that channel exists remotely.
local tool_arch
tool_arch=$(docker_native_arch) || {
print_error "Unsupported host architecture: $(uname -m)"
exit 1
}
build_docker_image "$BUILD_DIR" "$tool_arch" "edge"
# repo-add is architecture- and mirror-independent, so always use the edge
# x86_64 image. This also lets bootstrap-rc build the rc database before the
# rc channel exists remotely (an rc image can only build after it does).
build_docker_image "$BUILD_DIR" "x86_64" "edge"
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$tool_arch")" \
docker run --rm --platform linux/amd64 \
-e ARCH="$ARCH" \
-e MIRROR="$MIRROR" \
-v "$REPO_ROOT:/output" \
-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
+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; \
fi; \
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
# 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)
# 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
# stale legacy layout). It is always taken from the x86_64 tree, whatever the
# 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.
# stale legacy layout); %s keeps pacman's $arch literal while MIRROR expands.
ARG MIRROR=edge
RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \
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 -S --noconfirm omarchy-keyring && \
pacman-key --populate omarchy && \
@@ -129,7 +126,6 @@ RUN pacman -Syu --noconfirm && \
wget \
curl \
jq \
rclone \
gnupg && \
pacman -Scc --noconfirm && \
rm -rf /var/cache/pacman/pkg/*
@@ -141,14 +137,8 @@ RUN useradd -m -G wheel -s /bin/bash builder && \
chmod 700 /home/builder/.gnupg && \
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 && \
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
sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf
# Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust).
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
+145 -246
View File
@@ -1,88 +1,21 @@
#!/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.
# Explicit --package selections may build packages with skip_build=true.
# Setup directories
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}
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}
BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
HELPERS_DIR=${HELPERS_DIR:-/helpers}
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"
# 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 [[ -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
/build/import-gpg-keys.sh || exit 1
@@ -94,66 +27,49 @@ if [[ "$DRY_RUN" != true ]]; then
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
# Done before the Omarchy repos are added so only core/extra participate.
echo "==> Updating build container packages..."
sudo pacman -Syu --noconfirm || exit 1
sudo pacman -Syu --noconfirm
# Configure Omarchy repositories for dependency resolution
echo "==> Configuring Omarchy repositories for dependency resolution..."
# Always add omarchy-build repo first (for incremental builds). Repository
# order is pacman's priority order, so this must precede the official repos;
# otherwise pacman can select an older official package with the same name.
# 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
# Always add omarchy-build repo (for incremental builds)
# Packages in build-output are unsigned, so use SigLevel = Never
sudo tee -a /etc/pacman.conf > /dev/null <<EOF
[omarchy-build]
SigLevel = Never
Server = file://$BUILD_OUTPUT_DIR
EOF
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
# 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
# Create an empty database
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1 || exit 1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
fi
# Fold any packages already in the workspace into the database, whether
# they came with an existing database or were dropped in by an earlier
# 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
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
ln -sf omarchy-build.db.tar.zst omarchy-build.db
else
# Database exists, check if we need to rebuild it from packages
if ls *.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | grep -q .; then
echo "==> Rebuilding build database from existing packages..."
repo-add omarchy-build.db.tar.zst "${staged_packages[@]}" >/dev/null 2>&1 || exit 1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || 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
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
touch "$BUILD_PLAN_DIR/repository-initialized" || exit 1
# Add omarchy repo if it has a database (stable packages). The local tree
# is trusted as-is; the public channel is verified against the omarchy
# keyring the image already carries.
# Add omarchy repo if it has a database (stable packages)
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"
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
# Sync pacman database
sudo pacman -Sy || exit 1
sudo pacman -Sy
fi
echo "==> Package Builder"
@@ -166,6 +82,8 @@ if [[ "$DRY_RUN" == true ]]; then
echo "==> Dry run: yes (plan only; makepkg will not run)"
fi
FAILED_PACKAGES=""
SUCCESSFUL_PACKAGES=""
SKIPPED_PACKAGES=""
# Find package directory
@@ -187,10 +105,10 @@ LOCAL_VERSION_CACHE_LOADED=false
LOCAL_VERSION_CACHE_DB=""
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
db="$PUBLISHED_DB_DIR/omarchy.db"
db="$FINAL_OUTPUT_DIR/omarchy.db"
fi
[[ -f "$db" ]] || return 0
@@ -237,9 +155,26 @@ get_local_version() {
# Returns 0 (success) if should build, 1 if should skip
should_build_for_arch() {
local pkg="$1"
local pkgdir
pkgdir=$(find_package_dir "$pkg")
[[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH"
local current_arch="$ARCH"
local pkgdir=$(find_package_dir "$pkg")
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
@@ -289,62 +224,22 @@ refresh_vcs_pkgver_preserving_local_pkgrel() {
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_package() {
local pkg="$1"
local pkgdir
pkgdir=$(find_package_dir "$pkg") || return 1
local pkgdir=$(find_package_dir "$pkg")
echo ""
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
cd /src || return 1
rm -rf "$pkg" || return 1
cp -r "$pkgdir" "$pkg" || return 1
cd /src
rm -rf "$pkg"
cp -r "$pkgdir" "$pkg"
cd "/src/$pkg" || return 1
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1
}
@@ -353,6 +248,7 @@ build_package() {
if [[ -z "$pkgbuild_version" ]]; then
echo " Failed to read PKGBUILD version"
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1
fi
@@ -384,72 +280,37 @@ build_package() {
# Build package without signing (signing is done separately)
# 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
local -a 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
MAKEPKG_FLAGS="-scf --noconfirm"
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
mkdir -p "$BUILD_OUTPUT_DIR"
# Copy only the artifacts makepkg declares as outputs. A PKGBUILD may use
# another pacman package as a source (schist-bin does); a *.pkg.tar.* glob
# 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")
for pkg_file in *.pkg.tar.*; do
[[ -f "$pkg_file" ]] && cp "$pkg_file" "$BUILD_OUTPUT_DIR/"
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
repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 || return 1
ln -sf omarchy-build.db.tar.zst omarchy-build.db || 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
sudo pacman -Sy >/dev/null 2>&1
fi
# A release may publish successful builds even when a peer fails. Record
# 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
cd /src/$pkg
echo " Successfully built $pkg"
SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $pkg"
return 0
else
echo " Makepkg failed for $pkg"
echo " DEBUG: Files in build directory:"
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
return 1
fi
}
@@ -464,17 +325,11 @@ get_package_deps() {
return
fi
# Include test dependencies and target-specific arrays: each container must
# receive its prerequisites through the repository, not a previous build.
# Extract depends and makedepends, filter for packages in our pkgbuilds/
(
CARCH="$ARCH"
source "$pkgbuild" 2>/dev/null
for kind in depends makedepends checkdepends; do
generic="${kind}[@]"
specific="${kind}_${CARCH}[@]"
printf '%s\n' "${!generic}" "${!specific}"
done
) | awk 'NF && !seen[$0]++' | while read -r dep; do
echo "${depends[@]} ${makedepends[@]}"
) | tr ' ' '\n' | while read -r dep; do
# Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade')
dep=$(echo "$dep" | sed 's/[<>=].*$//')
# Check if this dependency exists in our pkgbuilds
@@ -559,37 +414,27 @@ check_needs_build() {
if [[ "$local_version" == "$pkgbuild_version" ]]; then
return 1 # Already up to date
else
return 0 # Needs building
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() {
packages_for_unscoped_build "$MIRROR" "$ARCH"
packages_for_unscoped_build "$MIRROR"
}
# Main execution
if [[ "$DRY_RUN" != true ]]; then
cd "$SRC_DIR" || exit 1
build_package "$BUILD_PACKAGE"
exit $?
cd "$SRC_DIR"
fi
TOTAL_COUNT=0
echo "==> Checking which packages need building..."
# First pass: determine which packages need building
PACKAGES_TO_BUILD=()
ORDERED_PACKAGES=()
PLANNED_DEPENDENCIES=()
# If PACKAGES is specified, only check those packages
if [[ -n "$PACKAGES" ]]; then
@@ -628,6 +473,13 @@ if [[ -n "$PACKAGES" ]]; then
else
# Build all packages that need updates from the relevant directories
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
PACKAGES_TO_BUILD+=("$pkg")
else
@@ -640,7 +492,7 @@ fi
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
echo "==> All packages are up to date!"
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..."
# Second pass: order only the packages that need building
@@ -663,7 +515,6 @@ else
((unmet_deps_count[$pkg]++))
# Track that dep blocks pkg from building
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
PLANNED_DEPENDENCIES+=("$pkg $dep")
fi
done
done < <(get_package_deps "$pkg")
@@ -678,6 +529,7 @@ else
done
# Build packages as dependencies become available
ORDERED_PACKAGES=()
while [[ ${#ready_to_build[@]} -gt 0 ]]; do
# Take the first ready package
current="${ready_to_build[0]}"
@@ -699,16 +551,63 @@ else
exit 1
fi
echo "==> Build order: ${ORDERED_PACKAGES[*]}"
fi
echo "==> Build order: ${ORDERED_PACKAGES[@]}"
# The host consumes plain data, never shell code or parsed human log output.
if [[ -n "$BUILD_PLAN_DIR" ]]; then
mkdir -p "$BUILD_PLAN_DIR" || exit 1
printf '%s\n' "${ORDERED_PACKAGES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/packages" || exit 1
printf '%s\n' "${PLANNED_DEPENDENCIES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/dependencies" || exit 1
printf '%s\n' $SKIPPED_PACKAGES | sed '/^$/d' > "$BUILD_PLAN_DIR/skipped" || exit 1
if [[ "$DRY_RUN" == true ]]; then
echo ""
echo "==> Dry run complete. Packages that would build: ${ORDERED_PACKAGES[@]}"
exit 0
fi
# 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
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!"
-96
View File
@@ -1,96 +0,0 @@
# CI spike: build PRs on ephemeral DigitalOcean droplets
Status: spike. Nothing here publishes. The repository host keeps building and
signing on merge exactly as before.
## Pieces
- `.github/workflows/build-pr.yml` — on a PR touching `pkgbuilds/**`, one job
per changed package on runners labelled `omarchy-builder`. aarch64 jobs
run on GitHub's native `ubuntu-24.04-arm` runners instead. Uploads the unsigned
`.pkg.tar.zst` as a workflow artifact (7 days).
- `runner-cloud-init.yaml` — Ubuntu 24.04 user-data: docker + buildx, the
GitHub runner registered `--ephemeral`, runs one job, powers off.
- `controller.sh` — systemd timer every minute on a small always-on droplet.
Polls for queued jobs with our label, creates one g5-32vcpu-64gb-50gb droplet per job up
to `MAX_DROPLETS`, deletes droplets that are powered off or older than
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`. Builders go in any region DigitalOcean lists the size in
stock in (`REGIONS` only sets which to try first); a refused create, logged
with DigitalOcean's message, falls back to the next region, then the next
of `SIZES`. No inbound endpoint. Plain curl against both APIs, no
doctl and no gh: a token in the environment cannot pick the wrong account
the way a saved doctl context can. Needs curl and jq.
`tests/controller.sh` exercises every decision against canned responses.
- `controller-box/` — the always-on droplet: unit, timer, env template,
cloud-init, and `create.sh` to stand it up with one API call.
## Standing up the controller box
DIGITALOCEAN_TOKEN=<omarchy account> GITHUB_TOKEN=<fine-grained PAT> \
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh <branch>
The GitHub PAT is fine-grained, scoped to the one repo: Actions read,
Administration read+write (registration tokens). The DO token is baked into
the box's env file, so it is the account that pays for builder droplets.
Watch it with `journalctl -u omarchy-controller -f` on the box.
Each tick pulls the box's checkout first, so a merged `controller.sh` is live
within a minute. The unit and timer are copies made at creation; after
changing them, on the box:
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
systemctl daemon-reload
## What the spike proved (2026-09-17, fork ryanrhughes/omarchy-pkgs)
- `bin/build` works from a bare clone: with no local published tree it
plans against and resolves from `https://pkgs.omarchy.org/<mirror>/<arch>`.
- Droplet create → runner registered: ~70 s. omarchy-fish PR job: 2 min
including the builder image build. Droplet powers off after the job.
- linux-omarchy on a c-32 droplet: 30 min wall clock for the build job
(23:39 → 00:09), 254 MB artifact. Cold start ~90 s before the job began.
- A PR whose PKGBUILD fails to build turns the required check red and GitHub
refuses the merge (`mergeStateStatus=BLOCKED`, `gh pr merge` refuses
without `--admin`).
- Controller: one queued job + one busy droplet ⇒ creates exactly one more;
reaps powered-off droplets on the next tick.
## Not done (required before this touches the real repo)
- Tooling from base: check out master's `bin/ helpers/ build/` and overlay
only the PR's `pkgbuilds/<name>`; today a PR can edit the build script
and it runs on the droplet. The vouch gate limits who can do that, not
what they can do.
- DigitalOcean cloud firewall on the `omarchy-builder` tag: no inbound, no
egress to private ranges or the metadata address.
- A fine-grained GitHub token for the real repository (the one on the
controller box is scoped to the fork), and the publish environment's
secrets set there.
- Disable the host's auto-release timers for any channel CI publishes to,
so two writers never touch one database.
## Done since the spike README was first written
- Controller as a systemd timer on its own droplet, plain curl, self-test.
- Build once against edge; one artifact per package per architecture,
published into every channel it belongs to (fast ring: all three at
once). arch=any builds once for every architecture database.
- Publish is incremental and immutable: pull the channel db, refuse
different bytes under an existing name, accept identical bytes, upload
packages then signatures then the db.
- aarch64 under QEMU with credential-preserving binfmt. PR builds now run
aarch64 natively on `ubuntu-24.04-arm` (QEMU was up to ~15x slower). When a
merged tree has no artifact, publish.yml rebuilds it in its own job, aarch64
there and x86_64 on a droplet, outside the publish lock.
- Publish itself builds nothing: it signs and uploads on `ubuntu-latest` in the
tested builder image pulled from GHCR, and only that job holds the `publish`
concurrency group, so a merge waits for seconds of signing, not for builds.
- Vouch gate: collaborators, `.github/VOUCHED.td`, or the `build-approved`
label; denounced authors cannot be overridden by the label.
- Tests run on PRs only; `result`, `self-tests`, `build-isolation` are the
required checks with strict up-to-date branches.
## Cleanup
doctl compute droplet list --tag-name omarchy-builder
doctl compute droplet delete -f <id>
-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
-19
View File
@@ -1,19 +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=[]
-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}
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
-195
View File
@@ -1,195 +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:-[]}
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" \
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, 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
-83
View File
@@ -1,83 +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__).
disable_root: 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
-222
View File
@@ -1,222 +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 leaves the existing pin alone; the watch never walks backward to
an older commit. This uses Git's committer date, not the time a commit was
pushed. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
Packages marked `"auto_merge": true` ride the unattended lane
(`track-branches.yml`) instead of the reviewed sync PR: their bump PR is opened
and auto-merged as soon as the build checks pass. `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.
-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/"
}
+25 -127
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() {
[[ "$CONTAINER_ENGINE" == "docker" || "$CONTAINER_ENGINE" == "podman" ]]
}
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)"
check_docker() {
if ! command -v docker &>/dev/null; then
print_error "Docker is not installed"
exit 1
fi
if [[ -z "$CONTAINER_ENGINE" ]]; then
print_error "No working container engine found (tried Docker, then Podman)"
if command -v docker >/dev/null 2>&1; then
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
if ! docker info &>/dev/null; then
print_error "Docker daemon is not running"
print_warning "Start Docker with: sudo systemctl start docker"
exit 1
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() {
local target_arch="${1:-aarch64}"
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
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"
# 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
print_error "Failed to setup QEMU for ARM64 emulation"
exit 1
fi
# 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"
print_success "QEMU ARM64 emulation enabled"
}
build_docker_image() {
@@ -127,45 +39,31 @@ build_docker_image() {
;;
esac
print_info "Building container image for $arch ($platform) using $mirror mirror..."
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
"$CONTAINER_ENGINE" buildx build \
--platform "$platform" \
--build-arg MIRROR="$mirror" \
--load \
-t "$image_tag" \
-f "$build_dir/Dockerfile" \
"$build_dir"
else
"$CONTAINER_ENGINE" build \
--platform "$platform" \
--build-arg MIRROR="$mirror" \
-t "$image_tag" \
-f "$build_dir/Dockerfile" \
"$build_dir"
fi
print_info "Building Docker image for $arch ($platform) using $mirror mirror..."
docker buildx build \
--platform "$platform" \
--build-arg MIRROR="$mirror" \
--load \
-t "$image_tag" \
-f "$build_dir/Dockerfile" \
"$build_dir"
}
get_platform_arg() {
local arch="$1"
case "$arch" in
x86_64) echo "--platform=linux/amd64" ;;
aarch64) echo "--platform=linux/arm64" ;;
*) return 1 ;;
x86_64) echo "--platform linux/amd64" ;;
aarch64) echo "--platform linux/arm64" ;;
*) echo "" ;;
esac
}
make_dir_writable() {
local dir="$1"
if (( EUID == 0 )); then
if [ "$(id -u)" -eq 0 ]; then
chmod -R 777 "$dir"
else
# chown can succeed on part of the tree and fail on the rest (files a
# 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"
sudo chown -R $(id -u):$(id -g) "$dir" 2>/dev/null || chmod -R 777 "$dir"
fi
}
+20 -180
View File
@@ -3,24 +3,21 @@
# Expects package directories in $PKGBUILDS_DIR, each with:
# .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", "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", "rc", "stable"] }
# { "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", "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"] } } }
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
#
# 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.
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
@@ -143,67 +140,6 @@ package_has_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
# `channels` key are members of every channel (they flow edge -> rc -> stable).
package_has_channels() {
@@ -280,11 +216,6 @@ package_builds_for_mirror() {
package_moves_to_channel() {
local pkgdir="$1" channel="$2"
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"
}
@@ -328,31 +259,6 @@ packages_for_upstream_sync() {
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. 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,
# even though nothing in their own source moved. `rebuild_on` names those
# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was
@@ -392,12 +298,9 @@ packages_for_mirror() {
packages_for_unscoped_build() {
local mirror="$1"
local arch="${2:-${ARCH:-x86_64}}"
package_dirs | while IFS= read -r pkgdir; do
if package_builds_for_mirror "$pkgdir" "$mirror" &&
! package_build_skipped "$pkgdir" &&
package_supports_arch "$pkgdir" "$arch"; then
if package_builds_for_mirror "$pkgdir" "$mirror" && ! package_build_skipped "$pkgdir"; then
basename "$pkgdir"
fi
done
@@ -540,70 +443,23 @@ validate_package_metadata() {
return 1
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
# let "upstream": false slip through as an empty declaration.
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
elif (.upstream | type) != "object" then false
else .upstream |
([has("github"), has("git_tags"), has("npm"), has("debian"), has("watch")] | map(select(.)) | length) == 1
and if has("watch") then (.watch | type == "object")
elif has("github") then
(.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
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
((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
and ((.checksums // "") | type == "string" and length > 0)
and ((.assets // {}) | type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0)
)))
end
' "$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
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")
case "$pkgrel_type" in
object|missing) ;;
@@ -625,28 +481,12 @@ validate_package_metadata() {
return 1
fi
if ! jq -e '
def version_map:
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"
if ! jq -e '(.rebuilt_against // {}) | type == "object" and (to_entries | all(.value | type == "string" and length > 0))' "$metadata" >/dev/null; then
echo "invalid rebuilt_against for $(basename "$pkgdir"): must be an object mapping package names to versions"
return 1
fi
if ! jq -e '
(.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
if ! jq -e '((.rebuilt_against // {}) | keys) - (.rebuild_on // []) | length == 0' "$metadata" >/dev/null; then
echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name"
return 1
fi
-70
View File
@@ -5,76 +5,6 @@
ARCH=${ARCH:-x86_64}
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_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
# hook: the whole feed is data, declared in .omarchy/package.json --
# A package whose upstream ships tagged GitHub releases with a checksum
# manifest asset needs no upstream.sh hook: the whole feed is data, declared
# in .omarchy/package.json --
#
# "upstream": {
# "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
# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The
# provider emits the same JSON contract as an upstream.sh hook, so
# bin/sync-upstream's validation and min_release_age backstop apply
# unchanged. Git-tag, npm, and Debian Packages providers below cover projects
# without GitHub releases; a feed that fits no convention keeps a bespoke hook.
# unchanged; a feed that fits no convention keeps a bespoke upstream.sh.
# Return the single declarative provider selected by a package. An empty
# result means either no provider or an invalid/ambiguous declaration; the
# caller distinguishes those through package_has_upstream_provider().
package_upstream_provider() {
local pkgdir="$1" metadata
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"
package_upstream_github_repo() {
local pkgdir="$1"
# `objects` drops a non-object upstream value (validation rejects those
# separately) instead of erroring the jq pipeline.
package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' ""
}
# 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"
}
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
# 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.
@@ -276,7 +51,7 @@ debian_upstream_release() {
# not silently choose from.
github_upstream_release() {
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")
repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata")
@@ -284,46 +59,9 @@ github_upstream_release() {
echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2
return 1
fi
# Enforced here as well as in validate_package_metadata: the scheduled sync
# reaches this provider without running the validator first.
checksums_name=$(jq -r '(.upstream? | objects | .checksums) | strings' "$metadata")
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
checksums_name=$(jq -r '(.upstream? | objects | .checksums) // ""' "$metadata")
if [[ -z "$checksums_name" ]]; then
echo "upstream.checksums names the checksum manifest asset and is required" >&2
return 1
fi
local arches
@@ -339,9 +77,6 @@ github_upstream_release() {
echo "could not fetch the release feed for $repo" >&2
return 1
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 tag published_at pkgver published_epoch
@@ -394,57 +129,33 @@ github_upstream_release() {
return 0
fi
local checksums=""
if [[ "$use_digests" != "true" ]] \
&& ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
local checksums
if ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
echo "could not fetch $checksums_name for $repo $best_tag" >&2
return 1
fi
local result
result=$(jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published_at" \
'{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}')
local arch template filename checksum checksum_source sums
local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at")
local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}'
local arch template filename checksum
for arch in "${arches[@]}"; do
if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then
echo "invalid architecture key in upstream.assets: '$arch'" >&2
return 1
fi
sums='[]'
while IFS= read -r template; do
filename=${template//\{pkgver\}/$best_pkgver}
filename=${filename//\{tag\}/$best_tag}
if [[ "$use_digests" == "true" ]]; then
# Only a "sha256:<hex>" digest is stripped to its hex; any other shape
# falls through empty and fails the check below.
checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" '
first(.[] | select(.tag_name == $tag)) | (.assets // [])[]
| select(.name == $name) | (.digest // "")
| if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end
' <<<"$releases")
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")
template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata")
filename=${template//\{pkgver\}/$best_pkgver}
filename=${filename//\{tag\}/$best_tag}
# 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")
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
echo "no valid checksum for $filename in $repo $best_tag $checksums_name" >&2
return 1
fi
jq_args+=(--arg "sum_$arch" "$checksum")
jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]"
done
if jq -e '.upstream | has("sources")' "$metadata" >/dev/null; then
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"
jq -n "${jq_args[@]}" "$jq_filter"
}
-631
View File
@@ -1,631 +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):
"""Describe the current tip of an upstream branch:
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.
select_release applies the age hold to this tip, without walking back
into history (which could select a commit from a merged side branch).
"""
https(url)
key = hashlib.sha256(f"{url}#{branch}".encode()).hexdigest()
work = Path(cache) / f"{key}.branch.git"
if not work.exists():
scratch = work.with_name(f"{work.name}.{os.getpid()}.tmp")
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", branch, url, str(scratch)], check=True)
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")
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):
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)
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), 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",
"upstream": {
"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"
}
"upstream_commit": "a0f4262f94eb8a5b604146e71d42a3bb522feedf"
}
+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
_tarver=8.12.40-27.BETA
_tarver=8.12.34-29.BETA
case "${CARCH}" in
x86_64)
_archdir="x64"
@@ -9,8 +9,8 @@ case "${CARCH}" in
_archdir="arm64"
;;
esac
pkgver=8.12.40_27.BETA
pkgrel=2
pkgver=${_tarver//-/_}
pkgrel=29.1
conflicts=('1password' '1password-beta-bin')
pkgdesc="Password manager and secure wallet"
arch=('x86_64' 'aarch64')
@@ -22,8 +22,10 @@ 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=('1327d102255b5c347e6c5e19b1a6581986446e06848015b93b5d7b10b9bc3f52' '2a19fba2b81ade500d9707a20829930d4fca972254fdc653ffa27cee57638098')
sha256sums_aarch64=('54b14cae2a676480f3f2df7b85e42bf389d1f84207b0a4ad1e32382071625146' '3a55dc9cdee5729e4bdf6830e04d9813c11546b328003800a837df83f79e33f9')
sha256sums_x86_64=('6894b283a534cf94b07903fb38966a0aab2e37f75ee3848ce340308819aadddb'
'8d4df4d0a80d2be7aad7d91ad964a750c8f32db5007261045003c191690c8246')
sha256sums_aarch64=('c77ce6ddf36dbd6054c64d91b7f644274d3218f465fd60e211d0296f6443124a'
'91c7249a1cf5e7924ef2810cb0cb1b893d8a9a424b373b433f45d7aaa5a8369b')
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
package() {
@@ -40,7 +42,7 @@ package() {
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
done
# 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.
export POLICY_OWNERS
@@ -63,7 +65,8 @@ EOF" > ./com.1password.1Password.policy
# 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 -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
install -dm0755 "${pkgdir}"/usr/bin
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
+2 -11
View File
@@ -1,14 +1,5 @@
{
"source": "local",
"source": "aur",
"release_ring": "fast",
"upstream": {
"watch": {
"json": "https://app-updates.agilebits.com/check/1/0/CLI2/en/0",
"path": "version"
}
},
"origin": {
"aur": "1password-cli",
"commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
}
"upstream_commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
}
+7 -7
View File
@@ -2,8 +2,8 @@
# Contributors: Felix Seidel, Claudia Pellegrino, Liu Yuxuan
pkgname=1password-cli
pkgver=2.40.0
pkgrel=2
pkgver=2.39.0
pkgrel=1
pkgdesc="1Password command line tool"
arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64')
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_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip")
sha256sums_x86_64=('74277219e8da60958c00f9aee9d2023225e98fdda8bfd2156a5d9e85e0edaab3')
sha256sums_i686=('adb1da45c0a40bb97c1e1ad62119d0a1aeb4ed9d979c06a7b1f95a48d9142b3d')
sha256sums_arm=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
sha256sums_armv6h=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
sha256sums_aarch64=('0e8ac99ee93d661aa725dc24a5ef8bf344741d224064a5dfb469dc689faec86a')
sha256sums_x86_64=('6fba7f376b6c6dec49f41b06408930a43ad064cce103c6a2ce5b3d0413a86434')
sha256sums_i686=('387d95f046ec9334e9de63514f96767fdd5dacbae292eb086fdfa0b0da310ec4')
sha256sums_arm=('673913f24ff57ce43e9d25ee451121d4733d188f45f6ab0468983fad85f8cc42')
sha256sums_armv6h=("${sha256sums_arm}")
sha256sums_aarch64=('829baeff1c07e055cfa132031b1d9f2282ccdf5076258e482caf2fda70aea5d0')
check() {
if (( ! SKIPPGPCHECK )); then
+2 -15
View File
@@ -1,18 +1,5 @@
{
"source": "local",
"source": "aur",
"release_ring": "fast",
"upstream": {
"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"
]
}
}
"upstream_commit": "e323d0d1f8dea6b75bb651ce14acc73904cd0326"
}
+13 -40
View File
@@ -1,47 +1,27 @@
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')
pkgdesc="Password manager and secure wallet"
arch=('x86_64' 'aarch64')
arch=('x86_64')
url='https://1password.com'
license=('LicenseRef-1Password-Proprietary')
options=(!strip)
install="1password.install"
source_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"
)
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'
source=(https://downloads.1password.com/linux/tar/stable/${CARCH}/${_tar}{,.sig})
sha256sums=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13'
'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7'
)
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
package() {
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
cd "1password-${pkgver}.${_tararch}"
cd "1password-${_tarver}.x64"
# Install icons
resolutions=(32x32 64x64 256x256 512x512)
@@ -51,14 +31,7 @@ package() {
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
done
# Install desktop file
install -Dm0644 resources/com.onepassword.OnePassword.desktop \
"${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"
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.
export POLICY_OWNERS
@@ -76,12 +49,12 @@ EOF" > ./com.1password.1Password.policy
# Move package contents to /opt/1Password
cd "${srcdir}"
install -dm0755 "${pkgdir}"/opt
mv "1password-${pkgver}.${_tararch}" "${pkgdir}/opt/1Password"
mv "1password-${_tarver}.x64" "${pkgdir}/opt/1Password"
# 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 -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
install -dm0755 "${pkgdir}"/usr/bin
+2 -11
View File
@@ -1,14 +1,5 @@
{
"source": "local",
"source": "aur",
"release_ring": "fast",
"upstream": {
"watch": {
"github": "omacom/aether",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "aether",
"commit": "0f047f40a200121075ca3cedce59ddf226f2a0f1"
}
"upstream_commit": "04592d5713f9c09ea2d81cf4b8476714d80014bc"
}
+9 -9
View File
@@ -1,18 +1,18 @@
# Maintainer: Bjarne Øverli <bjarne@oever.li>
pkgname=aether
pkgver=4.32.0
pkgrel=2
pkgver=4.29.6
pkgrel=1
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
arch=('x86_64' 'aarch64')
url='https://github.com/omacom/aether'
url='https://github.com/omacom-io/aether'
license=('MIT')
depends=('webkit2gtk-4.1' 'gtk3')
source=("aether-${pkgver}.tar.gz::https://github.com/omacom/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_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-arm64")
sha256sums=('3234e5138a46699f8f47330ba582244d5063c8356ffaae1458323e4dda02be82')
sha256sums_x86_64=('c705a4abef734b17ae37cdbff58a9a796517e6aa5f93ef18b68e3ee99d547ff9')
sha256sums_aarch64=('f70195deba008204d58661c3ec1442a16e63f3358b4579969facb0e55dbf63c4')
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-io/aether/releases/download/v${pkgver}/aether-linux-amd64")
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom-io/aether/releases/download/v${pkgver}/aether-linux-arm64")
sha256sums=('8b2e97283007c9eefda879e17c5fecb9d59fc90bf2160af93eeba57f11fcdb25')
sha256sums_x86_64=('47b5afa4144b3a3cd7755524956ffe6b074ed5f2f90dadda23e424efeaf88790')
sha256sums_aarch64=('52d1ffb201970ddb6205882a8e1c583cae63470d4180f4bc2fc6298dfb71ddd9')
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
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
pkgrel=2
pkgdesc="Control brightness on Apple Displays connected via USB-C"
arch=('x86_64' 'aarch64')
arch=('x86_64')
url="https://github.com/omakasui/asdcontrol"
license=('GPL2')
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-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 -11
View File
@@ -1,14 +1,5 @@
{
"source": "local",
"source": "aur",
"release_ring": "fast",
"upstream": {
"watch": {
"github": "basecamp/basecamp-cli",
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "basecamp-cli",
"commit": "89c4eb5a0ac9ffe98aecd4ea8b789cf1fee42bf1"
}
"upstream_commit": "fe2a6345e271f618dc3adf31e48557a717866395"
}
+5 -5
View File
@@ -1,6 +1,6 @@
# Maintainer: Basecamp <support@basecamp.com>
pkgname=basecamp-cli
pkgver=0.12.0
pkgver=0.9.1
pkgrel=1
pkgdesc="CLI for Basecamp project management"
arch=('x86_64' 'aarch64')
@@ -13,10 +13,10 @@ optdepends=(
'zsh: for zsh 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_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_arm64.tar.gz")
sha256sums_x86_64=('25d61c38de5660e97855661a2bf7329264912fcd46e2b3fb893fbc70bb467b5c')
sha256sums_aarch64=('1f692d2a8cc733ef95232eeb107414e32f1dd3228a0dde6c9039538661e0f2ca')
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/v0.9.1/basecamp_${pkgver}_linux_arm64.tar.gz")
sha256sums_x86_64=('bcb1fa3a03e702bbf81a3004ded4bc7808605e41106cbc768ba4006b89e0db2f')
sha256sums_aarch64=('ff50313024b6ca14e40146e30e1c9c9c00a4e4103fe55615eeee3e7c359d88d1')
package() {
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"
}
-15
View File
@@ -1,15 +0,0 @@
{
"source": "local",
"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.96.61
pkgrel=2
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=('c68cf179603470e8001a949294fe98b593f0749ca95f42687d855081b1c394a4')
sha256sums_aarch64=('33a1513379505642e4df0dda36a1dd78f735ee351631cffd4a4ac90b1cec0cd5')
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,15 +0,0 @@
{
"source": "local",
"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"
}
}
-46
View File
@@ -1,46 +0,0 @@
# Maintainer: brave <aur-release@brave.com>
pkgname=brave-origin-bin
pkgver=1.96.61
pkgrel=2
epoch=1
pkgdesc='The minimalist browser from the makers of Brave (binary release).'
arch=(x86_64 aarch64)
url=https://brave.com/origin/download
license=('MPL2')
depends=(alsa-lib gtk3 libxss nss ttf-font)
optdepends=('cups: Printer support'
'libgnome-keyring: Enable GNOME keyring support'
'libnotify: Native notification support')
provides=("$pkgname" "${pkgname%-bin}")
conflicts=("${pkgname%-bin}")
options=(!strip)
source=($pkgname.sh "${pkgname%-bin}.desktop")
source_x86_64=("${pkgname}-${pkgver}-x86_64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-origin-${pkgver}-linux-amd64.zip")
source_aarch64=("${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-origin-${pkgver}-linux-arm64.zip")
noextract=("${pkgname}-${pkgver}-x86_64.zip" "${pkgname}-${pkgver}-aarch64.zip")
sha256sums=('5ff70ee473f35c2fc7642c422c8abe20aaac0d7cc30a3292744eb9fbeafba1bd'
'c70bc71c696b6764247070375ae111bd76c8bad9c7bda4d46e03975b95571a8a')
sha256sums_x86_64=('06e4c48b71c65a33bdb94b6909aab505237b5939adf8985b332cb14557792816')
sha256sums_aarch64=('df07224b25284b67b5c7d0e8859d16f3d2533abcf588c81437df59e9fd4f4c62')
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"
chmod 4755 "$pkgdir/opt/$pkgname/chrome-sandbox"
install -Dm0755 "$pkgname.sh" "$pkgdir/usr/bin/${pkgname%-bin}"
install -Dm0644 -t "$pkgdir/usr/share/applications/" "${pkgname%-bin}.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-origin.png"
done
}
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-"${HOME}/.config"}"
CONF_FILE="${XDG_CONFIG_HOME}/brave-origin-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-origin-bin/brave-origin "${FLAG_LIST[@]}" "${@}"
@@ -1,222 +0,0 @@
[Desktop Entry]
Version=1.0
Name=Brave Origin
# 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-origin
Exec=brave-origin %U
Terminal=false
Icon=brave-origin
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/chromium;
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-origin
[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-origin --incognito
+2 -11
View File
@@ -1,14 +1,5 @@
{
"source": "local",
"source": "aur",
"release_ring": "fast",
"upstream": {
"watch": {
"github": "oven-sh/bun",
"pattern": "bun-v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
}
},
"origin": {
"aur": "bun-bin",
"commit": "195b828d52ce9a181215f753927123e7ef2af252"
}
"upstream_commit": "195b828d52ce9a181215f753927123e7ef2af252"
}
+6 -3
View File
@@ -3,7 +3,7 @@
# Contributor: 37h4n (aarch64 support added by Ethan Reece <aur at ethanreece dot com>)
# Contributor: sh!zeeg (shizeeque@gmail.com) support for non-avx2 CPUs, shell completions.
pkgname=bun-bin
pkgver=1.4.2
pkgver=1.3.11
pkgrel=1
pkgdesc="All-in-one JavaScript runtime built for speed, with bundler, transpiler, test runner, and package manager. Includes bunx, shell completions and support for baseline CPUs"
arch=('x86_64' 'aarch64')
@@ -12,8 +12,11 @@ license=('MIT')
provides=('bun')
conflicts=('bun')
options=('!debug')
sha256sums_x86_64=('36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913' 'c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_aarch64=('54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_x86_64=('8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed'
'abe346f63414547cdf6b35b7a649a490c728b93d006226156923918a84c0e59b'
'9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
sha256sums_aarch64=('d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf'
'9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
source_x86_64=(
"bun-x64.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64.zip"
"bun-x64-baseline.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64-baseline.zip"
Loaded 100 of 1560 files, more files were not shown because too many files have changed in this diff. Show more