common-python-tasks 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl

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.
@@ -1,7 +1,7 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
3
  if TYPE_CHECKING:
4
- from typing import Sequence
4
+ from collections.abc import Sequence
5
5
 
6
6
  from poethepoet_tasks import TaskCollection
7
7
 
@@ -9,7 +9,8 @@ __all__ = ["TaskCollection"]
9
9
 
10
10
 
11
11
  def tasks(
12
- include_tags: "Sequence[str] | None" = None, exclude_tags: "Sequence[str]" = tuple()
12
+ include_tags: "Sequence[str] | None" = None,
13
+ exclude_tags: "Sequence[str] | None" = None,
13
14
  ) -> dict:
14
15
  """Return the task collection filtered by the requested tags.
15
16
 
@@ -25,6 +26,9 @@ def tasks(
25
26
  if include_tags is None and not exclude_tags:
26
27
  include_tags = ("common",)
27
28
  elif include_tags is None:
28
- include_tags = tuple()
29
+ include_tags = ()
30
+
31
+ if exclude_tags is None:
32
+ exclude_tags = ()
29
33
 
30
34
  return task_collection(include_tags=include_tags, exclude_tags=exclude_tags)
@@ -9,8 +9,7 @@ x-common-environment: &common-environment
9
9
  x-common-build-args: &common-build-args
10
10
  PACKAGE_NAME: ${PACKAGE_NAME}
11
11
  PYTHON_VERSION: ${PYTHON_VERSION}
12
- PACKAGE_MANAGER: ${PACKAGE_MANAGER}
13
- PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
12
+ UV_VERSION: ${UV_VERSION}
14
13
 
15
14
  services:
16
15
  api:
@@ -8,8 +8,7 @@ x-common-environment: &common-environment
8
8
  x-common-build-args: &common-build-args
9
9
  PACKAGE_NAME: ${PACKAGE_NAME}
10
10
  PYTHON_VERSION: ${PYTHON_VERSION}
11
- PACKAGE_MANAGER: ${PACKAGE_MANAGER}
12
- PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
11
+ UV_VERSION: ${UV_VERSION}
13
12
  WORKDIR_PATH: ${WORKDIR_PATH}
14
13
 
15
14
  services:
@@ -22,7 +22,7 @@ services:
22
22
  path: ./src/${PACKAGE_UNDERSCORE_NAME}
23
23
  target: /${PACKAGE_UNDERSCORE_NAME}
24
24
  - action: rebuild
25
- path: poetry.lock
25
+ path: uv.lock
26
26
  ports:
27
27
  - mode: ingress
28
28
  target: 5678
@@ -1,7 +1,7 @@
1
1
  *
2
2
  !dist/*.whl
3
3
  !pyproject.toml
4
- !poetry.lock
4
+ !uv.lock
5
5
  !README.md
6
6
  !LICENSE
7
7
 
@@ -5,7 +5,12 @@
5
5
  ARG PYTHON_VERSION=3
6
6
  ARG PYTHON_VARIANT=slim
7
7
 
8
- FROM python:${PYTHON_VERSION}-${PYTHON_VARIANT} AS deps
8
+ FROM python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}} AS deps
9
+ ARG PYTHON_VERSION
10
+ ARG PYTHON_VARIANT
11
+
12
+ LABEL org.opencontainers.image.base.name="python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}}" \
13
+ org.opencontainers.image.python.variant="${PYTHON_VARIANT}"
9
14
 
10
15
  ARG CACHE_ID_SUFFIX
11
16
  RUN printf '%s' "$CACHE_ID_SUFFIX" >/tmp/.cache_id_suffix
@@ -4,36 +4,12 @@
4
4
  # the latest. The `build-image` task passes the version properly here
5
5
  ARG PYTHON_VERSION=3
6
6
  # Variant for the runtime image, e.g. slim, alpine, etc.
7
- # See https://hub.docker.com/_/python for available variants
7
+ # See https://hub.docker.com/_/python for available variants. Leave empty for no variant.
8
8
  ARG PYTHON_VARIANT=slim
9
- ARG WORKDIR_PATH=/workspace
10
- {% set package_manager = PACKAGE_MANAGER|default('poetry') %}
11
9
 
12
10
  FROM python:${PYTHON_VERSION} AS builder
13
11
 
14
12
  ARG PACKAGE_VERSION
15
-
16
- {% if package_manager == 'poetry' %}
17
- ARG POETRY_VERSION
18
- ARG POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION
19
- ARG POETRY_PLUGIN_EXPORT_VERSION
20
-
21
- # Bypass VCS-based version detection: use the host-computed version directly.
22
- # This avoids requiring a .git directory in the build context and prevents
23
- # false dirty-version detection from a partial working tree.
24
- ENV POETRY_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION}
25
-
26
- # Install Poetry and required Poetry plugins for package build
27
- RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
28
- sh -c 'set -eu; \
29
- poetry_spec="poetry"; \
30
- if [ -n "${POETRY_VERSION:-}" ]; then poetry_spec="poetry==$POETRY_VERSION"; fi; \
31
- dynamic_spec="poetry-dynamic-versioning[plugin]"; \
32
- if [ -n "${POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION:-}" ]; then dynamic_spec="poetry-dynamic-versioning[plugin]==$POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION"; fi; \
33
- export_spec="poetry-plugin-export"; \
34
- if [ -n "${POETRY_PLUGIN_EXPORT_VERSION:-}" ]; then export_spec="poetry-plugin-export==$POETRY_PLUGIN_EXPORT_VERSION"; fi; \
35
- pip install --root-user-action=ignore "$poetry_spec" "$dynamic_spec" "$export_spec"'
36
- {% else %}
37
13
  ARG UV_VERSION
38
14
 
39
15
  # Bypass VCS-based version detection: use the host-computed version directly.
@@ -48,7 +24,6 @@ RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }}
48
24
  else \
49
25
  pip install --root-user-action=ignore uv; \
50
26
  fi'
51
- {% endif %}
52
27
 
53
28
  # Build package
54
29
  WORKDIR /tmp/build
@@ -56,28 +31,27 @@ COPY . /tmp/build/
56
31
 
57
32
  {% if HAS_DEBUG_DEPS %}
58
33
  # Export debug requirements when the project defines a debug dependency group
59
- {% if package_manager == 'poetry' %}
60
- RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
61
- poetry export --only debug --without-hashes -f requirements.txt --output requirements-debug.txt
62
- {% else %}
63
- RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} \
34
+ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }}{% for mount in UV_INDEX_SECRET_MOUNTS %} \
35
+ --mount={{ mount }}{% endfor %} \
64
36
  uv export --group debug --no-hashes --format requirements-txt --output-file requirements-debug.txt
65
37
  {% endif %}
66
- {% endif %}
67
38
 
68
- # Build the wheel, caching the package-manager cache directory to speed up subsequent builds.
39
+ # Build the wheel, caching the uv cache directory to speed up subsequent builds.
69
40
  # If running this in a CI system, you must be using a persistent builder to take
70
41
  # advantage of the cache mount, and you should configure your CI to persist the
71
42
  # cache directory between builds
72
- {% if package_manager == 'poetry' %}
73
- RUN --mount=type=cache,target=/root/.cache/pypoetry,id=pypoetry-cache{{ CACHE_ID_SUFFIX }} poetry build --format=wheel
74
- {% else %}
75
- RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} uv build --wheel
76
- {% endif %}
43
+ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }}{% for mount in UV_INDEX_SECRET_MOUNTS %} \
44
+ --mount={{ mount }}{% endfor %} \
45
+ uv build --wheel
77
46
 
78
- FROM python:${PYTHON_VERSION}-${PYTHON_VARIANT} AS runtime
47
+ FROM python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}} AS runtime
48
+ ARG PYTHON_VERSION
49
+ ARG PYTHON_VARIANT
79
50
  ARG WORKDIR_PATH=/workspace
80
51
 
52
+ LABEL org.opencontainers.image.base.name="python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}}" \
53
+ org.opencontainers.image.python.variant="${PYTHON_VARIANT}"
54
+
81
55
  ENV PYTHONUNBUFFERED=1
82
56
  ENV PYTHONDONTWRITEBYTECODE=1
83
57
 
@@ -1,2 +1,2 @@
1
1
  line-length = 88
2
- extend-exclude = [".dist", ".poetry", "build"]
2
+ extend-exclude = [".dist", "build"]
@@ -3,14 +3,14 @@ import logging
3
3
  import platform
4
4
  import re
5
5
  import tempfile
6
+ from collections.abc import Sequence
6
7
  from dataclasses import dataclass
7
8
  from enum import IntEnum, StrEnum
8
9
  from pathlib import Path
9
10
  from shlex import quote
10
- from typing import Sequence
11
11
 
12
12
  from . import git, project, utils
13
- from .env import get_cache_id_suffix, get_workdir_path
13
+ from .env import get_cache_id_suffix, get_python_variant, get_workdir_path
14
14
 
15
15
  LOGGER = logging.getLogger(__name__)
16
16
 
@@ -289,37 +289,17 @@ def render_build_image(
289
289
  commit_tag = version_tag
290
290
 
291
291
  python_version = platform.python_version()
292
- package_manager = project.get_package_manager()
293
- package_manager_version = project.get_package_manager_version(package_manager)
294
-
295
- manager_build_args: dict[str, str | None]
296
- if package_manager == project.PackageManager.POETRY:
297
- manager_build_args = {
298
- "POETRY_VERSION": package_manager_version,
299
- "POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
300
- "poetry-dynamic-versioning[plugin]"
301
- ),
302
- "POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
303
- "poetry-plugin-export"
304
- ),
305
- }
306
- else:
307
- manager_build_args = {
308
- "UV_VERSION": package_manager_version,
309
- }
310
292
 
311
293
  build_args = _merge_build_args(
312
294
  {
313
295
  "PYTHON_VERSION": python_version,
314
- "PACKAGE_MANAGER": package_manager,
315
- "PACKAGE_MANAGER_VERSION": package_manager_version,
296
+ "UV_VERSION": project.get_uv_version(),
316
297
  "PACKAGE_VERSION": package_version,
317
298
  "PACKAGE_NAME": utils.get_package_name(use_underscores=True),
318
299
  "AUTHORS": ",".join(
319
300
  [f"{name} <{email}>" for name, email in project.get_authors()]
320
301
  ),
321
302
  "GIT_COMMIT": commit_tag,
322
- **manager_build_args,
323
303
  },
324
304
  extra_build_args,
325
305
  )
@@ -580,7 +560,10 @@ def build_deps_image(
580
560
  archs = _resolve_archs(single_arch)
581
561
 
582
562
  build_args = _merge_build_args(
583
- {"PYTHON_VERSION": python_version},
563
+ {
564
+ "PYTHON_VERSION": python_version,
565
+ "PYTHON_VARIANT": get_python_variant(),
566
+ },
584
567
  extra_build_args,
585
568
  )
586
569
  if cache_id_suffix:
@@ -5,16 +5,17 @@ import logging
5
5
  import os
6
6
  import platform
7
7
  import tempfile
8
+ from collections.abc import Sequence
8
9
  from pathlib import Path
9
10
  from shlex import quote
10
- from typing import NoReturn, Sequence
11
+ from typing import NoReturn
11
12
 
12
13
  from jinja2 import Template
13
14
  from poethepoet_tasks import TaskCollection
14
15
 
15
16
  from . import utils
16
17
  from .env import get_workdir_path
17
- from .project import get_package_manager, get_package_manager_version
18
+ from .project import get_uv_version
18
19
 
19
20
  LOGGER = logging.getLogger(__name__)
20
21
 
@@ -113,16 +114,15 @@ def render_file(
113
114
  if extra_template_vars:
114
115
  template_vars.update(extra_template_vars)
115
116
  rendered = Template(path_obj.read_text()).render(**template_vars)
116
- tf = tempfile.NamedTemporaryFile(
117
+ with tempfile.NamedTemporaryFile(
117
118
  mode="w",
118
119
  encoding="utf-8",
119
120
  delete=False,
120
121
  prefix=path_obj.stem + ".",
121
122
  suffix=suffix,
122
- )
123
- temp_path = Path(tf.name)
124
- tf.write(rendered)
125
- tf.close()
123
+ ) as temp_file:
124
+ temp_path = Path(temp_file.name)
125
+ temp_file.write(rendered)
126
126
  return temp_path, True
127
127
 
128
128
  return path_obj, False
@@ -154,7 +154,7 @@ def read_dotenv(path: Path, error_on_fail: bool = False) -> dict[str, str]:
154
154
  value = value.strip().strip('"').strip("'")
155
155
  if key:
156
156
  env[key] = value
157
- except Exception as exc:
157
+ except (OSError, UnicodeError) as exc:
158
158
  if error_on_fail:
159
159
  utils.fatal(f"Failed to parse .env file {path}: {exc}")
160
160
  else:
@@ -169,9 +169,9 @@ def append_dotenv(path: Path, items: dict[str, str]) -> None:
169
169
  path: Path to the `.env` file.
170
170
  items: Environment variables to append to the file.
171
171
  """
172
- from datetime import datetime
172
+ from datetime import UTC, datetime
173
173
 
174
- header = f"\n# Auto-generated by common_python_tasks on {datetime.now().isoformat(timespec='seconds')}\n"
174
+ header = f"\n# Auto-generated by common_python_tasks on {datetime.now(UTC).astimezone().isoformat(timespec='seconds')}\n"
175
175
  with path.open("a", encoding="utf-8") as f:
176
176
  f.write(header)
177
177
  for k, v in items.items():
@@ -214,7 +214,7 @@ def get_or_generate_secret(
214
214
  try:
215
215
  append_dotenv(dotenv_path, {key_name: token})
216
216
  LOGGER.info("Generated %s and stored it in .env", key_name)
217
- except Exception as exc:
217
+ except OSError as exc:
218
218
  LOGGER.warning(
219
219
  "Failed to persist %s to .env (%s); using in-memory only", key_name, exc
220
220
  )
@@ -239,8 +239,7 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
239
239
  "ENVIRONMENT",
240
240
  "IMAGE_TAG",
241
241
  "PYTHON_VERSION",
242
- "PACKAGE_MANAGER",
243
- "PACKAGE_MANAGER_VERSION",
242
+ "UV_VERSION",
244
243
  },
245
244
  "compose-db": {
246
245
  "PACKAGE_NAME",
@@ -250,8 +249,7 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
250
249
  "DB_PORT",
251
250
  "IMAGE_TAG",
252
251
  "PYTHON_VERSION",
253
- "PACKAGE_MANAGER",
254
- "PACKAGE_MANAGER_VERSION",
252
+ "UV_VERSION",
255
253
  "POSTGRES_VERSION",
256
254
  "WORKDIR_PATH",
257
255
  },
@@ -337,13 +335,12 @@ def get_compose_env(
337
335
  "ENVIRONMENT": os.getenv("ENVIRONMENT", "production"),
338
336
  "IMAGE_TAG": image_tag or "latest",
339
337
  "PACKAGE_NAME": package_name,
340
- "PACKAGE_MANAGER": get_package_manager(),
341
- "PACKAGE_MANAGER_VERSION": get_package_manager_version(),
342
338
  "PACKAGE_UNDERSCORE_NAME": utils.package_name_to_underscore(package_name),
343
339
  "WORKDIR_PATH": get_workdir_path(),
344
340
  "POSTGRES_VERSION": os.getenv("POSTGRES_VERSION", "17"),
345
341
  "PYTHON_VERSION": platform.python_version(),
346
342
  "SECRET_KEY": os.getenv("SECRET_KEY", ""),
343
+ "UV_VERSION": get_uv_version(),
347
344
  }
348
345
 
349
346
  if compose_type and compose_files:
@@ -593,14 +590,14 @@ def build_exec_script(
593
590
  Returns:
594
591
  Absolute path to the temporary script file.
595
592
  """
596
- script_fd = tempfile.NamedTemporaryFile(
593
+ with tempfile.NamedTemporaryFile(
597
594
  mode="w",
598
595
  encoding="utf-8",
599
596
  delete=False,
600
597
  prefix="compose-exec.",
601
598
  suffix=".sh",
602
- )
603
- script_path = Path(script_fd.name)
599
+ ) as temp_file:
600
+ script_path = Path(temp_file.name)
604
601
  script_path_str = str(script_path.resolve())
605
602
 
606
603
  lines = [
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import re
3
4
  import secrets
4
5
  import shlex
5
6
  from pathlib import Path
@@ -21,6 +22,82 @@ TRUTHY_VALUES = {
21
22
  WORKDIR_PATH_ENV_VAR = "WORKDIR_PATH"
22
23
  WORKDIR_PATH_DEFAULT = "/workspace"
23
24
  _AUTO_INJECTED_BUILD_ARG_ENV_VARS = (WORKDIR_PATH_ENV_VAR,)
25
+ _UV_INDEX_CREDENTIAL_PATTERN = re.compile(r"^UV_INDEX_(.+)_(USERNAME|PASSWORD)$")
26
+
27
+
28
+ def collect_uv_index_credentials() -> list[dict[str, str | None]]:
29
+ """Collect present `UV_INDEX_{name}_{USERNAME,PASSWORD}` host env vars.
30
+
31
+ Returns:
32
+ A list of dicts with keys `index_name`, `username_env`, and
33
+ `password_env`. Only indices with at least one credential set are
34
+ included, sorted by index name.
35
+ """
36
+ found = {}
37
+ for key in os.environ:
38
+ match = _UV_INDEX_CREDENTIAL_PATTERN.match(key)
39
+ if match:
40
+ found.setdefault(match.group(1), set()).add(match.group(2))
41
+
42
+ return [
43
+ {
44
+ "index_name": name,
45
+ "username_env": f"UV_INDEX_{name}_USERNAME"
46
+ if "USERNAME" in found[name]
47
+ else None,
48
+ "password_env": f"UV_INDEX_{name}_PASSWORD"
49
+ if "PASSWORD" in found[name]
50
+ else None,
51
+ }
52
+ for name in sorted(found)
53
+ ]
54
+
55
+
56
+ def uv_index_secret_mounts(credentials: list[dict[str, str | None]]) -> list[str]:
57
+ """Return Dockerfile ``--mount`` strings for UV index credentials.
58
+
59
+ Args:
60
+ credentials: Output of `collect_uv_index_credentials`.
61
+
62
+ Returns:
63
+ A list of ``type=secret,...`` strings for Dockerfile ``RUN --mount=``
64
+ directives.
65
+ """
66
+ mounts = []
67
+ for cred in credentials:
68
+ name = cred["index_name"].lower()
69
+ if cred["username_env"]:
70
+ mounts.append(
71
+ f"type=secret,id=uv_index_{name}_username,env={cred['username_env']}"
72
+ )
73
+ if cred["password_env"]:
74
+ mounts.append(
75
+ f"type=secret,id=uv_index_{name}_password,env={cred['password_env']}"
76
+ )
77
+ return mounts
78
+
79
+
80
+ def uv_index_secret_build_args(credentials: list[dict[str, str | None]]) -> list[str]:
81
+ """Return ``docker build --secret`` arg pairs for UV index credentials.
82
+
83
+ Args:
84
+ credentials: Output of `collect_uv_index_credentials`.
85
+
86
+ Returns:
87
+ A flat list of ``--secret`` flag-value pairs for the docker build command.
88
+ """
89
+ args = []
90
+ for cred in credentials:
91
+ name = cred["index_name"].lower()
92
+ if cred["username_env"]:
93
+ args.extend(
94
+ ["--secret", f"id=uv_index_{name}_username,env={cred['username_env']}"]
95
+ )
96
+ if cred["password_env"]:
97
+ args.extend(
98
+ ["--secret", f"id=uv_index_{name}_password,env={cred['password_env']}"]
99
+ )
100
+ return args
24
101
 
25
102
 
26
103
  def env_truthy(env_var: str) -> bool:
@@ -44,6 +121,16 @@ def get_workdir_path() -> str:
44
121
  return os.getenv(WORKDIR_PATH_ENV_VAR, WORKDIR_PATH_DEFAULT)
45
122
 
46
123
 
124
+ def get_python_variant() -> str:
125
+ """Return the Python image variant for container builds.
126
+
127
+ Returns:
128
+ The variant from `CONTAINER_PYTHON_VARIANT`, or 'slim' by default.
129
+ An empty string disables the variant (e.g., `FROM python:3.11`).
130
+ """
131
+ return os.getenv("CONTAINER_PYTHON_VARIANT", "slim").strip()
132
+
133
+
47
134
  def inject_auto_build_args_from_env(
48
135
  build_args: dict[str, str] | None,
49
136
  ) -> dict[str, str]:
@@ -454,7 +541,7 @@ def render_container_deps_move_script(mappings: dict[str, str]) -> str:
454
541
  "import shutil",
455
542
  "import sys",
456
543
  "",
457
- f"mappings = {repr(mappings)}",
544
+ f"mappings = {mappings!r}",
458
545
  'source_root = pathlib.Path("/tmp/deps")',
459
546
  "",
460
547
  "for name, dest in mappings.items():",
@@ -523,7 +610,7 @@ def get_prune_keep() -> int:
523
610
  return -1
524
611
  try:
525
612
  return int(raw)
526
- except Exception:
613
+ except ValueError:
527
614
  LOGGER.warning(
528
615
  "Invalid CONTAINER_PRUNE_KEEP value '%s' - defaulting to -1 (no prune)",
529
616
  raw,
@@ -178,13 +178,13 @@ def infer_bump_component_from_git_cliff() -> ReleaseComponent:
178
178
  normalized_version = version_text.lstrip("v")
179
179
  try:
180
180
  bumped = Version.parse(normalized_version)
181
- except Exception:
181
+ except ValueError:
182
182
  utils.fatal(f"Unable to parse bumped version from git-cliff: {version_text!r}")
183
183
 
184
184
  current_version = project.get_project_version()
185
185
  try:
186
186
  current = Version.parse(current_version)
187
- except Exception:
187
+ except ValueError:
188
188
  utils.fatal(f"Unable to parse current project version: {current_version!r}")
189
189
 
190
190
  def _parse_semver(version_value: str | Any) -> tuple[int, int, int]:
@@ -28,6 +28,18 @@ def _is_unreleased_placeholder(notes: str) -> bool:
28
28
  return normalized in {"[unreleased]", "## [unreleased]", "## unreleased"}
29
29
 
30
30
 
31
+ def _pattern_targets_hidden_paths(pattern: str) -> bool:
32
+ return any(
33
+ part.startswith(".")
34
+ for part in pattern.split("/")
35
+ if part and part not in {".", ".."}
36
+ )
37
+
38
+
39
+ def _path_has_hidden_segment(path: Path) -> bool:
40
+ return any(part.startswith(".") for part in path.parts if part not in {".", ".."})
41
+
42
+
31
43
  def _latest_tagged_changelog() -> str | None:
32
44
  result = utils.run_git_cliff(["--latest"], capture_output=True)
33
45
  changelog = result.stdout.strip()
@@ -538,6 +550,8 @@ def get_github_release_asset_paths(
538
550
  interpreted as colon-separated paths or glob patterns.
539
551
  If not provided, the default value is `dist/*`. An empty list
540
552
  explicitly disables asset selection.
553
+ Wildcard patterns only include hidden files when the pattern itself
554
+ explicitly targets hidden paths (for example, `dist/.*`).
541
555
 
542
556
  Returns:
543
557
  A sorted list of existing asset paths.
@@ -561,9 +575,14 @@ def get_github_release_asset_paths(
561
575
  asset_paths = []
562
576
  for part in source_items:
563
577
  if any(char in part for char in "*?[]"):
578
+ matches = [path for path in Path(".").glob(part) if path.is_file()]
579
+ if not _pattern_targets_hidden_paths(part):
580
+ matches = [
581
+ path for path in matches if not _path_has_hidden_segment(path)
582
+ ]
564
583
  asset_paths.extend(
565
584
  sorted(
566
- [path for path in Path(".").glob(part) if path.is_file()],
585
+ matches,
567
586
  key=lambda p: str(p),
568
587
  )
569
588
  )