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
38 changes: 38 additions & 0 deletions .tasks/skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"

vars:
DIST_DIR: '{{.DIST_DIR | default "dist"}}'

tasks:
skills:build:
desc: Build deterministic Aether distributions
cmds:
- ./aether distribution build --output-directory "{{.DIST_DIR}}"

skills:publish:dry-run:
desc: Build and validate the gh skill publish payload without publishing
preconditions:
- sh: command -v gh >/dev/null 2>&1
msg: GitHub CLI (`gh`) is required for skill publish validation.
cmds:
- task: skills:build
vars:
DIST_DIR: "{{.DIST_DIR}}"
- gh skill publish "{{.DIST_DIR}}" --dry-run

skills:publish:
desc: Publish a tagged Aether skill release after the dry-run gate
requires:
vars:
- RELEASE_TAG
preconditions:
- sh: command -v gh >/dev/null 2>&1
msg: GitHub CLI (`gh`) is required for skill publication.
cmds:
- task: skills:publish:dry-run
vars:
DIST_DIR: "{{.DIST_DIR}}"
- gh skill publish "{{.DIST_DIR}}" --tag "{{.RELEASE_TAG}}"
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Machine-readable catalog and provenance:
- Python 3.12+
- Git
- GitHub CLI (`gh`) 2.96+ for skill publish/install flows
- Task 3 (optional) for convenience wrappers in `Taskfile.yml`

Install dev dependencies:

Expand Down Expand Up @@ -141,6 +142,16 @@ Validate publishability (no release write):
gh skill publish "dist" --dry-run
```

Optional Taskfile convenience wrappers build first and delegate to the same canonical command surface:

```sh
task skills:build
task skills:publish:dry-run
task skills:publish RELEASE_TAG="v1.0.0"
```

The live Taskfile publish path requires an explicit release tag and runs the dry-run task before publishing. See `docs/taskfile-workflows.md` for direct-command equivalents and ownership boundaries.

## 9) Install locally with GitHub CLI

Install from local build output:
Expand Down
11 changes: 11 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"

# Keep Taskfile as an ergonomic orchestration layer. Canonical implementation
# remains in ./aether, GitHub CLI, and the repository's versioned build scripts.
includes:
skills:
taskfile: ./.tasks/skills.yml
flatten: true
62 changes: 62 additions & 0 deletions docs/taskfile-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Aether Taskfile workflows

Taskfile is an optional developer-experience layer over Aether's canonical command surfaces. The repository does not require Taskfile for CI, release automation, or direct use of `./aether` and GitHub CLI.

## Build distributions

Convenience command:

```sh
task skills:build
```

Direct equivalent:

```sh
./aether distribution build --output-directory "dist"
```

Override the output directory when needed:

```sh
task skills:build DIST_DIR="/tmp/aether-dist"
```

## Validate a skill publication

Preferred local convenience command:

```sh
task skills:publish:dry-run
```

This performs the deterministic distribution build first and then runs:

```sh
gh skill publish "dist" --dry-run
```

No release is created by the dry-run task.

## Publish an explicit tagged release

Publication is intentionally gated on an explicit `RELEASE_TAG` value:

```sh
task skills:publish RELEASE_TAG="v1.2.3"
```

The task runs the complete `skills:publish:dry-run` path before invoking:

```sh
gh skill publish "dist" --tag "v1.2.3"
```

The GitHub Actions release workflow remains the protected production publication path. This local task is an explicit convenience wrapper, not a replacement for repository release policy or environment review gates.

## Authority boundary

- `./aether distribution build` owns deterministic distribution generation.
- `gh skill publish` owns GitHub CLI validation/publication behavior.
- `Taskfile.yml` only sequences those commands for developer ergonomics.
- Credentials remain in the developer or CI environment; Taskfiles must never contain token values.
51 changes: 51 additions & 0 deletions tests/test_taskfile_workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Contract tests for Aether's optional Taskfile workflow wrappers."""

from __future__ import annotations

from pathlib import Path
import unittest

import yaml


ROOT = Path(__file__).resolve().parents[1]


class TaskfileWorkflowTests(unittest.TestCase):
def test_root_taskfile_composes_skill_tasks(self) -> None:
root = yaml.safe_load((ROOT / "Taskfile.yml").read_text(encoding="utf-8"))
self.assertEqual(str(root["version"]), "3")
self.assertEqual(root["includes"]["skills"]["taskfile"], "./.tasks/skills.yml")
self.assertTrue(root["includes"]["skills"]["flatten"])

def test_publish_tasks_delegate_to_canonical_commands(self) -> None:
workflow = yaml.safe_load(
(ROOT / ".tasks" / "skills.yml").read_text(encoding="utf-8")
)
tasks = workflow["tasks"]
self.assertEqual(
tasks["skills:build"]["cmds"],
['./aether distribution build --output-directory "{{.DIST_DIR}}"'],
)
dry_run = tasks["skills:publish:dry-run"]["cmds"]
self.assertEqual(dry_run[0]["task"], "skills:build")
self.assertEqual(dry_run[1], 'gh skill publish "{{.DIST_DIR}}" --dry-run')
publish = tasks["skills:publish"]
self.assertEqual(publish["requires"]["vars"], ["RELEASE_TAG"])
self.assertEqual(publish["cmds"][0]["task"], "skills:publish:dry-run")
self.assertEqual(
publish["cmds"][1],
'gh skill publish "{{.DIST_DIR}}" --tag "{{.RELEASE_TAG}}"',
)

def test_taskfiles_do_not_embed_credentials(self) -> None:
content = "\n".join(
path.read_text(encoding="utf-8")
for path in (ROOT / "Taskfile.yml", ROOT / ".tasks" / "skills.yml")
).lower()
for forbidden in ("github_token:", "gh_token:", "personal_access_token:", "bearer "):
self.assertNotIn(forbidden, content)


if __name__ == "__main__":
unittest.main()
Loading