plainx.dev 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ .venv
2
+ /.env
3
+ *.egg-info
4
+ *.py[co]
5
+ __pycache__
6
+ *.DS_Store
7
+
8
+ /*.code-workspace
9
+
10
+ # Test apps
11
+ plain*/tests/.plain
12
+
13
+ # Agent scratch files
14
+ /scratch
15
+
16
+ # Plain temp dirs
17
+ .plain
18
+
19
+ .vscode
20
+ /.claude/settings.local.json
21
+ /.benchmarks
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: plainx.dev
3
+ Version: 0.1.0
4
+ Summary: Development tools for plainx package developers, including release automation.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-Expression: BSD-3-Clause
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+
10
+ # plainx-dev
11
+
12
+ Development tools for plainx package developers.
13
+
14
+ ## Installation
15
+
16
+ Add as a dev dependency:
17
+
18
+ ```bash
19
+ uv add --dev plainx-dev
20
+ ```
21
+
22
+ ## Skills
23
+
24
+ After installing, run `plain agent install` to copy skills to your `.claude/` directory.
25
+
26
+ ### /plainx-release
27
+
28
+ A release workflow skill that helps you:
29
+
30
+ - Analyze commits and suggest version bump type (major/minor/patch)
31
+ - Generate release notes from actual code changes
32
+ - Update CHANGELOG.md
33
+ - Guide through commit, tag, and push steps
34
+
35
+ Usage:
36
+
37
+ ```
38
+ /plainx-release
39
+ /plainx-release --major
40
+ /plainx-release --minor
41
+ /plainx-release --patch
42
+ ```
@@ -0,0 +1,33 @@
1
+ # plainx-dev
2
+
3
+ Development tools for plainx package developers.
4
+
5
+ ## Installation
6
+
7
+ Add as a dev dependency:
8
+
9
+ ```bash
10
+ uv add --dev plainx-dev
11
+ ```
12
+
13
+ ## Skills
14
+
15
+ After installing, run `plain agent install` to copy skills to your `.claude/` directory.
16
+
17
+ ### /plainx-release
18
+
19
+ A release workflow skill that helps you:
20
+
21
+ - Analyze commits and suggest version bump type (major/minor/patch)
22
+ - Generate release notes from actual code changes
23
+ - Update CHANGELOG.md
24
+ - Guide through commit, tag, and push steps
25
+
26
+ Usage:
27
+
28
+ ```
29
+ /plainx-release
30
+ /plainx-release --major
31
+ /plainx-release --minor
32
+ /plainx-release --patch
33
+ ```
@@ -0,0 +1,11 @@
1
+ # plainx-dev changelog
2
+
3
+ ## [0.1.0](https://github.com/dropseed/plain/releases/plainx-dev@0.1.0) (2026-02-05)
4
+
5
+ Initial release.
6
+
7
+ - `/plainx-release` skill for releasing plainx packages with guided workflow
8
+ - Version bump suggestions based on commit analysis
9
+ - First release detection (0.0.0 convention)
10
+ - Changelog generation from code diffs
11
+ - GitHub Actions workflow template for PyPI trusted publishing
File without changes
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: plainx-release
3
+ description: Releases plainx packages with version suggestions, changelog generation, and git tagging. Use when releasing a package to PyPI.
4
+ ---
5
+
6
+ # Release Package
7
+
8
+ Release a plainx package with version bumping, changelog generation, and git tagging.
9
+
10
+ ## Arguments
11
+
12
+ ```
13
+ /plainx-release [--major|--minor|--patch]
14
+ ```
15
+
16
+ - No args: analyze commits and prompt for version type
17
+ - `--major`: auto-select major release
18
+ - `--minor`: auto-select minor release
19
+ - `--patch`: auto-select patch release
20
+
21
+ ## Scripts
22
+
23
+ | Script | Purpose |
24
+ | ------------------ | --------------------------------------------------- |
25
+ | `get-package-info` | Get package metadata and commits since last release |
26
+
27
+ ## Workflow
28
+
29
+ ### Phase 1: Check Preconditions
30
+
31
+ 1. Check git status is clean:
32
+
33
+ ```
34
+ git status --porcelain
35
+ ```
36
+
37
+ If not clean, stop and ask user to commit or stash changes.
38
+
39
+ ### Phase 2: Get Package Info
40
+
41
+ ```
42
+ uv run ./.claude/skills/plainx-release/get-package-info
43
+ ```
44
+
45
+ This outputs JSON with:
46
+
47
+ - `name`: Package name from pyproject.toml
48
+ - `current_version`: Current version
49
+ - `changelog_path`: Path to CHANGELOG.md
50
+ - `last_tag`: Most recent git tag for this package
51
+ - `repo_url`: GitHub URL (extracted from pyproject.toml)
52
+ - `commits`: List of commits since last tag (excluding tests)
53
+
54
+ If no commits since last release, inform the user and stop.
55
+
56
+ ### Phase 2b: First Release Detection
57
+
58
+ If `current_version` is `0.0.0`, this is the **first release**:
59
+
60
+ 1. Inform the user: "This package has never been released (version 0.0.0)."
61
+ 2. Ask what version to release:
62
+ - **0.1.0** - First development release (recommended for most packages)
63
+ - **1.0.0** - First stable release (if the package is already production-ready)
64
+ 3. Skip to Phase 4 with the chosen version (use `uv version <version>` to set it directly)
65
+
66
+ ### Phase 3: Collect Release Decision
67
+
68
+ 1. Display the commits since last release
69
+ 2. **Analyze commits and suggest release type**:
70
+ - **Major**: breaking changes, major API redesigns, significant removals
71
+ - **Minor**: new features, significant additions, new APIs
72
+ - **Patch**: small bugfixes, minor tweaks, documentation updates, refactors
73
+ 3. Ask user to confirm or adjust (major/minor/patch/skip)
74
+ - If `--major`, `--minor`, or `--patch` was passed, auto-select that type
75
+ - If user chooses to skip, stop
76
+
77
+ ### Phase 4: Set Version
78
+
79
+ For first releases (from 0.0.0):
80
+
81
+ ```
82
+ uv version <version>
83
+ ```
84
+
85
+ Where `<version>` is the chosen version like `0.1.0` or `1.0.0`.
86
+
87
+ For subsequent releases:
88
+
89
+ ```
90
+ uv version --bump <type>
91
+ ```
92
+
93
+ Where `<type>` is `major`, `minor`, or `patch`.
94
+
95
+ ### Phase 5: Generate Release Notes
96
+
97
+ 1. Get the new version:
98
+
99
+ ```
100
+ uv version --short
101
+ ```
102
+
103
+ 2. Get the file changes since the last release:
104
+
105
+ ```
106
+ git diff <last_tag>..HEAD -- . ":(exclude)tests"
107
+ ```
108
+
109
+ If no `last_tag`, use the initial commit or recent history.
110
+
111
+ 3. Read the existing `<changelog_path>` file.
112
+
113
+ 4. Prepend a new release entry to the changelog.
114
+
115
+ **For first releases:**
116
+
117
+ ```
118
+ ## [<new_version>](<repo_url>/releases/v<new_version>) (<today's date>)
119
+
120
+ Initial release.
121
+
122
+ - Brief summary of what the package provides
123
+ - Key features or capabilities
124
+ ```
125
+
126
+ **For subsequent releases:**
127
+
128
+ ```
129
+ ## [<new_version>](<repo_url>/releases/v<new_version>) (<today's date>)
130
+
131
+ ### What's changed
132
+
133
+ - Summarize user-facing changes based on the actual diff (not just commit messages)
134
+ - Include commit hash links: ([abc1234](<repo_url>/commit/abc1234))
135
+ - Skip test changes, internal refactors that don't affect public API
136
+
137
+ ### Upgrade instructions
138
+
139
+ - Specific steps if any API changed
140
+ - If no changes required: "- No changes required."
141
+ ```
142
+
143
+ ### Phase 6: Commit, Tag, and Push
144
+
145
+ Guide the user through these steps explicitly:
146
+
147
+ 1. **Stage files**:
148
+
149
+ ```
150
+ git add pyproject.toml <changelog_path> uv.lock
151
+ ```
152
+
153
+ 2. **Show what will be committed**:
154
+
155
+ ```
156
+ git diff --cached --stat
157
+ ```
158
+
159
+ 3. **Create commit**:
160
+
161
+ ```
162
+ git commit -m "Release v<new_version>"
163
+ ```
164
+
165
+ 4. **Create tag**:
166
+
167
+ ```
168
+ git tag -a v<new_version> -m "Release v<new_version>"
169
+ ```
170
+
171
+ 5. **GitHub Workflow Setup (first release only)**:
172
+
173
+ Check if `.github/workflows/release.yml` exists. If not, ask:
174
+
175
+ > "No release workflow found. Would you like to set up GitHub Actions to publish to PyPI when you push a tag?"
176
+
177
+ If yes, copy `.claude/skills/plainx-release/release-workflow.yml` to `.github/workflows/release.yml`, then amend the commit:
178
+
179
+ ```
180
+ git add .github/workflows/release.yml
181
+ git commit --amend --no-edit
182
+ git tag -fa v<new_version> -m "Release v<new_version>"
183
+ ```
184
+
185
+ Remind the user to configure PyPI trusted publishing after pushing:
186
+ - Go to https://pypi.org/manage/account/publishing/
187
+ - Add trusted publisher: GitHub owner, repo name, workflow "release.yml" (no environment needed)
188
+
189
+ 6. **Push commit and tag**:
190
+ ```
191
+ git push && git push --tags
192
+ ```
193
+
194
+ Ask user to confirm before each destructive step (commit, push).
195
+
196
+ ### Phase 7: GitHub Release (Optional)
197
+
198
+ After pushing, wait for the GitHub workflow to publish to PyPI. Once published, ask the user if they want to create a GitHub release:
199
+
200
+ ```
201
+ gh release create v<new_version> --notes "<changelog entry summary>"
202
+ ```
203
+
204
+ Or to use the full changelog entry from the file, extract and pass it manually.
205
+
206
+ ## Release Type Guidelines
207
+
208
+ Consider the current version when suggesting release types:
209
+
210
+ ### Pre-1.0 packages (0.x.y)
211
+
212
+ Most packages stay pre-1.0 for a long time. For these packages:
213
+
214
+ - **Minor (0.x.0)**: New features, breaking changes, new APIs, significant additions
215
+ - **Patch (0.0.x)**: Bugfixes, minor tweaks, documentation, refactors
216
+ - **Major (1.0.0)**: Only suggest if the user explicitly wants to mark the package as stable/production-ready
217
+
218
+ ### Post-1.0 packages (x.y.z where x >= 1)
219
+
220
+ Once a package has reached 1.0, follow semver strictly:
221
+
222
+ - **Major (x.0.0)**: Breaking changes, API removals, incompatible changes
223
+ - **Minor (x.y.0)**: New features, new APIs, backwards-compatible additions
224
+ - **Patch (x.y.z)**: Bugfixes, minor tweaks, documentation, refactors
225
+
226
+ ### Commit message indicators
227
+
228
+ - Breaking/major indicators: "breaking", "remove", "rename API", "redesign", "incompatible"
229
+ - Feature/minor indicators: "add", "new", "feature", "implement"
230
+ - Fix/patch indicators: "fix", "bugfix", "typo", "docs", "refactor", "update"
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Get package metadata and commits since last release.
4
+
5
+ Outputs JSON with package info for the current directory's pyproject.toml.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ try:
16
+ import tomllib
17
+ except ImportError:
18
+ import tomli as tomllib
19
+
20
+
21
+ def run(cmd: list[str], cwd: Path | None = None) -> str:
22
+ """Run a command and return stdout."""
23
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
24
+ return result.stdout.strip()
25
+
26
+
27
+ def get_package_name(pyproject: dict) -> str:
28
+ """Get the package name from pyproject.toml."""
29
+ return pyproject.get("project", {}).get("name", "")
30
+
31
+
32
+ def get_current_version() -> str:
33
+ """Get the current version using uv."""
34
+ return run(["uv", "version", "--short"])
35
+
36
+
37
+ def get_repo_url(pyproject: dict) -> str | None:
38
+ """Extract GitHub repo URL from pyproject.toml."""
39
+ project = pyproject.get("project", {})
40
+
41
+ # Check project.urls
42
+ urls = project.get("urls", {})
43
+ for key in ["Repository", "repository", "Source", "source", "GitHub", "github"]:
44
+ if key in urls:
45
+ url = urls[key]
46
+ # Normalize to https://github.com/owner/repo format
47
+ if "github.com" in url:
48
+ return url.rstrip("/").rstrip(".git")
49
+
50
+ # Check project.homepage
51
+ homepage = project.get("homepage", "")
52
+ if "github.com" in homepage:
53
+ return homepage.rstrip("/").rstrip(".git")
54
+
55
+ return None
56
+
57
+
58
+ def get_changelog_path() -> str:
59
+ """Find the CHANGELOG.md file."""
60
+ root = Path.cwd()
61
+
62
+ # Common locations
63
+ candidates = [
64
+ root / "CHANGELOG.md",
65
+ root / "changelog.md",
66
+ ]
67
+
68
+ # Check for package-specific changelog (src layout or namespace package)
69
+ for pyproject_path in [root / "pyproject.toml"]:
70
+ if pyproject_path.exists():
71
+ with open(pyproject_path, "rb") as f:
72
+ pyproject = tomllib.load(f)
73
+ name = get_package_name(pyproject)
74
+ if name:
75
+ # Handle namespace packages like plainx.sentry -> plainx/sentry
76
+ parts = name.replace(".", "/").replace("-", "/")
77
+ candidates.extend([
78
+ root / parts / "CHANGELOG.md",
79
+ root / "src" / parts / "CHANGELOG.md",
80
+ ])
81
+
82
+ for path in candidates:
83
+ if path.exists():
84
+ return str(path.relative_to(root))
85
+
86
+ # Default to root CHANGELOG.md even if it doesn't exist yet
87
+ return "CHANGELOG.md"
88
+
89
+
90
+ def get_last_tag(name: str) -> str | None:
91
+ """Get the most recent release tag for this package."""
92
+ # Try v-prefixed tags first (most common for single-package repos)
93
+ output = run(["git", "tag", "-l", "v*", "--sort=-v:refname"])
94
+ tags = [t for t in output.split("\n") if t]
95
+ if tags:
96
+ return tags[0]
97
+
98
+ # Fall back to name@version format (monorepo style)
99
+ output = run(["git", "tag", "-l", f"{name}@*", "--sort=-v:refname"])
100
+ tags = [t for t in output.split("\n") if t]
101
+ if tags:
102
+ return tags[0]
103
+
104
+ return None
105
+
106
+
107
+ def get_commits_since(since_ref: str | None) -> list[dict]:
108
+ """Get commits since a given ref, excluding tests."""
109
+ if since_ref:
110
+ cmd = [
111
+ "git", "log", "--format=%H|%s",
112
+ f"{since_ref}..HEAD",
113
+ "--", ".",
114
+ ":(exclude)tests",
115
+ ":(exclude)**/tests",
116
+ ]
117
+ else:
118
+ # No previous tag, get recent commits
119
+ cmd = [
120
+ "git", "log", "--format=%H|%s",
121
+ "-n", "50",
122
+ "--", ".",
123
+ ":(exclude)tests",
124
+ ":(exclude)**/tests",
125
+ ]
126
+
127
+ output = run(cmd)
128
+
129
+ commits = []
130
+ for line in output.split("\n"):
131
+ if "|" in line:
132
+ hash_, subject = line.split("|", 1)
133
+ commits.append({
134
+ "hash": hash_[:12],
135
+ "subject": subject
136
+ })
137
+
138
+ return commits
139
+
140
+
141
+ def main():
142
+ root = Path.cwd()
143
+ pyproject_path = root / "pyproject.toml"
144
+
145
+ if not pyproject_path.exists():
146
+ print(json.dumps({"error": "No pyproject.toml found in current directory"}))
147
+ sys.exit(1)
148
+
149
+ with open(pyproject_path, "rb") as f:
150
+ pyproject = tomllib.load(f)
151
+
152
+ name = get_package_name(pyproject)
153
+ if not name:
154
+ print(json.dumps({"error": "No project.name found in pyproject.toml"}))
155
+ sys.exit(1)
156
+
157
+ current_version = get_current_version()
158
+ changelog_path = get_changelog_path()
159
+ last_tag = get_last_tag(name)
160
+ repo_url = get_repo_url(pyproject)
161
+ commits = get_commits_since(last_tag)
162
+
163
+ result = {
164
+ "name": name,
165
+ "current_version": current_version,
166
+ "changelog_path": changelog_path,
167
+ "last_tag": last_tag,
168
+ "repo_url": repo_url,
169
+ "commits": commits,
170
+ }
171
+
172
+ print(json.dumps(result, indent=2))
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
@@ -0,0 +1,32 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv python install
17
+
18
+ # https://docs.pypi.org/trusted-publishers/using-a-publisher/
19
+ - name: Mint API token
20
+ id: mint-token
21
+ run: |
22
+ resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
23
+ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
24
+ oidc_token=$(jq -r '.value' <<< "${resp}")
25
+ resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}")
26
+ api_token=$(jq -r '.token' <<< "${resp}")
27
+ echo "::add-mask::${api_token}"
28
+ echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}"
29
+
30
+ - run: uv build && uv publish
31
+ env:
32
+ UV_PUBLISH_TOKEN: ${{ steps.mint-token.outputs.api-token }}
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "plainx.dev"
3
+ version = "0.1.0"
4
+ description = "Development tools for plainx package developers, including release automation."
5
+ authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
+ license = "BSD-3-Clause"
7
+ readme = "README.md"
8
+ requires-python = ">=3.13"
9
+ dependencies = []
10
+
11
+ [tool.hatch.build.targets.wheel]
12
+ packages = ["plainx"]
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"