Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@

# changelog

## Unreleased

### Added

* Allow configuring `decree new` templates via `ADR_TEMPLATE`, still overridable by
the `--template` flag.

## 0.1.0

* Initial CLI and API with `init`, `new`, `link`, `list`, `generate toc`,
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ decree generate graph # exits non-zero, not implemented

* `ADR_DATE`: if set, used verbatim as the ADR date
* `DECREE_TZ`: IANA timezone for date formatting (default: `UTC`)
* `ADR_TEMPLATE`: path to a custom template file
* `ADR_TEMPLATE`: path to a custom template file used by `decree new` when
`--template` is not provided

`decree new --template` overrides `ADR_TEMPLATE`, which overrides the built-in template.

```
ADR_TEMPLATE=/path/to/template.md decree new "Use beartype"
decree new --template /other/path.md "Use beartype"
```

## license

Expand Down
31 changes: 29 additions & 2 deletions src/decree/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@
app = typer.Typer(add_completion=False, help="Decree: typed Python reimplementation of adr-tools")


def _resolve_template_option(template: Path | None) -> Path | None:
if template is None:
return None
candidate = template.expanduser().resolve()
if not candidate.exists():
raise typer.BadParameter(f"Template file not found: {candidate}")
if not candidate.is_file():
raise typer.BadParameter(f"Template path is not a file: {candidate}")
try:
with candidate.open("r", encoding="utf-8"):
pass
except OSError as exc:
detail = exc.strerror or str(exc)
raise typer.BadParameter(f"Could not read template file {candidate}: {detail}") from exc
return candidate


@app.command()
def init(
dir: Annotated[
Expand All @@ -28,11 +45,21 @@ def new(
status: Annotated[
AdrStatus, typer.Option("--status", case_sensitive=False)
] = AdrStatus.Accepted,
template: Annotated[Path | None, typer.Option("--template", help="Path to template")] = None,
template: Annotated[
Path | None,
typer.Option(
"--template",
help="Path to template (can also be set via ADR_TEMPLATE)",
envvar="ADR_TEMPLATE",
),
] = None,
dir: Annotated[Path | None, typer.Option("--dir", help="ADR directory")] = None,
) -> None:
"""Create a new ADR."""
rec = AdrLog(dir or Path("doc/adr")).new(" ".join(title), status=status, template=template)
template_path = _resolve_template_option(template)
rec = AdrLog(dir or Path("doc/adr")).new(
" ".join(title), status=status, template=template_path
)
typer.echo(str(rec.path))


Expand Down
99 changes: 99 additions & 0 deletions tests/test_new_template_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

from pathlib import Path

import pytest
from typer.testing import CliRunner

from decree.cli import app

runner = CliRunner()


def _ensure_adr_dir(base: Path) -> Path:
target = base / "doc" / "adr"
target.mkdir(parents=True, exist_ok=True)
return target


def _read_created_path(result_stdout: str) -> Path:
created = Path(result_stdout.strip())
assert created.exists()
return created


def test_new_uses_template_from_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
target_dir = _ensure_adr_dir(tmp_path)
template_path = tmp_path / "template-env.md"
sentinel = "__ENV_SENTINEL__"
template_path.write_text(f"{sentinel}\n", encoding="utf-8")
monkeypatch.setenv("ADR_TEMPLATE", str(template_path))

result = runner.invoke(app, ["new", "--dir", str(target_dir), "Env", "First"])

assert result.exit_code == 0, result.stderr
created_path = _read_created_path(result.stdout)
content = created_path.read_text(encoding="utf-8")
assert sentinel in content


def test_cli_template_overrides_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
target_dir = _ensure_adr_dir(tmp_path)
env_template = tmp_path / "template-env.md"
env_template.write_text("__ENV_ONLY__\n", encoding="utf-8")
cli_template = tmp_path / "template-cli.md"
cli_sentinel = "__CLI_SENTINEL__"
cli_template.write_text(f"{cli_sentinel}\n", encoding="utf-8")
monkeypatch.setenv("ADR_TEMPLATE", str(env_template))

result = runner.invoke(
app,
[
"new",
"--dir",
str(target_dir),
"--template",
str(cli_template),
"Cli",
"Wins",
],
)

assert result.exit_code == 0, result.stderr
created_path = _read_created_path(result.stdout)
content = created_path.read_text(encoding="utf-8")
assert cli_sentinel in content
assert "__ENV_ONLY__" not in content


def test_new_uses_default_template_when_not_overridden(tmp_path: Path) -> None:
target_dir = _ensure_adr_dir(tmp_path)

result = runner.invoke(
app,
["new", "--dir", str(target_dir), "Default", "Template"],
)

assert result.exit_code == 0, result.stderr
created_path = _read_created_path(result.stdout)
content = created_path.read_text(encoding="utf-8")
assert "__ENV_SENTINEL__" not in content
assert "__CLI_SENTINEL__" not in content


def test_new_errors_on_missing_template_from_env(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
target_dir = _ensure_adr_dir(tmp_path)
missing = tmp_path / "missing-template.md"
monkeypatch.setenv("ADR_TEMPLATE", str(missing))

result = runner.invoke(
app,
["new", "--dir", str(target_dir), "Missing", "Template"],
)

assert result.exit_code != 0
output = result.stderr
assert str(missing.resolve()) in output
assert "Template" in output
Loading