odoo-dev 1.2.0__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. odoo_dev-1.2.1/.release-please-manifest.json +3 -0
  2. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/CHANGELOG.md +10 -0
  3. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/PKG-INFO +1 -1
  4. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/pyproject.toml +1 -1
  5. odoo_dev-1.2.1/src/odoo_dev/__init__.py +3 -0
  6. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/run.py +37 -8
  7. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/vendor.py +9 -0
  8. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/develop.py +15 -0
  9. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/edit.py +9 -2
  10. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/migrate.py +11 -12
  11. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/update.py +26 -7
  12. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_develop.py +37 -0
  13. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_edit.py +33 -0
  14. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_migrate.py +45 -0
  15. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_update.py +25 -0
  16. odoo_dev-1.2.0/.release-please-manifest.json +0 -3
  17. odoo_dev-1.2.0/src/odoo_dev/__init__.py +0 -3
  18. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/.github/workflows/release-please.yml +0 -0
  19. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/.gitignore +0 -0
  20. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/.gitmodules +0 -0
  21. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/.python-version +0 -0
  22. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/README.md +0 -0
  23. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/release-please-config.json +0 -0
  24. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/cli.py +0 -0
  25. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/__init__.py +0 -0
  26. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/bump.py +0 -0
  27. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/db.py +0 -0
  28. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/docker.py +0 -0
  29. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/commands/setup.py +0 -0
  30. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/config.py +0 -0
  31. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/preflight.py +0 -0
  32. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/__init__.py +0 -0
  33. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/docker/Dockerfile +0 -0
  34. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/docker/__init__.py +0 -0
  35. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
  36. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/vscode/__init__.py +0 -0
  37. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/vscode/launch.json +0 -0
  38. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/vscode/settings.json +0 -0
  39. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/templates/vscode/tasks.json +0 -0
  40. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/utils/__init__.py +0 -0
  41. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/utils/console.py +0 -0
  42. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/utils/manifest.py +0 -0
  43. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/__init__.py +0 -0
  44. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/lock.py +0 -0
  45. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/materialize.py +0 -0
  46. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/sources.py +0 -0
  47. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/sync.py +0 -0
  48. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/src/odoo_dev/vendor/verify.py +0 -0
  49. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/__init__.py +0 -0
  50. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/fixtures/__init__.py +0 -0
  51. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_bump.py +0 -0
  52. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_cli.py +0 -0
  53. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_config.py +0 -0
  54. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_db.py +0 -0
  55. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_db_config.py +0 -0
  56. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_drop_database.py +0 -0
  57. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_integration.py +0 -0
  58. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_preflight.py +0 -0
  59. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_run.py +0 -0
  60. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_setup.py +0 -0
  61. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_setup_db.py +0 -0
  62. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_lock.py +0 -0
  63. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_materialize.py +0 -0
  64. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/tests/test_vendor_verify.py +0 -0
  65. {odoo_dev-1.2.0 → odoo_dev-1.2.1}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.2.1"
3
+ }
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.1](https://github.com/bemade/odoo-dev/compare/v1.2.0...v1.2.1) (2026-07-14)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **test:** make full-suite runs work under a dev odoo.conf ([2a1b752](https://github.com/bemade/odoo-dev/commit/2a1b7527d29b3bf8bb776e41ce739b4864543b73))
9
+ * **vendor:** bump clears stale version when pinning to a commit ([19544ba](https://github.com/bemade/odoo-dev/commit/19544bac55a3dded4b912cc9f2ac7226f5d72d61))
10
+ * **vendor:** constrain `vendor update` to the pin's own major series ([5b58e26](https://github.com/bemade/odoo-dev/commit/5b58e2676de6c1b63a678d1dd365efca9e4c8b99))
11
+ * **vendor:** migrate sets version only when the tag resolves to the pin ([ea59b1d](https://github.com/bemade/odoo-dev/commit/ea59b1d62c9844fb831925d9973b4ec179b803e6))
12
+
3
13
  ## [1.2.0](https://github.com/bemade/odoo-dev/compare/v1.1.0...v1.2.0) (2026-07-07)
4
14
 
5
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-dev
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Odoo Development Environment Helper
5
5
  Project-URL: Homepage, https://git.bemade.org/bemade/odoo-dev
6
6
  Project-URL: Repository, https://git.bemade.org/bemade/odoo-dev
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odoo-dev"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  description = "Odoo Development Environment Helper"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,3 @@
1
+ """Odoo Development Environment Helper."""
2
+
3
+ __version__ = "1.2.1" # x-release-please-version
@@ -235,17 +235,18 @@ def test(
235
235
  break
236
236
 
237
237
  # Auto-load exclusions from repo_deps.yaml when not explicitly provided
238
- if exclude is None and repo_deps_file is not None:
238
+ no_ci_modules: list[str] = []
239
+ if repo_deps_file is not None:
239
240
  try:
240
241
  import yaml
241
242
 
242
243
  data = yaml.safe_load(repo_deps_file.read_text()) or {}
243
- no_ci = data.get("no_ci", [])
244
- if no_ci:
245
- exclude = ",".join(no_ci)
246
- info(f"Excluding (from {repo_deps_file} no_ci): {exclude}")
244
+ no_ci_modules = list(data.get("no_ci", []) or [])
247
245
  except Exception:
248
- pass
246
+ no_ci_modules = []
247
+ if exclude is None and no_ci_modules:
248
+ exclude = ",".join(no_ci_modules)
249
+ info(f"Excluding (from {repo_deps_file} no_ci): {exclude}")
249
250
 
250
251
  # Verify setup
251
252
  if not cfg.venv_path.exists():
@@ -376,6 +377,9 @@ def test(
376
377
  "--stop-after-init",
377
378
  "--http-port",
378
379
  str(http_port),
380
+ # No need to fork prefork workers for a stop-after-init install.
381
+ "--workers",
382
+ "0",
379
383
  ],
380
384
  )
381
385
 
@@ -422,11 +426,36 @@ def test(
422
426
  "--stop-after-init",
423
427
  "--http-port",
424
428
  str(http_port),
429
+ # HttpCase needs the threaded server (ThreadedServer.httpd);
430
+ # a prefork/multi-worker conf raises
431
+ # "'PreforkServer' object has no attribute 'httpd'" in
432
+ # setUpClass. Force single-process regardless of odoo.conf.
433
+ "--workers",
434
+ "0",
435
+ # A restrictive dbfilter in odoo.conf (e.g. `dbfilter = ^rwi$`)
436
+ # never matches the ephemeral test DB, so HttpCase requests get
437
+ # "dbfilter rejects it; logging session out" -> auth='user'
438
+ # routes 404 and JSON endpoints return non-JSON. Scope the
439
+ # filter to this run's test DB so the test server serves it.
440
+ f"--db-filter=^{db_name}$",
425
441
  ]
426
442
  )
427
443
 
428
- if test_tags:
429
- test_cmd.extend(["--test-tags", test_tags])
444
+ # Excluding no_ci modules from the tested-module *list* is not enough:
445
+ # they still install as transitive deps, and Odoo's button_upgrade
446
+ # cascades 'to upgrade' to downstream dependents of any upgraded target
447
+ # module (`-u modules`). A no_ci module that depends on a tested module
448
+ # thus gets re-upgraded and its post_install tests run. Exclude their
449
+ # tests explicitly via Odoo test-tags. The TagsSelector auto-adds
450
+ # '+standard' when only '-' filters are present, so this keeps running
451
+ # every standard test except those belonging to a no_ci module.
452
+ effective_test_tags = test_tags
453
+ if effective_test_tags is None and no_ci_modules:
454
+ effective_test_tags = ",".join(f"-/{m}" for m in no_ci_modules)
455
+ info(f"Excluding no_ci module tests via --test-tags: {effective_test_tags}")
456
+
457
+ if effective_test_tags:
458
+ test_cmd.extend(["--test-tags", effective_test_tags])
430
459
 
431
460
  result = subprocess.run(test_cmd)
432
461
  test_exit_code = result.returncode
@@ -18,6 +18,7 @@ from odoo_dev.utils.console import error, info, success
18
18
  from odoo_dev.vendor.develop import (
19
19
  DevelopError,
20
20
  develop_state,
21
+ ensure_addons_path,
21
22
  start_develop,
22
23
  stop_develop,
23
24
  )
@@ -159,6 +160,14 @@ def migrate_cmd(
159
160
 
160
161
  lock, unused = migrate_repo(cfg.project_dir, addons=addon or None, deinit=deinit)
161
162
  success(f"Migrated {len(lock.entries)} addon(s) to vendored/ (see addons.lock).")
163
+
164
+ # Wire vendored/ into the local conf's addons_path. A fresh `setup` adds it,
165
+ # but `setup` won't overwrite an existing conf, so a repo vendored for the
166
+ # first time would otherwise leave Odoo unable to find the new addons
167
+ # ("invalid module names, ignored"). Local conf only — never Docker/CI.
168
+ if cfg.config_file.exists() and cfg.vendored_dir.is_dir():
169
+ if ensure_addons_path(cfg.config_file, str(cfg.vendored_dir)):
170
+ info(f"Added {cfg.vendored_dir} to {cfg.config_file} addons_path.")
162
171
  if unused:
163
172
  info("")
164
173
  if deinit:
@@ -167,6 +167,21 @@ def remove_addons_path(conf_file: Path, path: str) -> None:
167
167
  _write_addons_path(conf_file, [p for p in read_addons_path(conf_file) if p != path])
168
168
 
169
169
 
170
+ def ensure_addons_path(conf_file: Path, path: str) -> bool:
171
+ """Append ``path`` to the conf's addons_path if it isn't already listed.
172
+
173
+ Idempotent; returns True only when it actually added the entry. Used by
174
+ ``vendor migrate`` to wire ``vendored/`` into a conf that predates vendoring
175
+ (``setup`` adds it for fresh confs but never overwrites an existing one).
176
+ """
177
+ paths = read_addons_path(conf_file)
178
+ if path in paths:
179
+ return False
180
+ paths.append(path)
181
+ _write_addons_path(conf_file, paths)
182
+ return True
183
+
184
+
170
185
  # --- overlay / gitignore / state ------------------------------------------------
171
186
 
172
187
 
@@ -76,8 +76,15 @@ def bump_addon(
76
76
  entry = lock.entries[name]
77
77
  sha = _resolve_pin(name, entry.source, version, commit, entry.branch, cache_dir)
78
78
  entry.commit = sha
79
- if version is not None:
80
- entry.version = version
79
+ # ``version`` is the tag-intent metadata, and ``vendor check``'s moved-tag
80
+ # tripwire (verify.py) requires ``<name>/<version>`` to still resolve to
81
+ # ``commit``. A ``--commit`` / branch-HEAD bump moves the pin off the tagged
82
+ # commit, so the old ``version`` is now stale and would (correctly) fail the
83
+ # tripwire. Set it to whatever the bump was keyed on: the new tag for a
84
+ # ``--version`` bump, or ``None`` for a raw ``--commit`` / branch bump — a
85
+ # commit pin carries no version metadata (matching how ``migrate`` renders
86
+ # untagged pins).
87
+ entry.version = version
81
88
  lock.entries[name] = entry
82
89
  lock.dump(project_dir / "addons.lock")
83
90
  sync_addons(project_dir, lock, cache_dir=cache_dir, names=[name])
@@ -2,7 +2,9 @@
2
2
 
3
3
  Reads the ``addons/<addon>`` symlinks that point into ``.repos/*`` submodules,
4
4
  derives a per-addon pin (submodule remote URL = source, submodule HEAD = commit,
5
- manifest version -> version when a matching ``<addon>/<version>`` tag exists),
5
+ manifest version -> version ONLY when the ``<addon>/<version>`` tag resolves to
6
+ the pinned commit; a commit that isn't the tagged release (a pin behind or ahead
7
+ of the tag, or any intermediate commit) is left commit-only, no version),
6
8
  writes ``addons.lock``, materializes ``vendored/<addon>/`` from the submodule,
7
9
  and removes the now-redundant symlink. Client-private real dirs under ``addons/``
8
10
  are left untouched; only symlinks into submodules are migrated. Submodule removal
@@ -19,6 +21,7 @@ from typing import Iterable, Optional
19
21
 
20
22
  from odoo_dev.utils.manifest import manifest_path, read_version
21
23
  from odoo_dev.vendor.lock import LockEntry, Lockfile
24
+ from odoo_dev.vendor.sources import tag_resolves_to
22
25
  from odoo_dev.vendor.sync import sync_addons
23
26
 
24
27
 
@@ -63,16 +66,6 @@ def _submodule_head(project_dir: Path, sub_path: str) -> str:
63
66
  ).stdout.strip()
64
67
 
65
68
 
66
- def _tag_exists(repo: Path, tag: str) -> bool:
67
- return (
68
- subprocess.run(
69
- ["git", "-C", str(repo), "rev-parse", f"refs/tags/{tag}"],
70
- capture_output=True,
71
- ).returncode
72
- == 0
73
- )
74
-
75
-
76
69
  def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) -> list:
77
70
  """Return a list of planned pins without changing anything.
78
71
 
@@ -118,7 +111,13 @@ def plan_migration(project_dir: Path, addons: Optional[Iterable[str]] = None) ->
118
111
  mf = manifest_path(real)
119
112
  if mf is not None:
120
113
  v = read_version(mf.read_text())
121
- if v is not None and _tag_exists(sub_root, f"{name}/{v}"):
114
+ # Only claim a ``version`` when the tag actually points at the pinned
115
+ # commit. Checking mere tag EXISTENCE was wrong: the same manifest
116
+ # version can live at several commits (the tag is cut at one of
117
+ # them), so a pin at any other commit with that version would carry a
118
+ # ``version`` whose tag resolves elsewhere — tripping ``vendor
119
+ # check``'s moved-tag guard. A non-release pin is commit-only.
120
+ if v is not None and tag_resolves_to(sub_root, f"{name}/{v}", commit):
122
121
  version = v
123
122
  plans.append(
124
123
  dict(
@@ -24,27 +24,46 @@ def _version_key(v: str) -> tuple:
24
24
  return tuple(int(p) if p.isdigit() else 0 for p in v.replace("-", ".").split("."))
25
25
 
26
26
 
27
+ def _series(v: str) -> str:
28
+ """The major-series segment of a version — the cross-series guard.
29
+
30
+ An Odoo addon carries both ``18.0.*`` and ``19.0.*`` version tags in its source;
31
+ version-tracking must stay within the pin's own series (``18`` vs ``19``), since
32
+ moving across Odoo series is a deliberate migration, never an auto-update.
33
+ """
34
+ return v.split(".", 1)[0]
35
+
36
+
27
37
  def list_addon_versions(
28
- source: str, addon: str, cache_dir: Optional[Path] = None
38
+ source: str,
39
+ addon: str,
40
+ cache_dir: Optional[Path] = None,
41
+ series: Optional[str] = None,
29
42
  ) -> list[str]:
30
- """All ``<addon>/<version>`` tag versions present in ``source``."""
43
+ """``<addon>/<version>`` tag versions in ``source``, optionally same-series only."""
31
44
  repo = _ensure_clone(source, cache_dir)
32
45
  r = subprocess.run(
33
46
  ["git", "-C", str(repo), "tag", "-l", f"{addon}/*"],
34
47
  capture_output=True,
35
48
  text=True,
36
49
  )
37
- return [
50
+ versions = [
38
51
  t.split("/", 1)[1]
39
52
  for t in r.stdout.split()
40
53
  if t.startswith(f"{addon}/") and "/" in t
41
54
  ]
55
+ if series is not None:
56
+ versions = [v for v in versions if _series(v) == series]
57
+ return versions
42
58
 
43
59
 
44
60
  def latest_version(
45
- source: str, addon: str, cache_dir: Optional[Path] = None
61
+ source: str,
62
+ addon: str,
63
+ cache_dir: Optional[Path] = None,
64
+ series: Optional[str] = None,
46
65
  ) -> Optional[str]:
47
- versions = list_addon_versions(source, addon, cache_dir)
66
+ versions = list_addon_versions(source, addon, cache_dir, series)
48
67
  return max(versions, key=_version_key) if versions else None
49
68
 
50
69
 
@@ -65,8 +84,8 @@ def find_updates(
65
84
  for name, e in lock.entries.items():
66
85
  if only is not None and name not in only:
67
86
  continue
68
- if e.version: # version-tracked
69
- latest = latest_version(e.source, name, cache_dir)
87
+ if e.version: # version-tracked (within the pin's own major series)
88
+ latest = latest_version(e.source, name, cache_dir, series=_series(e.version))
70
89
  if latest and _version_key(latest) > _version_key(e.version):
71
90
  updates.append(
72
91
  {
@@ -14,6 +14,7 @@ import pytest
14
14
  from odoo_dev.vendor.develop import (
15
15
  DevelopError,
16
16
  develop_state,
17
+ ensure_addons_path,
17
18
  read_addons_path,
18
19
  start_develop,
19
20
  stop_develop,
@@ -187,3 +188,39 @@ def test_two_addons_share_overlay(tmp_path):
187
188
  stop_develop(proj, "shared_addon", conf_file=conf)
188
189
  assert str(dev / "overlay") in read_addons_path(conf)
189
190
  assert [p.name for p in (dev / "overlay").iterdir()] == ["other_addon"]
191
+
192
+
193
+ # --- ensure_addons_path: wiring vendored/ into a pre-existing conf --------------
194
+
195
+
196
+ def test_ensure_addons_path_appends_when_missing(tmp_path: Path):
197
+ """A conf that predates vendoring gets vendored/ appended (returns True)."""
198
+ conf = tmp_path / "odoo.conf"
199
+ conf.write_text("[options]\naddons_path = /p/odoo/addons,/p/addons\n")
200
+ vendored = str(tmp_path / "vendored")
201
+
202
+ assert ensure_addons_path(conf, vendored) is True
203
+ paths = read_addons_path(conf)
204
+ assert paths[-1] == vendored
205
+ assert paths[:2] == ["/p/odoo/addons", "/p/addons"]
206
+
207
+
208
+ def test_ensure_addons_path_is_idempotent(tmp_path: Path):
209
+ """A second call is a no-op (returns False, no duplicate entry)."""
210
+ conf = tmp_path / "odoo.conf"
211
+ conf.write_text("[options]\naddons_path = /p/addons\n")
212
+ vendored = str(tmp_path / "vendored")
213
+
214
+ assert ensure_addons_path(conf, vendored) is True
215
+ assert ensure_addons_path(conf, vendored) is False
216
+ assert read_addons_path(conf).count(vendored) == 1
217
+
218
+
219
+ def test_ensure_addons_path_inserts_line_when_absent(tmp_path: Path):
220
+ """A conf with no addons_path line gets one created under [options]."""
221
+ conf = tmp_path / "odoo.conf"
222
+ conf.write_text("[options]\nadmin_passwd = x\n")
223
+ vendored = str(tmp_path / "vendored")
224
+
225
+ assert ensure_addons_path(conf, vendored) is True
226
+ assert read_addons_path(conf) == [vendored]
@@ -99,6 +99,39 @@ def test_bump_moves_pin_and_rematerializes(tmp_path):
99
99
  assert verify(proj, lock, cache_dir=tmp_path / "c") == []
100
100
 
101
101
 
102
+ def _untagged_commit(repo: Path) -> str:
103
+ """A commit that is NOT a version tag (a between-releases pin target)."""
104
+ (repo / "shared_addon" / "m.py").write_text("v = 3\n")
105
+ _git(repo, "add", "-A")
106
+ _git(repo, "commit", "-qm", "v3-untagged")
107
+ return _git(repo, "rev-parse", "HEAD")
108
+
109
+
110
+ def test_bump_by_commit_clears_stale_version(tmp_path):
111
+ """A ``--commit`` bump moves the pin off the tagged commit, so it must clear
112
+ the now-stale ``version`` — otherwise ``vendor check``'s moved-tag tripwire
113
+ (correctly) fails because ``<name>/<old-version>`` no longer resolves to the
114
+ new pin. Regression for the bug where ``bump`` left ``version`` untouched.
115
+ """
116
+ repo = _source_repo(tmp_path)
117
+ proj = tmp_path / "client"
118
+ proj.mkdir()
119
+ add_addon(
120
+ proj, "shared_addon", str(repo), version="18.0.1.0.0", cache_dir=tmp_path / "c"
121
+ )
122
+ sha = _untagged_commit(repo)
123
+
124
+ entry = bump_addon(proj, "shared_addon", commit=sha, cache_dir=tmp_path / "c")
125
+
126
+ assert entry.commit == sha
127
+ assert entry.version is None, "commit bump must clear the stale version tag"
128
+ assert (proj / "vendored" / "shared_addon" / "m.py").read_text() == "v = 3\n"
129
+ lock = Lockfile.load(proj / "addons.lock")
130
+ assert "version" not in (lock.entries["shared_addon"].to_dict())
131
+ # The moved-tag tripwire must NOT fire (it would if version stayed stale).
132
+ assert verify(proj, lock, cache_dir=tmp_path / "c") == []
133
+
134
+
102
135
  def test_bump_unknown_addon_raises(tmp_path):
103
136
  proj = tmp_path / "client"
104
137
  proj.mkdir()
@@ -92,6 +92,51 @@ def test_plan_leaves_version_none_when_no_tag(tmp_path):
92
92
  assert plans[0]["version"] is None
93
93
 
94
94
 
95
+ def _source_repo_tag_behind_head(tmp_path: Path) -> Path:
96
+ """Source where the ``<addon>/<version>`` tag sits at an OLDER commit than
97
+ HEAD, and HEAD's manifest still carries that same version (no bump between).
98
+ Mirrors the real case: a pin at an intermediate commit that no release tag
99
+ points at, even though the manifest version string matches a tag.
100
+ """
101
+ repo = tmp_path / "sale-workflow"
102
+ (repo / "shared_addon" / "models").mkdir(parents=True)
103
+ (repo / "shared_addon" / "__manifest__.py").write_text(
104
+ "{'name': 'shared', 'version': '18.0.2.0.0'}\n"
105
+ )
106
+ (repo / "shared_addon" / "models" / "m.py").write_text("z = 3\n")
107
+ _git(repo, "init", "-q")
108
+ _git(repo, "config", "user.email", "t@t")
109
+ _git(repo, "config", "user.name", "t")
110
+ _git(repo, "add", "-A")
111
+ _git(repo, "commit", "-qm", "init")
112
+ c1 = _git(repo, "rev-parse", "HEAD")
113
+ _git(repo, "tag", "shared_addon/18.0.2.0.0", c1) # tag at the OLDER commit
114
+ # A later commit with the SAME manifest version — the pin target.
115
+ (repo / "shared_addon" / "models" / "m.py").write_text("z = 4\n")
116
+ _git(repo, "add", "-A")
117
+ _git(repo, "commit", "-qm", "later, same version")
118
+ return repo
119
+
120
+
121
+ def test_plan_omits_version_when_tag_points_elsewhere(tmp_path):
122
+ """Regression: a pin that is NOT the tagged commit (same manifest version at
123
+ several commits) must be commit-only. Checking mere tag EXISTENCE wrote a
124
+ ``version`` whose tag resolved to a different commit, tripping ``vendor
125
+ check``'s moved-tag guard. The full migrate+verify must be clean.
126
+ """
127
+ source = _source_repo_tag_behind_head(tmp_path)
128
+ head = _git(source, "rev-parse", "HEAD")
129
+ client = _client_repo(tmp_path, source) # submodule pins source HEAD
130
+ plans = plan_migration(client)
131
+ assert plans[0]["commit"] == head
132
+ assert plans[0]["version"] is None # tag resolves to the older commit, not HEAD
133
+
134
+ migrate_repo(client, deinit=False)
135
+ lock = Lockfile.load(client / "addons.lock")
136
+ assert lock.entries["shared_addon"].version is None
137
+ assert verify(client, lock) == [] # no moved-tag failure
138
+
139
+
95
140
  def test_migrate_materializes_removes_symlink_and_verifies(tmp_path):
96
141
  source, sha = _source_repo(tmp_path, tag="shared_addon/18.0.2.0.0")
97
142
  client = _client_repo(tmp_path, source)
@@ -121,3 +121,28 @@ def test_find_updates_skips_bare_commit_pin(tmp_path):
121
121
  _cut_version(repo, "18.0.1.1.0", 2)
122
122
  # A bare-commit pin (no version, no branch) is deliberately fixed.
123
123
  assert find_updates(proj, Lockfile.load(proj / "addons.lock"), cache_dir=tmp_path / "c2") == []
124
+
125
+
126
+ def test_find_updates_stays_in_series(tmp_path):
127
+ repo = _source(tmp_path)
128
+ proj = tmp_path / "client"
129
+ proj.mkdir()
130
+ add_addon(proj, "shared", str(repo), version="18.0.1.0.0", cache_dir=tmp_path / "c")
131
+ # A newer 18.0 release AND a cross-series 19.0 tag both exist upstream
132
+ # (as after baselining both branches). Must NOT jump 18.0 -> 19.0.
133
+ _cut_version(repo, "18.0.1.1.0", 2)
134
+ _git(repo, "tag", "shared/19.0.0.1.0")
135
+ updates = find_updates(
136
+ proj, Lockfile.load(proj / "addons.lock"), cache_dir=tmp_path / "c2"
137
+ )
138
+ assert updates == [
139
+ {"name": "shared", "kind": "version", "current": "18.0.1.0.0", "latest": "18.0.1.1.0"}
140
+ ]
141
+
142
+
143
+ def test_latest_version_series_filter(tmp_path):
144
+ repo = _source(tmp_path)
145
+ _cut_version(repo, "18.0.1.1.0", 2)
146
+ _git(repo, "tag", "shared/19.0.5.0.0")
147
+ assert latest_version(str(repo), "shared", tmp_path / "c", series="18") == "18.0.1.1.0"
148
+ assert latest_version(str(repo), "shared", tmp_path / "c", series="19") == "19.0.5.0.0"
@@ -1,3 +0,0 @@
1
- {
2
- ".": "1.2.0"
3
- }
@@ -1,3 +0,0 @@
1
- """Odoo Development Environment Helper."""
2
-
3
- __version__ = "1.2.0" # x-release-please-version
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes