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
27 changes: 20 additions & 7 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@ on: push

jobs:
build:
name: Build
name: Build (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
steps:
- uses: actions/checkout@v4
- uses: eifinger/setup-rye@v2
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: rye sync
- run: rye fmt -- --check
- run: rye check
- run: rye run basedpyright
- run: rye run pytest -vvv
- run: uv sync --locked
- run: uv run ruff format --check
- run: uv run ruff check
- run: uv run basedpyright
- run: uv run pytest -vvv
- run: uv build
- run: uv run --no-project --isolated --with dist/*.whl saltstack-age --help
- run: uv run --no-project --isolated --with dist/*.tar.gz saltstack-age --help
47 changes: 19 additions & 28 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: Release

on:
push:
branches:
- main
tags:
- "*.*.*"

permissions:
contents: write
contents: read

jobs:

Expand All @@ -15,30 +15,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: eifinger/setup-rye@v2
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: rye build --clean
- run: rye publish --skip-existing --token ${{ secrets.SALTSTACK_AGE_PYPI_TOKEN }} --yes

tag:
name: Create Git tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
git config --global user.name 'Github workflow'
git config --global user.email 'pmuller@users.noreply.github.com'

VERSION=$(grep -oP '^version = "\K(\d+\.\d+\.\d+)' pyproject.toml)

if git rev-parse --verify $VERSION >/dev/null 2>&1
then
echo "Tag $VERSION already exists"
exit
fi

git tag $VERSION -a -m "Automatically created"
git push origin $VERSION
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- id: version
run: echo "version=$(grep -oP '^version = \"\K(\d+\.\d+\.\d+)' pyproject.toml)" >> "$GITHUB_OUTPUT"
- run: test "$GITHUB_REF_NAME" = "${{ steps.version.outputs.version }}"
- run: uv sync --locked
- run: uv run ruff format --check
- run: uv run ruff check
- run: uv run basedpyright
- run: uv run pytest -vvv
- run: uv build
- run: uv run --no-project --isolated --with dist/*.whl saltstack-age --help
- run: uv run --no-project --isolated --with dist/*.tar.gz saltstack-age --help
- run: uv publish --check-url https://pypi.org/simple/ --token ${{ secrets.SALTSTACK_AGE_PYPI_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/example/run
/run
/dist
133 changes: 133 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Agent Instructions

## Scope

These instructions apply to the entire repository.

Work from the repository root. Do not read or edit parent worktree directories
unless the user explicitly asks for that.

## Project Shape

This is a `uv`-managed Python package for a SaltStack renderer and CLI that
decrypts age-encrypted secrets.

Important paths:

* `src/saltstack_age/` contains package code.
* `src/saltstack_age/renderers/age.py` is the Salt renderer entry point.
* `tests/unit/` contains fast renderer/unit coverage.
* `tests/integration/` contains Salt and CLI integration coverage.
* `typings/` contains local stubs for external packages with incomplete types.
* `example/` contains user-facing Salt configuration examples.

## Preserve Conventions

* Keep the project managed by `uv`; do not introduce `pip`, `poetry`, `tox`,
`nox`, or ad-hoc virtualenv workflows.
* Do not hand-edit `uv.lock`. Use `uv add`, `uv lock`, or another `uv` command
that updates both `pyproject.toml` and `uv.lock` consistently.
* Keep direct dev dependency floors in `pyproject.toml` aligned with deliberate
tool upgrades.
* Preserve the `src/` package layout and Hatchling build configuration.
* Keep Salt-specific globals such as `__salt__` compatible with the current
Ruff configuration.
* Keep external typing workarounds in `typings/` instead of weakening project
type checking globally.
* Do not remove the explicit Salt runtime dependency pins unless Salt packaging
has been verified to declare those dependencies itself.
* Do not commit real secrets, private age identities, passphrases, or realistic
credentials. Use throwaway test values only.

## Python Style

Follow the existing Ruff and BasedPyright settings in `pyproject.toml`.

* Prefer small, typed functions with explicit return annotations.
* Use `pathlib.Path` for filesystem paths.
* Use specific exceptions and preserve useful error messages for Salt users.
* Keep `__init__.py` files minimal.
* Keep comments rare and useful; avoid comments that restate the code.
* Add or update tests with behavior changes.

## Tests And Validation

Before finishing a code or dependency change, run:

```sh
uv run lefthook run pre-commit --all-files
```

The individual checks are:

```sh
uv run ruff format --check
uv run ruff check
uv run basedpyright
uv run pytest
```

Fix all errors and warnings from these tools. Do not leave the repository in a
state where the configured checks fail.

## Lefthook

Lefthook is the configured Git hook runner. Keep checks in `pre-commit` unless
the user asks for a different hook split.

If the command names are changed, verify the resolved order with:

```sh
uv run lefthook dump
```

## Documentation

* Keep README examples accurate for the CLI and renderer behavior.
* Use Markdown links for URLs; do not add bare URLs.
* Update `example/` when a behavior change affects documented Salt
configuration.

## Release Management

Releases are tag-driven. A push to `main` must not publish to PyPI by itself.

To prepare a release:

1. Update `CHANGELOG.md` so the release notes cover all changes since the
previous version tag.
2. Bump `project.version` in `pyproject.toml`.
3. Run `uv run lefthook run pre-commit --all-files`.
4. Run `uv build`.
5. Smoke-test both built artifacts:

```sh
uv run --no-project --isolated --with dist/*.whl saltstack-age --help
uv run --no-project --isolated --with dist/*.tar.gz saltstack-age --help
```

6. Commit the release prep with a focused conventional commit message such as
`chore(release): prepare 0.4.1`.
7. After the release prep commit is on `main`, run the post-merge release
script with the exact release version:

```sh
scripts/release.sh 0.4.1
```

The script fast-forwards local `main` from `origin/main`, verifies that the
requested version matches `pyproject.toml`, verifies that `CHANGELOG.md`
contains the release section, creates the annotated tag, and pushes it to
origin. The GitHub release workflow then verifies that the pushed tag matches
`pyproject.toml`, runs checks, builds wheel and source distribution artifacts,
smoke-tests both artifacts, then publishes to PyPI.

Do not create a release tag for a dirty worktree, an unmerged feature branch,
or a commit whose `pyproject.toml` version does not match the tag.

## Git Hygiene

* Inspect `git status --short` before editing and before committing.
* Do not revert unrelated user changes.
* Keep commits focused and use conventional commit messages like
`fix: ...`, `feat: ...`, and `chore: ...`.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# saltstack-age change log

## Unreleased

## 0.5.0

* feat: publish both wheel and source distribution artifacts
* feat(cli): support encrypting to an age recipient public key with `--recipient`
* feat(renderer): support loading age identities from an external command
* fix(cli): stop CLI logs from reaching Salt's deferred handler
* ci: harden build and release automation
* chore: migrate project management from Rye to uv
* chore: add Lefthook pre-commit checks
* chore: upgrade development tooling for pytest, Ruff, and BasedPyright
* docs: add repository agent and release-management instructions

## 0.4.0

* feat: allow configuration of an identity string using the `AGE_IDENTITY`
Expand Down
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,29 @@ sudo salt-pip install saltstack-age
age can be used to encrypt data using either a passphrase or an identity file.
This extension supports both, and they can be defined either in the Saltstack
daemon configuration file, or in the daemon environment.
For encryption, you can also provide a recipient public key (`age1...`) via
the CLI `-r`/`--recipient` flag — useful when you only have the public key
of whoever will decrypt the secret.

| Type | Configuration directive | Environment variable | Expected value |
| ------------ | ----------------------- | -------------------- | ---------------------------- |
| identity | `age_identity_file` | `AGE_IDENTITY_FILE` | Path of an age identity file |
| identity | `age_identity` | `AGE_IDENTITY` | An age identity string |
| identity | `age_identity_command` | | Command returning an identity |
| passphrase | `age_passphrase` | `AGE_PASSPHRASE` | An age passphrase |

`age_identity_command` must be a list of command arguments. It is executed
without a shell, and its standard output must contain the age identity.
For example, to keep the identity in
[pass](https://www.passwordstore.org/):

```yaml
age_identity_command:
- pass
- show
- infra/salt/age-identity
```

You can check this [example configuration](./example/config/minion).

## Secret encryption
Expand All @@ -61,6 +77,13 @@ $ saltstack-age -P secret-passphrase enc secret-value
ENC[age-passphrase,YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCB1QndwT3dJejhaSEtZZlIxeFEvZk5RIDIwCmhrcm9OY0tTOWdwNkhWbDdadlNIOHRFYmFLdkpZSjhLTktTWXhZVHFHKzgKLS0tIHFJWVRNc0JzTkpKNHJ1TFBuZ2tybWt0WWVQR0wrbjVnMmlZYzRaWVlBbFkKPWQu4lawaAu1owDXPDwwmj9/tN9/5NF/Avd4jPrLoy/ugUb0ciqm8H5My44=]
```

You can also encrypt to a recipient public key (no access to the private
key required):

```sh
saltstack-age -r age1spnr3m67eudy9p5cvtj3jz8kfzz5t7dv3wy6t694suv3s84fg4jqj0ydnk enc secret-value
```

> [!CAUTION]
> While it is convenient to pass all arguments to the command-line,
> be careful to not leak credentials while doing it.
Expand Down Expand Up @@ -145,18 +168,20 @@ that can help you effectively utilize age encryption in your SaltStack projects.

## Development

* Environment is managed with [rye](https://rye-up.com/)
* Create a virtualenv: `rye sync`
* Check typing: `rye run basedpyright`
* Check formatting with ruff: `rye fmt -- --check`
* Check linting with ruff: `rye check`
* Run tests: `rye run pytest`
* Environment is managed with [uv](https://docs.astral.sh/uv/)
* Create a virtualenv: `uv sync`
* Install Git hooks: `uv run lefthook install`
* Run all configured Git hooks manually: `uv run lefthook run pre-commit --all-files`
* Check typing: `uv run basedpyright`
* Check formatting with ruff: `uv run ruff format --check`
* Check linting with ruff: `uv run ruff check`
* Run tests: `uv run pytest`

See [workflow](./.github/workflows/build.yaml) for reference.

## Release

* Build package: `rye build --clean --wheel`
* Publish package: `rye publish`
* Build package: `uv build`
* Publish package: `uv publish`

See [workflow](./.github/workflows/release.yaml) for reference.
15 changes: 15 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pre-commit:
piped: true
commands:
01-ruff-check:
glob: "*.py"
run: uv run ruff check --fix {staged_files}
stage_fixed: true
02-ruff-format:
glob: "*.py"
run: uv run ruff format {staged_files}
stage_fixed: true
03-basedpyright:
run: uv run basedpyright
04-pytest:
run: uv run pytest
Loading
Loading