Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Patches in `source=()` trigger a warning and are not blocked.

## Downgrades

aurtomator follows upstream when it publishes a version older than the tracked one, for example after a tag is deleted and recreated. Upstream is the source of truth. A downgrade is logged with the literal keyword `DOWNGRADE` and shown in the package table with a `⬇️` icon. It counts as a successful run, so the badge stays green. Details are in [docs/WORKFLOW.md section 4.3](docs/WORKFLOW.md#43-downgrade-behavior).
aurtomator publishes a version older than the tracked one only when the package YAML sets `allow_downgrade: true`. Without it, the downgrade is skipped: the latest release of a GitHub project can be a maintenance release of an older line. A downgrade is logged with the literal keyword `DOWNGRADE` and shown in the package table with a `⬇️` icon. It counts as a successful run, so the badge stays green. Details are in [docs/WORKFLOW.md section 4.3](docs/WORKFLOW.md#43-downgrade-behavior).

## Resource usage

Expand Down
32 changes: 21 additions & 11 deletions docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Each package has a file at `packages/<name>.yml`. The file name, the `name` fiel
| `upstream` | depends | Strategy-specific settings. `archpkg`, `repology` and `kde-tarball` need none. |
| `current` | no | Currently tracked version. Written after every push. |
| `last_updated` | no | Date of the last push, `YYYY-MM-DD`. Written after every push. |
| `allow_downgrade` | no | `true` publishes an upstream version lower than `current`. Default `false`, see [4.3](#43-downgrade-behavior). |

Example:

Expand Down Expand Up @@ -572,19 +573,25 @@ check-all.sh [--update | --prepare <dir> | --verify <dir> | --publish <dir>] [--

### 4.3 Downgrade behavior

aurtomator follows upstream when it publishes a version older than the tracked one, and does **not** block it. Upstream is the source of truth: a packager that refuses a re-published older release only drifts out of sync. A downgrade is logged and shown, and it does not count as a failure.
An upstream version lower than `current` reaches the AUR only when the package YAML sets `allow_downgrade: true`. Without it, `check-all.sh` skips the version, writes `downgrade_skipped: <version>` and pushes nothing. A skipped downgrade opens no issue and does not count as a failure. `--prepare` and `--publish` apply the same rule, so a downgrade of a package without the flag is never prepared or published.

The default guards against a lower version that upstream does not mean as a step back. `github-release` reads `/releases/latest`, and GitHub picks that release by the date of the commit it was made from, not by the date it was published. A maintenance release of an older line, made from a commit newer than the one of the current release, becomes the latest release. Without the flag, aurtomator would push it as a downgrade and push the newer version again at the next release.

`allow_downgrade: true` fits a package whose upstream does mean it, for example one that rolls back a broken release and the AUR package should follow.

#### When does this happen?

- Upstream publishes a maintenance release of an older line, and `/releases/latest` returns it.
- Upstream deletes and recreates a release tag. In the gap, `/releases/latest` returns the previous release, so a cycle like `v1.2.1 → v1.2.0 → v1.2.1` can take only minutes.
- Upstream rolls back a broken release and removes its tag.
- Upstream marks an older release line as latest again, for example when a 2.x release is pulled and 1.x becomes the newest release.

#### How to detect downgrades in logs

Every comparison that finds the current version newer than the latest one (`vercmp "$current" "$latest"` returns `1`) logs the literal token `DOWNGRADE`. In a CI run, `check-all.sh` logs:
Every comparison that finds the current version newer than the latest one (`vercmp "$current" "$latest"` returns `1`) logs the literal token `DOWNGRADE`. In a CI run, `check-all.sh` logs the first line for a package without `allow_downgrade: true` and the second for one with it:

```text
! myapp-bin: DOWNGRADE detected 1.2.1 → 1.2.0, skipped: allow_downgrade is not set
! myapp-bin: DOWNGRADE detected 1.2.1 → 1.2.0 (mirroring upstream)
```

Expand All @@ -598,19 +605,21 @@ Every comparison that finds the current version newer than the latest one (`verc

#### Status values and README icons

`check-all.sh` writes two status tokens for downgrades to `.status/<pkg>`:
`check-all.sh` writes three status tokens for downgrades to `.status/<pkg>`:

| Token | Meaning |
|-------------------------------|---------------------------------------------------|
| `downgrade: <version>` | `--update` mode: the downgrade was pushed to the AUR |
| `available_downgrade: <ver>` | Check-only mode: a downgrade is available and not applied |
| Token | Meaning |
|--------------------------------|----------------------------------------------------------------|
| `downgrade_skipped: <version>` | A downgrade was found for a package without `allow_downgrade: true`. Nothing is pushed |
| `available_downgrade: <ver>` | A downgrade the package allows was found and is not pushed yet |
| `downgrade: <version>` | The downgrade was pushed to the AUR |

`update-readme.sh` renders them as:

- `downgrade: 1.2.0` → `⬇️ downgraded to 1.2.0`
- `downgrade_skipped: 1.2.0` → `⬇️ 1.2.0 skipped (downgrade)`
- `available_downgrade: 1.2.0` → `⬇️ 1.2.0 available (downgrade)`
- `downgrade: 1.2.0` → `⬇️ downgraded to 1.2.0`

Both count toward `pkg_ok`, so the packages badge stays `brightgreen`. Only `check_failed` and `update_failed` turn it red.
All three count toward `pkg_ok`, so the packages badge stays `brightgreen`. Only `check_failed` and `update_failed` turn it red.

### 4.4 Prepare and publish

Expand Down Expand Up @@ -640,11 +649,11 @@ A PKGBUILD is a bash script, and every step that reads it executes it: the audit
5. Each `source` line of the prepared `.SRCINFO`, with or without an arch suffix, must be the line of the AUR copy with the old version replaced by `<version>`. A source that is `SKIP` in every checksum array of its arch may also change the digits in the name it is saved under, the part before `::`, as `neovim-20261014.tar.gz` becomes `neovim-20261016.tar.gz` in a PKGBUILD that names a nightly download by `$(date -u +%Y%m%d)`. Its URL after `::` must still be the AUR URL with the version replaced. Verify never downloads a `SKIP` source, and makepkg takes the sources from the `PKGBUILD`. Any other line must either equal the line of the AUR copy or be that line with the version replaced in the same way, because a license, a dependency or a description can hold the old version without taking it from `pkgver`, as `GPL-2.0` does in a package at version `2.0`. The replacement also covers three forms a PKGBUILD often derives from `pkgver`: dots turned into underscores (`1_86_0`), the part before the first `+` (`0.13.0` of `0.13.0+dev+1853+g6edb38ca98`), and the version without its last component (`6.19` of `6.19.0`, as in `frameworks/${pkgver%.*}/` of KDE Frameworks). The last form counts only while it keeps a dot. Each part of a line is replaced at most once, so `5.1` inside an already replaced `5.10.0` stays. A line built from any other form of the version, a lone major version for example, stops the publish, and so does a line that has the version replaced in one place and kept in another. The package then needs a manual update. `pkgrel` and the checksums follow the rules of step 4.
6. Any other difference, and a NUL byte in either file, stops the publish with exit code 1. Otherwise the index of the clone takes the tree of `HEAD`, and the two prepared files enter it byte for byte through `git hash-object --no-filters` and `git update-index`, so the commit keeps every other file of the AUR repo. `git add` would pass them through the attributes of the AUR repo, and `working-tree-encoding=UTF-7` there turns `+AAo-` in a comment into a new line that the checks above never saw. Steps 10 to 13 of [4.2](#42-step-by-step) follow. When the prepared files equal the AUR copy, only `current` is written to the YAML and the exit code is 2.

`check-all.sh --prepare <dir>` runs no strategy and needs no `GITHUB_TOKEN`. It takes the versions from the statuses that a check run, `check-all.sh` without a mode, wrote to `.status/` before it, and reads them before the first PKGBUILD runs. It prepares every package with `new_version` or `available_downgrade` into `<dir>/prepared/` and copies the statuses to `<dir>/status/`. A failed preparation becomes `update_failed`, and a package without a status from the check run becomes `check_failed`. The other statuses stay as the check run wrote them.
`check-all.sh --prepare <dir>` runs no strategy and needs no `GITHUB_TOKEN`. It takes the versions from the statuses that a check run, `check-all.sh` without a mode, wrote to `.status/` before it, and reads them before the first PKGBUILD runs. It prepares every package with `new_version` or `available_downgrade` into `<dir>/prepared/` and copies the statuses to `<dir>/status/`. A downgrade of a package without `allow_downgrade: true` gets `downgrade_skipped` instead, see [4.3](#43-downgrade-behavior). A failed preparation becomes `update_failed`, and a package without a status from the check run becomes `check_failed`. The other statuses stay as the check run wrote them.

`check-all.sh --verify <dir>` runs `verify-pkg.sh` for every package in `packages/` that has a directory in `<dir>/prepared/`, and runs no strategy. An update that fails is removed from `<dir>/prepared/`, its status in `<dir>/status/` becomes `update_failed`, and `<dir>/status/_error_count` grows by one. Exit code 3 counts as a failure as well: the prepare run writes `base` for every package, so a moved AUR repo looks the same as a wrong `base`. The next run prepares the package again.

`check-all.sh --publish <dir>` takes the statuses of the check run from `.status/`, as `--prepare` does, and writes `.status/` anew. `up_to_date` and `check_failed` stay as they are. A missing status, and one the check run does not write, become `check_failed`. For a package with `new_version` or `available_downgrade` and a directory in `<dir>/prepared/`, it publishes the version in that status and runs no strategy, so nothing contacts an upstream host while the AUR and GPG keys are in place. Exit code 3, an AUR repo that moved, leaves the status at `new_version` or `available_downgrade` without an error, and the next run prepares the package again. A package with `new_version` or `available_downgrade` and no prepared directory gets the `update_failed` status that the prepare or the verify run wrote for it to `<dir>/status/`, or `update_failed: prepare and verify handed over no update`. Nothing else is taken from `<dir>/status/`, and only a regular file without a NUL byte: the prepare run executes PKGBUILDs, and code from one of them can write the files of every package. `_error_count` is the number of `check_failed` and `update_failed` statuses.
`check-all.sh --publish <dir>` takes the statuses of the check run from `.status/`, as `--prepare` does, and writes `.status/` anew. `up_to_date`, `check_failed` and `downgrade_skipped` stay as they are, and a downgrade of a package without `allow_downgrade: true` gets `downgrade_skipped` and is not published. A missing status, and one the check run does not write, become `check_failed`. For a package with `new_version` or `available_downgrade` and a directory in `<dir>/prepared/`, it publishes the version in that status and runs no strategy, so nothing contacts an upstream host while the AUR and GPG keys are in place. Exit code 3, an AUR repo that moved, leaves the status at `new_version` or `available_downgrade` without an error, and the next run prepares the package again. A package with `new_version` or `available_downgrade` and no prepared directory gets the `update_failed` status that the prepare or the verify run wrote for it to `<dir>/status/`, or `update_failed: prepare and verify handed over no update`. Nothing else is taken from `<dir>/status/`, and only a regular file without a NUL byte: the prepare run executes PKGBUILDs, and code from one of them can write the files of every package. `_error_count` is the number of `check_failed` and `update_failed` statuses.

## 5. CI Workflow: check-updates.yml

Expand Down Expand Up @@ -733,6 +742,7 @@ Steps 9 and 10 run as root with `GH_TOKEN` set to the workflow token, `github.to
| `.status/<name>` | `new_version: 2.0.0` | Update found, not pushed yet |
| `.status/<name>` | `downgrade: 1.2.0` | Downgrade pushed to the AUR |
| `.status/<name>` | `available_downgrade: 1.2.0` | Downgrade found, not pushed yet |
| `.status/<name>` | `downgrade_skipped: 1.2.0` | Downgrade found, not allowed by the YAML |
| `.status/<name>` | `check_failed: <error>` | The strategy failed |
| `.status/<name>` | `update_failed: <error>` | The update failed |
| `.status/_error_count` | `0` | Total error count of the run |
Expand Down
19 changes: 15 additions & 4 deletions scripts/check-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,12 @@ checked=0
errors=0

# The version the check run found for a package, printed the way
# check-update.sh prints it: nothing when up to date, an error on failure
# check-update.sh prints it: nothing when up to date, an error on failure.
# A skipped downgrade prints its version, and the check below skips it again.
checked_version() {
local status="${CHECKED[$1]:-}"
case "$status" in
"new_version: "* | "available_downgrade: "*)
"new_version: "* | "available_downgrade: "* | "downgrade_skipped: "*)
echo "${status#*: }"
;;
up_to_date) ;;
Expand Down Expand Up @@ -218,7 +219,7 @@ for pkg_file in $(printf '%s\n' "${AURTOMATOR_DIR}"/packages/*.yml | sort); do
continue
fi
;;
up_to_date | check_failed:*)
up_to_date | check_failed:* | "downgrade_skipped: "*)
printf '%s\n' "$found" >"${STATUS_DIR}/${pkg_name}"
continue
;;
Expand Down Expand Up @@ -267,11 +268,21 @@ for pkg_file in $(printf '%s\n' "${AURTOMATOR_DIR}"/packages/*.yml | sort); do
cmp=$(vercmp "$current" "$latest")
if [[ "$cmp" -gt 0 ]]; then
is_downgrade=true
log_warn "$pkg_name: DOWNGRADE detected $current → $latest (mirroring upstream)"
fi
fi

# A lower version reaches the AUR only for a package that allows it. The
# latest release of a GitHub project can be a maintenance release of an
# older line.
if [[ "$is_downgrade" == "true" ]] &&
[[ "$(pkg_get "$pkg_file" '.allow_downgrade // false')" != "true" ]]; then
log_warn "$pkg_name: DOWNGRADE detected $current → $latest, skipped: allow_downgrade is not set"
echo "downgrade_skipped: ${latest}" >"${STATUS_DIR}/${pkg_name}"
continue
fi

if [[ "$is_downgrade" == "true" ]]; then
log_warn "$pkg_name: DOWNGRADE detected $current → $latest (mirroring upstream)"
available="available_downgrade: ${latest}"
pushed="downgrade: ${latest}"
else
Expand Down
4 changes: 4 additions & 0 deletions scripts/update-readme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ for pkg_file in $(printf '%s\n' "${AURTOMATOR_DIR}"/packages/*.yml | sort); do
status="⬇️ ${run_status#available_downgrade: } available (downgrade)"
((pkg_ok++)) || true
;;
downgrade_skipped:*)
status="⬇️ ${run_status#downgrade_skipped: } skipped (downgrade)"
((pkg_ok++)) || true
;;
check_failed:* | update_failed:*)
status="❌ failed"
((pkg_fail++)) || true
Expand Down
9 changes: 9 additions & 0 deletions scripts/validate-pkg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ validate_pkg() {
return 1
fi

# --- Optional fields ---

local allow_downgrade
allow_downgrade=$(pkg_get "$file" '.allow_downgrade | tag')
if [[ "$allow_downgrade" != "!!null" && "$allow_downgrade" != "!!bool" ]]; then
log_err "$file: allow_downgrade must be true or false"
((errors++))
fi

# --- Strategy-specific fields ---

# Validate optional tag_pattern as regex (used by *-tag strategies)
Expand Down
42 changes: 41 additions & 1 deletion tests/publish.bats
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ EOF
current: '1.0.0'"
create_test_package "older" "github-release" "upstream:
project: owner/repo
current: '1.0.0'"
current: '1.0.0'
allow_downgrade: true"
echo "new_version: 2.1.0" >"$TEST_TMPDIR/.status/pkg"
echo "available_downgrade: 0.9.0" >"$TEST_TMPDIR/.status/older"
stub_script check-update.sh 1 "strategy ran"
Expand Down Expand Up @@ -732,6 +733,19 @@ current: '1.0.0'"
[ "$(cat "$TEST_TMPDIR/out/status/_error_count")" = "2" ]
}

@test "check-all --prepare: prepares no downgrade the check run skipped" {
create_test_package "older" "github-release" "upstream:
project: owner/repo
current: '1.0.0'"
echo "downgrade_skipped: 0.9.0" >"$TEST_TMPDIR/.status/older"
stub_script update-pkg.sh 0
run "$TEST_TMPDIR/scripts/check-all.sh" --prepare "$TEST_TMPDIR/out"
[ "$status" -eq 0 ]
[ ! -e "$TEST_TMPDIR/update-pkg.sh.args" ]
[ "$(cat "$TEST_TMPDIR/out/status/older")" = "downgrade_skipped: 0.9.0" ]
[ "$(cat "$TEST_TMPDIR/out/status/_error_count")" = "0" ]
}

@test "check-all --prepare: records a failed preparation" {
create_test_package "pkg" "github-release" "upstream:
project: owner/repo
Expand Down Expand Up @@ -779,6 +793,32 @@ current: '2.0.0'"
[ "$(cat "$TEST_TMPDIR/.status/_error_count")" = "1" ]
}

@test "check-all --publish: keeps a skipped downgrade and publishes nothing" {
create_test_package "older" "github-release" "upstream:
project: owner/repo
current: '1.0.0'"
checked_status older "downgrade_skipped: 0.9.0"
stub_script publish-pkg.sh 0
run "$TEST_TMPDIR/scripts/check-all.sh" --publish "$TEST_TMPDIR/out"
[ "$status" -eq 0 ]
[ ! -e "$TEST_TMPDIR/publish-pkg.sh.args" ]
[ "$(cat "$TEST_TMPDIR/.status/older")" = "downgrade_skipped: 0.9.0" ]
[ "$(cat "$TEST_TMPDIR/.status/_error_count")" = "0" ]
}

@test "check-all --publish: publishes no downgrade of a package that does not allow it" {
create_test_package "older" "github-release" "upstream:
project: owner/repo
current: '1.0.0'"
checked_status older "available_downgrade: 0.9.0"
mkdir -p "$TEST_TMPDIR/out/prepared/older"
stub_script publish-pkg.sh 0
run "$TEST_TMPDIR/scripts/check-all.sh" --publish "$TEST_TMPDIR/out"
[ "$status" -eq 0 ]
[ ! -e "$TEST_TMPDIR/publish-pkg.sh.args" ]
[ "$(cat "$TEST_TMPDIR/.status/older")" = "downgrade_skipped: 0.9.0" ]
}

@test "check-all --publish: marks a package the check run left out" {
create_test_package "pkg" "github-release" "upstream:
project: owner/repo
Expand Down
Loading
Loading