Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ reportUnusedCallResult = false

[tool.poe.tasks."env:configure"]
help = "Setup project environment."
script = "scripts.env:configure"
script = "scripts.tasks.env:configure"

[tool.poe.tasks."project:upgrade"]
help = "Upgrade project dependencies."
Expand Down Expand Up @@ -90,11 +90,11 @@ interpreter = "python"

[tool.poe.tasks.generate]
help = "Generate pyinstaller config."
script = "scripts.generate:run"
script = "scripts.tasks.generate:run"

[tool.poe.tasks.lint]
help = "Lint code project."
script = "scripts.lint:run"
script = "scripts.tasks.lint:run"

[tool.poe.tasks.fmt]
help = "Format code project."
Expand All @@ -106,7 +106,7 @@ sequence = [
[tool.poe.tasks.build]
deps = ["env:wipe"]
help = "Build standalone binary."
script = "scripts.build:run"
script = "scripts.tasks.build:run"

[tool.poe.tasks."test"]
help = "Run all tests."
Expand All @@ -118,12 +118,12 @@ cmd = "pytest --junitxml=reports/junit-report.xml -n=auto --timeout=900 --dist=l

[tool.poe.tasks."release:pre"]
help = "Create a PR with changes for release."
script = "scripts.release:pre"
script = "scripts.tasks.release:pre"

[tool.poe.tasks."release"]
help = "Create a new terranova release."
script = "scripts.release:run"
script = "scripts.tasks.release:run"

[tool.poe.tasks."release:post"]
help = "Prepare next iteration."
script = "scripts.release:post"
script = "scripts.tasks.release:post"
16 changes: 16 additions & 0 deletions scripts/binds/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
38 changes: 38 additions & 0 deletions scripts/binds/basedpyright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from scripts.utils import fatal
from terranova.process import Bind, CommandNotFound


class BasedPyright(Bind):
"""Represents a bind to basedpyright command."""

def __init__(self) -> None:
"""Init basedpyright bind."""
try:
super().__init__("basedpyright")
except CommandNotFound as err:
fatal("detect basedpyright binary", err)

def check(self) -> None:
"""
Type check the project.

Raises:
ErrorReturnCode: if type errors are found.
"""
self._cmd.inherit_out().exec()
107 changes: 107 additions & 0 deletions scripts/binds/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from io import StringIO
from pathlib import Path
from typing import override

from scripts.utils import fatal
from terranova.process import Bind, Command, CommandNotFound, EnvCmd


class Container(Bind):
"""Represents a bind to a container backend, either docker or podman."""

def __init__(self) -> None:
"""Init container bind."""
backend: str | None = None
for candidate in ("docker", "podman"):
try:
Command(candidate)
backend = candidate
break
except CommandNotFound:
continue

if backend is None:
fatal(
"detect container backend",
CommandNotFound("docker or podman"),
)

self.__backend: str = backend
super().__init__(backend)

@override
def create(self, cmd_path: str | Path) -> Command:
env = EnvCmd.inherit()
if self.__backend == "podman":
env.add({"BUILDAH_FORMAT": "docker"})
return Command(cmd_path).env(env.build())

def build_image(self, platform: str, python_version: str, tag: str) -> None:
"""Build a multi-platform image via buildx."""
(
self._cmd.args(
"buildx",
"build",
"--load",
"--platform",
platform,
"--build-arg",
f"base_image_version={python_version}",
"-t",
tag,
"-f",
"Containerfile",
".",
)
.inherit_out()
.exec()
)

def run_detached(self, platform: str, image: str) -> str:
"""Run a detached container with a no-op entrypoint and return its id."""
capture = StringIO()
(
self._cmd.args(
"run",
"-d",
"--platform",
platform,
"--entrypoint=cat",
image,
)
.stdout(capture)
.exec()
)
return capture.getvalue().strip()

def copy_from(self, container_id: str, container_path: str, dest: Path) -> None:
"""Copy a path out of a container."""
(
self._cmd.args(
"cp",
f"{container_id}:{container_path}",
dest.as_posix(),
)
.inherit_out()
.exec()
)

def remove(self, container_id: str) -> None:
"""Remove a container."""
self._cmd.args("rm", "-f", container_id).exec()
50 changes: 50 additions & 0 deletions scripts/binds/gh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from scripts.utils import fatal
from terranova.process import Bind, CommandNotFound


class Gh(Bind):
"""Represents a bind to gh command."""

def __init__(self) -> None:
"""Init gh bind."""
try:
super().__init__("gh")
except CommandNotFound as err:
fatal("detect gh binary", err)

def pr_create(self, base: str, head: str) -> None:
"""Create a pull request, filling title/body from commits."""
(
self._cmd.args("pr", "create", "--fill", f"--base={base}", f"--head={head}")
.inherit_out()
.exec()
)

def release_create(self, version: str, title: str, binaries: list[str]) -> None:
"""Create a release with generated notes and attached binaries."""
args = [
"release",
"create",
"--generate-notes",
"--latest",
f"--title={title}",
version,
*binaries,
]
self._cmd.args(*args).inherit_out().exec()
95 changes: 95 additions & 0 deletions scripts/binds/git.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
from io import StringIO

from scripts.utils import fatal
from terranova.process import Bind, CommandNotFound, ErrorReturnCode


class Git(Bind):
"""Represents a bind to git command."""

def __init__(self) -> None:
"""Init git bind."""
try:
super().__init__("git")
except CommandNotFound as err:
fatal("detect git binary", err)

def short_head(self) -> str:
"""Return the short commit hash of HEAD."""
capture = StringIO()
self._cmd.args("rev-parse", "--short", "HEAD").stdout(capture).exec()
return capture.getvalue().strip()

def head(self) -> str:
"""Return the full commit hash of HEAD."""
capture = StringIO()
self._cmd.args("rev-parse", "HEAD").stdout(capture).stderr(sys.stderr).exec()
return capture.getvalue().strip()

def current_branch(self) -> str:
"""Return the name of the current branch."""
capture = StringIO()
self._cmd.args("rev-parse", "--abbrev-ref", "HEAD").stdout(capture).exec()
return capture.getvalue().strip()

def checkout(self, name: str) -> None:
"""Switch to an existing branch."""
self._cmd.args("checkout", name).inherit_out().exec()

def checkout_new_branch(self, name: str) -> None:
"""Create and switch to a new branch."""
self._cmd.args("checkout", "-b", name).inherit_out().exec()

def add_all(self) -> None:
"""Stage all changes."""
self._cmd.args("add", "--all").inherit_out().exec()

def commit(self, message: str, no_verify: bool = False) -> None:
"""Commit staged changes."""
args = ["commit", "-m", message]
if no_verify:
args.append("--no-verify")
self._cmd.args(*args).inherit_out().exec()

def push(self, remote: str, ref: str) -> None:
"""Push a ref to a remote."""
self._cmd.args("push", remote, ref).inherit_out().exec()

def tag(self, name: str) -> None:
"""
Create a tag.

Raises:
ErrorReturnCode: if the tag already exists.
"""
self._cmd.args("tag", name).exec()

def branch_delete(self, name: str) -> None:
"""Delete a branch if it exists."""
try:
self._cmd.args("branch", "-D", name).inherit_out().exec()
except ErrorReturnCode:
pass

def status_short(self) -> str:
"""Return the short status of the working tree."""
capture = StringIO()
self._cmd.args("status", "-s").stdout(capture).stderr(sys.stderr).exec()
return capture.getvalue().strip()
33 changes: 33 additions & 0 deletions scripts/binds/pre_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# Copyright 2023-2025 Elasticsearch B.V.
# Copyright 2026-present Adrien Mannocci
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from scripts.utils import fatal
from terranova.process import Bind, CommandNotFound


class PreCommit(Bind):
"""Represents a bind to pre-commit command."""

def __init__(self) -> None:
"""Init pre-commit bind."""
try:
super().__init__("pre-commit")
except CommandNotFound as err:
fatal("detect pre-commit binary", err)

def install(self) -> None:
"""Install the pre-commit hooks."""
self._cmd.args("install").inherit_out().exec()
Loading