From ddfbcde662754d9d49ba6b056ff8f3091cb4fa6f Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:01:18 +0500 Subject: [PATCH] fix(strategy): keep release tags by default Without tag_pattern, the tag strategies take only v1.2.3 and 1.2.3. --- docs/SUPPORTED-PACKAGES.md | 8 ++--- docs/WORKFLOW.md | 15 ++++---- packages/example.yml.sample | 16 +++++---- scripts/lib.sh | 8 +++++ strategies/gitea-tag.sh | 8 ++++- strategies/github-tag.sh | 8 ++++- strategies/gitlab-tag.sh | 8 ++++- tests/helpers/mock-curl.sh | 11 ++++++ tests/strategies.bats | 68 +++++++++++++++++++++++++++++++++++++ 9 files changed, 128 insertions(+), 22 deletions(-) diff --git a/docs/SUPPORTED-PACKAGES.md b/docs/SUPPORTED-PACKAGES.md index 38b66e9..9bfee58 100644 --- a/docs/SUPPORTED-PACKAGES.md +++ b/docs/SUPPORTED-PACKAGES.md @@ -188,11 +188,11 @@ aurtomator includes 14 version detection strategies. Choose by where your upstre | Strategy | Source | Best for | Pre-release filter | |---|---|---|---| | `github-release` | GitHub Releases API | Most GitHub projects | Auto (excludes pre-releases) | -| `github-tag` | GitHub Tags API | Projects without formal releases | Manual (`tag_pattern`) | +| `github-tag` | GitHub Tags API | Projects without formal releases | Auto (`v1.2.3` tags only) | | `github-nightly` | GitHub Releases API | Nightly/dev builds (4 patterns) | Nightly-specific | | `github-asset` | GitHub Releases API | Assets re-uploaded on a fixed tag (model zoos) | N/A (fixed tag) | -| `gitlab-tag` | GitLab Tags API | GitLab.com, invent.kde.org, self-hosted | Manual (`tag_pattern`) | -| `gitea-tag` | Gitea/Forgejo Tags API | Codeberg, self-hosted Gitea | Manual (`tag_pattern`) | +| `gitlab-tag` | GitLab Tags API | GitLab.com, invent.kde.org, self-hosted | Auto (`v1.2.3` tags only) | +| `gitea-tag` | Gitea/Forgejo Tags API | Codeberg, self-hosted Gitea | Auto (`v1.2.3` tags only) | | `git-latest` | Bare git clone | `-git` packages (commit tracking) | N/A | | `pypi` | PyPI JSON API | Python packages | Auto (stable only) | | `npm` | npm registry | Node.js packages | Auto (`latest` tag) | @@ -206,7 +206,7 @@ aurtomator includes 14 version detection strategies. Choose by where your upstre - **GitHub rate limit:** the GitHub strategies send `GITHUB_TOKEN` when it is set in the environment. The `check-updates` workflow sets it to the workflow token, which allows 1,000 requests per hour per repository. Without a token, the limit is 60 requests per hour. - **No retry on API failure:** strategies make one attempt. A transient failure fails the package for that run. -- **Tag strategies include pre-releases:** `github-tag`, `gitlab-tag` and `gitea-tag` pick the highest tag with `sort -V`, which places `1.0.0-rc1` above `1.0.0`. Filter with `tag_pattern`. +- **Tag strategies take only `v1.2.3` and `1.2.3` by default:** without `tag_pattern`, and without `tag_version_regex` for `github-tag`, `github-tag`, `gitlab-tag` and `gitea-tag` leave out every other tag, `latest`, `1.0.0-rc1` and `1.0.0rc1` included. A `tag_pattern` replaces that filter. They pick the highest tag with `sort -V`, which places `1.0.0-rc1` above `1.0.0`, so a pattern that lets pre-release tags through picks them. - **`gitea-tag` fetches at most 50 tags:** there is no pagination. - **`gitlab-tag` has no authentication:** private GitLab projects cannot be queried. diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 4c75dc7..d1dd129 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -139,10 +139,10 @@ upstream: | Field | Required | Description | |------------------------------|----------|------------------------------------------| | `upstream.project` | yes | `owner/repo` format | -| `upstream.tag_pattern` | no | Glob to filter tags (e.g. `v*`) | -| `upstream.tag_version_regex` | no | ERE with one capture group | +| `upstream.tag_pattern` | no | Glob to filter tags (e.g. `release-*`), replaces the default filter | +| `upstream.tag_version_regex` | no | ERE with one capture group, replaces the default filter | -**How the version is extracted:** fetches the first 100 tags and filters them by `tag_pattern`, where `*` matches anything and the match is anchored at the start. It strips a leading `v`, or applies `tag_version_regex`, and returns the highest tag by `sort -V`. `sort -V` places `1.0.0-rc1` above `1.0.0`, so filter pre-release tags with `tag_pattern`. +**How the version is extracted:** fetches the first 100 tags. Without `tag_pattern` and `tag_version_regex`, it keeps the tags of the form `v1.2.3` or `1.2.3`, so `latest`, `1.0.0-rc1` and `1.0.0rc1` are left out, and a project without such a tag fails the check with a message that names both fields. `tag_pattern` filters the tags instead, where `*` matches anything and the match is anchored at the start. The strategy strips a leading `v`, or applies `tag_version_regex`, and returns the highest tag by `sort -V`. `sort -V` places `1.0.0-rc1` above `1.0.0`, so a `tag_pattern` that lets pre-release tags through picks them. **Authentication:** same as `github-release`. @@ -153,7 +153,6 @@ name: my-package strategy: github-tag upstream: project: owner/repo - tag_pattern: "v*" ``` ### 3.3 github-nightly @@ -268,9 +267,9 @@ upstream: |------------------------|----------|-------------------------------------------------------| | `upstream.host` | yes | GitLab hostname (e.g. `invent.kde.org`, `gitlab.com`) | | `upstream.project` | yes | Project path (e.g. `network/my-kde-app`) | -| `upstream.tag_pattern` | no | Glob to filter tags | +| `upstream.tag_pattern` | no | Glob to filter tags, replaces the default filter | -**How the version is extracted:** URL-encodes the project path (`/` becomes `%2F`), fetches the first 100 tags, filters them by `tag_pattern`, strips a leading `v` and returns the highest by `sort -V`. There is no authentication, so private projects cannot be queried. +**How the version is extracted:** URL-encodes the project path (`/` becomes `%2F`) and fetches the first 100 tags. Without `tag_pattern`, it keeps the tags of the form `v1.2.3` or `1.2.3`, and a project without such a tag fails the check with a message that names `tag_pattern`. With it, it filters the tags by the pattern. It strips a leading `v` and returns the highest by `sort -V`. There is no authentication, so private projects cannot be queried. **Example YAML:** @@ -294,9 +293,9 @@ upstream: |------------------------|----------|------------------------------------------| | `upstream.host` | yes | Instance hostname (e.g. `codeberg.org`) | | `upstream.project` | yes | `owner/repo` format | -| `upstream.tag_pattern` | no | Glob to filter tags | +| `upstream.tag_pattern` | no | Glob to filter tags, replaces the default filter | -**How the version is extracted:** fetches up to 50 tags without pagination, filters them by `tag_pattern`, strips a leading `v` and returns the highest by `sort -V`. +**How the version is extracted:** fetches up to 50 tags without pagination. Without `tag_pattern`, it keeps the tags of the form `v1.2.3` or `1.2.3`, and a project without such a tag fails the check with a message that names `tag_pattern`. With it, it filters the tags by the pattern. It strips a leading `v` and returns the highest by `sort -V`. **Example YAML:** diff --git a/packages/example.yml.sample b/packages/example.yml.sample index 7efa3a6..8678345 100644 --- a/packages/example.yml.sample +++ b/packages/example.yml.sample @@ -50,13 +50,13 @@ upstream: # tag_version_regex: '(.+)-stable' # optional, ERE with a capture group # --- github-tag ------------------------------------------------------------- -# Reads up to 100 tags from /repos/{project}/tags, pre-release tags included. -# Filter them with tag_pattern or tag_version_regex. +# Reads up to 100 tags from /repos/{project}/tags and keeps v1.2.3 and 1.2.3. +# tag_pattern or tag_version_regex replaces that filter. # # strategy: github-tag # upstream: # project: owner/repo # required -# tag_pattern: 'v*' # optional, glob filter +# tag_pattern: 'release-*' # optional, glob filter # tag_version_regex: 'release-(.+)' # optional, ERE with a capture group # --- github-nightly --------------------------------------------------------- @@ -102,22 +102,24 @@ upstream: # - voice-b.tar.bz2 # --- gitlab-tag ------------------------------------------------------------- -# Any GitLab instance, public API only (no authentication). +# Any GitLab instance, public API only (no authentication). Keeps the tags +# v1.2.3 and 1.2.3; tag_pattern replaces that filter. # # strategy: gitlab-tag # upstream: # host: gitlab.com # required, hostname without protocol # project: group/subgroup/project # required, full path -# tag_pattern: 'v*' # optional +# tag_pattern: 'release-*' # optional # --- gitea-tag -------------------------------------------------------------- -# Codeberg, self-hosted Gitea/Forgejo. Reads at most 50 tags. +# Codeberg, self-hosted Gitea/Forgejo. Reads at most 50 tags and keeps +# v1.2.3 and 1.2.3; tag_pattern replaces that filter. # # strategy: gitea-tag # upstream: # host: codeberg.org # required # project: owner/repo # required -# tag_pattern: 'v*' # optional +# tag_pattern: 'release-*' # optional # --- git-latest ------------------------------------------------------------- # For -git packages. Produces rCOUNT.HASH (e.g. r435.abc1234). diff --git a/scripts/lib.sh b/scripts/lib.sh index 218ffa5..56338fc 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -118,6 +118,14 @@ validate_pkgver() { fi } +# The tags of the form v1.2.3 or 1.2.3, for a tag strategy without a filter +# of its own. sort -V puts latest, 2.1.0-rc1 and 2.1.0rc1 above 2.1.0, so +# they are left out. +# Args: $1=tags, one per line +release_tags() { + grep -E '^v?[0-9]+(\.[0-9]+)*$' <<<"$1" || true +} + # ============================================================================= # CHECKSUMS # ============================================================================= diff --git a/strategies/gitea-tag.sh b/strategies/gitea-tag.sh index b5bce41..8ef08d5 100755 --- a/strategies/gitea-tag.sh +++ b/strategies/gitea-tag.sh @@ -34,10 +34,16 @@ if [[ -z "$tags" ]]; then exit 1 fi -# Filter by pattern if set +# Filter by pattern if set, by the form of a release if not if [[ -n "$tag_pattern" ]]; then regex="^${tag_pattern//\*/.*}" tags=$(echo "$tags" | grep -E "$regex" || true) +else + tags=$(release_tags "$tags") + if [[ -z "$tags" ]]; then + echo "No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern" >&2 + exit 1 + fi fi # Strip v prefix, sort semver, take latest diff --git a/strategies/github-tag.sh b/strategies/github-tag.sh index c4bfa8e..18e0807 100755 --- a/strategies/github-tag.sh +++ b/strategies/github-tag.sh @@ -34,10 +34,16 @@ if [[ -z "$tags" ]]; then exit 1 fi -# Filter by pattern if set +# Filter by pattern if set, by the form of a release if neither field is if [[ -n "$tag_pattern" ]]; then regex="^${tag_pattern//\*/.*}" tags=$(echo "$tags" | grep -E "$regex" || true) +elif [[ -z "$tag_version_regex" ]]; then + tags=$(release_tags "$tags") + if [[ -z "$tags" ]]; then + echo "No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern or upstream.tag_version_regex" >&2 + exit 1 + fi fi # Extract version from tags diff --git a/strategies/gitlab-tag.sh b/strategies/gitlab-tag.sh index d21c211..2191e42 100755 --- a/strategies/gitlab-tag.sh +++ b/strategies/gitlab-tag.sh @@ -37,11 +37,17 @@ if [[ -z "$tags" ]]; then exit 1 fi -# Filter by pattern if set +# Filter by pattern if set, by the form of a release if not if [[ -n "$tag_pattern" ]]; then # Convert glob pattern (v*) to grep regex (^v) regex="^${tag_pattern//\*/.*}" tags=$(echo "$tags" | grep -E "$regex" || true) +else + tags=$(release_tags "$tags") + if [[ -z "$tags" ]]; then + echo "No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern" >&2 + exit 1 + fi fi # Strip v prefix, sort semver, take latest diff --git a/tests/helpers/mock-curl.sh b/tests/helpers/mock-curl.sh index f9291ed..d4c3aa2 100755 --- a/tests/helpers/mock-curl.sh +++ b/tests/helpers/mock-curl.sh @@ -97,6 +97,17 @@ case "$url" in *api.github.com/repos/custom-prefix/repo/tags*) echo '[{"name": "bun-v1.3.11"}, {"name": "bun-v1.2.0"}, {"name": "bun-v1.0.0"}]' ;; + # Tags of which some are not releases, and tags of which none is + *api.github.com/repos/mixed-owner/mixed-repo/tags* | \ + *gitlab.com/api/v4/projects/mixed-group%2Fmixed-project/repository/tags* | \ + *codeberg.org/api/v1/repos/mixed-owner/mixed-repo/tags*) + echo '[{"name": "latest"}, {"name": "v2.1.0-rc1"}, {"name": "v2.1.0rc2"}, {"name": "v2.0.0"}, {"name": "v1.9.0"}]' + ;; + *api.github.com/repos/named-owner/named-repo/tags* | \ + *gitlab.com/api/v4/projects/named-group%2Fnamed-project/repository/tags* | \ + *codeberg.org/api/v1/repos/named-owner/named-repo/tags*) + echo '[{"name": "latest"}, {"name": "nightly"}]' + ;; # --- Normal success patterns (wildcards) --- diff --git a/tests/strategies.bats b/tests/strategies.bats index c8108fd..9b95291 100644 --- a/tests/strategies.bats +++ b/tests/strategies.bats @@ -73,6 +73,34 @@ teardown() { [ "$output" = "1.3.11" ] } +@test "github-tag: takes only tags of the form v1.2.3 by default" { + create_test_package "test-gh-tag" "github-tag" "upstream: + project: mixed-owner/mixed-repo" + + run "$TEST_TMPDIR/strategies/github-tag.sh" "test-gh-tag" + [ "$status" -eq 0 ] + [ "$output" = "2.0.0" ] +} + +@test "github-tag: names both fields when no tag is a release" { + create_test_package "test-gh-tag" "github-tag" "upstream: + project: named-owner/named-repo" + + run "$TEST_TMPDIR/strategies/github-tag.sh" "test-gh-tag" + [ "$status" -eq 1 ] + [[ "$output" == *"No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern or upstream.tag_version_regex"* ]] +} + +@test "github-tag: tag_pattern replaces the default filter" { + create_test_package "test-gh-tag" "github-tag" "upstream: + project: mixed-owner/mixed-repo + tag_pattern: 'latest'" + + run "$TEST_TMPDIR/strategies/github-tag.sh" "test-gh-tag" + [ "$status" -eq 0 ] + [ "$output" = "latest" ] +} + # === gitlab-tag === @test "gitlab-tag: returns latest tag from GitLab" { @@ -86,6 +114,26 @@ teardown() { [ "$output" = "3.1.0" ] } +@test "gitlab-tag: takes only tags of the form v1.2.3 by default" { + create_test_package "test-gl-tag" "gitlab-tag" "upstream: + host: gitlab.com + project: mixed-group/mixed-project" + + run "$TEST_TMPDIR/strategies/gitlab-tag.sh" "test-gl-tag" + [ "$status" -eq 0 ] + [ "$output" = "2.0.0" ] +} + +@test "gitlab-tag: names tag_pattern when no tag is a release" { + create_test_package "test-gl-tag" "gitlab-tag" "upstream: + host: gitlab.com + project: named-group/named-project" + + run "$TEST_TMPDIR/strategies/gitlab-tag.sh" "test-gl-tag" + [ "$status" -eq 1 ] + [[ "$output" == *"No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern"* ]] +} + # === gitea-tag === @test "gitea-tag: returns latest tag from Gitea/Codeberg" { @@ -99,6 +147,26 @@ teardown() { [ "$output" = "1.5.0" ] } +@test "gitea-tag: takes only tags of the form v1.2.3 by default" { + create_test_package "test-gitea" "gitea-tag" "upstream: + host: codeberg.org + project: mixed-owner/mixed-repo" + + run "$TEST_TMPDIR/strategies/gitea-tag.sh" "test-gitea" + [ "$status" -eq 0 ] + [ "$output" = "2.0.0" ] +} + +@test "gitea-tag: names tag_pattern when no tag is a release" { + create_test_package "test-gitea" "gitea-tag" "upstream: + host: codeberg.org + project: named-owner/named-repo" + + run "$TEST_TMPDIR/strategies/gitea-tag.sh" "test-gitea" + [ "$status" -eq 1 ] + [[ "$output" == *"No tag of the form v1.2.3 or 1.2.3; set upstream.tag_pattern"* ]] +} + # === pypi === @test "pypi: returns latest version from PyPI" {