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
2 changes: 1 addition & 1 deletion .github/workflows/push_pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
fetch-depth: 0

- name: Push artifact
run: make push
run: make push LOG_LEVEL=TRACE
2 changes: 1 addition & 1 deletion .github/workflows/test_pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
fetch-depth: 0

- name: Test
run: make test
run: make test LOG_LEVEL=TRACE
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ Read that file for the full list. The most important ones for day-to-day work a

Always run `make test` before considering any ticket complete.

When running `make` commands, use the default (low) verbosity. Increase output only
when needed: :code:`LOG_LEVEL=DEBUG` shows command lines; :code:`LOG_LEVEL=TRACE`
shows task stdout/stderr (e.g. :code:`make test LOG_LEVEL=DEBUG`). Failures always
log the failing command, return code, and that command's stdout and stderr at ERROR,
so they are visible at any level.

---

## 3. Running Commands in Docker
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ BASE_DOCKER_BUILD_ENV_COMMAND = docker run --rm \
--workdir=$(DOCKER_REMOTE_PROJECT_ROOT) \
-e PYTHONPATH=/usr/dev/build_support/src \
-e TAG_SUFFIX=$(TAG_SUFFIX) \
-e LOG_LEVEL=$(LOG_LEVEL) \
$(GIT_MOUNT) \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(NON_DOCKER_ROOT):$(DOCKER_REMOTE_PROJECT_ROOT)
Expand Down
1 change: 1 addition & 0 deletions build_support/src/build_support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
project that is not part of the standard pipeline.

Modules:
| build_logging: TRACE log level and registration for the build pipeline.
| dag_engine: Contains the logic for resolving task dependencies and running
tasks in a coherent order.
| dump_ci_cd_run_info: A "main" that records project level variables for the CI/CD
Expand Down
49 changes: 49 additions & 0 deletions build_support/src/build_support/build_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Build pipeline logging: TRACE level and registration.

The build uses three verbosity levels: INFO (steps only), DEBUG (+ command
lines), TRACE (+ task stdout/stderr). TRACE is a custom level below DEBUG.
This module defines the TRACE constant and registers it with the standard
logging module so LOG_LEVEL=TRACE works.

Attributes:
| TRACE: Numeric log level (5) for most verbose build output.
"""

import logging
import os
import sys

# Below DEBUG (10); used for task stdout/stderr so they can be suppressed at DEBUG.
TRACE = 5


def register_trace_level() -> None:
"""Register the TRACE level name with the logging module.

Call once at build startup (e.g. from execute_build_steps) so that
LOG_LEVEL=TRACE is recognized and logger.log(TRACE, ...) displays
correctly.

Returns:
None
"""
logging.addLevelName(TRACE, "TRACE")


def _configure_build_logging() -> None:
"""Configure build logging from LOG_LEVEL.

Default is INFO (steps only). DEBUG adds commands. TRACE adds stdout/stderr.
Invalid or unset values fall back to INFO.

Returns:
None
"""
register_trace_level()
level_name = os.environ.get("LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, None)
if not isinstance(level, int):
level = TRACE if level_name == "TRACE" else logging.INFO
logging.basicConfig(
level=level, format="%(message)s", stream=sys.stdout, force=True
)
2 changes: 1 addition & 1 deletion build_support/src/build_support/ci_cd_tasks/build_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def run(self) -> None:
),
"uv",
"build",
"--output",
"--out-dir",
get_dist_dir(project_root=self.docker_project_root),
]
)
Expand Down
17 changes: 12 additions & 5 deletions build_support/src/build_support/dag_engine.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""Logic for building a DAG of tasks and running them in order."""
"""Logic for building a DAG of tasks and running them in order.

Attributes:
| logger: Module-level logger for task execution and report output.
"""

import logging
from datetime import UTC, datetime, timedelta
from pathlib import Path

Expand All @@ -9,6 +14,8 @@
from build_support.ci_cd_tasks.task_node import TaskNode
from build_support.ci_cd_vars.build_paths import get_build_runtime_report_path

logger = logging.getLogger(__name__)


def _add_tasks_to_list_with_dfs(
execution_order: list[TaskNode], tasks_added: set[TaskNode], task_to_add: TaskNode
Expand Down Expand Up @@ -90,17 +97,17 @@ def run_tasks(tasks: list[TaskNode], project_root: Path) -> None:
"""
run_report = BuildRunReport()
task_execution_order = get_task_execution_order(requested_tasks=tasks)
print("Will execute the following tasks:", flush=True) # noqa: T201
logger.info("Will execute the following tasks:")
for task in task_execution_order:
print(f" - {task.task_label()}", flush=True) # noqa: T201
logger.info(" - %s", task.task_label())
for task in task_execution_order:
print(f"Starting: {task.task_label()}", flush=True) # noqa: T201
logger.info("Starting: %s", task.task_label())
start = datetime.now(tz=UTC)
task.run()
duration = datetime.now(tz=UTC) - start
run_report.report.append(
TaskRunReport(task_name=task.task_label(), duration=duration)
)
report_content = run_report.to_yaml()
print(report_content) # noqa: T201
logger.info("%s", report_content)
get_build_runtime_report_path(project_root=project_root).write_text(report_content)
21 changes: 16 additions & 5 deletions build_support/src/build_support/execute_build_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

Attributes:
| CLI_ARG_TO_TASK: A dictionary of the CLI arg to the corresponding task to run.
| logger: Module-level logger for build failure and diagnostics.
"""

import logging
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass
from pathlib import Path

from build_support.build_logging import _configure_build_logging
from build_support.ci_cd_tasks.build_tasks import BuildAll, BuildDocs, BuildPypi
from build_support.ci_cd_tasks.env_setup_tasks import (
Clean,
Expand Down Expand Up @@ -36,7 +39,9 @@
from build_support.ci_cd_vars.subproject_structure import SubprojectContext
from build_support.dag_engine import run_tasks
from build_support.new_project_setup.setup_new_project import MakeProjectFromTemplate
from build_support.process_runner import ProcessVerbosity, concatenate_args, run_process
from build_support.process_runner import concatenate_args, run_process

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
Expand Down Expand Up @@ -152,8 +157,7 @@ def fix_permissions(local_user_uid: int, local_user_gid: int) -> None:
if path.name not in [".git", "test_scratch_folder"]
],
]
),
verbosity=ProcessVerbosity.SILENT,
)
)


Expand Down Expand Up @@ -197,7 +201,11 @@ def run_main(args: Namespace) -> None:

Returns:
None

Raises:
Exception: Re-raised after logging if any task or setup fails.
"""
_configure_build_logging()
local_info_yaml = get_local_info_yaml(project_root=args.docker_project_root)
basic_task_info = BasicTaskInfo.from_yaml(local_info_yaml.read_text())
requested_tasks = [
Expand All @@ -208,8 +216,11 @@ def run_main(args: Namespace) -> None:
run_tasks(
tasks=requested_tasks, project_root=basic_task_info.docker_project_root
)
except Exception as e: # noqa: BLE001
print(e) # noqa: T201
except Exception:
# logger.exception() logs at ERROR and automatically includes the exception
# message and full traceback (equivalent to exc_info=True).
logger.exception("Build failed")
raise
finally:
fix_permissions(
local_user_uid=basic_task_info.local_uid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def formatted_name_and_email(self) -> str:
"""
return f"{self.name} <{self.contact_email}>"

def as_pyproject_author(self) -> dict[str, str]:
"""Returns the organization as a PEP 621 author inline table entry.

Returns:
dict[str, str]: A dict with 'name' and 'email' for project.authors.
"""
return {"name": self.name, "email": self.contact_email}


class ProjectSettings(BaseModel):
"""An object containing the project settings for this project."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def update_pyproject_toml(
project["version"] = "0.0.0" # type: ignore[index]
project["license"] = new_project_settings.license # type: ignore[index]
project["authors"] = [ # type: ignore[index]
new_project_settings.organization.formatted_name_and_email()
new_project_settings.organization.as_pyproject_author()
]
hatch = pyproject_data["tool"]["hatch"] # type: ignore[index]
hatch["build"]["targets"]["wheel"]["packages"] = [ # type: ignore[index]
Expand Down
Loading