AGENTS.md

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 #Channel at 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: .desktop file (with an [AppImageHub] section of X-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/ (notably app_teaser.html, app_notes.html), feed.json, search.json: the site. app.html sets post = page, so app templates use post.desktop.AppImageHub.X-AppImage-* etc.

How testing a submission works

  1. .github/workflows/test.yml (on PRs and pushes touching data/**) runs code/worker.sh data/<Name> for each added or changed file, under a per-app timeout, 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 /) via code/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; then code/find-appimage.sh picks the AppImage by rules (among them: the newest toolkit build, e.g. Qt6 over Qt5; and the channel asked for with #Channel at the end of the URL in data/, 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), runs code/appdir-lint.sh (vendored from AppImage/AppImages with fixes: errors are reported instead of ending the script under set -e, the excludelist check works with several matches; the excludelist itself 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 in data/ (errors only for files a PR adds, STRICT=true; warnings otherwise), and remarks about the AppImage’s file name; it also prints a NOTE: (shown under “Notes” in the test result, not a warning) for an all-lowercase name without Terminal=true in the desktop file, or a name with capitals with Terminal=true: all-lowercase names are typically command line tools; the publish workflow only shows a note that check-name.sh --note-text generates exactly;
    • code/check-name.sh also rejects the word “App” in a name (Foo-App, foo_app, App_Outlet: a token between -, _, . or blanks; not WhatsApp, Apple), an error for files a PR adds and a warning for the 32 old ones, and code/pick-name.sh (auto-discovery) leaves such a word out of the proposed name (aniwings-app gives AniWings), 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.sh finds a tray API in its files (Qt, GTK, libappindicator, Electron new 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.sh gets tray (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; label error-not-english);
    • warns when the AppImage has no (or broken) update information;
    • writes database/ and apps/. links: in apps/<Name>.md (shown on the page and in feed.json) gets the repository and its releases for GitHub, Codeberg and GitLab entries, and otherwise a Download link to the URL in data/ 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; on master it commits the result ([ci skip]). Afterwards the workflow collects a pr-result artifact: PR number, screenshot (or screen capture on failure), code/diagnose.sh hints and the excerpt of the log around the first error, and the libc flags.
  2. .github/workflows/publish-pr-screenshot.yml runs on workflow_run of “Test” (from master, 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 the SCREENSHOT_UPLOAD_TOKEN secret (classic PAT), falling back to assets of the ci-screenshots release, which it prunes. Renewing the token: see MAINTAINER.md. It also sets red error-* labels on the PR for the known causes of a failed test (defined next to the hints in code/diagnose.sh, which holds triples of pattern, label and hint; error-other when none applies) and removes error-* labels that no longer apply; the green screenshot-ok label marks a passed test whose screenshot code/check-screenshot.sh found 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.sh on the first line of the PR’s data/ 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 the URL: line of the log, which the test result artifact carries as tested.txt; only URLs of a strict form are accepted). Finally it auto-merges (squash) a PR from a returning contributor (author_association CONTRIBUTOR or above) that changes exactly one file in data/, passed with screenshot-ok, has no manual-check-needed or do-not-merge label, and does not change the first line of an existing file in data/ (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/releases or .../releases/download/... to https://github.com/o/r); even a change within the same owner may be a different project that shares the file name (#8569). merge-labeled.yml applies the same rule. A merge with the workflow’s token does not trigger push workflows, so it starts test.yml by workflow_dispatch with the merge commit (input commit), which writes database/ as a push would. Re-test PRs (branch retest/* in this repository, opened by /retest on a merged PR) are merged whenever their test passes, even with warnings, as long as they only modify existing files in data/ and keep each file’s first line (or only change it from a link to one particular release to its repository, see /retest below). Switch off: repository variable AUTO_MERGE=false.

  3. .github/workflows/check-origin.yml (pull_request_target, from master) compares the first line of each changed file in data/ with master (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 yellow manual-check-needed label plus origin-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.sh exits 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 whose homepage field 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.sh exits 1 or 2 for a changed owner; the step must read that with RC=0 ; RESULT=$(...) || RC=$?, as RESULT=$(...) ; RC=$? ends the step under bash -e before anything is reported.) The same workflow also sets manual-check-needed (with its own comment, <!-- appimagehub-name-check -->) on a PR that adds or renames to a file in data/ whose name has more than two hyphens (often a version or file name instead of the application’s name), plus the reason label name-check; withdrawn when a later push fixes it, and manual-check-needed stays while either check flags the PR.

    .github/workflows/one-file-per-pr.yml (pull_request_target, opened and reopened) 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 in data/ 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_dispatch only; inputs limit (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 than days → re-test (retest-pr.sh, with SCREENSHOT_UPLOAD_TOKEN), label retested-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 /retest when fixed and announces the closing, label pinged-author (marker <!-- appimagehub-stale-ping -->); (3) close_days after the ping with no human comment → closed with a comment, label auto-closed (the other two removed). A PR that passes the re-test keeps screenshot-ok and waits for a maintainer. discover-apps.sh treats an auto-closed PR 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) for days; the run stops with exit 4 when fewer than RESERVE (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, and workflow_dispatch; code/health-report.sh) writes one issue, updated in place (marker <!-- appimagehub-health-report -->, label health-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 without database/, stale, pinned to a release, entry-unavailable issues), and the automation (whether SCREENSHOT_UPLOAD_TOKEN works and when it expires, last runs). It sets the needs-maintainer label exactly on the PRs blocked on a maintainer (and removes it elsewhere), so that they can be found among the thousands handled by bots.

  4. .github/workflows/ping-authors.yml (monthly schedule, a real run, and workflow_dispatch with dry_run (default true), max_issues (default 5) and full_scan) runs code/check-entry.sh over the files in data/ (10 in parallel, 10 s timeouts; it stops once max_issues new dead entries are found unless full_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 are unknown and are never pinged, and so is a gone release asset whose repository still has AppImages that find-appimage.sh cannot pick for the entry, e.g. python2 and python3 from one release), writes a summary, and runs code/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), is fixable (pointing to the repository or directory): it opens a pull request (branch ping-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.sh opens (at most max_issues) issues labeled entry-unavailable for dead entries that never had one (open or closed), @-mentioning the author of the commit that added the entry and the upstream GitHub owner (never probonopd or 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); see MAINTAINER.md.

    .github/workflows/remove-dead-entries.yml (workflow_dispatch only; inputs limit (default 10), days (default 90), dry_run (default true); code/remove-dead-entries.sh) removes the entries whose entry-unavailable issue is at least days old, has no comment by anyone but github-actions, and whose entry check-entry.sh finds dead again right now (unknown, fixable and ok keep it): remove-entry.sh in one commit on master, and each issue is closed with a comment that a new PR brings the application back. Same concurrency group as remove-entry.yml.

  5. .github/workflows/remove-entry.yml removes entries completely with code/remove-entry.sh (data/NAME, database/NAME/, apps/NAME.md): on a maintainer’s /remove comment 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 /remove comment on a pull request (it is closed and labeled opt-out; the entries it adds or changes are removed if they are in the catalog), by hand with names, and on pushes to master that delete or rename files in data/ (it removes what is left of them). Deleting only data/NAME used to leave the page on the site.

  6. .github/workflows/discover-apps.yml (workflow_dispatch only; inputs count, default 1, no upper limit, months (default 3) and dry_run) runs code/discover-apps.sh to 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, labeled auto-discovered, for a maintainer to review. Recent mode (months N > 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 has lastrun=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 (months 0, 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 branch discover/NAME that exists already (an earlier PR for the same name) skips the app (outcome name) 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 a topic:appimage and an appimage in:name,description,readme query, going back month after month until count apps 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 in data/, is added by an open pull request, was proposed before (any pull request labeled auto-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 the Repository: line of the PR bodies), was opted out (a PR labeled black opt-out, any PR in any state, names the repository or adds a data/ file with the app’s name, compared by letters and digits, so the app is not proposed from another repository either; outcome opt-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) with code/fetch-releases.sh and code/find-appimage.sh (exactly one x86_64 AppImage, in a release less than 2 years old) and code/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 (outcome copy for 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 the not-upstream label. 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 of commits?per_page=1). Then it gets the labels suspicious-stars (the reason) and manual-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, and workflow_dispatch with days (30), limit (50) and dry_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 in data/, branch and PR title) is the proper spelling picked by code/pick-name.sh from 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 in Uploader.PRO, become _: photoapp + PhotoApp-1.2.AppImage gives PhotoApp, photo-app + “Photo App is …” gives Photo_App); an app already in data/ 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_dispatch only; inputs limit (maximum number of apps to add, default 1) and dry_run) does the same for the AppImages that https://github.com/pkgforge-dev builds, with code/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 in data/ (by URL or by name), added by an open PR or named by an auto-discovered/opt-out PR (any state); the rest is checked like in discover-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 gets auto-discovered and repackaged (not not-upstream: repackaged already 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_dispatch only; inputs count (default 1) and dry_run) does the same for AppImageHub (the openDesktop/Pling store) with code/discover-apphub.sh. AppImageHub’s own API host is deprecated, but the shared OCS v1 API still serves it anonymously at https://api.opendesktop.org/ocs/v1/; the app categories are the ones whose xdg_type is bin. An entry’s downloadlink is a tokenized, expiring Pling URL and its homepage points 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” .AppImage link; Pling-hosted files, versioned files and non-listing HTML are skipped. Apps already in data/, added by an open PR, or named by an auto-discovered/opt-out PR (any state) are skipped by name. Same auto-discovered label and DISCOVER_TOKEN/SCREENSHOT_UPLOAD_TOKEN/GITHUB_TOKEN handling as discover-apps.yml; a maintainer reviews the PRs.

  7. .github/workflows/auto-retest.yml (issue_comment only) 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.sh on the first line of the PR’s data/ 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 (an error-* label, or no screenshot-ok). It closes and reopens the PR with code/retest-pr.sh (SCREENSHOT_UPLOAD_TOKEN; reopens with GITHUB_TOKEN should 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.

  8. Answers to comments, without a maintainer:
    • auto-retest.yml also 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.yml labels a PR opt-out and 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 labeled not-upstream (a README saying “unofficial”, “not affiliated”, … or a fork), when the GitHub account of the repository in the PR’s single data/ 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), the not-upstream label is removed and manual-check-needed is 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_dispatch only; inputs limit (default 100) and dry_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 the error-missing-file label, 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.sh has hints for AppRun.wrapped: Permission denied (Tauri/linuxdeploy mode 0770) and for a .DirIcon that cannot be read (absolute symlink).

Rules that are easy to get wrong

  • pull_request_target workflows must never check out or run code from the PR. check-origin.yml only 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 eval anything from it; cross-check the PR number against the tested commit; only accept hints listed by code/diagnose.sh --list from master, 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 to publish-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.sh leave background processes behind. Anything started with & inherits stdout, which is piped into tee; a survivor keeps the pipe open and the job hangs until the Actions timeout instead of failing. The cleanup EXIT trap kills them and unmounts; redirect the output of new background processes, and wrap X11 calls in timeout.
  • worker.sh runs with set -v (and partly set -x), so the log contains its own source lines and traces. Patterns in code/diagnose.sh for the worker’s own messages are anchored with ^, and the excerpt drops lines that are verbatim lines of worker.sh or traces (^+).
  • The “first error” excerpt (code/diagnose.sh --excerpt) starts at a line with error while loading shared libraries wherever it is (a missing library is almost always the real cause; label error-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 --help in xterm; screenshot checks only warn for them.
  • Thresholds in code/check-screenshot.sh were calibrated on the ~1500 screenshots in database/. When changing them, re-run over those screenshots (OMP_THREAD_LIMIT=1 for tesseract, or it crawls when run in parallel) and report how many would change outcome.
  • Adding apt packages to test.yml: bump the cache version: 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.sh can 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 of data/ (git log --diff-filter=M -- data/), about a quarter of the URL changes move to another owner.
  • code/find-appimage.sh releases.json NAME runs on a saved repos/OWNER/REPO/releases response (GitHub’s shape; code/fetch-releases.sh normalizes Codeberg and GitLab to it). When changing its rules, compare its picks with the AppImages that the data/ files with direct links point to.
  • Site: bundle install (Gemfile: github-pages), then bundle exec jekyll build on a copy with only a few apps/*.md for speed. Note: feed.json is 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 in test.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 a data/ 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 touches data/. New PR comments from master’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.sh does this for all open PRs that change one file in data/ 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; -n to only list them). For one PR, comment /retest on it (maintainers, the PR’s author, and the GitHub account the AppImage comes from, per code/upstream-owner.sh): .github/workflows/retest.yml closes and reopens it with SCREENSHOT_UPLOAD_TOKEN (a reopen with GITHUB_TOKEN starts 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 (branch retest/<PR>-<time>, with the personal token) that only toggles the trailing newline of the merged PR’s files in data/ (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 refreshes database/); one open re-test PR per merged PR. Failed PRs are also re-tested when their author says it is fixed (item 7 above), and code/retest-pr.sh [-n] NUMBER re-tests one PR by hand (needs PAT). The comment patterns: echo "v1.2 is out" | code/retest-comment.sh.
  • .github/workflows/retest-stale.yml (monthly schedule, a real run of 20 entries; workflow_dispatch with inputs limit (default 20), days (default 365), pinned and dry_run (default true); --pinned re-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 whose database/NAME was last changed more than days days ago (code/retest-stale.sh: from the git history, oldest first, about 1200 of them in October 2026): one retest/stale-NAME-<time> pull request per entry (the Test workflow stops at the first failing entry of a PR), changing data/NAME the way /retest does on a merged PR (a link to one particular release becomes the repository URL, else the trailing newline is toggled), with SCREENSHOT_UPLOAD_TOKEN so that Test starts. A passing one is auto-merged like any re-test PR, which refreshes database/; 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 in data/, are skipped.
  • /appstream (maintainers, or the author of the issue or PR): .github/workflows/appstream-help.yml posts code/appstream-help.md from master, how to ship an AppStream metainfo file with screenshots. The catalog shows the metainfo’s screenshot (type="default", else the first; from *.metainfo.xml or *.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/latest redirects 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. with objdump -T for *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, --help in xterm for Terminal=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() in stale-discovered.sh), count them, check rate_limit every 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 limit actions, not after the whole list is read.
  • Never run two request-heavy jobs at once (concurrency in 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/head gets a pull request’s files and content (one fetch for hundreds of pull requests), git ls-remote https://github.com/o/r tells whether a repository is gone, raw.githubusercontent.com serves 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 the X-RateLimit-Reset header).

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.