AGENTS.md
Notes for AI coding agents (and humans) working on this repository.
What this repository is
AppImageHub, the catalog of AppImages at https://appimage.github.io, built by GitHub Pages (Jekyll) from this repository.
data/<Name>: one file per application, submitted by contributors in PRs. Its first line is a download URL, a GitHub, Codeberg or GitLab repository URL, (optionally with#Channelat the end, for a repository with a release per channel), or a download directory (e.g. https://download.kde.org/stable/digikam/ or an openSUSE Build Service.../AppImage/directory); nothing else is read.database/<Name>/: generated by CI from the AppImage:.desktopfile (with an[AppImageHub]section ofX-AppImage-*keys), icon,screenshot.png, AppStream metainfo. Do not edit by hand.apps/<Name>.md: generated by CI; YAML front matter that the Jekyll templates render. Do not edit by hand._layouts/,_includes/(notablyapp_teaser.html,app_notes.html),feed.json,search.json: the site.app.htmlsetspost = page, so app templates usepost.desktop.AppImageHub.X-AppImage-*etc.
How testing a submission works
.github/workflows/test.yml(on PRs and pushes touchingdata/**) runscode/worker.sh data/<Name>for each added or changed file, under a per-apptimeout, on Xvfb:- downloads the AppImage (resolving GitHub, Codeberg or GitLab repository
URLs via
code/fetch-releases.sh(each forge’s API, normalized to GitHub’s releases shape), and directory URLs (ending with/) viacode/fetch-listing.sh, which reads the server’s directory listing (Apache/nginx), descends into the newest version directory and into linux/, bin/, appimage/ and the like, and writes the AppImages found in the same shape with the listing’s dates; thencode/find-appimage.shpicks the AppImage by rules (among them: the newest toolkit build, e.g. Qt6 over Qt5; and the channel asked for with#Channelat the end of the URL indata/, e.g.https://github.com/srevinsaju/Firefox-Appimage#ESR, independent of the name of the file: only releases or AppImages with that word in their tag or file name count, and it is an error if there are none; without a channel, those of the other channels (esr, nightly, beta, …) are left out, so that the entries of a repository with a release per channel do not collide) and fails rather than guesses when several fit; a URL ending with/that is no listing is downloaded as it is), checks its type, mounts it with a separate runtime (never executes the AppImage’s own runtime), runscode/appdir-lint.sh(vendored from AppImage/AppImages with fixes: errors are reported instead of ending the script underset -e, the excludelist check works with several matches; theexcludelistitself is still downloaded from there), finds the icon; code/check-libc.sh: does it need a compatible C library on the host? (X-AppImage-Libc=none|bundled|host,X-AppImage-Runtime,X-AppImage-Self-Contained,X-AppImage-Glibc-Required=GLIBC_2.xx, stored with the prefix because YAML would turn 2.40 into the number 2.4; an upper bound, as not every library in an AppImage is necessarily loaded);code/check-name.sh: rules for the name of the file indata/(errors only for files a PR adds,STRICT=true; warnings otherwise), and remarks about the AppImage’s file name; it also prints aNOTE:(shown under “Notes” in the test result, not a warning) for an all-lowercase name withoutTerminal=truein the desktop file, or a name with capitals withTerminal=true: all-lowercase names are typically command line tools; the publish workflow only shows a note thatcheck-name.sh --note-textgenerates exactly;code/check-name.shalso rejects the word “App” in a name (Foo-App,foo_app,App_Outlet: a token between-,_,.or blanks; notWhatsApp,Apple), an error for files a PR adds and a warning for the 32 old ones, andcode/pick-name.sh(auto-discovery) leaves such a word out of the proposed name (aniwings-appgivesAniWings), so the discovery workflows do not propose names the test rejects;code/check-names-agree.sh: a warning when the name of the repository, the AppImage’s name and the README’s headline (code/fetch-readme.sh) disagree within a tolerance (equal, one contains the other, or >= 80 % similar, ignoring case and punctuation); shown under “Names” and “Warnings” in the test result; it also warns when the file name has a-where the README writes blanks (XI-on-Anything, README “XI on Anything”): use_(XI_on_Anything);code/pick-name.sh(discovery) does that itself;- runs the application in firejail without network, waits 30 s, takes a screenshot of the active window;
- system tray applications: only if the application showed no window (or
exited) and
code/tray-hint.shfinds a tray API in its files (Qt, GTK, libappindicator, Electronnew Tray(, XEmbed, StatusNotifierItem), it runs it again with a tray (stalonetray, XEmbed). If its icon docks there, it clicks the icon and takes the window that opens, else right-clicks it and takes the tray and menu;check-screenshot.shgetstray(no empty-window check) and a warning asks the maintainer to check it; code/check-screenshot.sh: fails on an (almost) empty window or an error message found by OCR (tesseract), or text mostly in Chinese, Arabic, Cyrillic or Greek script (OCR with those models, confident words only; labelerror-not-english);- warns when the AppImage has no (or broken) update information;
- writes
database/andapps/.links:inapps/<Name>.md(shown on the page and infeed.json) gets the repository and its releases for GitHub, Codeberg and GitLab entries, and otherwise aDownloadlink to the URL indata/if it stays valid for new versions (a download directory or a “latest” link: no version number in its path). On PRs it stops there; onmasterit commits the result ([ci skip]). Afterwards the workflow collects apr-resultartifact: PR number, screenshot (or screen capture on failure),code/diagnose.shhints and the excerpt of the log around the first error, and the libc flags.
- downloads the AppImage (resolving GitHub, Codeberg or GitLab repository
URLs via
-
.github/workflows/publish-pr-screenshot.ymlruns onworkflow_runof “Test” (frommaster, with write permissions, because PRs come from forks whose runs have a read-only token) and posts a new comment on the PR for every test run (earlier results stay as they were), marked<!-- appimagehub-test-result -->. Screenshots are uploaded as GitHub attachments with theSCREENSHOT_UPLOAD_TOKENsecret (classic PAT), falling back to assets of theci-screenshotsrelease, which it prunes. Renewing the token: seeMAINTAINER.md. It also sets rederror-*labels on the PR for the known causes of a failed test (defined next to the hints incode/diagnose.sh, which holds triples of pattern, label and hint;error-otherwhen none applies) and removeserror-*labels that no longer apply; the greenscreenshot-oklabel marks a passed test whose screenshotcode/check-screenshot.shfound fine (no warnings). When the test failed or reports warnings or name remarks, the comment @mentions the GitHub account the AppImage comes from (code/upstream-owner.shon the first line of the PR’sdata/files, read via the API at the tested commit, not from the artifact), unless it is the PR’s author or a bot, or was mentioned in an earlier test result comment already; other hosts have no GitHub account to mention. The comment says which file was tested (Tested: the file name, linked, and the release it comes from, with a remark if the release is marked as a pre-release or its tag looks like one: alpha, beta, rc; from theURL:line of the log, which the test result artifact carries astested.txt; only URLs of a strict form are accepted). Finally it auto-merges (squash) a PR from a returning contributor (author_associationCONTRIBUTOR or above) that changes exactly one file indata/, passed withscreenshot-ok, has nomanual-check-neededordo-not-mergelabel, and does not change the first line of an existing file indata/(checked itself via the API, not via the artifact), except to shorten a long-form link to the short form of the SAME repository (code/repo-url-for-entry.sh --shortens:https://github.com/o/r/releasesor.../releases/download/...tohttps://github.com/o/r); even a change within the same owner may be a different project that shares the file name (#8569).merge-labeled.ymlapplies the same rule. A merge with the workflow’s token does not trigger push workflows, so it startstest.ymlbyworkflow_dispatchwith the merge commit (inputcommit), which writesdatabase/as a push would. Re-test PRs (branchretest/*in this repository, opened by/reteston a merged PR) are merged whenever their test passes, even with warnings, as long as they only modify existing files indata/and keep each file’s first line (or only change it from a link to one particular release to its repository, see/retestbelow). Switch off: repository variableAUTO_MERGE=false. -
.github/workflows/check-origin.yml(pull_request_target, frommaster) compares the first line of each changed file indata/withmaster(code/check-origin.sh: the GitHub user or organization, the project on other forges, the owner on shared hosting, else the domain). If it points to a different owner, it posts a comment marked<!-- appimagehub-origin-check -->and sets the yellowmanual-check-neededlabel plusorigin-changed(the reason; possible takeover of an entry); all are withdrawn when a later push undoes the change. Exception: when the two are the same project after all,check-origin.shexits 2 and the workflow posts a neutral informational note instead — no warning, no labels: both are GitHub repositories and GitHub itself redirects the old one to the new one (a rename or transfer), or exactly one of them is a GitHub repository whosehomepagefield is on the other one’s site (a project moving between its own website, download server or CI bucket and its repository; in October 2026, 22 of 26 flagged PRs were of this kind). An OBS sub-project of the same user (home:/USER:/apps/) counts as the same owner. This does not depend on the PR’s own test run, which a PR could alter. (check-origin.shexits 1 or 2 for a changed owner; the step must read that withRC=0 ; RESULT=$(...) || RC=$?, asRESULT=$(...) ; RC=$?ends the step underbash -ebefore anything is reported.) The same workflow also setsmanual-check-needed(with its own comment,<!-- appimagehub-name-check -->) on a PR that adds or renames to a file indata/whose name has more than two hyphens (often a version or file name instead of the application’s name), plus the reason labelname-check; withdrawn when a later push fixes it, andmanual-check-neededstays while either check flags the PR..github/workflows/one-file-per-pr.yml(pull_request_target,openedandreopened) closes a pull request from a fork whose author is not a maintainer (OWNER, MEMBER, COLLABORATOR; not a bot) as soon as it is opened when it does not change exactly one file indata/and nothing else (the rule of the README, which the test also checks; #3977 added a file in the root), with a comment that lists the changed files and says what to do (marker<!-- appimagehub-one-file -->). A maintainer who reopens it on purpose is respected. Only the list of changed files is read, via the API..github/workflows/stale-discovered.yml(workflow_dispatchonly; inputslimit(default 20),days(default 90),close_days(default 60),dry_run(default true);code/stale-discovered.sh) walks auto-discovered PRs whose test failed and that no human commented on up a ladder, one step per PR and run: (1) a last result older thandays→ re-test (retest-pr.sh, withSCREENSHOT_UPLOAD_TOKEN), labelretested-stale; (2) the re-test’s result is in and still failed → one comment that @mentions the GitHub account the AppImage comes from (upstream-owner.sh; never a maintainer or bot), asks for/retestwhen fixed and announces the closing, labelpinged-author(marker<!-- appimagehub-stale-ping -->); (3)close_daysafter the ping with no human comment → closed with a comment, labelauto-closed(the other two removed). A PR that passes the re-test keepsscreenshot-okand waits for a maintainer.discover-apps.shtreats anauto-closedPR like a repository checked without success on its closing day (proposed again when pushed to later), not like one rejected by a maintainer. API budget: the comments of a PR are only fetched when it is on the ladder or had no activity (updated_at) fordays; the run stops with exit 4 when fewer thanRESERVE(200) requests of the hour are left, and prints how many it used (see “GitHub API budget” below)..github/workflows/health-report.yml(weekly, Monday, andworkflow_dispatch;code/health-report.sh) writes one issue, updated in place (marker<!-- appimagehub-health-report -->, labelhealth-report): what needs a maintainer (auto-discovered PRs that passed, contributors’ PRs that passed but were not merged,manual-check-needed), what the bots handle (open PRs by error label), the catalog (entries withoutdatabase/, stale, pinned to a release,entry-unavailableissues), and the automation (whetherSCREENSHOT_UPLOAD_TOKENworks and when it expires, last runs). It sets theneeds-maintainerlabel exactly on the PRs blocked on a maintainer (and removes it elsewhere), so that they can be found among the thousands handled by bots. -
.github/workflows/ping-authors.yml(monthlyschedule, a real run, andworkflow_dispatchwithdry_run(default true),max_issues(default 5) andfull_scan) runscode/check-entry.shover the files indata/(10 in parallel, 10 s timeouts; it stops oncemax_issuesnew dead entries are found unlessfull_scan) to find entries whose AppImage is gone (repository not found, no AppImage in its releases or in a directory listing, or a direct download URL returning 404/410; timeouts and other errors areunknownand are never pinged, and so is a gone release asset whose repository still has AppImages thatfind-appimage.shcannot pick for the entry, e.g. python2 and python3 from one release), writes a summary, and runscode/ping-authors.sh. A dead link to a release asset (GitHub, Codeberg or GitLab) whose repository still has an AppImage (find-appimage.sh), or to a file in a directory listing when a directory up to 3 levels above it has an AppImage of the app (e.g. download.kde.org), isfixable(pointing to the repository or directory): it opens a pull request (branchping-authors/fixes-*, at most 5 entries per run, none already in an open fix PR) that points those entries to the repository and closes open issues about them when merged; PRs opened with the workflow’s token start no workflows, so close and reopen it to test. Only the rest is pinged:code/ping-authors.shopens (at mostmax_issues) issues labeledentry-unavailablefor dead entries that never had one (open or closed), @-mentioning the author of the commit that added the entry and the upstream GitHub owner (neverprobonopdor bots), each entry at most once. It never removes an entry. One run at a time (concurrency). Run it by hand from the Actions tab (dry run first); seeMAINTAINER.md..github/workflows/remove-dead-entries.yml(workflow_dispatchonly; inputslimit(default 10),days(default 90),dry_run(default true);code/remove-dead-entries.sh) removes the entries whoseentry-unavailableissue is at leastdaysold, has no comment by anyone but github-actions, and whose entrycheck-entry.shfindsdeadagain right now (unknown,fixableandokkeep it):remove-entry.shin one commit on master, and each issue is closed with a comment that a new PR brings the application back. Same concurrency group asremove-entry.yml. -
.github/workflows/remove-entry.ymlremoves entries completely withcode/remove-entry.sh(data/NAME,database/NAME/,apps/NAME.md): on a maintainer’s/removecomment on a “Where did the AppImage of NAME go?” issue (the name comes from the title, never from the comment; the issue is then closed), on a maintainer’s/removecomment on a pull request (it is closed and labeledopt-out; the entries it adds or changes are removed if they are in the catalog), by hand with names, and on pushes tomasterthat delete or rename files indata/(it removes what is left of them). Deleting onlydata/NAMEused to leave the page on the site. -
.github/workflows/discover-apps.yml(workflow_dispatchonly; inputscount, default 1, no upper limit,months(default 3) anddry_run) runscode/discover-apps.shto find GitHub repositories that publish AppImages on their releases but are not in the catalog yet, and proposes each as a new entry in its own pull request, labeledauto-discovered, for a maintainer to review. Recent mode (monthsN > 0, the default 3): it searches the repositories of any age pushed to since the last complete run (sorted by update; the state’s first line haslastrun=DAY), then those created in the last N months, and stops after minutes; a repository checked before is checked again when it was pushed to since, at least a week later. A full sweep over every month since 2012 (months0, about 500 searches, two hours with the checks) found 135 new apps in October 2026, of which 108 were created in 2026: it is only needed now and then. The text below describes that sweep. A branchdiscover/NAMEthat exists already (an earlier PR for the same name) skips the app (outcomename) instead of failing the run; the state is saved every 10 PRs. Candidates come from the GitHub search API (most stars first), one month of repository creation dates at a time with both atopic:appimageand anappimage in:name,description,readmequery, going back month after month untilcountapps qualify (or every month since 2012 was searched, or 5.5 h passed; rate limits are waited out). A cursor in the state file, saved after every PR and every 10 months, lets the next run continue where this one stopped. A repository is skipped when it is already indata/, is added by an open pull request, was proposed before (any pull request labeledauto-discovered, in any state: merged means it is in the catalog, closed unmerged means it was rejected, and neither is retried; all of them are read, via the issues endpoint and theRepository:line of the PR bodies), was opted out (a PR labeled blackopt-out, any PR in any state, names the repository or adds adata/file with the app’s name, compared by letters and digits, so the app is not proposed from another repository either; outcomeopt-out), or was checked in the last 90 days without a usable AppImage (30 days if it had fewer than 5 stars, as it may have gained some). What remains is checked (in random order) withcode/fetch-releases.shandcode/find-appimage.sh(exactly one x86_64 AppImage, in a release less than 2 years old) andcode/check-name.sh(STRICT=true). If the repository is a fork or a copy of another that publishes an AppImage too (the fork’s source or parent, a repository with the same letters and digits in its name linked from the description, homepage or README, or one the search finds by name with more stars), that repository, the application’s own, is checked and proposed instead (outcomecopyfor the fork or copy; skipped if the original is known already), and the PR says which repository led to it; a fork whose source publishes no AppImage gets thenot-upstreamlabel. A very high star count for a brand-new repository (more than ~15 stars per day since it was created, at least 300, under ~2 years old) can mean bought or botted stars, but genuine projects take off like that too (in October 2026, 61 of 73 PRs flagged by that rule alone were genuine): so the PR is only flagged when the repository also looks hollow, with fewer than 10 forks or more than 30 stars per fork AND fewer than 300 commits (one request, the last page ofcommits?per_page=1). Then it gets the labelssuspicious-stars(the reason) andmanual-check-needed(what auto-merge and the health report look at) and a note with the numbers; a fast-growing but established repository gets the numbers as a neutral note instead..github/workflows/review-suspicious-stars.yml(monthly, andworkflow_dispatchwithdays(30),limit(50) anddry_run(default true);code/review-suspicious-stars.sh) looks at the labeled PRs again once they are 30 days old and removes both labels with one comment (marker<!-- appimagehub-stars-review -->) when forks or commits have caught up; a repository that still looks hollow keeps them. The name (file indata/, branch and PR title) is the proper spelling picked bycode/pick-name.shfrom the repository name, the AppImage’s name, the description and the README’s headline (same letters and digits as the repository name; most capitals win, then the headline; blanks, and dots between words as inUploader.PRO, become_: photoapp +PhotoApp-1.2.AppImagegivesPhotoApp, photo-app + “Photo App is …” givesPhoto_App); an app already indata/in any spelling is skipped. If the repository or AppImage name, the description or the README (first 50 KB) says “unofficial”, “not official”, “not affiliated”, “repackaged” or the like with the word AppImage or AppImages within three words of it, in the same sentence (code/not-upstream-phrase.sh; “not affiliated with" alone is no reason), the PR also gets the `not-upstream` label and a note naming the phrase and where it was found (the `repackaged` label instead, if the phrase is "unofficial", "repackaged", "third party" or "community build": built by someone else from the application, as pkgforge-dev's and ivan-hc's AppImages are); every repository checked is recorded in the state file with its outcome, so it is not checked again for 90 days. The state file (`discover-state.tsv`, with the cursor on its first line) lives on its own orphan branch, `discover-state`, never on `master`. The pull request's branch (`discover/ `) is pushed, and the pull request opened, with `DISCOVER_TOKEN` or `SCREENSHOT_UPLOAD_TOKEN` when set (see `MAINTAINER.md`), so its Test workflow actually starts; otherwise `GITHUB_TOKEN` is used and the pull request body says to close and reopen it. `publish-pr-screenshot.yml`'s auto-merge skips pull requests whose branch starts with `discover/` or that are labeled `auto-discovered`: a maintainer always reviews these. .github/workflows/discover-pkgforge.yml(workflow_dispatchonly; inputslimit(maximum number of apps to add, default 1) anddry_run) does the same for the AppImages that https://github.com/pkgforge-dev builds, withcode/discover-pkgforge.sh: it reads the organization’s repositories (most recently pushed first), skips archived ones, forks and those whose name does not end in-AppImage(its own tools such as sharun), and apps already indata/(by URL or by name), added by an open PR or named by anauto-discovered/opt-outPR (any state); the rest is checked like indiscover-apps.yml(find-appimage.sh: exactly one x86_64 AppImage, in a release less than 2 years old;pick-name.sh;check-name.sh). pkgforge-dev builds other projects’ applications, so every PR getsauto-discoveredandrepackaged(notnot-upstream:repackagedalready says it): a maintainer decides. It keeps no state (repositories without a usable AppImage are checked again on the next run)..github/workflows/discover-apphub.yml(workflow_dispatchonly; inputscount(default 1) anddry_run) does the same for AppImageHub (the openDesktop/Pling store) withcode/discover-apphub.sh. AppImageHub’s own API host is deprecated, but the shared OCS v1 API still serves it anonymously athttps://api.opendesktop.org/ocs/v1/; the app categories are the ones whosexdg_typeisbin. An entry’sdownloadlinkis a tokenized, expiring Pling URL and itshomepagepoints back to the openDesktop page — neither is stable — but for “link” entries the Pling URL simply redirects to the real upstream (e.g. digiKam →https://download.kde.org/stable/digikam/). So the script resolves each entry’s download to its final URL and proposes it only when that is stable and resolvable by the existing helpers: a GitHub/GitLab/Codeberg repository (fetch-releases.sh+find-appimage.sh), a download directory ending in/(fetch-listing.sh), or a versionless “latest”.AppImagelink; Pling-hosted files, versioned files and non-listing HTML are skipped. Apps already indata/, added by an open PR, or named by anauto-discovered/opt-outPR (any state) are skipped by name. Sameauto-discoveredlabel andDISCOVER_TOKEN/SCREENSHOT_UPLOAD_TOKEN/GITHUB_TOKENhandling asdiscover-apps.yml; a maintainer reviews the PRs. -
.github/workflows/auto-retest.yml(issue_commentonly) re-tests a failed pull request without anyone typing/retest: on a comment (not starting with/, not by a bot) from a maintainer, the PR’s author or the GitHub account the AppImage comes from (code/upstream-owner.shon the first line of the PR’sdata/files at its head commit) that says it is fixed or asks for a re-test (code/retest-comment.sh: “retest”, “re-run”, “is out”, “new release”, “fixed in”, “should now work”, …; quoted lines, code and sentences about the future such as “will fix” or “in the next release” do not count), on a PR whose latest test did not pass cleanly (anerror-*label, or noscreenshot-ok). It closes and reopens the PR withcode/retest-pr.sh(SCREENSHOT_UPLOAD_TOKEN; reopens withGITHUB_TOKENshould that fail, never leaves a PR closed; skips a PR whose Test run for its current commit started less than 15 minutes ago) and reacts 🚀. Nothing from the PR is checked out or run; it reads the PR’s files as text via the API. - Answers to comments, without a maintainer:
auto-retest.ymlalso answers a question about how to test again (code/retest-question.sh, e.g. “how do I ask it to retry?”) once per PR with “comment/retest” (marker<!-- appimagehub-retest-howto -->)..github/workflows/reopened-notice.yml(pull_request_target) tells a non-maintainer author who closes and reopens their own PR the same, once..github/workflows/opt-out-request.ymllabels a PRopt-outand answers once when its author, or the GitHub account the AppImage comes from, says the application should not be in the catalog (code/optout-comment.sh: “please remove my app”, “opt out”; questions, conditions and “close this PR” do not count). It never merges, closes or removes anything: a maintainer confirms with/remove..github/workflows/not-upstream-explained.yml: on a PR labelednot-upstream(a README saying “unofficial”, “not affiliated”, … or a fork), when the GitHub account of the repository in the PR’s singledata/file (not a fork, on GitHub) explains in a comment, in at least 15 words, that the application is its own upstream project (code/upstream-claim.sh; any sign of repackaging means no), thenot-upstreamlabel is removed andmanual-check-neededis added, with one comment: a maintainer still decides. Of 549 PRs with the label (534 open), most are misdetections or own projects, and maintainers have never ruled on the label itself: all 13 labeled PRs merged were merged by hand after the tests passed..github/workflows/notify-tauri-diricon.yml(workflow_dispatchonly; inputslimit(default 100) anddry_run(default true),code/notify-tauri-diricon.sh): Tauri before 2.11.4 built AppImages without.DirIcon(https://github.com/tauri-apps/tauri/pull/15596). It posts one friendly message (marker<!-- appimagehub-tauri-diricon -->: use Tauri 2.11.4 or later, build a new release, comment/retest) on each open PR that has theerror-missing-filelabel, mentions “tauri” in its title, description or comments, and whose last test result says.DirIcon is missing(the label alone also covers other missing files). Run the dry run first.code/diagnose.shhas hints forAppRun.wrapped: Permission denied(Tauri/linuxdeploy mode 0770) and for a.DirIconthat cannot be read (absolute symlink).
Rules that are easy to get wrong
-
pull_request_targetworkflows must never check out or run code from the PR.check-origin.ymlonly reads the PR’s files as text via the API. - The artifact is untrusted (it comes from a PR’s test run). In the publish
workflow never execute or
evalanything from it; cross-check the PR number against the tested commit; only accept hints listed bycode/diagnose.sh --listfrommaster, and values of known form; put log text only inside a code fence (four backticks, longer runs are neutralized). - workflow_run workflows always run from
master. Changes topublish-pr-screenshot.yml(and to the hint list it validates against) only take effect after merging; a PR cannot test them.workflows:takes the workflow name (Test), not its path. - Never let
worker.shleave background processes behind. Anything started with&inherits stdout, which is piped intotee; a survivor keeps the pipe open and the job hangs until the Actions timeout instead of failing. ThecleanupEXIT trap kills them and unmounts; redirect the output of new background processes, and wrap X11 calls intimeout. worker.shruns withset -v(and partlyset -x), so the log contains its own source lines and traces. Patterns incode/diagnose.shfor the worker’s own messages are anchored with^, and the excerpt drops lines that are verbatim lines ofworker.shor traces (^+).- The “first error” excerpt (
code/diagnose.sh --excerpt) starts at a line witherror while loading shared librarieswherever it is (a missing library is almost always the real cause; labelerror-missing-library), else at the first line matching an error pattern; patterns of hints without a label (warnings, remarks) never start it. - The test runs without network (
firejail --net=none); many applications show “update check failed” and similar. That is why soft error words only warn. - Terminal applications (
Terminal=true) run with--helpin xterm; screenshot checks only warn for them. - Thresholds in
code/check-screenshot.shwere calibrated on the ~1500 screenshots indatabase/. When changing them, re-run over those screenshots (OMP_THREAD_LIMIT=1for tesseract, or it crawls when run in parallel) and report how many would change outcome. - Adding apt packages to
test.yml: bump the cacheversion:next to the list.
Testing changes
- Shell:
bash -n,shellcheck -S warning code/*.sh,actionlint .github/workflows/*.yml. code/diagnose.sh,code/check-screenshot.sh,code/check-libc.shcan be run locally on a saved log, a PNG, or an AppImage plus its extracted AppDir (unsquashfs -o <offset>, where the offset is the end of the runtime’s ELF section headers).code/check-origin.sh OLD_URL NEW_URL(or--origin URL) runs locally; over the history ofdata/(git log --diff-filter=M -- data/), about a quarter of the URL changes move to another owner.code/find-appimage.sh releases.json NAMEruns on a savedrepos/OWNER/REPO/releasesresponse (GitHub’s shape;code/fetch-releases.shnormalizes Codeberg and GitLab to it). When changing its rules, compare its picks with the AppImages that thedata/files with direct links point to.- Site:
bundle install(Gemfile:github-pages), thenbundle exec jekyll buildon a copy with only a fewapps/*.mdfor speed. Note:feed.jsonis only valid JSON if the last page in permalink order is an app (pre-existing quirk of its comma logic). - PRs from forks must change exactly one file in
data/and nothing else (checked first intest.yml); PRs from branches in this repository are exempt. - End-to-end: the Test workflow only runs when the PR touches
data/**. Test in a separate throwaway PR (from a branch in this repository) that adds a throwaway change to adata/file (e.g., a trailing newline; only the first line is read) on top of your change; check the run and the PR comment, then close it. Doing this in the real PR leaves a test-result comment there that nothing removes once the PR no longer touchesdata/. New PR comments frommaster’s publish workflow appear only for PRs whose head is still the tested commit. - To re-test an existing contributor PR with new workflow code, close and reopen
it (a re-run would reuse the old workflow files).
code/retest-prs.shdoes this for all open PRs that change one file indata/and last tested green (-u: instead those without a test result label;-f: instead those whose test failed and that have no labels at all;-nto only list them). For one PR, comment/reteston it (maintainers, the PR’s author, and the GitHub account the AppImage comes from, percode/upstream-owner.sh):.github/workflows/retest.ymlcloses and reopens it withSCREENSHOT_UPLOAD_TOKEN(a reopen withGITHUB_TOKENstarts no workflows), or without that secret re-runs its last Test run (old workflow files); it reacts 👀, then 🚀 once the test is started. On a merged PR it opens a new PR (branchretest/<PR>-<time>, with the personal token) that only toggles the trailing newline of the merged PR’s files indata/(an entry that links to one particular release,.../releases/download/v1.3.2/..., is changed to its repository URL instead,code/repo-url-for-entry.sh, so that the newest release is tested), so that Test runs for those entries again (and, if green, auto-merge takes it, which refreshesdatabase/); one open re-test PR per merged PR. Failed PRs are also re-tested when their author says it is fixed (item 7 above), andcode/retest-pr.sh [-n] NUMBERre-tests one PR by hand (needsPAT). The comment patterns:echo "v1.2 is out" | code/retest-comment.sh. .github/workflows/retest-stale.yml(monthlyschedule, a real run of 20 entries;workflow_dispatchwith inputslimit(default 20),days(default 365),pinnedanddry_run(default true);--pinnedre-tests the entries pinned to one release (.../releases/download/..., 235 in October 2026) instead, whatever their age, which points them to the repository) re-tests the entries whosedatabase/NAMEwas last changed more thandaysdays ago (code/retest-stale.sh: from the git history, oldest first, about 1200 of them in October 2026): oneretest/stale-NAME-<time>pull request per entry (the Test workflow stops at the first failing entry of a PR), changingdata/NAMEthe way/retestdoes on a merged PR (a link to one particular release becomes the repository URL, else the trailing newline is toggled), withSCREENSHOT_UPLOAD_TOKENso that Test starts. A passing one is auto-merged like any re-test PR, which refreshesdatabase/; a failing one stays open with the test result (and the upstream account is mentioned). Entries named in the title of an open PR (not their files: thousands of open PRs, 1000 API requests an hour), or that this script re-tested in the last 60 days (any state), and entries without a file indata/, are skipped./appstream(maintainers, or the author of the issue or PR):.github/workflows/appstream-help.ymlpostscode/appstream-help.mdfrommaster, how to ship an AppStream metainfo file with screenshots. The catalog shows the metainfo’s screenshot (type="default", else the first; from*.metainfo.xmlor*.appdata.xml) instead of the automated one, which the test still takes and checks.
Investigating and answering in pull requests
Only when the maintainer explicitly asks for it (e.g. “look into the complaint in PR #123 and answer”). Never comment on, close, label or merge PRs on your own initiative, and never start conversations with contributors.
Investigate before answering, and keep the context small:
- Read the comment, the PR’s
data/file and the bot’s test result comment (<!-- appimagehub-test-result -->). - Download the job log (
repos/…/actions/jobs/<id>/logs, the job of the run linked from the test result) and grep it locally instead of reading it:URL from GitHub API:(the release asset that was tested),X-AppImage-,Screenshot:,^ERROR,^WARNING. - Which release is current:
https://github.com/<owner>/<repo>/releases/latestredirects to it. - Claims about the AppImage itself (e.g. which glibc it needs): download it,
extract it with
unsquashfs -o <offset>(see “Testing changes”), and find the files responsible, e.g. withobjdump -Tfor*UND*GLIBC_symbols. Everything inside the AppImage counts, even a rarely loaded library, as long as whatever loads it is also in the AppImage (not on the host). - Remember how the test runs the app: without network,
--helpin xterm forTerminal=true, screenshot after at least 10 s. - If our test or wording is wrong, fix it (in a PR as usual) and re-test the contributor’s PR by closing and reopening it.
Write the answer:
- Show the draft to the maintainer first; post only after they approve it, or hand it to them to post themselves.
- Be very polite: thank them, apologize if the PR waited long, agree where they are right.
- State facts with evidence: the exact version, file path, symbol or log line. No guesses; say what they can do (e.g. remove or rebuild a file), and mention further findings only as friendly suggestions.
- Follow the attribution rules of the environment the agent runs in.
GitHub API budget
GitHub allows 5000 REST requests an hour per user, and GITHUB_TOKEN in a
workflow 1000 an hour per repository; every token and tool that acts as the
same account (a personal token, gh api, an agent’s own client) draws from
one bucket, so one careless loop stops everything else for the rest of the
hour. The repository has thousands of open pull requests: one request per
pull request is already most of the budget. In October 2026 an agent spent
the whole hour with an analysis that made four requests per pull request,
eight in parallel, while a dry run of stale-discovered.sh walked the
comments of 3400 pull requests at the same time.
Rules for scripts and workflows:
- Count the requests before writing the loop:
(items / 100)pages for a list, plus the per-item requests, plus the actions. Anything over a few hundred per run needs a cheaper design. Prefer one list of all pull requests (100 per page: 50 requests for 5000) and filter it locally by its fields (labels,updated_at,head.ref,title) over anything per pull request; fetch comments, files or events only for the handful of candidates that survive (stale-discovered.sh,retest-stale.sh). - Send every request through one function (
api()instale-discovered.sh), count them, checkrate_limitevery 50 requests and stop when fewer than a reserve (200) are left, with a clear message and exit code; print the count in the summary so that a regression shows. - Stop after
limitactions, not after the whole list is read. - Never run two request-heavy jobs at once (
concurrencyin the workflow; by hand, one at a time). - The search API has its own limit (30 requests a minute); wait it out
(
discover-apps.sh), do not retry in a tight loop.
Rules for an agent working on the repository by hand:
- Use git where it gives the same answer for free:
git fetch origin refs/pull/N/headgets a pull request’s files and content (one fetch for hundreds of pull requests),git ls-remote https://github.com/o/rtells whether a repository is gone,raw.githubusercontent.comserves file contents. - Before a batch, read
rate_limit(gh api rate_limit --jq .resources.core), budget the batch and keep a reserve for the actions that follow; save responses to disk so that a re-run does not fetch them again; run at most 2-3 requests in parallel; and stop at the first “API rate limit exceeded” instead of retrying (the reset time is in theX-RateLimit-Resetheader).
Conventions
- Keep the shell style of the surrounding code (plain bash,
[ x"$A" == x... ]in old code is fine to leave). - Messages meant for contributors should say what to do, in plain words.
- Existing entries are not re-tested automatically; new checks and flags apply to PRs and to entries as they are updated.