phabfive 0.6.0__tar.gz → 0.7.1__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.
Files changed (102) hide show
  1. phabfive-0.7.1/.github/workflows/release.yml +191 -0
  2. phabfive-0.7.1/AGENTS.md +89 -0
  3. {phabfive-0.6.0 → phabfive-0.7.1}/CHANGELOG.md +37 -0
  4. phabfive-0.7.1/CLAUDE.md +1 -0
  5. {phabfive-0.6.0 → phabfive-0.7.1}/PKG-INFO +3 -3
  6. {phabfive-0.6.0 → phabfive-0.7.1}/README.md +1 -1
  7. {phabfive-0.6.0 → phabfive-0.7.1}/docs/releasing.md +19 -3
  8. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/__init__.py +0 -5
  9. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/cli.py +82 -30
  10. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/resolvers.py +78 -12
  11. phabfive-0.7.1/phabfive/setup.py +359 -0
  12. {phabfive-0.6.0 → phabfive-0.7.1}/pyproject.toml +2 -2
  13. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_maniphest.py +42 -4
  14. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_phabfive.py +4 -4
  15. phabfive-0.7.1/tests/test_setup.py +211 -0
  16. {phabfive-0.6.0 → phabfive-0.7.1}/uv.lock +1 -1
  17. phabfive-0.6.0/.github/workflows/release.yml +0 -78
  18. {phabfive-0.6.0 → phabfive-0.7.1}/.editorconfig +0 -0
  19. {phabfive-0.6.0 → phabfive-0.7.1}/.flake8 +0 -0
  20. {phabfive-0.6.0 → phabfive-0.7.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {phabfive-0.6.0 → phabfive-0.7.1}/.github/ISSUE_TEMPLATE/documentation_change.md +0 -0
  22. {phabfive-0.6.0 → phabfive-0.7.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {phabfive-0.6.0 → phabfive-0.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {phabfive-0.6.0 → phabfive-0.7.1}/.github/dependabot.yml +0 -0
  25. {phabfive-0.6.0 → phabfive-0.7.1}/.github/workflows/gitleaks.yml +0 -0
  26. {phabfive-0.6.0 → phabfive-0.7.1}/.github/workflows/tox.yml +0 -0
  27. {phabfive-0.6.0 → phabfive-0.7.1}/.gitignore +0 -0
  28. {phabfive-0.6.0 → phabfive-0.7.1}/.gitleaksignore +0 -0
  29. {phabfive-0.6.0 → phabfive-0.7.1}/.mailmap +0 -0
  30. {phabfive-0.6.0 → phabfive-0.7.1}/.pre-commit-config.yaml +0 -0
  31. {phabfive-0.6.0 → phabfive-0.7.1}/.readthedocs.yaml +0 -0
  32. {phabfive-0.6.0 → phabfive-0.7.1}/.travis.yml +0 -0
  33. {phabfive-0.6.0 → phabfive-0.7.1}/.vscode/settings.json +0 -0
  34. {phabfive-0.6.0 → phabfive-0.7.1}/.yamllint.yml +0 -0
  35. {phabfive-0.6.0 → phabfive-0.7.1}/Dockerfile +0 -0
  36. {phabfive-0.6.0 → phabfive-0.7.1}/Dockerfile.phorge +0 -0
  37. {phabfive-0.6.0 → phabfive-0.7.1}/LICENSE +0 -0
  38. {phabfive-0.6.0 → phabfive-0.7.1}/MANIFEST.in +0 -0
  39. {phabfive-0.6.0 → phabfive-0.7.1}/Makefile +0 -0
  40. {phabfive-0.6.0 → phabfive-0.7.1}/compose.yml +0 -0
  41. {phabfive-0.6.0 → phabfive-0.7.1}/docs/create-templates.md +0 -0
  42. {phabfive-0.6.0 → phabfive-0.7.1}/docs/development.md +0 -0
  43. {phabfive-0.6.0 → phabfive-0.7.1}/docs/index.md +0 -0
  44. {phabfive-0.6.0 → phabfive-0.7.1}/docs/maniphest-cli.md +0 -0
  45. {phabfive-0.6.0 → phabfive-0.7.1}/docs/maniphest-show.png +0 -0
  46. {phabfive-0.6.0 → phabfive-0.7.1}/docs/phorge-setup.md +0 -0
  47. {phabfive-0.6.0 → phabfive-0.7.1}/docs/search-templates.md +0 -0
  48. {phabfive-0.6.0 → phabfive-0.7.1}/mkdocs.yml +0 -0
  49. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/constants.py +0 -0
  50. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/core.py +0 -0
  51. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/__init__.py +0 -0
  52. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/core.py +0 -0
  53. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/fetchers.py +0 -0
  54. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/formatters.py +0 -0
  55. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/resolvers.py +0 -0
  56. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/diffusion/validators.py +0 -0
  57. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/exceptions.py +0 -0
  58. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/__init__.py +0 -0
  59. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/core.py +0 -0
  60. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/fetchers.py +0 -0
  61. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/filters.py +0 -0
  62. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/formatters.py +0 -0
  63. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/utils.py +0 -0
  64. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/maniphest/validators.py +0 -0
  65. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/passphrase.py +0 -0
  66. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/paste.py +0 -0
  67. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/project_filters.py +0 -0
  68. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/repl.py +0 -0
  69. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/transitions/__init__.py +0 -0
  70. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/transitions/base.py +0 -0
  71. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/transitions/column.py +0 -0
  72. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/transitions/priority.py +0 -0
  73. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/transitions/status.py +0 -0
  74. {phabfive-0.6.0 → phabfive-0.7.1}/phabfive/user.py +0 -0
  75. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/entrypoint.sh +0 -0
  76. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/init-phorge.sh +0 -0
  77. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/lib/common.sh +0 -0
  78. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/lib/setup-auth.sh +0 -0
  79. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/lib/setup-projects.sh +0 -0
  80. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/lib/setup-tokens.sh +0 -0
  81. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/lib/setup-users.sh +0 -0
  82. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/phabfive +0 -0
  83. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/phorge.conf +0 -0
  84. {phabfive-0.6.0 → phabfive-0.7.1}/phorge/phorge.ini +0 -0
  85. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-create/mega-2024-simulation.yml +0 -0
  86. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-create/test-template-v2.yml +0 -0
  87. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-create/test-template.yaml +0 -0
  88. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/blocked-tasks.yaml +0 -0
  89. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/development-workflow-audit.yaml +0 -0
  90. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/escalated-priorities.yaml +0 -0
  91. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/high-priority-stale-tasks.yaml +0 -0
  92. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/project-status-overview.yaml +0 -0
  93. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/recently-moved-to-review.yaml +0 -0
  94. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/tasks-resolved-but-not-in-done.yaml +0 -0
  95. {phabfive-0.6.0 → phabfive-0.7.1}/templates/task-search/team-productivity-report.yaml +0 -0
  96. {phabfive-0.6.0 → phabfive-0.7.1}/tests/__init__.py +0 -0
  97. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_arcrc.py +0 -0
  98. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_constants.py +0 -0
  99. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_priority_transitions.py +0 -0
  100. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_project_filters.py +0 -0
  101. {phabfive-0.6.0 → phabfive-0.7.1}/tests/test_status_transitions.py +0 -0
  102. {phabfive-0.6.0 → phabfive-0.7.1}/tox.ini +0 -0
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: Release
3
+
4
+ on:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ concurrency:
10
+ group: release-${{ github.ref }}
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ build:
15
+ name: Build distribution
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v6
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.13
28
+
29
+ - name: Build package
30
+ run: uv build
31
+
32
+ - name: Upload distribution artifacts
33
+ uses: actions/upload-artifact@v4
34
+ with:
35
+ name: python-package-distributions
36
+ path: dist/
37
+
38
+ build-executables:
39
+ name: Build ${{ matrix.os }} ${{ matrix.arch }} executable
40
+ runs-on: ${{ matrix.runner }}
41
+ permissions:
42
+ id-token: write
43
+ contents: read
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ include:
48
+ # AMD64 builds
49
+ - os: linux
50
+ arch: amd64
51
+ runner: ubuntu-latest
52
+ artifact_name: phabfive
53
+ asset_name: phabfive-linux-amd64
54
+ - os: macos
55
+ arch: amd64
56
+ runner: macos-14
57
+ artifact_name: phabfive
58
+ asset_name: phabfive-macos-amd64
59
+ - os: windows
60
+ arch: amd64
61
+ runner: windows-latest
62
+ artifact_name: phabfive.exe
63
+ asset_name: phabfive-windows-amd64.exe
64
+ # ARM64 builds
65
+ - os: linux
66
+ arch: arm64
67
+ runner: ubuntu-24.04-arm
68
+ artifact_name: phabfive
69
+ asset_name: phabfive-linux-arm64
70
+ - os: macos
71
+ arch: arm64
72
+ runner: macos-latest
73
+ artifact_name: phabfive
74
+ asset_name: phabfive-macos-arm64
75
+ - os: windows
76
+ arch: arm64
77
+ runner: windows-11-arm
78
+ artifact_name: phabfive.exe
79
+ asset_name: phabfive-windows-arm64.exe
80
+ skip_signing: true
81
+ steps:
82
+ - name: Checkout repository
83
+ uses: actions/checkout@v4
84
+
85
+ - name: Install Cosign
86
+ if: ${{ !matrix.skip_signing }}
87
+ uses: sigstore/cosign-installer@v3
88
+
89
+ - name: Set up Python
90
+ uses: actions/setup-python@v5
91
+ with:
92
+ python-version: "3.12"
93
+
94
+ - name: Install dependencies
95
+ run: |
96
+ python -m pip install --upgrade pip
97
+ pip install pyinstaller
98
+ pip install .
99
+
100
+ - name: Build executable
101
+ shell: bash
102
+ run: |
103
+ pyinstaller --onefile --name phabfive phabfive/cli.py \
104
+ --collect-data rich \
105
+ --copy-metadata rich \
106
+ --collect-data phabricator \
107
+ --hidden-import phabricator \
108
+ --hidden-import anyconfig.backend.yaml \
109
+ --exclude-module mkdocs
110
+
111
+ - name: Rename artifact
112
+ shell: bash
113
+ run: mv dist/${{ matrix.artifact_name }} dist/${{ matrix.asset_name }}
114
+
115
+ - name: Sign executable with Sigstore
116
+ if: ${{ !matrix.skip_signing }}
117
+ shell: bash
118
+ run: |
119
+ cosign sign-blob dist/${{ matrix.asset_name }} \
120
+ --bundle dist/${{ matrix.asset_name }}.sigstore.json \
121
+ --yes
122
+
123
+ - name: Upload executable and signature
124
+ if: ${{ !matrix.skip_signing }}
125
+ uses: actions/upload-artifact@v4
126
+ with:
127
+ name: ${{ matrix.asset_name }}
128
+ path: |
129
+ dist/${{ matrix.asset_name }}
130
+ dist/${{ matrix.asset_name }}.sigstore.json
131
+
132
+ - name: Upload executable (unsigned)
133
+ if: ${{ matrix.skip_signing }}
134
+ uses: actions/upload-artifact@v4
135
+ with:
136
+ name: ${{ matrix.asset_name }}
137
+ path: dist/${{ matrix.asset_name }}
138
+
139
+ publish-pypi:
140
+ name: Publish to PyPI
141
+ needs: build
142
+ if: ${{ !contains(github.ref_name, '-rc') }}
143
+ runs-on: ubuntu-latest
144
+ environment:
145
+ name: pypi
146
+ url: https://pypi.org/p/phabfive
147
+ permissions:
148
+ id-token: write
149
+ steps:
150
+ - name: Download distribution artifacts
151
+ uses: actions/download-artifact@v4
152
+ with:
153
+ name: python-package-distributions
154
+ path: dist/
155
+
156
+ - name: Publish to PyPI
157
+ uses: pypa/gh-action-pypi-publish@release/v1
158
+
159
+ github-release:
160
+ name: Create GitHub Release
161
+ needs: [build, publish-pypi, build-executables]
162
+ if: ${{ always() && needs.build.result == 'success' && needs.build-executables.result == 'success' && (needs.publish-pypi.result == 'success' || needs.publish-pypi.result == 'skipped') }}
163
+ runs-on: ubuntu-latest
164
+ permissions:
165
+ contents: write
166
+ steps:
167
+ - name: Checkout repository
168
+ uses: actions/checkout@v4
169
+
170
+ - name: Download all artifacts
171
+ uses: actions/download-artifact@v4
172
+ with:
173
+ path: artifacts/
174
+
175
+ - name: Prepare release assets
176
+ run: |
177
+ mkdir -p release-assets
178
+ # Copy Python distributions
179
+ cp artifacts/python-package-distributions/* release-assets/
180
+ # Copy executables
181
+ for dir in artifacts/phabfive-*/; do
182
+ cp "$dir"* release-assets/ 2>/dev/null || true
183
+ done
184
+ ls -la release-assets/
185
+
186
+ - name: Create GitHub Release
187
+ uses: softprops/action-gh-release@v2
188
+ with:
189
+ generate_release_notes: true
190
+ files: |
191
+ release-assets/*
@@ -0,0 +1,89 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI coding agents when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ phabfive is a CLI for Phabricator and Phorge. It provides commands for interacting with Passphrase, Diffusion, Paste, User, and Maniphest applications.
8
+
9
+ ## Common Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ uv sync --group dev
14
+
15
+ # Run the CLI
16
+ uv run phabfive --help
17
+
18
+ # Run tests
19
+ uv run pytest # quick test with current Python
20
+ uv run pytest tests/test_foo.py # run single test file
21
+ uv run pytest -k test_name # run specific test
22
+ uv run tox # test all Python versions (3.10-3.14)
23
+
24
+ # Lint
25
+ uv run flake8 phabfive/ tests/
26
+
27
+ # Local Phorge instance for testing
28
+ make up # start local Phorge
29
+ make down # stop containers
30
+
31
+ # Merge PRs (rebase only - merge and squash are disabled)
32
+ gh pr merge --rebase
33
+ ```
34
+
35
+ ## Architecture
36
+
37
+ ### CLI Layer (`cli.py`)
38
+ - Uses `docopt-ng` for argument parsing with docstring-based CLI definitions
39
+ - Monogram shortcuts allow direct access: `phabfive T123` expands to `phabfive maniphest show T123`
40
+ - Entry point: `cli_entrypoint()` → `parse_cli()`
41
+
42
+ ### Core Layer (`core.py`)
43
+ - `Phabfive` class: central configuration and API client management
44
+ - Loads config from environment variables or `~/.config/phabfive.yaml`
45
+ - Uses the `phabricator` library for Conduit API calls
46
+ - Output formatting: supports `rich` (terminal), `yaml`, and `strict` (machine-readable) modes
47
+
48
+ ### Feature Modules
49
+ Complex features use a consistent subpackage structure:
50
+ - `diffusion/` and `maniphest/` follow this pattern:
51
+ - `core.py` - main class inheriting from `Phabfive`
52
+ - `fetchers.py` - API data fetching
53
+ - `resolvers.py` - ID/name resolution
54
+ - `formatters.py` - output formatting
55
+ - `validators.py` - input validation
56
+
57
+ ### Simpler Modules
58
+ - `passphrase.py`, `paste.py`, `user.py` - single-file implementations
59
+ - `transitions/` - state machine for task status/priority/column changes
60
+
61
+ ### Configuration
62
+ Required: `PHAB_TOKEN` and `PHAB_URL` (via environment or config file)
63
+
64
+ ## Version Management
65
+
66
+ Version is defined only in `pyproject.toml`. Access it via:
67
+ ```python
68
+ from importlib.metadata import version
69
+ version("phabfive")
70
+ ```
71
+
72
+ ## Release Workflow
73
+
74
+ Releases are triggered by pushing a git tag matching `v*`:
75
+
76
+ ```bash
77
+ git tag -a v0.7.0 -m "Release v0.7.0"
78
+ git push origin v0.7.0
79
+ ```
80
+
81
+ **Artifacts produced:**
82
+ - Python wheel and sdist → PyPI
83
+ - Standalone executables for 6 platforms → GitHub Releases:
84
+ - `phabfive-linux-amd64`, `phabfive-linux-arm64`
85
+ - `phabfive-macos-amd64`, `phabfive-macos-arm64`
86
+ - `phabfive-windows-amd64.exe`, `phabfive-windows-arm64.exe`
87
+ - Sigstore signatures (`.sigstore.json`) for all executables except Windows ARM64
88
+
89
+ **RC tags** (containing `-rc`) skip PyPI but still build executables and create GitHub releases.
@@ -1,3 +1,40 @@
1
+ # 0.7.1 (2026-03-07)
2
+
3
+ ## Bug Fixes
4
+
5
+ * Fix standalone executables exiting silently without output
6
+
7
+
8
+ # 0.7.0 (2026-03-07)
9
+
10
+ ## Prelude
11
+
12
+ This release adds interactive first-run configuration, standalone executables for all major platforms, and improved developer experience with Sigstore-signed releases.
13
+
14
+ ## New Features
15
+
16
+ ### Interactive Setup
17
+ * **First-run configuration** - New `phabfive user setup` command guides users through initial configuration with interactive prompts
18
+ * **Secure token input** - API token input is masked with dots for security
19
+ * **Smart reconfiguration** - Warns before overwriting existing working configuration
20
+
21
+ ### Release Artifacts
22
+ * **Standalone executables** - Pre-built binaries for Linux, macOS, and Windows (AMD64 and ARM64)
23
+ * **Sigstore signing** - All executables are cryptographically signed for verification
24
+ * **RC tag support** - Release candidates skip PyPI for testing the release process
25
+
26
+ ## Bug Fixes
27
+
28
+ * Fix Rich markup escaping for square brackets in user content
29
+ * Fix project lookup failing when more than 100 projects exist
30
+
31
+ ## Other Notes
32
+
33
+ * Version now sourced solely from `pyproject.toml` using `importlib.metadata`
34
+ * Added `AGENTS.md` for AI coding agent guidance
35
+ * Removed unused `__author__`, `__email__`, `__url__` module attributes
36
+
37
+
1
38
  # 0.6.0 (2026-03-07)
2
39
 
3
40
  ## Prelude
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phabfive
3
- Version: 0.6.0
4
- Summary: A command line tool to interact with Phabricator
3
+ Version: 0.7.1
4
+ Summary: CLI for Phabricator and Phorge - built for humans and AI agents
5
5
  Project-URL: Homepage, https://github.com/dynamist/phabfive
6
6
  Project-URL: Download, https://github.com/dynamist/phabfive/releases
7
7
  Author-email: Rickard Eriksson <rickard@dynamist.se>
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  # phabfive
43
43
 
44
- A command line tool to interact with [Phabricator](https://www.phacility.com/phabricator/) and [Phorge](https://www.phorge.it/).
44
+ CLI for [Phabricator](https://www.phacility.com/phabricator/) and [Phorge](https://we.phorge.it/) - built for humans and AI agents.
45
45
 
46
46
  ![phabfive maniphest show](docs/maniphest-show.png)
47
47
 
@@ -1,6 +1,6 @@
1
1
  # phabfive
2
2
 
3
- A command line tool to interact with [Phabricator](https://www.phacility.com/phabricator/) and [Phorge](https://www.phorge.it/).
3
+ CLI for [Phabricator](https://www.phacility.com/phabricator/) and [Phorge](https://we.phorge.it/) - built for humans and AI agents.
4
4
 
5
5
  ![phabfive maniphest show](docs/maniphest-show.png)
6
6
 
@@ -77,9 +77,25 @@ git push origin v0.6.0
77
77
 
78
78
  This triggers the GitHub Actions workflow which will:
79
79
 
80
- 1. Build the package
81
- 2. Publish to PyPI using trusted publishing
82
- 3. Create GitHub Release with auto-generated notes and artifacts
80
+ 1. Build the Python package (wheel and sdist)
81
+ 2. Build standalone executables for 6 platforms:
82
+ - Linux (AMD64, ARM64)
83
+ - macOS (AMD64, ARM64)
84
+ - Windows (AMD64, ARM64)
85
+ 3. Sign executables with [Sigstore](https://www.sigstore.dev/) (keyless OIDC signing)
86
+ 4. Publish to PyPI using trusted publishing
87
+ 5. Create GitHub Release with auto-generated notes and all artifacts
88
+
89
+ **Testing with RC tags:** Tags containing `-rc` (e.g., `v0.7.0-rc.1`) will skip PyPI publishing but still build executables and create a GitHub Release. Useful for testing the release process.
90
+
91
+ **Verifying signatures:** Users can verify downloaded executables with [cosign](https://docs.sigstore.dev/):
92
+
93
+ ```bash
94
+ cosign verify-blob phabfive-linux-amd64 \
95
+ --bundle phabfive-linux-amd64.sigstore.json \
96
+ --certificate-identity-regexp="https://github.com/dynamist/phabfive" \
97
+ --certificate-oidc-issuer="https://token.actions.githubusercontent.com"
98
+ ```
83
99
 
84
100
  **5. Bump to Dev Version**
85
101
 
@@ -5,11 +5,6 @@ import logging
5
5
  import logging.config
6
6
  import sys
7
7
 
8
- __author__ = "Rickard Eriksson"
9
- __email__ = "rickard@dynamist.se"
10
- __version__ = "0.4.0"
11
- __url__ = "https://github.com/dynamist/phabfive"
12
-
13
8
 
14
9
  def init_logging(log_level):
15
10
  """
@@ -5,10 +5,12 @@ import logging
5
5
  import os
6
6
  import re
7
7
  import sys
8
+ from importlib.metadata import version
8
9
  from io import StringIO
9
10
 
10
11
  # 3rd party imports
11
12
  from docopt import DocoptExit, Option, docopt, extras
13
+ from rich.markup import escape
12
14
  from rich.text import Text
13
15
  from rich.tree import Tree
14
16
  from ruamel.yaml import YAML
@@ -25,6 +27,32 @@ log = logging.getLogger(__name__)
25
27
  # =============================================================================
26
28
 
27
29
 
30
+ def _escape_for_rich(content):
31
+ """Escape user content for safe Rich printing.
32
+
33
+ Rich interprets square brackets [...] as markup syntax.
34
+ This causes issues with user content containing:
35
+ - [/path] patterns -> MarkupError crash (interpreted as closing tag)
36
+ - [bug] tags -> Silently stripped (interpreted as invalid style)
37
+
38
+ Parameters
39
+ ----------
40
+ content : any
41
+ User-provided content to escape
42
+
43
+ Returns
44
+ -------
45
+ str or Text
46
+ Empty string for None, Text objects unchanged, escaped string otherwise
47
+ """
48
+ if content is None:
49
+ return ""
50
+ if isinstance(content, Text):
51
+ # Text objects are already safe Rich objects (e.g., hyperlinks)
52
+ return content
53
+ return escape(str(content))
54
+
55
+
28
56
  def _needs_yaml_quoting(value):
29
57
  """Check if a string value needs YAML quoting.
30
58
 
@@ -71,12 +99,12 @@ def display_task_rich(console, task_dict, phabfive_instance):
71
99
  # Multi-line value
72
100
  console.print(f" {key}: |-")
73
101
  for line in str(value).splitlines():
74
- console.print(f" {line}")
102
+ console.print(f" {_escape_for_rich(line)}")
75
103
  elif _needs_yaml_quoting(value):
76
- escaped = value.replace("'", "''")
77
- console.print(f" {key}: '{escaped}'")
104
+ escaped = str(value).replace("'", "''")
105
+ console.print(f" {key}: '{_escape_for_rich(escaped)}'")
78
106
  else:
79
- console.print(f" {key}: {value}")
107
+ console.print(f" {key}: {_escape_for_rich(value)}")
80
108
 
81
109
  # Print Assignee
82
110
  if assignee:
@@ -126,10 +154,10 @@ def display_task_rich(console, task_dict, phabfive_instance):
126
154
  )
127
155
  continue
128
156
  if _needs_yaml_quoting(value):
129
- escaped = value.replace("'", "''")
130
- console.print(f" {key}: '{escaped}'")
157
+ escaped = str(value).replace("'", "''")
158
+ console.print(f" {key}: '{_escape_for_rich(escaped)}'")
131
159
  else:
132
- console.print(f" {key}: {value}")
160
+ console.print(f" {key}: {_escape_for_rich(value)}")
133
161
 
134
162
  # Print History section
135
163
  if history:
@@ -138,13 +166,13 @@ def display_task_rich(console, task_dict, phabfive_instance):
138
166
  if hist_key == "Boards" and isinstance(hist_value, dict):
139
167
  console.print(" Boards:")
140
168
  for board_name, transitions in hist_value.items():
141
- console.print(f" {board_name}:")
169
+ console.print(f" {_escape_for_rich(board_name)}:")
142
170
  for trans in transitions:
143
- console.print(f" - {trans}")
171
+ console.print(f" - {_escape_for_rich(trans)}")
144
172
  elif isinstance(hist_value, list):
145
173
  console.print(f" {hist_key}:")
146
174
  for trans in hist_value:
147
- console.print(f" - {trans}")
175
+ console.print(f" - {_escape_for_rich(trans)}")
148
176
 
149
177
  # Print Comments section
150
178
  comments = task_dict.get("Comments", [])
@@ -154,11 +182,11 @@ def display_task_rich(console, task_dict, phabfive_instance):
154
182
  if isinstance(comment, PreservedScalarString) or "\n" in str(comment):
155
183
  # Multi-line comment
156
184
  lines = str(comment).splitlines()
157
- console.print(f" - {lines[0]}")
185
+ console.print(f" - {_escape_for_rich(lines[0])}")
158
186
  for line in lines[1:]:
159
- console.print(f" {line}")
187
+ console.print(f" {_escape_for_rich(line)}")
160
188
  else:
161
- console.print(f" - {comment}")
189
+ console.print(f" - {_escape_for_rich(comment)}")
162
190
 
163
191
  # Print Metadata section
164
192
  if metadata:
@@ -168,11 +196,11 @@ def display_task_rich(console, task_dict, phabfive_instance):
168
196
  if meta_value:
169
197
  console.print(f" {meta_key}:")
170
198
  for item in meta_value:
171
- console.print(f" - {item}")
199
+ console.print(f" - {_escape_for_rich(item)}")
172
200
  else:
173
201
  console.print(f" {meta_key}: []")
174
202
  else:
175
- console.print(f" {meta_key}: {meta_value}")
203
+ console.print(f" {meta_key}: {_escape_for_rich(meta_value)}")
176
204
 
177
205
 
178
206
  def display_task_tree(console, task_dict, phabfive_instance):
@@ -207,9 +235,9 @@ def display_task_tree(console, task_dict, phabfive_instance):
207
235
  first_line = str(value).split("\n")[0]
208
236
  if len(first_line) > 60:
209
237
  first_line = first_line[:57] + "..."
210
- task_branch.add(f"{key}: {first_line}")
238
+ task_branch.add(f"{key}: {_escape_for_rich(first_line)}")
211
239
  else:
212
- task_branch.add(f"{key}: {value}")
240
+ task_branch.add(f"{key}: {_escape_for_rich(value)}")
213
241
 
214
242
  # Add Assignee
215
243
  if assignee:
@@ -243,7 +271,7 @@ def display_task_tree(console, task_dict, phabfive_instance):
243
271
  )
244
272
  board_branch.add(Text.assemble("Column: ", column_link))
245
273
  continue
246
- board_branch.add(f"{key}: {value}")
274
+ board_branch.add(f"{key}: {_escape_for_rich(value)}")
247
275
 
248
276
  # Add History section
249
277
  if history:
@@ -252,13 +280,13 @@ def display_task_tree(console, task_dict, phabfive_instance):
252
280
  if hist_key == "Boards" and isinstance(hist_value, dict):
253
281
  boards_hist = history_branch.add("Boards")
254
282
  for board_name, transitions in hist_value.items():
255
- board_hist = boards_hist.add(board_name)
283
+ board_hist = boards_hist.add(_escape_for_rich(board_name))
256
284
  for trans in transitions:
257
- board_hist.add(trans)
285
+ board_hist.add(_escape_for_rich(trans))
258
286
  elif isinstance(hist_value, list):
259
287
  hist_type_branch = history_branch.add(hist_key)
260
288
  for trans in hist_value:
261
- hist_type_branch.add(trans)
289
+ hist_type_branch.add(_escape_for_rich(trans))
262
290
 
263
291
  # Add Comments section
264
292
  comments = task_dict.get("Comments", [])
@@ -270,9 +298,9 @@ def display_task_tree(console, task_dict, phabfive_instance):
270
298
  first_line = str(comment).split("\n")[0]
271
299
  if len(first_line) > 60:
272
300
  first_line = first_line[:57] + "..."
273
- comments_branch.add(first_line)
301
+ comments_branch.add(_escape_for_rich(first_line))
274
302
  else:
275
- comments_branch.add(str(comment))
303
+ comments_branch.add(_escape_for_rich(str(comment)))
276
304
 
277
305
  # Add Metadata section
278
306
  if metadata:
@@ -282,11 +310,11 @@ def display_task_tree(console, task_dict, phabfive_instance):
282
310
  if meta_value:
283
311
  list_branch = meta_branch.add(meta_key)
284
312
  for item in meta_value:
285
- list_branch.add(str(item))
313
+ list_branch.add(_escape_for_rich(str(item)))
286
314
  else:
287
315
  meta_branch.add(f"{meta_key}: []")
288
316
  else:
289
- meta_branch.add(f"{meta_key}: {meta_value}")
317
+ meta_branch.add(f"{meta_key}: {_escape_for_rich(meta_value)}")
290
318
 
291
319
  console.print(tree)
292
320
 
@@ -408,7 +436,7 @@ Available phabfive commands are:
408
436
  maniphest The maniphest app
409
437
  paste The paste app
410
438
  repl Enter a REPL with API access
411
- user Information on users
439
+ user Information on users, setup wizard
412
440
 
413
441
  Shortcuts to view Phabricator monograms (example: phabfive T123):
414
442
 
@@ -489,9 +517,12 @@ Paste Create Options:
489
517
  sub_user_args = """
490
518
  Usage:
491
519
  phabfive user whoami [options]
520
+ phabfive user setup [options]
492
521
 
493
522
  Options:
494
523
  -h, --help Show this help message and exit
524
+
525
+ The setup command configures phabfive with your Phabricator URL and API token.
495
526
  """
496
527
 
497
528
  sub_repl_args = """
@@ -681,17 +712,19 @@ def parse_cli():
681
712
  """
682
713
  import phabfive
683
714
 
715
+ pkg_version = version("phabfive")
716
+
684
717
  try:
685
718
  cli_args = docopt(
686
719
  base_args,
687
720
  options_first=True,
688
- version=phabfive.__version__,
721
+ version=pkg_version,
689
722
  default_help=True,
690
723
  )
691
724
  except DocoptExit:
692
725
  extras(
693
726
  True,
694
- phabfive.__version__,
727
+ pkg_version,
695
728
  [Option("-h", "--help", 0, True)],
696
729
  base_args,
697
730
  )
@@ -778,7 +811,7 @@ def parse_cli():
778
811
  else:
779
812
  extras(
780
813
  True,
781
- phabfive.__version__,
814
+ pkg_version,
782
815
  [Option("-h", "--help", 0, True)],
783
816
  base_args,
784
817
  )
@@ -798,7 +831,7 @@ def run(cli_args, sub_args):
798
831
  from phabfive import diffusion, maniphest, passphrase, paste, repl, user
799
832
  from phabfive.constants import REPO_STATUS_CHOICES
800
833
  from phabfive.core import Phabfive
801
- from phabfive.exceptions import PhabfiveException
834
+ from phabfive.exceptions import PhabfiveConfigException, PhabfiveException
802
835
  from phabfive.transitions import parse_column_patterns, parse_priority_patterns
803
836
 
804
837
  # Validate and process output options
@@ -961,6 +994,13 @@ def run(cli_args, sub_args):
961
994
  print(f"{p['id']} {p['title']}")
962
995
 
963
996
  if cli_args["<command>"] == "user":
997
+ # Handle setup first - it doesn't require existing configuration
998
+ if sub_args.get("setup"):
999
+ from phabfive.setup import SetupWizard
1000
+
1001
+ wizard = SetupWizard()
1002
+ return 0 if wizard.run() else 1
1003
+
964
1004
  user_app = user.User()
965
1005
 
966
1006
  if sub_args["whoami"]:
@@ -1232,6 +1272,14 @@ def run(cli_args, sub_args):
1232
1272
  show_comments=show_comments,
1233
1273
  )
1234
1274
  display_tasks(result, output_format, maniphest_app)
1275
+ except PhabfiveConfigException as e:
1276
+ # Offer interactive setup for configuration errors
1277
+ from phabfive.setup import offer_setup_on_error
1278
+
1279
+ if offer_setup_on_error(str(e)):
1280
+ # Setup succeeded - connection verified
1281
+ return 0
1282
+ retcode = 1
1235
1283
  except PhabfiveException as e:
1236
1284
  # Catch all types of phabricator base exceptions
1237
1285
  log.critical(str(e))
@@ -1264,3 +1312,7 @@ def cli_entrypoint():
1264
1312
  sys.exit(130) # Standard Unix exit code for SIGINT (128 + 2)
1265
1313
  except Exception:
1266
1314
  raise
1315
+
1316
+
1317
+ if __name__ == "__main__":
1318
+ cli_entrypoint()