Skip to content

Commit 532d670

Browse files
authored
Merge branch 'master' into dependabot/github_actions/actions/download-artifact-8
2 parents dd089a5 + b3578fe commit 532d670

File tree

13 files changed

+117
-15
lines changed

13 files changed

+117
-15
lines changed

.github/workflows/dockerpublish.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,28 @@ jobs:
2525
uses: actions/checkout@v3
2626

2727
- name: Log in to Docker Hub
28-
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
28+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
2929
with:
3030
username: ${{ secrets.DOCKER_USERNAME }}
3131
password: ${{ secrets.DOCKER_TOKEN }}
3232

3333
- name: Log in to the Container registry
34-
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
34+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
3535
with:
3636
registry: ghcr.io
3737
username: ${{ github.actor }}
3838
password: ${{ secrets.GITHUB_TOKEN }}
3939

4040
- name: Extract metadata (tags, labels) for Docker
4141
id: meta
42-
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
42+
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
4343
with:
4444
images: |
4545
${{ github.repository }}
4646
ghcr.io/${{ github.repository }}
4747
4848
- name: Build and push Docker images
49-
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
49+
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
5050
with:
5151
context: .
5252
push: true

.github/workflows/rpmbuild.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Install SSH key
12-
uses: shimataro/ssh-key-action@v2
12+
uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 # v2.8.1
1313
with:
1414
key: ${{ secrets.BUILDER_SSH_KEY }}
1515
known_hosts: ${{ secrets.BUILDER_SSH_KNOWN_HOSTS }}
1616
- name: Trigger RPM build
17-
run: ${{ secrets.RPM_BUILD_CMD }}
17+
run: eval "$RPM_BUILD_CMD"
18+
env:
19+
RPM_BUILD_CMD: ${{ secrets.RPM_BUILD_CMD }}

CLAUDE.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project
6+
7+
**lastversion** — CLI tool and Python library that finds the latest stable release version of software projects across multiple hosting platforms (GitHub, GitLab, PyPI, Bitbucket, SourceForge, WordPress, etc.). Used heavily in RPM build automation and CI/CD pipelines.
8+
9+
## Development Setup
10+
11+
- **Virtualenv:** `~/.virtualenvs/lastversion/`
12+
- **Secrets:** Source `~/.secrets` before running tests (contains `GITHUB_API_TOKEN` and other API tokens)
13+
14+
## Common Commands
15+
16+
```bash
17+
# Run all tests (sources secrets, activates venv, parallel execution with 10min timeout)
18+
make test
19+
20+
# Run a single test
21+
source ~/.secrets && ~/.virtualenvs/lastversion/bin/python -m pytest tests/test_github.py::test_function_name -v
22+
23+
# Lint
24+
~/.virtualenvs/lastversion/bin/python -m flake8 src tests
25+
~/.virtualenvs/lastversion/bin/python -m pylint src tests
26+
27+
# Security scan
28+
~/.virtualenvs/lastversion/bin/python -m bandit -c pyproject.toml -r src/lastversion
29+
30+
# Release (CI auto-publishes to PyPI on GitHub release creation)
31+
# 1. Bump __version__ in src/lastversion/__about__.py
32+
# 2. Commit: git commit -m "chore(release): X.Y.Z"
33+
# 3. Tag: git tag -s vX.Y.Z -m "vX.Y.Z"
34+
# 4. Push: git push origin master && git push origin vX.Y.Z
35+
# 5. Create release: gh release create vX.Y.Z --title "vX.Y.Z" --notes "..."
36+
# CI workflow pythonpublish.yml handles PyPI upload automatically.
37+
# Do NOT use `make publish` or manual `twine upload`.
38+
make publish # backup only, prefer CI
39+
40+
# Build single binary
41+
make one-file
42+
```
43+
44+
## Architecture
45+
46+
### Repository Holder Pattern
47+
48+
Core abstraction: `BaseProjectHolder` (in `src/lastversion/repo_holders/base.py`) is the abstract base class for all hosting platform adapters. Each adapter lives in `src/lastversion/repo_holders/` and implements release discovery for its platform.
49+
50+
`HolderFactory` (`holder_factory.py`) maintains an ordered registry of adapters and selects the appropriate one based on the input URL/name. Detection order matters — adapters are tried in registration order.
51+
52+
### Key Modules
53+
54+
- **`lastversion.py`** — Core logic: `latest()`, `has_update()`, `check_version()` entry points
55+
- **`cli.py`** — CLI argument parsing, all subcommands (get, download, extract, install, update-spec, etc.)
56+
- **`version.py`**`Version` class extending `packaging.Version` with normalization for messy real-world version strings (dashed versions, Java-style, pre-release variants)
57+
- **`cache.py`** — Dual-level caching: HTTP-level (CacheControl/ETag) + release-data-level (File or Redis backend) with PID-based file locking
58+
- **`config.py`** — Platform-aware config singleton (`~/.config/lastversion/lastversion.yml` on Linux, `~/Library/Application Support/lastversion/lastversion.yml` on macOS)
59+
60+
### GitHub Adapter (`repo_holders/github.py`)
61+
62+
Most complex adapter. Uses GraphQL API with REST fallback. Handles rate limiting, API tokens (`GITHUB_API_TOKEN`/`GITHUB_TOKEN` env vars), commit-based version extraction, and release note parsing.
63+
64+
### RPM Spec Integration
65+
66+
`update-spec` command parses `.spec` files for `%{upstream_github}`, `%global lastversion_repo`, and `%global lastversion_*` directives to determine repo and version constraints.
67+
68+
## Testing
69+
70+
- **Framework:** pytest with pytest-xdist (parallel: `-n auto`)
71+
- **CI matrix:** Python 3.6, 3.9, 3.13
72+
- Tests include live API calls — require valid API tokens
73+
- Test files mirror the module structure: `test_github.py`, `test_gitlab.py`, `test_pypi.py`, etc.
74+
- Helper utilities in `tests/helpers.py`
75+
76+
## Linting
77+
78+
- **flake8:** max-line-length=120, select C,E,F,W,B,B950, ignore E203/E501/E704 (configured in `setup.cfg`)
79+
- **bandit:** security scanning configured in `pyproject.toml`
80+
- **pre-commit:** ruff, flake8, black, isort, bandit — all at line-length 120
81+
82+
## Code Style
83+
84+
- **Docstrings:** Google-style on all new/modified functions. First line: one-sentence imperative summary ending with a period. Include Args, Returns, Raises sections with type info even if type hints exist. Document coroutine behavior where relevant.
85+
- **Interpreter:** Always use `~/.virtualenvs/lastversion/bin/python` — never bare `python` or `pytest`.
86+
- **PRs:** Must include docstrings for new/modified functions. Must update `.cursor/rules/` if adding new modules or architectural changes.
87+
88+
## Python Compatibility
89+
90+
Supports Python 3.6+. Conditional dependencies exist for `cachecontrol` and `urllib3` based on Python version (see `setup.py`).

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Use a lightweight base image with Python and pip installed
2-
FROM python:3.13-alpine
2+
FROM python:3.13-alpine # NOSONAR - root required for GitHub Actions workspace compatibility
33

44
# Using "lastversion" user as provided by some linter was a mistake and causes issues with GitHub actions being ran as "runner"
55
# and lastversion running as a different user and being unable to work with workspace files for extracting to its directory

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ mkdocs-material
44
mkdocstrings[crystal,python]
55
markdown-include
66
pymdown-extensions
7+
markdown>=3.8.1 # not directly required, pinned by Snyk to avoid a vulnerability

scripts/check_docs_frontmatter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
REQUIRED_FIELDS = ["title", "description"]
2828

2929
# Regex to extract YAML front-matter
30-
FRONTMATTER_REGEX = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
30+
FRONTMATTER_REGEX = re.compile(r"^---[ \t]*\n(.*?)\n---[ \t]*\n", re.DOTALL)
3131

3232

3333
def is_excluded(filepath: Path) -> bool:

src/lastversion/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,8 @@ def main(argv=None):
494494
args.repo = __self__
495495

496496
# "expand" repo:1.2 as repo --branch 1.2
497-
# noinspection HttpUrlsUsage
498-
if ":" in args.repo and not (args.repo.startswith(("https://", "http://")) and args.repo.count(":") == 1):
497+
_url_prefixes = ("https://", "http://") # NOSONAR - URL parsing, not HTTP request
498+
if ":" in args.repo and not (args.repo.startswith(_url_prefixes) and args.repo.count(":") == 1):
499499
# right split ':' once only to preserve it in protocol of URLs
500500
# https://github.com/repo/owner:2.1
501501
repo_args = args.repo.rsplit(":", 1)

src/lastversion/lastversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def get_repo_data_from_spec(rpmspec_filename):
9999
elif line.startswith("Source0:"):
100100
source0 = line.split("Source0:")[1].strip()
101101
# noinspection HttpUrlsUsage
102-
if source0.startswith("https://") or source0.startswith("http://"):
102+
if source0.startswith("https://") or source0.startswith("http://"): # NOSONAR
103103
spec_urls.append(source0)
104104
elif line.startswith("%global upstream_version "):
105105
current_version = shlex.split(line)[2].strip()

src/lastversion/repo_holders/alpine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def _fetch_package_from_index(self, branch, apk_repo, arch):
101101

102102
# APKINDEX is a tar.gz containing an APKINDEX file
103103
tar_bytes = io.BytesIO(response.content)
104-
with tarfile.open(fileobj=tar_bytes, mode="r:gz") as tar:
104+
with tarfile.open(fileobj=tar_bytes, mode="r:gz") as tar: # NOSONAR
105105
for member in tar.getmembers():
106106
if member.name == "APKINDEX":
107107
file_obj = tar.extractfile(member)

src/lastversion/repo_holders/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _is_process_alive(pid):
6868
return False
6969
try:
7070
# On Unix, signal 0 doesn't kill but checks if process exists
71-
os.kill(pid, 0)
71+
os.kill(pid, 0) # NOSONAR
7272
return True
7373
except OSError as err:
7474
# ESRCH = No such process, EPERM = Permission denied (process exists)

0 commit comments

Comments
 (0)