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.
- pfsentinel-0.1.0/.gitattributes +30 -0
- pfsentinel-0.1.0/.github/CODEOWNERS +2 -0
- pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
- pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- pfsentinel-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +36 -0
- pfsentinel-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- pfsentinel-0.1.0/.github/workflows/ci.yml +476 -0
- pfsentinel-0.1.0/.gitignore +42 -0
- pfsentinel-0.1.0/CHANGELOG.md +49 -0
- pfsentinel-0.1.0/CONTRIBUTING.md +125 -0
- pfsentinel-0.1.0/LICENSE +21 -0
- pfsentinel-0.1.0/PKG-INFO +146 -0
- pfsentinel-0.1.0/README.md +101 -0
- pfsentinel-0.1.0/ROADMAP.md +51 -0
- pfsentinel-0.1.0/SECURITY.md +173 -0
- pfsentinel-0.1.0/THIRD_PARTY_LICENSES.txt +122 -0
- pfsentinel-0.1.0/assets/pfsentinel.ico +0 -0
- pfsentinel-0.1.0/docs/LICENSE_AUDIT.md +83 -0
- pfsentinel-0.1.0/docs/extended-backups.md +228 -0
- pfsentinel-0.1.0/docs/installation.md +113 -0
- pfsentinel-0.1.0/docs/usage.md +581 -0
- pfsentinel-0.1.0/pyproject.toml +107 -0
- pfsentinel-0.1.0/renovate.json +52 -0
- pfsentinel-0.1.0/scripts/build_exe.py +129 -0
- pfsentinel-0.1.0/src/pfsentinel/__init__.py +3 -0
- pfsentinel-0.1.0/src/pfsentinel/__main__.py +6 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/app.py +244 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/__init__.py +1 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/backup.py +693 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/config.py +126 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/device.py +362 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/notify.py +383 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/schedule.py +129 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/commands/update.py +100 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/formatters.py +132 -0
- pfsentinel-0.1.0/src/pfsentinel/cli/list.py +44 -0
- pfsentinel-0.1.0/src/pfsentinel/models/__init__.py +18 -0
- pfsentinel-0.1.0/src/pfsentinel/models/backup.py +156 -0
- pfsentinel-0.1.0/src/pfsentinel/models/config.py +217 -0
- pfsentinel-0.1.0/src/pfsentinel/models/device.py +107 -0
- pfsentinel-0.1.0/src/pfsentinel/models/zfs.py +50 -0
- pfsentinel-0.1.0/src/pfsentinel/services/__init__.py +1 -0
- pfsentinel-0.1.0/src/pfsentinel/services/archive_backup.py +110 -0
- pfsentinel-0.1.0/src/pfsentinel/services/backup.py +343 -0
- pfsentinel-0.1.0/src/pfsentinel/services/connection.py +592 -0
- pfsentinel-0.1.0/src/pfsentinel/services/credentials.py +217 -0
- pfsentinel-0.1.0/src/pfsentinel/services/diff.py +139 -0
- pfsentinel-0.1.0/src/pfsentinel/services/extra_backup.py +371 -0
- pfsentinel-0.1.0/src/pfsentinel/services/notifications.py +172 -0
- pfsentinel-0.1.0/src/pfsentinel/services/orchestrator.py +207 -0
- pfsentinel-0.1.0/src/pfsentinel/services/retention.py +113 -0
- pfsentinel-0.1.0/src/pfsentinel/services/scheduler.py +180 -0
- pfsentinel-0.1.0/src/pfsentinel/services/updater.py +470 -0
- pfsentinel-0.1.0/src/pfsentinel/services/zfs_backup.py +250 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/__init__.py +1 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/app.py +85 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/__init__.py +1 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/backups.py +162 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/dashboard.py +95 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/devices.py +105 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/logs.py +64 -0
- pfsentinel-0.1.0/src/pfsentinel/tui/screens/settings.py +121 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/__init__.py +1 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/checksum.py +32 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/compression.py +41 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/naming.py +131 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/platform.py +117 -0
- pfsentinel-0.1.0/src/pfsentinel/utils/xml_parser.py +107 -0
- pfsentinel-0.1.0/tests/__init__.py +0 -0
- pfsentinel-0.1.0/tests/conftest.py +131 -0
- pfsentinel-0.1.0/tests/integration/__init__.py +0 -0
- pfsentinel-0.1.0/tests/integration/test_backup_workflow.py +188 -0
- pfsentinel-0.1.0/tests/unit/__init__.py +0 -0
- pfsentinel-0.1.0/tests/unit/test_archive_backup.py +128 -0
- pfsentinel-0.1.0/tests/unit/test_backup_models.py +250 -0
- pfsentinel-0.1.0/tests/unit/test_backup_service.py +164 -0
- pfsentinel-0.1.0/tests/unit/test_checksum.py +44 -0
- pfsentinel-0.1.0/tests/unit/test_compression.py +59 -0
- pfsentinel-0.1.0/tests/unit/test_config_models.py +128 -0
- pfsentinel-0.1.0/tests/unit/test_config_models_extended.py +129 -0
- pfsentinel-0.1.0/tests/unit/test_connection.py +183 -0
- pfsentinel-0.1.0/tests/unit/test_credentials.py +109 -0
- pfsentinel-0.1.0/tests/unit/test_device_models.py +112 -0
- pfsentinel-0.1.0/tests/unit/test_diff.py +112 -0
- pfsentinel-0.1.0/tests/unit/test_extra_backup.py +184 -0
- pfsentinel-0.1.0/tests/unit/test_formatters.py +148 -0
- pfsentinel-0.1.0/tests/unit/test_naming.py +107 -0
- pfsentinel-0.1.0/tests/unit/test_naming_extended.py +115 -0
- pfsentinel-0.1.0/tests/unit/test_notifications.py +153 -0
- pfsentinel-0.1.0/tests/unit/test_orchestrator.py +166 -0
- pfsentinel-0.1.0/tests/unit/test_platform.py +105 -0
- pfsentinel-0.1.0/tests/unit/test_retention_typed.py +162 -0
- pfsentinel-0.1.0/tests/unit/test_selective_backup.py +143 -0
- pfsentinel-0.1.0/tests/unit/test_ssh_extended.py +200 -0
- pfsentinel-0.1.0/tests/unit/test_ssh_key_auth.py +228 -0
- pfsentinel-0.1.0/tests/unit/test_updater.py +1232 -0
- pfsentinel-0.1.0/tests/unit/test_verify_typed.py +189 -0
- pfsentinel-0.1.0/tests/unit/test_xml_parser.py +172 -0
- pfsentinel-0.1.0/tests/unit/test_zfs_backup.py +179 -0
- 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,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
|