diff --git a/.github/actions/update-floating-tags/action.yml b/.github/actions/update-floating-tags/action.yml index 21ad9229d..a7d496626 100644 --- a/.github/actions/update-floating-tags/action.yml +++ b/.github/actions/update-floating-tags/action.yml @@ -1,5 +1,5 @@ name: 'Update floating tags' -description: 'Push floating major/minor/latest git tags and recreate corresponding GitHub releases' +description: 'Push floating major/minor/latest git tags for a released version' inputs: release-version: @@ -9,17 +9,6 @@ inputs: description: 'Git tag prefix (e.g. "gitlab/", "github/", "rules/", or empty for CLI)' required: true default: '' - component-name: - description: 'Human-readable component name for release notes (e.g. "GitLab CI template")' - required: true - release-assets: - description: 'Files to attach to floating releases (space-separated paths)' - required: false - default: '' - copy-assets-from: - description: 'Tag to copy assets from (e.g. v0.1.2). Mutually exclusive with release-assets.' - required: false - default: '' runs: using: 'composite' @@ -31,7 +20,6 @@ runs: VERSION="$(echo "${{ inputs.release-version }}" | sed 's/^v//')" PREFIX="${{ inputs.tag-prefix }}" - COMPONENT="${{ inputs.component-name }}" MAJOR="$(echo "$VERSION" | cut -d. -f1)" MAJOR_TAG="${PREFIX}v${MAJOR}" MINOR_TAG="${PREFIX}v$(echo "$VERSION" | cut -d. -f1-2)" @@ -46,36 +34,3 @@ runs: git push origin "$MINOR_TAG" --force git tag -f "$LATEST_TAG" git push origin "$LATEST_TAG" --force - - ASSET_ARGS=() - ASSETS_DIR="" - if [ -n "${{ inputs.copy-assets-from }}" ]; then - ASSETS_DIR=$(mktemp -d) - gh release download "${{ inputs.copy-assets-from }}" --dir "$ASSETS_DIR" - for f in "$ASSETS_DIR"/*; do - [ -f "$f" ] || continue - ASSET_ARGS+=("$f") - done - elif [ -n "${{ inputs.release-assets }}" ]; then - read -ra ASSET_ARGS <<< "${{ inputs.release-assets }}" - fi - - gh release delete "$LATEST_TAG" --yes || true - gh release create "$LATEST_TAG" \ - --title "$LATEST_TAG" \ - --notes "Floating release tracking the latest ${COMPONENT} version (${PREFIX}v${VERSION})" \ - --latest=false \ - --prerelease \ - "${ASSET_ARGS[@]+"${ASSET_ARGS[@]}"}" - - gh release delete "$MINOR_TAG" --yes || true - gh release create "$MINOR_TAG" \ - --title "$MINOR_TAG" \ - --notes "Floating release tracking the latest ${COMPONENT} ${MINOR_TAG#${PREFIX}}.x version (${PREFIX}v${VERSION})" \ - --latest=false \ - --prerelease \ - "${ASSET_ARGS[@]+"${ASSET_ARGS[@]}"}" - - if [ -n "$ASSETS_DIR" ]; then - rm -rf "$ASSETS_DIR" - fi diff --git a/.github/workflows/publish-analyzer.yaml b/.github/workflows/publish-analyzer.yaml index a3ac7821d..d7f970912 100644 --- a/.github/workflows/publish-analyzer.yaml +++ b/.github/workflows/publish-analyzer.yaml @@ -101,6 +101,5 @@ jobs: --title "analyzer/latest" \ --notes "Floating release tracking the latest analyzer version (v${{ steps.changes.outputs.version }})" \ --latest=false \ - --prerelease \ core/build/libs/opentaint-project-analyzer.jar \ opentaint-models.tar.gz diff --git a/.github/workflows/publish-autobuilder.yaml b/.github/workflows/publish-autobuilder.yaml index 74d5f1216..d915eab7a 100644 --- a/.github/workflows/publish-autobuilder.yaml +++ b/.github/workflows/publish-autobuilder.yaml @@ -95,5 +95,4 @@ jobs: --title "autobuilder/latest" \ --notes "Floating release tracking the latest autobuilder version (v${{ steps.changes.outputs.version }})" \ --latest=false \ - --prerelease \ core/opentaint-jvm-autobuilder/build/libs/opentaint-project-auto-builder.jar diff --git a/.github/workflows/publish-go-server.yaml b/.github/workflows/publish-go-server.yaml index ab07e8723..b5d4928a0 100644 --- a/.github/workflows/publish-go-server.yaml +++ b/.github/workflows/publish-go-server.yaml @@ -124,5 +124,4 @@ jobs: --title "go-server/latest" \ --notes "Floating release tracking the latest go-ssa-server version (v${{ steps.changes.outputs.version }})" \ --latest=false \ - --prerelease \ dist/* diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml index 9ad1613b0..b185e6e5f 100644 --- a/.github/workflows/release-cli.yaml +++ b/.github/workflows/release-cli.yaml @@ -347,8 +347,6 @@ jobs: with: release-version: ${{ steps.release_version.outputs.RELEASE_VERSION }} tag-prefix: '' - component-name: 'CLI' - copy-assets-from: v${{ steps.release_version.outputs.RELEASE_VERSION }} outputs: release_version: ${{ steps.release_version.outputs.RELEASE_VERSION }} diff --git a/.github/workflows/release-github.yaml b/.github/workflows/release-github.yaml index b4e4d871a..2fd28c062 100644 --- a/.github/workflows/release-github.yaml +++ b/.github/workflows/release-github.yaml @@ -112,7 +112,6 @@ jobs: with: release-version: ${{ steps.manual_release.outputs.new-version || steps.version.outputs.new_release_version }} tag-prefix: 'github/' - component-name: 'GitHub Action' outputs: release_version: ${{ steps.manual_release.outputs.new-version || steps.version.outputs.new_release_version }} diff --git a/.github/workflows/release-gitlab.yaml b/.github/workflows/release-gitlab.yaml index 1899c8ffa..58e6c5285 100644 --- a/.github/workflows/release-gitlab.yaml +++ b/.github/workflows/release-gitlab.yaml @@ -129,7 +129,6 @@ jobs: with: release-version: ${{ steps.manual_release.outputs.new-version || steps.release_version.outputs.RELEASE_VERSION }} tag-prefix: 'gitlab/' - component-name: 'GitLab CI template' outputs: release_version: ${{ steps.manual_release.outputs.new-version || steps.release_version.outputs.RELEASE_VERSION }} diff --git a/.github/workflows/release-rules.yaml b/.github/workflows/release-rules.yaml index 96d6897ff..2d95064d1 100644 --- a/.github/workflows/release-rules.yaml +++ b/.github/workflows/release-rules.yaml @@ -134,8 +134,6 @@ jobs: with: release-version: ${{ steps.manual_release.outputs.new-version || steps.release_version.outputs.RELEASE_VERSION }} tag-prefix: 'rules/' - component-name: 'rules' - release-assets: opentaint-rules.tar.gz outputs: release_version: ${{ steps.manual_release.outputs.new-version || steps.release_version.outputs.RELEASE_VERSION }} diff --git a/docs/installation.md b/docs/installation.md index d8d6b2d45..c1e400cc8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -33,15 +33,23 @@ The correct binary for your platform (Linux, macOS, or Windows; x64 or arm64) is ## Install Scripts -The install scripts accept an optional version argument. Without one, the latest GitHub release is installed. +The install scripts accept an optional version argument (leading `v` optional). Without one, the latest GitHub release is installed. You can pin to: + +- an exact version — `v0.4.5` +- the newest patch of a minor line — `v0.4` (resolves to the newest `v0.4.x`) +- the newest release of a major line — `v0` (resolves to the newest `v0.x.y`) + +The leading `v` is optional in every form (`0.4.5` works the same as `v0.4.5`). **Linux/macOS:** ```bash # Latest curl -fsSL https://opentaint.org/install.sh | bash -# Specific version (leading 'v' is optional) -curl -fsSL https://opentaint.org/install.sh | bash -s -- 1.2.3 +# Exact version, minor line, or major line +curl -fsSL https://opentaint.org/install.sh | bash -s -- v0.4.5 +curl -fsSL https://opentaint.org/install.sh | bash -s -- v0.4 +curl -fsSL https://opentaint.org/install.sh | bash -s -- v0 ``` **Windows (PowerShell):** @@ -49,8 +57,10 @@ curl -fsSL https://opentaint.org/install.sh | bash -s -- 1.2.3 # Latest irm https://opentaint.org/install.ps1 | iex -# Specific version -& ([scriptblock]::Create((irm https://opentaint.org/install.ps1))) -Version 1.2.3 +# Exact version, minor line, or major line +& ([scriptblock]::Create((irm https://opentaint.org/install.ps1))) -Version v0.4.5 +& ([scriptblock]::Create((irm https://opentaint.org/install.ps1))) -Version v0.4 +& ([scriptblock]::Create((irm https://opentaint.org/install.ps1))) -Version v0 ``` **Windows (CMD):** @@ -58,8 +68,10 @@ irm https://opentaint.org/install.ps1 | iex :: Latest curl -fsSL https://opentaint.org/install.cmd -o install.cmd && install.cmd && del install.cmd -:: Specific version -curl -fsSL https://opentaint.org/install.cmd -o install.cmd && install.cmd 1.2.3 && del install.cmd +:: Exact version, minor line, or major line +curl -fsSL https://opentaint.org/install.cmd -o install.cmd && install.cmd v0.4.5 && del install.cmd +curl -fsSL https://opentaint.org/install.cmd -o install.cmd && install.cmd v0.4 && del install.cmd +curl -fsSL https://opentaint.org/install.cmd -o install.cmd && install.cmd v0 && del install.cmd ``` ### Environment variables diff --git a/github/README.md b/github/README.md index 49e9ae80b..927229286 100644 --- a/github/README.md +++ b/github/README.md @@ -149,23 +149,36 @@ After the job completes, you’ll find: ## Version Selection +There are two independent version selectors. + +### GitHub Action version + +The tag after `@` selects the integration code. Major and minor tags move to the newest compatible release. Exact tags remain pinned: + +* `github/v0` — latest action in major version 0 +* `github/v0.4` — latest action in minor version 0.4 +* `github/v0.4.5` — exact action version 0.4.5 + +```yaml +uses: seqra/opentaint/github@github/v0.4 +``` + +### OpenTaint CLI version + `opentaint-version` supports flexible selectors so you do not need to update this action for every OpenTaint release: * `latest` - always use the latest stable release * `v0` - use the latest stable release in major version 0 (default) -* `v0.1` - use the latest stable patch in minor version 0.1 -* `v0.1.0` - pin an exact release +* `v0.4` - use the latest stable patch in minor version 0.4 +* `v0.4.5` - pin an exact release Examples: ```yaml with: - opentaint-version: 'latest' -``` - -```yaml -with: - opentaint-version: 'v0.1' + opentaint-version: 'v0' # latest v0.x.y + # opentaint-version: 'v0.4' # latest v0.4.x + # opentaint-version: 'v0.4.5' ``` diff --git a/gitlab/README.md b/gitlab/README.md index f7ce96d50..890b77ab1 100644 --- a/gitlab/README.md +++ b/gitlab/README.md @@ -98,16 +98,16 @@ There are two independent version selectors: ### CI template version -Controlled by the tag in the `include:` URL. Using a major-version tag ensures you always get the latest compatible template without manual updates. +Controlled by the tag in the `include:` URL. Major and minor tags move to the newest compatible template release. Exact tags remain pinned. -* `gitlab/v0` — latest stable template in major version 0 (recommended) -* `gitlab/v0.1` — pin to a specific minor version -* `gitlab/v0.1.0` — pin to an exact version -* `gitlab/latest` — always use the latest template +* `gitlab/v0` — latest template in major version 0 +* `gitlab/v0.4` — latest template in minor version 0.4 +* `gitlab/v0.4.5` — exact template version 0.4.5 +* `gitlab/latest` — latest template release ```yaml include: - - remote: https://raw.githubusercontent.com/seqra/opentaint/gitlab/v0.1/gitlab/opentaint.gitlab-ci.yml + - remote: https://raw.githubusercontent.com/seqra/opentaint/gitlab/v0.4/gitlab/opentaint.gitlab-ci.yml ``` ### OpenTaint CLI version @@ -116,17 +116,14 @@ Controlled by the `OPENTAINT_VERSION` variable. This determines which release of * `latest` — always use the latest stable release * `v0` — use the latest stable release in major version 0 (default) -* `v0.1` — use the latest stable patch in minor version 0.1 -* `v0.1.0` — pin an exact release +* `v0.4` — use the latest stable patch in minor version 0.4 +* `v0.4.5` — pin an exact release ```yaml variables: - OPENTAINT_VERSION: "latest" -``` - -```yaml -variables: - OPENTAINT_VERSION: "v0.1" + OPENTAINT_VERSION: "v0" # latest v0.x.y + # OPENTAINT_VERSION: "v0.4" # latest v0.4.x + # OPENTAINT_VERSION: "v0.4.5" ``` diff --git a/scripts/install/install.Tests.ps1 b/scripts/install/install.Tests.ps1 new file mode 100644 index 000000000..2183f8714 --- /dev/null +++ b/scripts/install/install.Tests.ps1 @@ -0,0 +1,44 @@ +BeforeAll { + $installer = Get-Content (Join-Path $PSScriptRoot "install.ps1") -Raw + $installer = $installer -replace '(?m)^Main\s*$', '' + Invoke-Expression $installer +} + +Describe "Resolve-FloatingSelector pagination" { + BeforeEach { + Mock Invoke-RestMethod { + param($Uri, $Headers, [switch]$UseBasicParsing) + + if ($Uri -match 'page=1$') { + return 1..100 | ForEach-Object { + [pscustomobject]@{ tag_name = "analyzer/2026.01.$_.abcdef0" } + } + } + if ($Uri -match 'page=2$') { + return @( + [pscustomobject]@{ tag_name = "v0.4.5" }, + [pscustomobject]@{ tag_name = "v0.5.1" } + ) + } + throw "unexpected page: $Uri" + } + } + + It "resolves a major selector from a later page" { + Resolve-FloatingSelector -Selector "v0" | Should -Be "v0.5.1" + Should -Invoke Invoke-RestMethod -Times 2 + } + + It "resolves a minor selector from a later page" { + Resolve-FloatingSelector -Selector "v0.4" | Should -Be "v0.4.5" + Should -Invoke Invoke-RestMethod -Times 2 + } +} + +Describe "Test-Version prerelease support" { + It "keeps exact prerelease installation supported" { + $result = Test-Version -Raw "v0.4.5-rc.1" + $result.Tag | Should -Be "v0.4.5-rc.1" + $result.PathSegment | Should -Be "download/v0.4.5-rc.1" + } +} diff --git a/scripts/install/install.cmd b/scripts/install/install.cmd index 530585660..129b4cf77 100644 --- a/scripts/install/install.cmd +++ b/scripts/install/install.cmd @@ -2,7 +2,9 @@ REM OpenTaint installer for Windows (CMD wrapper) REM This script invokes the PowerShell installer. REM Usage: install.cmd installs latest -REM install.cmd 1.2.3 installs version 1.2.3 +REM install.cmd v0.4.5 installs exact version ('v' optional) +REM install.cmd v0 installs newest v0.x.y +REM install.cmd v0.4 installs newest v0.4.x where powershell >nul 2>nul if %ERRORLEVEL% equ 0 ( diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 811b6ec38..1862af636 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -1,7 +1,9 @@ # OpenTaint installer for Windows (PowerShell) # Usage: # irm https://raw.githubusercontent.com/seqra/opentaint/main/scripts/install/install.ps1 | iex -# & ([scriptblock]::Create((irm https://raw.githubusercontent.com/seqra/opentaint/main/scripts/install/install.ps1))) -Version 1.2.3 +# & ([scriptblock]::Create((irm .../install.ps1))) -Version v0.4.5 # exact version ('v' optional) +# & ([scriptblock]::Create((irm .../install.ps1))) -Version v0 # newest v0.x.y +# & ([scriptblock]::Create((irm .../install.ps1))) -Version v0.4 # newest v0.4.x param( [string]$Version = "latest" @@ -9,6 +11,57 @@ param( $ErrorActionPreference = 'Stop' +$Repo = if ($env:OPENTAINT_REPOSITORY) { $env:OPENTAINT_REPOSITORY } else { "seqra/opentaint" } + +# Resolves a floating major/minor selector (e.g. v0 or v0.2) to the newest +# matching exact vX.Y.Z release tag via the GitHub releases API. Only exact +# vX.Y.Z tags are considered, mirroring scripts/resolve_opentaint_version.py. +function Resolve-FloatingSelector { + param( + [string]$Selector # e.g. v0 or v0.2 + ) + + $headers = @{ "Accept" = "application/vnd.github+json" } + $token = if ($env:OPENTAINT_GITHUB_TOKEN) { $env:OPENTAINT_GITHUB_TOKEN } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { $null } + if ($token) { $headers["Authorization"] = "Bearer $token" } + + $releases = @() + $page = 1 + while ($true) { + $apiUrl = "https://api.github.com/repos/$Repo/releases?per_page=100&page=$page" + try { + $pageReleases = @(Invoke-RestMethod -Uri $apiUrl -Headers $headers -UseBasicParsing) + } catch { + [Console]::Error.WriteLine("Error: failed to query the GitHub releases API to resolve '$Selector'.") + exit 2 + } + + $releases += $pageReleases + if ($pageReleases.Count -lt 100) { break } + $page++ + } + + # v0 must match v0.*, not v0.* only after a dot boundary; the trailing dot + # ensures v1 does not match v10.x and v0.1 does not match v0.10.x. + $prefix = "$Selector." + $best = $null + foreach ($release in $releases) { + $tag = $release.tag_name + if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+$') { continue } + if (-not $tag.StartsWith($prefix)) { continue } + $ver = [version]($tag.Substring(1)) + if (-not $best -or $ver -gt $best.Ver) { + $best = @{ Ver = $ver; Tag = $tag } + } + } + + if (-not $best) { + [Console]::Error.WriteLine("Error: no release found matching selector '$Selector'.") + exit 2 + } + return $best.Tag +} + function Test-Version { param([string]$Raw) @@ -21,7 +74,21 @@ function Test-Version { return @{ PathSegment = "download/v$normalized"; Tag = "v$normalized" } } - [Console]::Error.WriteLine("Error: Invalid version '$Raw'. Expected 'latest' or 'X.Y.Z' (optionally prefixed with 'v').") + if ($Raw -match '^(v)?(?[0-9]+\.[0-9]+)$') { + $selector = "v$($Matches['ver'])" + Write-Host "Resolving $selector to an exact release..." + $tag = Resolve-FloatingSelector -Selector $selector + return @{ PathSegment = "download/$tag"; Tag = $tag } + } + + if ($Raw -match '^(v)?(?[0-9]+)$') { + $selector = "v$($Matches['ver'])" + Write-Host "Resolving $selector to an exact release..." + $tag = Resolve-FloatingSelector -Selector $selector + return @{ PathSegment = "download/$tag"; Tag = $tag } + } + + [Console]::Error.WriteLine("Error: Invalid version '$Raw'. Expected 'latest', 'X', 'X.Y', or 'X.Y.Z' (optionally prefixed with 'v').") exit 2 } @@ -45,8 +112,6 @@ function Test-HomebrewInstall { return $null } -$Repo = if ($env:OPENTAINT_REPOSITORY) { $env:OPENTAINT_REPOSITORY } else { "seqra/opentaint" } - function Get-Architecture { $arch = $env:PROCESSOR_ARCHITECTURE switch ($arch) { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index d6ccf386d..698bdb12e 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -4,26 +4,35 @@ set -euo pipefail # OpenTaint installer for Linux and macOS # Usage: # curl -fsSL https://raw.githubusercontent.com/seqra/opentaint/main/scripts/install/install.sh | bash -# curl -fsSL https://raw.githubusercontent.com/seqra/opentaint/main/scripts/install/install.sh | bash -s -- 1.2.3 +# curl -fsSL .../install.sh | bash -s -- v0.4.5 # exact version ('v' optional) +# curl -fsSL .../install.sh | bash -s -- v0 # newest v0.x.y +# curl -fsSL .../install.sh | bash -s -- v0.4 # newest v0.4.x REPO="${OPENTAINT_REPOSITORY:-seqra/opentaint}" INSTALL_DIR="${OPENTAINT_INSTALL_DIR:-}" DOWNLOADER="" +# Populated by validate_version(): one of latest|exact|major|minor. +VERSION_SELECTOR_KIND="" +# Floating selector (e.g. v0 or v0.1) when kind is major|minor; resolved later. +VERSION_SELECTOR="" -# Populates VERSION_PATH_SEGMENT and VERSION_TAG from the raw version argument. +# Populates VERSION_SELECTOR_KIND, VERSION_SELECTOR, VERSION_PATH_SEGMENT and +# VERSION_TAG from the raw version argument. # Accepts: # (empty) -> latest # latest -# X.Y.Z -# vX.Y.Z -# X.Y.Z-suffix -# vX.Y.Z-suffix +# X / vX (floating major, resolved to newest vX.Y.Z) +# X.Y / vX.Y (floating minor, resolved to newest vX.Y.Z) +# X.Y.Z / vX.Y.Z (exact, optionally with -suffix) +# For floating selectors VERSION_PATH_SEGMENT is left empty and filled in later +# by resolve_floating_selector() once a downloader is available. # Exits 2 on invalid input. validate_version() { local raw="${1:-latest}" if [ "$raw" = "latest" ] || [ -z "$raw" ]; then + VERSION_SELECTOR_KIND="latest" VERSION_PATH_SEGMENT="latest/download" VERSION_TAG="latest" return @@ -31,16 +40,109 @@ validate_version() { if [[ "$raw" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?$ ]]; then local normalized="${raw#v}" + VERSION_SELECTOR_KIND="exact" VERSION_PATH_SEGMENT="download/v${normalized}" VERSION_TAG="v${normalized}" return fi + if [[ "$raw" =~ ^v?[0-9]+\.[0-9]+$ ]]; then + VERSION_SELECTOR_KIND="minor" + VERSION_SELECTOR="v${raw#v}" + VERSION_TAG="$VERSION_SELECTOR" + return + fi + + if [[ "$raw" =~ ^v?[0-9]+$ ]]; then + VERSION_SELECTOR_KIND="major" + VERSION_SELECTOR="v${raw#v}" + VERSION_TAG="$VERSION_SELECTOR" + return + fi + echo "Error: invalid version '$raw'." >&2 - echo "Expected 'latest' or 'X.Y.Z' (optionally prefixed with 'v')." >&2 + echo "Expected 'latest', 'X', 'X.Y', or 'X.Y.Z' (optionally prefixed with 'v')." >&2 exit 2 } +# Fetches a URL to stdout using the selected downloader. Adds a GitHub token +# header when OPENTAINT_GITHUB_TOKEN or GITHUB_TOKEN is set (avoids API rate +# limits). Returns non-zero on failure. +fetch_stdout() { + local url="$1" + local token="${OPENTAINT_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}" + + case "$DOWNLOADER" in + curl) + if [ -n "$token" ]; then + curl -fsSL -H "Authorization: Bearer $token" "$url" + else + curl -fsSL "$url" + fi + ;; + wget) + if [ -n "$token" ]; then + wget -qO- --header="Authorization: Bearer $token" "$url" + else + wget -qO- "$url" + fi + ;; + *) + return 1 + ;; + esac +} + +# Resolves a floating major/minor selector (e.g. v0 or v0.1) to the newest +# matching exact vX.Y.Z release tag by querying the GitHub releases API. Only +# exact vX.Y.Z tags are considered (floating and prefixed tags are ignored), +# mirroring scripts/resolve_opentaint_version.py. Prints the resolved tag. +resolve_floating_selector() { + local selector="$1" + local page=1 + local bodies="" + + while true; do + local api_url="https://api.github.com/repos/${REPO}/releases?per_page=100&page=${page}" + local body + if ! body="$(fetch_stdout "$api_url")"; then + echo "Error: failed to query the GitHub releases API to resolve '$selector'." >&2 + exit 2 + fi + + if [[ "$body" =~ ^[[:space:]]*\[[[:space:]]*\][[:space:]]*$ ]]; then + break + fi + + bodies+=$'\n'"$body" + page=$((page + 1)) + done + + # Match e.g. ^v0\. (major) or ^v0\.1\. (minor) against vX.Y.Z tags. + local pattern + if [ "$VERSION_SELECTOR_KIND" = "minor" ]; then + pattern="^${selector//./\\.}\\." + else + pattern="^${selector}\\." + fi + + local best + best="$(printf '%s' "$bodies" \ + | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v[0-9]+\.[0-9]+\.[0-9]+"' \ + | sed -E 's/.*"(v[0-9]+\.[0-9]+\.[0-9]+)"$/\1/' \ + | grep -E "$pattern" \ + | sed 's/^v//' \ + | sort -t. -k1,1n -k2,2n -k3,3n \ + | tail -1 || true)" + + if [ -z "$best" ]; then + echo "Error: no release found matching selector '$selector'." >&2 + exit 2 + fi + + echo "v${best}" +} + # Prints the resolved path of an existing opentaint binary if it appears to # belong to a Homebrew installation (mirrors cli/internal/utils/updater.go # classification). Prints nothing otherwise. @@ -206,6 +308,17 @@ main() { exit 3 fi + # Floating major/minor selectors are resolved to an exact tag, unless the + # caller already pinned a fully-resolved download base URL (e.g. CI). + if [ -z "${OPENTAINT_DOWNLOAD_BASE_URL:-}" ] && \ + { [ "$VERSION_SELECTOR_KIND" = "major" ] || [ "$VERSION_SELECTOR_KIND" = "minor" ]; }; then + echo "Resolving ${VERSION_SELECTOR} to an exact release..." + local resolved_tag + resolved_tag="$(resolve_floating_selector "$VERSION_SELECTOR")" + VERSION_PATH_SEGMENT="download/${resolved_tag}" + VERSION_TAG="$resolved_tag" + fi + DOWNLOAD_BASE_URL="${OPENTAINT_DOWNLOAD_BASE_URL:-https://github.com/${REPO}/releases/${VERSION_PATH_SEGMENT}}" echo "Version: $VERSION_TAG" diff --git a/scripts/install/install_test.sh b/scripts/install/install_test.sh new file mode 100644 index 000000000..7f97e35a9 --- /dev/null +++ b/scripts/install/install_test.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Load the installer functions without executing main. +# shellcheck disable=SC1090 +source <(sed '$d' "$SCRIPT_DIR/install.sh") + +assert_eq() { + local expected="$1" + local actual="$2" + if [ "$expected" != "$actual" ]; then + echo "Expected '$expected', got '$actual'" >&2 + exit 1 + fi +} + +fetch_stdout() { + case "$1" in + *"&page=1") printf '%s' '[{"tag_name":"analyzer/2026.01.01.abcdef0"}]' ;; + *"&page=2") printf '%s' '[{"tag_name":"v0.4.5"},{"tag_name":"v0.5.1"}]' ;; + *"&page=3") printf '%s' '[]' ;; + *) return 1 ;; + esac +} + +VERSION_SELECTOR_KIND="major" +assert_eq "v0.5.1" "$(resolve_floating_selector v0)" + +VERSION_SELECTOR_KIND="minor" +assert_eq "v0.4.5" "$(resolve_floating_selector v0.4)" + +VERSION_SELECTOR_KIND="major" +if (resolve_floating_selector v9) >/dev/null 2>&1; then + echo "Expected an unknown selector to fail" >&2 + exit 1 +elif [ "$?" -ne 2 ]; then + echo "Expected an unknown selector to exit 2" >&2 + exit 1 +fi + +validate_version v0.4.5-rc.1 +assert_eq "exact" "$VERSION_SELECTOR_KIND" +assert_eq "v0.4.5-rc.1" "$VERSION_TAG" + +echo "install.sh version resolution tests passed"