pfsentinel 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.
Files changed (100) hide show
  1. pfsentinel-0.1.0/.gitattributes +30 -0
  2. pfsentinel-0.1.0/.github/CODEOWNERS +2 -0
  3. pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
  4. pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +36 -0
  6. pfsentinel-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  7. pfsentinel-0.1.0/.github/workflows/ci.yml +476 -0
  8. pfsentinel-0.1.0/.gitignore +42 -0
  9. pfsentinel-0.1.0/CHANGELOG.md +49 -0
  10. pfsentinel-0.1.0/CONTRIBUTING.md +125 -0
  11. pfsentinel-0.1.0/LICENSE +21 -0
  12. pfsentinel-0.1.0/PKG-INFO +146 -0
  13. pfsentinel-0.1.0/README.md +101 -0
  14. pfsentinel-0.1.0/ROADMAP.md +51 -0
  15. pfsentinel-0.1.0/SECURITY.md +173 -0
  16. pfsentinel-0.1.0/THIRD_PARTY_LICENSES.txt +122 -0
  17. pfsentinel-0.1.0/assets/pfsentinel.ico +0 -0
  18. pfsentinel-0.1.0/docs/LICENSE_AUDIT.md +83 -0
  19. pfsentinel-0.1.0/docs/extended-backups.md +228 -0
  20. pfsentinel-0.1.0/docs/installation.md +113 -0
  21. pfsentinel-0.1.0/docs/usage.md +581 -0
  22. pfsentinel-0.1.0/pyproject.toml +107 -0
  23. pfsentinel-0.1.0/renovate.json +52 -0
  24. pfsentinel-0.1.0/scripts/build_exe.py +129 -0
  25. pfsentinel-0.1.0/src/pfsentinel/__init__.py +3 -0
  26. pfsentinel-0.1.0/src/pfsentinel/__main__.py +6 -0
  27. pfsentinel-0.1.0/src/pfsentinel/cli/app.py +244 -0
  28. pfsentinel-0.1.0/src/pfsentinel/cli/commands/__init__.py +1 -0
  29. pfsentinel-0.1.0/src/pfsentinel/cli/commands/backup.py +693 -0
  30. pfsentinel-0.1.0/src/pfsentinel/cli/commands/config.py +126 -0
  31. pfsentinel-0.1.0/src/pfsentinel/cli/commands/device.py +362 -0
  32. pfsentinel-0.1.0/src/pfsentinel/cli/commands/notify.py +383 -0
  33. pfsentinel-0.1.0/src/pfsentinel/cli/commands/schedule.py +129 -0
  34. pfsentinel-0.1.0/src/pfsentinel/cli/commands/update.py +100 -0
  35. pfsentinel-0.1.0/src/pfsentinel/cli/formatters.py +132 -0
  36. pfsentinel-0.1.0/src/pfsentinel/cli/list.py +44 -0
  37. pfsentinel-0.1.0/src/pfsentinel/models/__init__.py +18 -0
  38. pfsentinel-0.1.0/src/pfsentinel/models/backup.py +156 -0
  39. pfsentinel-0.1.0/src/pfsentinel/models/config.py +217 -0
  40. pfsentinel-0.1.0/src/pfsentinel/models/device.py +107 -0
  41. pfsentinel-0.1.0/src/pfsentinel/models/zfs.py +50 -0
  42. pfsentinel-0.1.0/src/pfsentinel/services/__init__.py +1 -0
  43. pfsentinel-0.1.0/src/pfsentinel/services/archive_backup.py +110 -0
  44. pfsentinel-0.1.0/src/pfsentinel/services/backup.py +343 -0
  45. pfsentinel-0.1.0/src/pfsentinel/services/connection.py +592 -0
  46. pfsentinel-0.1.0/src/pfsentinel/services/credentials.py +217 -0
  47. pfsentinel-0.1.0/src/pfsentinel/services/diff.py +139 -0
  48. pfsentinel-0.1.0/src/pfsentinel/services/extra_backup.py +371 -0
  49. pfsentinel-0.1.0/src/pfsentinel/services/notifications.py +172 -0
  50. pfsentinel-0.1.0/src/pfsentinel/services/orchestrator.py +207 -0
  51. pfsentinel-0.1.0/src/pfsentinel/services/retention.py +113 -0
  52. pfsentinel-0.1.0/src/pfsentinel/services/scheduler.py +180 -0
  53. pfsentinel-0.1.0/src/pfsentinel/services/updater.py +470 -0
  54. pfsentinel-0.1.0/src/pfsentinel/services/zfs_backup.py +250 -0
  55. pfsentinel-0.1.0/src/pfsentinel/tui/__init__.py +1 -0
  56. pfsentinel-0.1.0/src/pfsentinel/tui/app.py +85 -0
  57. pfsentinel-0.1.0/src/pfsentinel/tui/screens/__init__.py +1 -0
  58. pfsentinel-0.1.0/src/pfsentinel/tui/screens/backups.py +162 -0
  59. pfsentinel-0.1.0/src/pfsentinel/tui/screens/dashboard.py +95 -0
  60. pfsentinel-0.1.0/src/pfsentinel/tui/screens/devices.py +105 -0
  61. pfsentinel-0.1.0/src/pfsentinel/tui/screens/logs.py +64 -0
  62. pfsentinel-0.1.0/src/pfsentinel/tui/screens/settings.py +121 -0
  63. pfsentinel-0.1.0/src/pfsentinel/utils/__init__.py +1 -0
  64. pfsentinel-0.1.0/src/pfsentinel/utils/checksum.py +32 -0
  65. pfsentinel-0.1.0/src/pfsentinel/utils/compression.py +41 -0
  66. pfsentinel-0.1.0/src/pfsentinel/utils/naming.py +131 -0
  67. pfsentinel-0.1.0/src/pfsentinel/utils/platform.py +117 -0
  68. pfsentinel-0.1.0/src/pfsentinel/utils/xml_parser.py +107 -0
  69. pfsentinel-0.1.0/tests/__init__.py +0 -0
  70. pfsentinel-0.1.0/tests/conftest.py +131 -0
  71. pfsentinel-0.1.0/tests/integration/__init__.py +0 -0
  72. pfsentinel-0.1.0/tests/integration/test_backup_workflow.py +188 -0
  73. pfsentinel-0.1.0/tests/unit/__init__.py +0 -0
  74. pfsentinel-0.1.0/tests/unit/test_archive_backup.py +128 -0
  75. pfsentinel-0.1.0/tests/unit/test_backup_models.py +250 -0
  76. pfsentinel-0.1.0/tests/unit/test_backup_service.py +164 -0
  77. pfsentinel-0.1.0/tests/unit/test_checksum.py +44 -0
  78. pfsentinel-0.1.0/tests/unit/test_compression.py +59 -0
  79. pfsentinel-0.1.0/tests/unit/test_config_models.py +128 -0
  80. pfsentinel-0.1.0/tests/unit/test_config_models_extended.py +129 -0
  81. pfsentinel-0.1.0/tests/unit/test_connection.py +183 -0
  82. pfsentinel-0.1.0/tests/unit/test_credentials.py +109 -0
  83. pfsentinel-0.1.0/tests/unit/test_device_models.py +112 -0
  84. pfsentinel-0.1.0/tests/unit/test_diff.py +112 -0
  85. pfsentinel-0.1.0/tests/unit/test_extra_backup.py +184 -0
  86. pfsentinel-0.1.0/tests/unit/test_formatters.py +148 -0
  87. pfsentinel-0.1.0/tests/unit/test_naming.py +107 -0
  88. pfsentinel-0.1.0/tests/unit/test_naming_extended.py +115 -0
  89. pfsentinel-0.1.0/tests/unit/test_notifications.py +153 -0
  90. pfsentinel-0.1.0/tests/unit/test_orchestrator.py +166 -0
  91. pfsentinel-0.1.0/tests/unit/test_platform.py +105 -0
  92. pfsentinel-0.1.0/tests/unit/test_retention_typed.py +162 -0
  93. pfsentinel-0.1.0/tests/unit/test_selective_backup.py +143 -0
  94. pfsentinel-0.1.0/tests/unit/test_ssh_extended.py +200 -0
  95. pfsentinel-0.1.0/tests/unit/test_ssh_key_auth.py +228 -0
  96. pfsentinel-0.1.0/tests/unit/test_updater.py +1232 -0
  97. pfsentinel-0.1.0/tests/unit/test_verify_typed.py +189 -0
  98. pfsentinel-0.1.0/tests/unit/test_xml_parser.py +172 -0
  99. pfsentinel-0.1.0/tests/unit/test_zfs_backup.py +179 -0
  100. pfsentinel-0.1.0/tests/unit/test_zfs_models.py +115 -0
@@ -0,0 +1,30 @@
1
+ # Auto detect text files and ensure LF line endings
2
+ * text=auto eol=lf
3
+
4
+ # Explicitly declare text files
5
+ *.py text diff=python
6
+ *.md text diff=markdown
7
+ *.toml text
8
+ *.yml text
9
+ *.yaml text
10
+ *.json text
11
+ *.cfg text
12
+ *.txt text
13
+ *.ini text
14
+ *.sh text eol=lf
15
+ *.bat text eol=crlf
16
+ *.ps1 text eol=crlf
17
+
18
+ # Binary files
19
+ *.ico binary
20
+ *.png binary
21
+ *.jpg binary
22
+ *.exe binary
23
+ *.gz binary
24
+ *.tar binary
25
+ *.zip binary
26
+ *.zfs binary
27
+
28
+ # GitHub linguist overrides
29
+ tests/ linguist-detectable=false
30
+ docs/ linguist-documentation
@@ -0,0 +1,2 @@
1
+ # Default owner for everything in the repo
2
+ * @NX1X
@@ -0,0 +1,68 @@
1
+ name: Bug Report
2
+ description: Report a bug or unexpected behavior
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for reporting a bug. Please fill out the sections below so we can reproduce and fix the issue.
9
+
10
+ - type: input
11
+ id: version
12
+ attributes:
13
+ label: pfSentinel version
14
+ description: Run `pfs --version` and paste the output.
15
+ placeholder: "0.5.2"
16
+ validations:
17
+ required: true
18
+
19
+ - type: dropdown
20
+ id: os
21
+ attributes:
22
+ label: Operating system
23
+ options:
24
+ - Windows
25
+ - Linux
26
+ - macOS
27
+ validations:
28
+ required: true
29
+
30
+ - type: input
31
+ id: pfsense-version
32
+ attributes:
33
+ label: pfSense version (if relevant)
34
+ placeholder: "2.7.2"
35
+
36
+ - type: textarea
37
+ id: description
38
+ attributes:
39
+ label: What happened?
40
+ description: Describe the bug clearly.
41
+ validations:
42
+ required: true
43
+
44
+ - type: textarea
45
+ id: steps
46
+ attributes:
47
+ label: Steps to reproduce
48
+ description: Minimal steps to trigger the bug.
49
+ placeholder: |
50
+ 1. Run `pfs backup run --device myfw`
51
+ 2. ...
52
+ validations:
53
+ required: true
54
+
55
+ - type: textarea
56
+ id: expected
57
+ attributes:
58
+ label: Expected behavior
59
+ description: What did you expect to happen?
60
+ validations:
61
+ required: true
62
+
63
+ - type: textarea
64
+ id: logs
65
+ attributes:
66
+ label: Error output / logs
67
+ description: Paste any error messages or relevant log output. Run with `--debug` for verbose output.
68
+ render: text
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Documentation
4
+ url: https://github.com/NX1X/pfSentinel/tree/main/docs
5
+ about: Check the docs before opening an issue.
6
+ - name: Security vulnerability
7
+ url: https://github.com/NX1X/pfSentinel/security/advisories/new
8
+ about: Report security vulnerabilities privately (do not open a public issue).
@@ -0,0 +1,36 @@
1
+ name: Feature Request
2
+ description: Suggest a new feature or improvement
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Have an idea for pfSentinel? Describe it below.
9
+
10
+ - type: textarea
11
+ id: problem
12
+ attributes:
13
+ label: Problem or motivation
14
+ description: What problem does this solve? Why is it needed?
15
+ validations:
16
+ required: true
17
+
18
+ - type: textarea
19
+ id: solution
20
+ attributes:
21
+ label: Proposed solution
22
+ description: How would you expect this to work?
23
+ validations:
24
+ required: true
25
+
26
+ - type: textarea
27
+ id: alternatives
28
+ attributes:
29
+ label: Alternatives considered
30
+ description: Any other approaches you thought about?
31
+
32
+ - type: textarea
33
+ id: context
34
+ attributes:
35
+ label: Additional context
36
+ description: Screenshots, config snippets, or links to related issues.
@@ -0,0 +1,11 @@
1
+ ## What does this PR do?
2
+
3
+ <!-- Briefly describe the change and link any related issues (e.g. Fixes #123). -->
4
+
5
+ ## Checklist
6
+
7
+ - [ ] Tests pass (`pytest tests/ -v`)
8
+ - [ ] Linting passes (`ruff check src/ tests/`)
9
+ - [ ] Formatting passes (`ruff format --check src/ tests/`)
10
+ - [ ] CHANGELOG.md updated (if user-facing change)
11
+ - [ ] Docs updated (if applicable)
@@ -0,0 +1,476 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ inputs:
9
+ release_version:
10
+ description: 'Release version (e.g. 0.5.1) — leave empty for CI-only run'
11
+ required: false
12
+ type: string
13
+
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.ref }}
16
+ cancel-in-progress: true
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ env:
22
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
23
+
24
+ jobs:
25
+ # ---------------------------------------------------------------------------
26
+ # 1. Lint — runs once on Ubuntu (ruff is cross-platform)
27
+ # ---------------------------------------------------------------------------
28
+ lint:
29
+ name: Lint (ruff)
30
+ runs-on: ubuntu-latest
31
+
32
+ steps:
33
+ - name: Checkout repository
34
+ uses: actions/checkout@v6
35
+
36
+ - name: Set up Python 3.14
37
+ uses: actions/setup-python@v6
38
+ with:
39
+ python-version: '3.14'
40
+ cache: 'pip'
41
+
42
+ - name: Install ruff
43
+ run: pip install "ruff>=0.11,<0.12"
44
+
45
+ - name: Run ruff lint
46
+ run: ruff check src/ tests/
47
+
48
+ - name: Run ruff format check
49
+ run: ruff format --check src/ tests/
50
+
51
+ - name: Run ShellCheck on shell scripts
52
+ uses: ludeeus/action-shellcheck@2.0.0
53
+ with:
54
+ scandir: '.'
55
+ ignore_paths: '.venv'
56
+ severity: warning
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 2. Tests — matrix: Ubuntu + Windows × Python 3.13 + 3.14
60
+ # ---------------------------------------------------------------------------
61
+ test:
62
+ name: Test (${{ matrix.os }} / Python ${{ matrix.python-version }})
63
+ runs-on: ${{ matrix.os }}
64
+ needs: lint
65
+
66
+ strategy:
67
+ fail-fast: false
68
+ matrix:
69
+ os: [ubuntu-latest, windows-latest]
70
+ python-version: ['3.13', '3.14']
71
+
72
+ steps:
73
+ - name: Checkout repository
74
+ uses: actions/checkout@v6
75
+
76
+ - name: Set up Python ${{ matrix.python-version }}
77
+ uses: actions/setup-python@v6
78
+ with:
79
+ python-version: ${{ matrix.python-version }}
80
+ cache: 'pip'
81
+
82
+ - name: Install package with dev dependencies
83
+ shell: bash
84
+ run: |
85
+ python -m pip install --upgrade pip
86
+ pip install -e ".[dev]"
87
+
88
+ - name: Run pytest with coverage
89
+ shell: bash
90
+ run: |
91
+ pytest tests/ \
92
+ --tb=short \
93
+ --cov=src/pfsentinel \
94
+ --cov-report=term-missing \
95
+ --cov-report=xml:coverage.xml \
96
+ -v
97
+
98
+ - name: Upload coverage report (Ubuntu / Python 3.14 only)
99
+ if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
100
+ uses: actions/upload-artifact@v6
101
+ with:
102
+ name: coverage-report
103
+ path: |
104
+ coverage.xml
105
+ htmlcov/
106
+ retention-days: 14
107
+
108
+ - name: Upload coverage to Codecov (Ubuntu / Python 3.14 / push only)
109
+ if: >
110
+ matrix.python-version == '3.14' &&
111
+ matrix.os == 'ubuntu-latest' &&
112
+ github.event_name == 'push'
113
+ uses: codecov/codecov-action@v6
114
+ with:
115
+ files: coverage.xml
116
+ fail_ci_if_error: false
117
+ env:
118
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # 3. Resolve version — decide whether to release
122
+ # ---------------------------------------------------------------------------
123
+ resolve-version:
124
+ name: Resolve Version
125
+ runs-on: ubuntu-latest
126
+ needs: test
127
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
128
+ outputs:
129
+ should_release: ${{ steps.decide.outputs.should_release }}
130
+ version: ${{ steps.decide.outputs.version }}
131
+ tag: ${{ steps.decide.outputs.tag }}
132
+
133
+ steps:
134
+ - name: Checkout repository
135
+ uses: actions/checkout@v6
136
+ with:
137
+ fetch-depth: 0
138
+
139
+ - name: Resolve version
140
+ id: decide
141
+ shell: bash
142
+ run: |
143
+ # --- Manual release: use the provided version ---
144
+ MANUAL="${{ github.event.inputs.release_version }}"
145
+ if [[ -n "$MANUAL" ]]; then
146
+ VERSION="${MANUAL#v}"
147
+ if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
148
+ echo "::error::Invalid version format: '$VERSION'"
149
+ exit 1
150
+ fi
151
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
152
+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
153
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
154
+ echo "Manual release: v$VERSION"
155
+ exit 0
156
+ fi
157
+
158
+ # --- Auto release: parse CHANGELOG.md ---
159
+ CHANGELOG_FILE="CHANGELOG.md"
160
+ if [[ ! -f "$CHANGELOG_FILE" ]]; then
161
+ echo "No CHANGELOG.md — skipping release."
162
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
163
+ exit 0
164
+ fi
165
+
166
+ VERSIONED_HEADER=$(grep -n '^## \[' "$CHANGELOG_FILE" \
167
+ | grep -iv '\[unreleased\]' \
168
+ | head -n 1)
169
+
170
+ if [[ -z "$VERSIONED_HEADER" ]]; then
171
+ echo "No versioned header in CHANGELOG.md — skipping."
172
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
173
+ exit 0
174
+ fi
175
+
176
+ VERSION=$(echo "$VERSIONED_HEADER" | sed -E 's/.*## \[([^]]+)\].*/\1/')
177
+ if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
178
+ echo "Version '$VERSION' is not semver — skipping."
179
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
180
+ exit 0
181
+ fi
182
+
183
+ TAG="v$VERSION"
184
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
185
+ echo "Tag $TAG already exists — skipping."
186
+ echo "should_release=false" >> "$GITHUB_OUTPUT"
187
+ exit 0
188
+ fi
189
+
190
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
191
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
192
+ echo "should_release=true" >> "$GITHUB_OUTPUT"
193
+ echo "Auto release: $TAG"
194
+
195
+ - name: Extract release notes
196
+ if: steps.decide.outputs.should_release == 'true'
197
+ shell: bash
198
+ run: |
199
+ VERSION="${{ steps.decide.outputs.version }}"
200
+ CHANGELOG_FILE="CHANGELOG.md"
201
+
202
+ FIRST_LINE=$(grep -n "## \[$VERSION\]" "$CHANGELOG_FILE" | head -n 1 | cut -d: -f1)
203
+ NEXT_LINE=$(grep -n '^## \[' "$CHANGELOG_FILE" \
204
+ | awk -F: -v start="$FIRST_LINE" '$1 > start {print $1; exit}')
205
+
206
+ if [[ -z "$NEXT_LINE" ]]; then
207
+ NOTES=$(tail -n "+$((FIRST_LINE + 1))" "$CHANGELOG_FILE")
208
+ else
209
+ NOTES=$(sed -n "$((FIRST_LINE + 1)),$((NEXT_LINE - 1))p" "$CHANGELOG_FILE")
210
+ fi
211
+
212
+ echo "$NOTES" | sed '/^\[.*\]:.*$/d' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
213
+ > /tmp/release_notes.md
214
+
215
+ - name: Upload release notes
216
+ if: steps.decide.outputs.should_release == 'true'
217
+ uses: actions/upload-artifact@v6
218
+ with:
219
+ name: release-notes
220
+ path: /tmp/release_notes.md
221
+ retention-days: 1
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # 4. Build Linux binary (PyInstaller)
225
+ # ---------------------------------------------------------------------------
226
+ build-linux:
227
+ name: Build Linux Binary
228
+ runs-on: ubuntu-latest
229
+ needs: resolve-version
230
+ if: needs.resolve-version.outputs.should_release == 'true'
231
+
232
+ steps:
233
+ - name: Checkout repository
234
+ uses: actions/checkout@v6
235
+
236
+ - name: Set up Python 3.14
237
+ uses: actions/setup-python@v6
238
+ with:
239
+ python-version: '3.14'
240
+ cache: 'pip'
241
+
242
+ - name: Install dependencies and PyInstaller
243
+ run: |
244
+ python -m pip install --upgrade pip
245
+ pip install -e "." pyinstaller
246
+
247
+ - name: Patch version
248
+ shell: bash
249
+ run: |
250
+ VERSION="${{ needs.resolve-version.outputs.version }}"
251
+ sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" \
252
+ src/pfsentinel/__init__.py
253
+
254
+ - name: Build Linux binary
255
+ run: |
256
+ pyinstaller \
257
+ --onefile \
258
+ --console \
259
+ --name pfs \
260
+ --hidden-import paramiko \
261
+ --hidden-import paramiko.transport \
262
+ --hidden-import paramiko.sftp_client \
263
+ --hidden-import paramiko.sftp_handle \
264
+ --hidden-import paramiko.server \
265
+ --hidden-import paramiko.auth_handler \
266
+ --hidden-import cryptography.hazmat.backends \
267
+ --hidden-import cryptography.hazmat.backends.openssl \
268
+ --hidden-import keyring.backends.fail \
269
+ --hidden-import keyrings.alt \
270
+ --hidden-import keyrings.alt.file \
271
+ --hidden-import rich \
272
+ --hidden-import pydantic \
273
+ --hidden-import lxml \
274
+ --hidden-import lxml.etree \
275
+ --hidden-import lxml._elementpath \
276
+ --hidden-import schedule \
277
+ --hidden-import loguru \
278
+ --hidden-import pfsentinel.cli.commands.backup \
279
+ --hidden-import pfsentinel.cli.commands.config \
280
+ --hidden-import pfsentinel.cli.commands.device \
281
+ --hidden-import pfsentinel.cli.commands.update \
282
+ --exclude-module tkinter \
283
+ --exclude-module customtkinter \
284
+ src/pfsentinel/__main__.py
285
+
286
+ - name: Make binary executable
287
+ run: chmod +x dist/pfs
288
+
289
+ - name: Smoke test
290
+ run: dist/pfs --version
291
+
292
+ - name: Upload Linux binary
293
+ uses: actions/upload-artifact@v6
294
+ with:
295
+ name: binary-linux
296
+ path: dist/pfs
297
+ retention-days: 7
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # 5. Build Windows binary (PyInstaller)
301
+ # ---------------------------------------------------------------------------
302
+ build-windows:
303
+ name: Build Windows Binary
304
+ runs-on: windows-latest
305
+ needs: resolve-version
306
+ if: needs.resolve-version.outputs.should_release == 'true'
307
+
308
+ steps:
309
+ - name: Checkout repository
310
+ uses: actions/checkout@v6
311
+
312
+ - name: Set up Python 3.14
313
+ uses: actions/setup-python@v6
314
+ with:
315
+ python-version: '3.14'
316
+ cache: 'pip'
317
+
318
+ - name: Install dependencies and PyInstaller
319
+ run: |
320
+ python -m pip install --upgrade pip
321
+ pip install -e "." pyinstaller winotify
322
+
323
+ - name: Patch version
324
+ shell: bash
325
+ run: |
326
+ VERSION="${{ needs.resolve-version.outputs.version }}"
327
+ sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" \
328
+ src/pfsentinel/__init__.py
329
+
330
+ - name: Build Windows .exe
331
+ shell: bash
332
+ run: python scripts/build_exe.py
333
+
334
+ - name: Upload Windows binary
335
+ uses: actions/upload-artifact@v6
336
+ with:
337
+ name: binary-windows
338
+ path: dist/pfs.exe
339
+ retention-days: 7
340
+
341
+ # ---------------------------------------------------------------------------
342
+ # 6. Build Python wheel + sdist
343
+ # ---------------------------------------------------------------------------
344
+ build-wheel:
345
+ name: Build Python Package
346
+ runs-on: ubuntu-latest
347
+ needs: resolve-version
348
+ if: needs.resolve-version.outputs.should_release == 'true'
349
+
350
+ steps:
351
+ - name: Checkout repository
352
+ uses: actions/checkout@v6
353
+
354
+ - name: Set up Python 3.14
355
+ uses: actions/setup-python@v6
356
+ with:
357
+ python-version: '3.14'
358
+ cache: 'pip'
359
+
360
+ - name: Install build tooling
361
+ run: |
362
+ python -m pip install --upgrade pip
363
+ pip install build twine
364
+
365
+ - name: Patch version
366
+ shell: bash
367
+ run: |
368
+ VERSION="${{ needs.resolve-version.outputs.version }}"
369
+ sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" \
370
+ src/pfsentinel/__init__.py
371
+
372
+ - name: Build source and wheel distributions
373
+ run: python -m build
374
+
375
+ - name: Verify dist artifacts
376
+ run: twine check dist/*
377
+
378
+ - name: Upload wheel artifacts
379
+ uses: actions/upload-artifact@v6
380
+ with:
381
+ name: dist-wheel
382
+ path: dist/
383
+ retention-days: 7
384
+
385
+ # ---------------------------------------------------------------------------
386
+ # 7. Create GitHub Release
387
+ # ---------------------------------------------------------------------------
388
+ release:
389
+ name: Create GitHub Release
390
+ runs-on: ubuntu-latest
391
+ needs: [resolve-version, build-linux, build-windows, build-wheel]
392
+ if: needs.resolve-version.outputs.should_release == 'true'
393
+ permissions:
394
+ contents: write
395
+
396
+ steps:
397
+ - name: Download all artifacts
398
+ uses: actions/download-artifact@v6
399
+ with:
400
+ path: artifacts/
401
+
402
+ - name: Collect release files
403
+ shell: bash
404
+ run: |
405
+ mkdir -p release_files
406
+ cp artifacts/dist-wheel/*.whl release_files/ 2>/dev/null || true
407
+ cp artifacts/dist-wheel/*.tar.gz release_files/ 2>/dev/null || true
408
+ cp artifacts/binary-linux/* release_files/ 2>/dev/null || true
409
+ cp artifacts/binary-windows/* release_files/ 2>/dev/null || true
410
+ echo "Release files:"
411
+ ls -lh release_files/
412
+
413
+ - name: Generate SHA256 checksums
414
+ shell: bash
415
+ run: |
416
+ cd release_files
417
+ sha256sum * > checksums-sha256.txt
418
+ cat checksums-sha256.txt
419
+
420
+ - name: Create GitHub Release
421
+ uses: softprops/action-gh-release@v2
422
+ with:
423
+ tag_name: ${{ needs.resolve-version.outputs.tag }}
424
+ name: pfSentinel ${{ needs.resolve-version.outputs.tag }}
425
+ body_path: artifacts/release-notes/release_notes.md
426
+ draft: false
427
+ prerelease: false
428
+ files: release_files/*
429
+ env:
430
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
431
+
432
+ # ---------------------------------------------------------------------------
433
+ # 8. Publish to TestPyPI (trusted publishing / OIDC)
434
+ # ---------------------------------------------------------------------------
435
+ publish-testpypi:
436
+ name: Publish to TestPyPI
437
+ runs-on: ubuntu-latest
438
+ needs: [resolve-version, build-wheel, release]
439
+ if: needs.resolve-version.outputs.should_release == 'true'
440
+ environment: testpypi
441
+ permissions:
442
+ id-token: write
443
+
444
+ steps:
445
+ - name: Download dist artifacts
446
+ uses: actions/download-artifact@v6
447
+ with:
448
+ name: dist-wheel
449
+ path: dist/
450
+
451
+ - name: Publish to TestPyPI
452
+ uses: pypa/gh-action-pypi-publish@release/v1
453
+ with:
454
+ repository-url: https://test.pypi.org/legacy/
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # 9. Publish to PyPI (trusted publishing / OIDC)
458
+ # ---------------------------------------------------------------------------
459
+ publish-pypi:
460
+ name: Publish to PyPI
461
+ runs-on: ubuntu-latest
462
+ needs: [resolve-version, build-wheel, release, publish-testpypi]
463
+ if: needs.resolve-version.outputs.should_release == 'true'
464
+ environment: pypi
465
+ permissions:
466
+ id-token: write
467
+
468
+ steps:
469
+ - name: Download dist artifacts
470
+ uses: actions/download-artifact@v6
471
+ with:
472
+ name: dist-wheel
473
+ path: dist/
474
+
475
+ - name: Publish to PyPI
476
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,42 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.egg
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ .env
12
+ .venv
13
+ venv/
14
+ env/
15
+ *.spec
16
+ .pytest_cache/
17
+ .coverage
18
+ htmlcov/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ *.log
22
+ .venv-linux/
23
+ .venv-*/
24
+ .vscode*/
25
+
26
+ # Internal AI/IDE tooling (used locally, not published)
27
+ .github/instructions/
28
+
29
+ # Security audit (private — findings tracked in SECURITY.md changelog)
30
+ docs/security-audit.md
31
+
32
+ # Private keys and certificates — never commit these
33
+ *.pem
34
+ *.key
35
+ *.p12
36
+ *.pfx
37
+ *.crt
38
+ *.cer
39
+ id_rsa
40
+ id_ed25519
41
+ id_ecdsa
42
+ docs/PUBLIC_REPO_GUIDE.md