sshscript 2.0.2__tar.gz → 3.1.4__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.
- sshscript-3.1.4/.github/dependabot.yml +27 -0
- sshscript-3.1.4/.github/workflows/ci.yml +85 -0
- sshscript-3.1.4/.github/workflows/codeql.yml +30 -0
- sshscript-3.1.4/.github/workflows/release.yml +214 -0
- sshscript-3.1.4/CHANGELOG.md +55 -0
- sshscript-3.1.4/CONTRIBUTING.md +56 -0
- sshscript-3.1.4/EXCEPTIONS.md +45 -0
- {sshscript-2.0.2 → sshscript-3.1.4}/LICENSE.txt +1 -1
- sshscript-3.1.4/MANIFEST.in +7 -0
- sshscript-3.1.4/PKG-INFO +178 -0
- sshscript-3.1.4/README.md +153 -0
- sshscript-3.1.4/RELEASING.md +29 -0
- sshscript-3.1.4/SECURITY.md +37 -0
- sshscript-3.1.4/SUPPORT.md +23 -0
- sshscript-3.1.4/pyproject.toml +46 -0
- sshscript-3.1.4/setup.cfg +4 -0
- sshscript-3.1.4/src/sshscript/__init__.py +82 -0
- sshscript-3.1.4/src/sshscript/_version.py +3 -0
- sshscript-3.1.4/src/sshscript/channelgeneric.py +2028 -0
- sshscript-3.1.4/src/sshscript/channelssh.py +289 -0
- sshscript-3.1.4/src/sshscript/channelsubprocess.py +294 -0
- sshscript-3.1.4/src/sshscript/channelutils.py +444 -0
- sshscript-3.1.4/src/sshscript/dollar.py +589 -0
- sshscript-3.1.4/src/sshscript/dollarchanger.py +579 -0
- sshscript-3.1.4/src/sshscript/dollarparser.py +263 -0
- sshscript-3.1.4/src/sshscript/errorutils.py +486 -0
- sshscript-3.1.4/src/sshscript/patching.py +190 -0
- sshscript-3.1.4/src/sshscript/session.py +1537 -0
- sshscript-3.1.4/src/sshscript/sessionwrapper.py +300 -0
- sshscript-3.1.4/src/sshscript/spyimporter.py +157 -0
- sshscript-3.1.4/src/sshscript/sshscript.py +412 -0
- sshscript-3.1.4/src/sshscript/stdio.py +424 -0
- sshscript-3.1.4/src/sshscript/tokenparser.py +480 -0
- sshscript-3.1.4/src/sshscript/unittest/check_package_asserts.py +18 -0
- sshscript-3.1.4/src/sshscript/unittest/dollar_syntax.spy +253 -0
- sshscript-3.1.4/src/sshscript/unittest/dollar_syntax_fixture.spy +13 -0
- sshscript-3.1.4/src/sshscript/unittest/language.spy +533 -0
- sshscript-3.1.4/src/sshscript/unittest/language_fixture.spy +10 -0
- sshscript-3.1.4/src/sshscript/unittest/test_channelgeneric_expect.py +89 -0
- sshscript-3.1.4/src/sshscript/unittest/test_file_transfer.py +196 -0
- sshscript-3.1.4/src/sshscript/unittest/test_logger_api.py +232 -0
- sshscript-3.1.4/src/sshscript/unittest/test_logger_integration.py +229 -0
- sshscript-3.1.4/src/sshscript/unittest/test_openssh_integration.py +308 -0
- sshscript-3.1.4/src/sshscript/unittest/test_production_contract.py +371 -0
- sshscript-3.1.4/src/sshscript/unittest/test_session_close_reporting.py +183 -0
- sshscript-3.1.4/src/sshscript/unittest/test_session_proxy_cleanup.py +179 -0
- sshscript-3.1.4/src/sshscript/unittest/test_spy_source_mapping.py +297 -0
- sshscript-3.1.4/src/sshscript/unittest/test_spy_thread_session.py +230 -0
- sshscript-3.1.4/src/sshscript/unittest/test_ssh_security.py +281 -0
- sshscript-3.1.4/src/sshscript/unittest/test_sshscript_dollar_syntax.py +54 -0
- sshscript-3.1.4/src/sshscript/unittest/test_sshscript_module.py +479 -0
- sshscript-3.1.4/src/sshscript/unittest/test_stdio_dynamic_string.py +71 -0
- sshscript-3.1.4/src/sshscript/unittest/test_syntax_error.spy +21 -0
- sshscript-3.1.4/src/sshscript/unittest/test_update_check.py +76 -0
- sshscript-3.1.4/src/sshscript.egg-info/PKG-INFO +178 -0
- sshscript-3.1.4/src/sshscript.egg-info/SOURCES.txt +63 -0
- {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/dependency_links.txt +0 -0
- {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/entry_points.txt +0 -0
- sshscript-3.1.4/src/sshscript.egg-info/requires.txt +2 -0
- {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/top_level.txt +0 -0
- sshscript-3.1.4/tools/check_release.py +50 -0
- sshscript-3.1.4/tools/prepare_release.py +54 -0
- sshscript-3.1.4/tools/publish_release.py +26 -0
- sshscript-3.1.4/tools/run_checks.py +19 -0
- sshscript-3.1.4/tools/setup_openssh_ci.sh +192 -0
- sshscript-2.0.2/PKG-INFO +0 -134
- sshscript-2.0.2/README.rst +0 -106
- sshscript-2.0.2/setup.cfg +0 -13
- sshscript-2.0.2/setup.py +0 -44
- sshscript-2.0.2/src/sshscript/__init__.py +0 -33
- sshscript-2.0.2/src/sshscript/sshscript.py +0 -293
- sshscript-2.0.2/src/sshscript/sshscriptchannel.py +0 -1152
- sshscript-2.0.2/src/sshscript/sshscriptchannelutils.py +0 -506
- sshscript-2.0.2/src/sshscript/sshscriptdollar.py +0 -421
- sshscript-2.0.2/src/sshscript/sshscripterror.py +0 -70
- sshscript-2.0.2/src/sshscript/sshscriptparser.py +0 -1049
- sshscript-2.0.2/src/sshscript/sshscriptpatching.py +0 -110
- sshscript-2.0.2/src/sshscript/sshscriptsession.py +0 -988
- sshscript-2.0.2/src/sshscript.egg-info/PKG-INFO +0 -134
- sshscript-2.0.2/src/sshscript.egg-info/SOURCES.txt +0 -19
- sshscript-2.0.2/src/sshscript.egg-info/requires.txt +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: pip
|
|
4
|
+
directory: /
|
|
5
|
+
target-branch: release
|
|
6
|
+
schedule:
|
|
7
|
+
interval: weekly
|
|
8
|
+
groups:
|
|
9
|
+
python-patch-and-minor:
|
|
10
|
+
patterns:
|
|
11
|
+
- "*"
|
|
12
|
+
update-types:
|
|
13
|
+
- patch
|
|
14
|
+
- minor
|
|
15
|
+
|
|
16
|
+
- package-ecosystem: github-actions
|
|
17
|
+
directory: /
|
|
18
|
+
target-branch: release
|
|
19
|
+
schedule:
|
|
20
|
+
interval: weekly
|
|
21
|
+
groups:
|
|
22
|
+
actions-patch-and-minor:
|
|
23
|
+
patterns:
|
|
24
|
+
- "*"
|
|
25
|
+
update-types:
|
|
26
|
+
- patch
|
|
27
|
+
- minor
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
verify:
|
|
7
|
+
timeout-minutes: 20
|
|
8
|
+
strategy:
|
|
9
|
+
fail-fast: false
|
|
10
|
+
matrix:
|
|
11
|
+
os: [ubuntu-latest, macos-latest]
|
|
12
|
+
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install test and build dependencies
|
|
22
|
+
run: python -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
|
|
23
|
+
- name: Test source
|
|
24
|
+
run: python tools/run_checks.py
|
|
25
|
+
- name: Build and verify installed release
|
|
26
|
+
run: python tools/check_release.py --output '${{ runner.temp }}/verified-artifacts'
|
|
27
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
28
|
+
with:
|
|
29
|
+
name: sshscript-${{ matrix.os }}-${{ matrix.python-version }}
|
|
30
|
+
path: ${{ runner.temp }}/verified-artifacts/
|
|
31
|
+
|
|
32
|
+
openssh-integration:
|
|
33
|
+
name: OpenSSH integration (Python ${{ matrix.python-version }})
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
timeout-minutes: 25
|
|
36
|
+
strategy:
|
|
37
|
+
fail-fast: false
|
|
38
|
+
matrix:
|
|
39
|
+
python-version: ['3.11', '3.14']
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
42
|
+
with:
|
|
43
|
+
persist-credentials: false
|
|
44
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
45
|
+
with:
|
|
46
|
+
python-version: ${{ matrix.python-version }}
|
|
47
|
+
- name: Install test and build dependencies
|
|
48
|
+
run: python -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
|
|
49
|
+
- name: Build and verify wheel
|
|
50
|
+
run: python tools/check_release.py --output "$RUNNER_TEMP/verified-artifacts"
|
|
51
|
+
- name: Install verified wheel
|
|
52
|
+
run: python -m pip install --force-reinstall "$RUNNER_TEMP"/verified-artifacts/*.whl
|
|
53
|
+
- name: Provision loopback OpenSSH server
|
|
54
|
+
run: bash tools/setup_openssh_ci.sh "$GITHUB_ENV"
|
|
55
|
+
- name: Test installed wheel against OpenSSH
|
|
56
|
+
run: |
|
|
57
|
+
mkdir -p "$RUNNER_TEMP/openssh-tests"
|
|
58
|
+
cp src/sshscript/unittest/test_openssh_integration.py "$RUNNER_TEMP/openssh-tests/"
|
|
59
|
+
cd "$RUNNER_TEMP/openssh-tests"
|
|
60
|
+
python -I test_openssh_integration.py -v
|
|
61
|
+
- name: Upload OpenSSH server log
|
|
62
|
+
if: ${{ failure() && env.SSHSCRIPT_OPENSSH_LOG != '' }}
|
|
63
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
64
|
+
with:
|
|
65
|
+
name: openssh-log-python-${{ matrix.python-version }}
|
|
66
|
+
if-no-files-found: warn
|
|
67
|
+
path: ${{ env.SSHSCRIPT_OPENSSH_LOG }}
|
|
68
|
+
- name: Remove disposable OpenSSH state
|
|
69
|
+
if: always()
|
|
70
|
+
run: |
|
|
71
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_PID_FILE:-}" && -s "$SSHSCRIPT_OPENSSH_PID_FILE" ]]; then
|
|
72
|
+
sudo kill "$(cat "$SSHSCRIPT_OPENSSH_PID_FILE")" || true
|
|
73
|
+
fi
|
|
74
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_SUDOERS:-}" ]]; then
|
|
75
|
+
sudo rm -f "$SSHSCRIPT_OPENSSH_SUDOERS"
|
|
76
|
+
fi
|
|
77
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_USER:-}" ]]; then
|
|
78
|
+
sudo userdel -r "$SSHSCRIPT_OPENSSH_USER" 2>/dev/null || true
|
|
79
|
+
fi
|
|
80
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_TARGET_USER:-}" ]]; then
|
|
81
|
+
sudo userdel -r "$SSHSCRIPT_OPENSSH_TARGET_USER" 2>/dev/null || true
|
|
82
|
+
fi
|
|
83
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_ROOT:-}" && "$SSHSCRIPT_OPENSSH_ROOT" == "$RUNNER_TEMP"/sshscript-openssh.* ]]; then
|
|
84
|
+
sudo rm -rf -- "$SSHSCRIPT_OPENSSH_ROOT"
|
|
85
|
+
fi
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [release]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [release]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 3 * * 1"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
analyze:
|
|
17
|
+
name: CodeQL (Python and Actions)
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 20
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
security-events: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
- uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
|
|
28
|
+
with:
|
|
29
|
+
languages: python, actions
|
|
30
|
+
- uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Verify and attest release artifacts
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
timeout-minutes: 20
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
id-token: write
|
|
19
|
+
attestations: write
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.11"
|
|
27
|
+
- name: Install pinned build dependencies
|
|
28
|
+
run: >-
|
|
29
|
+
python -m pip install
|
|
30
|
+
'paramiko>=2.11,<5' 'packaging>=21'
|
|
31
|
+
'build==1.6.1' 'twine==7.0.0'
|
|
32
|
+
- name: Verify tag targets the current release branch
|
|
33
|
+
run: |
|
|
34
|
+
git fetch --no-tags --depth=1 origin release
|
|
35
|
+
test "$(git rev-parse HEAD)" = "$(git rev-parse FETCH_HEAD)"
|
|
36
|
+
- name: Verify tag and package version
|
|
37
|
+
env:
|
|
38
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
39
|
+
run: |
|
|
40
|
+
PACKAGE_VERSION=$(python -c "import runpy; print(runpy.run_path('src/sshscript/_version.py')['__version__'])")
|
|
41
|
+
test "$RELEASE_TAG" = "v$PACKAGE_VERSION"
|
|
42
|
+
- name: Stabilize build timestamps
|
|
43
|
+
run: echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)" >> "$GITHUB_ENV"
|
|
44
|
+
- name: Run release gates and build verified artifacts
|
|
45
|
+
run: |
|
|
46
|
+
python tools/run_checks.py
|
|
47
|
+
python tools/check_release.py --output "$RUNNER_TEMP/sshscript-release"
|
|
48
|
+
- name: Attest wheel and source distribution
|
|
49
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
|
|
50
|
+
with:
|
|
51
|
+
subject-path: |
|
|
52
|
+
${{ runner.temp }}/sshscript-release/*.whl
|
|
53
|
+
${{ runner.temp }}/sshscript-release/*.tar.gz
|
|
54
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
55
|
+
with:
|
|
56
|
+
name: python-package-distributions
|
|
57
|
+
if-no-files-found: error
|
|
58
|
+
path: ${{ runner.temp }}/sshscript-release/
|
|
59
|
+
|
|
60
|
+
openssh-integration:
|
|
61
|
+
name: Release OpenSSH integration (Python ${{ matrix.python-version }})
|
|
62
|
+
needs: build
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
timeout-minutes: 15
|
|
65
|
+
strategy:
|
|
66
|
+
fail-fast: false
|
|
67
|
+
matrix:
|
|
68
|
+
python-version: ['3.11', '3.14']
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
71
|
+
with:
|
|
72
|
+
persist-credentials: false
|
|
73
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
74
|
+
with:
|
|
75
|
+
python-version: ${{ matrix.python-version }}
|
|
76
|
+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
|
77
|
+
with:
|
|
78
|
+
name: python-package-distributions
|
|
79
|
+
path: dist
|
|
80
|
+
- name: Install verified wheel
|
|
81
|
+
run: python -m pip install dist/*.whl
|
|
82
|
+
- name: Provision loopback OpenSSH server
|
|
83
|
+
run: bash tools/setup_openssh_ci.sh "$GITHUB_ENV"
|
|
84
|
+
- name: Test installed wheel against OpenSSH
|
|
85
|
+
run: |
|
|
86
|
+
mkdir -p "$RUNNER_TEMP/openssh-tests"
|
|
87
|
+
cp src/sshscript/unittest/test_openssh_integration.py "$RUNNER_TEMP/openssh-tests/"
|
|
88
|
+
cd "$RUNNER_TEMP/openssh-tests"
|
|
89
|
+
python -I test_openssh_integration.py -v
|
|
90
|
+
- name: Upload OpenSSH server log
|
|
91
|
+
if: ${{ failure() && env.SSHSCRIPT_OPENSSH_LOG != '' }}
|
|
92
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
93
|
+
with:
|
|
94
|
+
name: release-openssh-log-python-${{ matrix.python-version }}
|
|
95
|
+
if-no-files-found: warn
|
|
96
|
+
path: ${{ env.SSHSCRIPT_OPENSSH_LOG }}
|
|
97
|
+
- name: Remove disposable OpenSSH state
|
|
98
|
+
if: always()
|
|
99
|
+
run: |
|
|
100
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_PID_FILE:-}" && -s "$SSHSCRIPT_OPENSSH_PID_FILE" ]]; then
|
|
101
|
+
sudo kill "$(cat "$SSHSCRIPT_OPENSSH_PID_FILE")" || true
|
|
102
|
+
fi
|
|
103
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_SUDOERS:-}" ]]; then
|
|
104
|
+
sudo rm -f "$SSHSCRIPT_OPENSSH_SUDOERS"
|
|
105
|
+
fi
|
|
106
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_USER:-}" ]]; then
|
|
107
|
+
sudo userdel -r "$SSHSCRIPT_OPENSSH_USER" 2>/dev/null || true
|
|
108
|
+
fi
|
|
109
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_TARGET_USER:-}" ]]; then
|
|
110
|
+
sudo userdel -r "$SSHSCRIPT_OPENSSH_TARGET_USER" 2>/dev/null || true
|
|
111
|
+
fi
|
|
112
|
+
if [[ -n "${SSHSCRIPT_OPENSSH_ROOT:-}" && "$SSHSCRIPT_OPENSSH_ROOT" == "$RUNNER_TEMP"/sshscript-openssh.* ]]; then
|
|
113
|
+
sudo rm -rf -- "$SSHSCRIPT_OPENSSH_ROOT"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
draft-github-release:
|
|
117
|
+
name: Create draft GitHub Release
|
|
118
|
+
needs: [build, openssh-integration]
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
permissions:
|
|
121
|
+
contents: write
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
124
|
+
with:
|
|
125
|
+
python-version: "3.11"
|
|
126
|
+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
|
127
|
+
with:
|
|
128
|
+
name: python-package-distributions
|
|
129
|
+
path: dist
|
|
130
|
+
- name: Verify artifact hashes
|
|
131
|
+
run: |
|
|
132
|
+
python - <<'PY'
|
|
133
|
+
import hashlib
|
|
134
|
+
import json
|
|
135
|
+
from pathlib import Path
|
|
136
|
+
|
|
137
|
+
root = Path("dist")
|
|
138
|
+
manifest = json.loads((root / "verified.json").read_text())
|
|
139
|
+
if len(manifest) != 2:
|
|
140
|
+
raise SystemExit("expected exactly two verified distributions")
|
|
141
|
+
for name, expected in manifest.items():
|
|
142
|
+
if Path(name).name != name:
|
|
143
|
+
raise SystemExit(f"invalid distribution name: {name}")
|
|
144
|
+
actual = hashlib.sha256((root / name).read_bytes()).hexdigest()
|
|
145
|
+
if actual != expected:
|
|
146
|
+
raise SystemExit(f"artifact hash mismatch: {name}")
|
|
147
|
+
PY
|
|
148
|
+
- name: Create immutable draft with verified assets
|
|
149
|
+
env:
|
|
150
|
+
GH_TOKEN: ${{ github.token }}
|
|
151
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
152
|
+
run: >-
|
|
153
|
+
gh release create "$RELEASE_TAG"
|
|
154
|
+
dist/*.whl dist/*.tar.gz dist/verified.json
|
|
155
|
+
--repo "$GITHUB_REPOSITORY"
|
|
156
|
+
--verify-tag --draft --generate-notes
|
|
157
|
+
--title "SSHScript ${RELEASE_TAG#v}"
|
|
158
|
+
|
|
159
|
+
publish-to-pypi:
|
|
160
|
+
name: Publish distributions to PyPI
|
|
161
|
+
needs: draft-github-release
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
environment:
|
|
164
|
+
name: pypi
|
|
165
|
+
url: https://pypi.org/p/sshscript
|
|
166
|
+
permissions:
|
|
167
|
+
contents: read
|
|
168
|
+
id-token: write
|
|
169
|
+
steps:
|
|
170
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
171
|
+
with:
|
|
172
|
+
python-version: "3.11"
|
|
173
|
+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
|
174
|
+
with:
|
|
175
|
+
name: python-package-distributions
|
|
176
|
+
path: dist
|
|
177
|
+
- name: Recheck hashes and isolate distributions
|
|
178
|
+
run: |
|
|
179
|
+
python - <<'PY'
|
|
180
|
+
import hashlib
|
|
181
|
+
import json
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
root = Path("dist")
|
|
185
|
+
manifest = json.loads((root / "verified.json").read_text())
|
|
186
|
+
if len(manifest) != 2:
|
|
187
|
+
raise SystemExit("expected exactly two verified distributions")
|
|
188
|
+
publish = Path("publish")
|
|
189
|
+
publish.mkdir()
|
|
190
|
+
for name, expected in manifest.items():
|
|
191
|
+
if Path(name).name != name:
|
|
192
|
+
raise SystemExit(f"invalid distribution name: {name}")
|
|
193
|
+
source = root / name
|
|
194
|
+
actual = hashlib.sha256(source.read_bytes()).hexdigest()
|
|
195
|
+
if actual != expected:
|
|
196
|
+
raise SystemExit(f"artifact hash mismatch: {name}")
|
|
197
|
+
(publish / name).write_bytes(source.read_bytes())
|
|
198
|
+
PY
|
|
199
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
200
|
+
with:
|
|
201
|
+
packages-dir: publish/
|
|
202
|
+
|
|
203
|
+
publish-github-release:
|
|
204
|
+
name: Publish GitHub Release
|
|
205
|
+
needs: publish-to-pypi
|
|
206
|
+
runs-on: ubuntu-latest
|
|
207
|
+
permissions:
|
|
208
|
+
contents: write
|
|
209
|
+
steps:
|
|
210
|
+
- name: Publish the verified draft
|
|
211
|
+
env:
|
|
212
|
+
GH_TOKEN: ${{ github.token }}
|
|
213
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
214
|
+
run: gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.1.4 - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Production hardening
|
|
6
|
+
|
|
7
|
+
- Require Python 3.11 or newer; CI targets 3.11, 3.12, 3.13, and 3.14.
|
|
8
|
+
- Replace package runtime assertions with the stable exception contract in
|
|
9
|
+
EXCEPTIONS.md, including validation under `python -O`.
|
|
10
|
+
- Make listener removal, hijack/release, and last-layer checks atomic and
|
|
11
|
+
non-mutating on failure; fix positive session-stack indexing.
|
|
12
|
+
- Reject invalid command, PTY, pattern, and output arguments before execution
|
|
13
|
+
or buffer mutation. Disconnected SFTP access raises SSHScriptException.
|
|
14
|
+
- Move embedded stdio tests into the credential-free suite and add normal,
|
|
15
|
+
optimized, and package AST assertion release gates.
|
|
16
|
+
- AssertionError was never a supported API contract. User-written `.spy`
|
|
17
|
+
assertions are preserved, but production command-success handling must use
|
|
18
|
+
explicit checks or `check=True`, because optimized Python removes assertions.
|
|
19
|
+
- Validate closed and hijacked channel operations, AST invariants, persistent
|
|
20
|
+
commands, PTY modes, and output buffers with stable exception types.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
- Verify SSH host keys by default; insecure automatic key acceptance now
|
|
26
|
+
requires an explicit Paramiko policy.
|
|
27
|
+
- Stop forwarding the complete local environment to interactive SSH sessions.
|
|
28
|
+
- Read remote private keys through SFTP instead of interpolated shell commands.
|
|
29
|
+
- Remove SSH hostnames and complete local/remote paths from upload/download
|
|
30
|
+
INFO logs.
|
|
31
|
+
- Add CodeQL, Dependabot, private vulnerability reporting guidance, and an
|
|
32
|
+
OIDC-based trusted publishing workflow.
|
|
33
|
+
- Pin release actions and build tooling, attest distributions, retain their
|
|
34
|
+
hash manifest, and keep the GitHub Release in draft until PyPI succeeds.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Drain remote stdout and stderr concurrently and send stdin EOF.
|
|
39
|
+
- Make session cleanup failures observable and optionally strict.
|
|
40
|
+
- Preserve `$.break(code)` as the CLI exit status.
|
|
41
|
+
- Simplify `run_file()` to execute exactly one file; compose scripts through
|
|
42
|
+
include syntax or Python imports instead of directory/glob execution.
|
|
43
|
+
- Make `.spy` importing explicit with `sshscript.spy_imports()` and scope the
|
|
44
|
+
importer automatically while `run_file()` is executing.
|
|
45
|
+
- Stop patching `threading.Thread`, `warnings`, and `__main__` during import.
|
|
46
|
+
- Preserve `.spy` thread session inheritance with a context-aware Thread
|
|
47
|
+
created by the source transformer instead of a process-wide monkey patch.
|
|
48
|
+
- Make `Session()` construction side-effect free and scope stack activation to
|
|
49
|
+
`run()`, context managers, or an explicit unscoped `$.connect()` operation.
|
|
50
|
+
|
|
51
|
+
### Project
|
|
52
|
+
|
|
53
|
+
- Add packaging metadata, license, contributor guidance, and CI configuration.
|
|
54
|
+
- Add a disposable loopback OpenSSH integration gate covering host keys, SFTP,
|
|
55
|
+
PTY behavior, sudo/su, and timeout handling.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
Use Python 3.11 or newer. The development checkout is a flat source tree, so
|
|
6
|
+
install its development dependencies rather than installing it in editable
|
|
7
|
+
mode:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
python3 -m venv .venv
|
|
11
|
+
. .venv/bin/activate
|
|
12
|
+
python3 -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Required checks
|
|
16
|
+
|
|
17
|
+
Run the complete credential-free gate before proposing a change:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python3 tools/run_checks.py
|
|
21
|
+
python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tests under `unittest/` must not require network access, SSH agents, private
|
|
25
|
+
keys, passwords, or host-specific configuration. A regression fix should add a
|
|
26
|
+
credential-free test whenever the behavior can be reproduced with a fake
|
|
27
|
+
Paramiko client or local subprocess.
|
|
28
|
+
|
|
29
|
+
## Credentialed integration tests
|
|
30
|
+
|
|
31
|
+
`unittest-v3/` contains historical and site-specific SSH scenarios. Treat them
|
|
32
|
+
as manual integration tests:
|
|
33
|
+
|
|
34
|
+
- run them only against disposable hosts;
|
|
35
|
+
- never commit credentials, keys, host inventories, or captured secrets;
|
|
36
|
+
- review commands for destructive effects before execution;
|
|
37
|
+
- use environment variables or ignored local files for configuration.
|
|
38
|
+
|
|
39
|
+
Credentialed tests are not evidence that the credential-free release gate may
|
|
40
|
+
fail. Both layers must be healthy before a release.
|
|
41
|
+
|
|
42
|
+
The public CI also provisions a disposable loopback OpenSSH server and runs
|
|
43
|
+
`unittest/test_openssh_integration.py` against the built wheel. The setup uses
|
|
44
|
+
ephemeral users, passwords and keys generated inside the CI runner; it must
|
|
45
|
+
never target a persistent host. See `tools/setup_openssh_ci.sh` for the exact
|
|
46
|
+
environment contract.
|
|
47
|
+
|
|
48
|
+
## Compatibility and public APIs
|
|
49
|
+
|
|
50
|
+
Changes to `Session`, `run_file()`, CLI exit statuses, dollar syntax, logging,
|
|
51
|
+
or SSH security defaults require synchronized implementation, tests, README,
|
|
52
|
+
and changelog updates. Avoid silently accepting insecure behavior.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Before release, run all normal, optimized, compile, and AST gates listed in
|
|
56
|
+
[EXCEPTIONS.md](EXCEPTIONS.md). CI targets Python 3.11 through 3.14.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Stable exception contract
|
|
2
|
+
|
|
3
|
+
The following contract applies with and without `python -O`.
|
|
4
|
+
|
|
5
|
+
| Failure | Exception |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Wrong argument type | `TypeError` |
|
|
8
|
+
| Correct type but invalid value, content, or combination | `ValueError` |
|
|
9
|
+
| Invalid session, console, or channel lifecycle state | `RuntimeError` |
|
|
10
|
+
| Operation on a closed channel or transport | `BrokenPipeError` |
|
|
11
|
+
| Channel ends while waiting | `EOFError` |
|
|
12
|
+
| Timeout | `TimeoutError` |
|
|
13
|
+
| Disconnected `Session.sftp`, upload, or download | `SSHScriptException` |
|
|
14
|
+
| Filesystem failure | Appropriate `OSError` subclass |
|
|
15
|
+
| Paramiko failure | Original Paramiko exception and traceback |
|
|
16
|
+
| Internal AST or data-structure invariant failure | Descriptive `RuntimeError` |
|
|
17
|
+
| Stack indexing outside its bounds | `IndexError`, following `deque` |
|
|
18
|
+
|
|
19
|
+
Nonzero command exit status remains data unless `check=True` requests failure
|
|
20
|
+
handling. `AssertionError` was never a supported SSHScript API contract.
|
|
21
|
+
|
|
22
|
+
User-written `assert` in `.spy` files is preserved as Python syntax. Python's
|
|
23
|
+
optimized mode removes these statements, including any calls inside them.
|
|
24
|
+
Production scripts must not depend on `assert` for command-success handling.
|
|
25
|
+
For local execution, use `session.exec_command(command, check=True)`, or explicitly inspect
|
|
26
|
+
`session.exitcode` and raise an application exception when appropriate.
|
|
27
|
+
|
|
28
|
+
`for_with` must be strictly bool. `get_pty` must be None or bool. Commands must
|
|
29
|
+
be nonempty strings, and persistent commands must contain only one line.
|
|
30
|
+
Compiled bytes regular expressions are not accepted by text-output matching.
|
|
31
|
+
Listener removal requires the identical top listener; failed removal, duplicate
|
|
32
|
+
hijack/release, and last-layer removal leave their associated state unchanged.
|
|
33
|
+
|
|
34
|
+
Run release gates from the project root using a supported interpreter:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
python -m unittest discover -v -s unittest -p 'test_*.py'
|
|
38
|
+
python -O -m unittest discover -v -s unittest -p 'test_*.py'
|
|
39
|
+
python sshscript.py unittest/dollar_syntax.spy
|
|
40
|
+
python -m compileall -q -x 'unittest-v3' .
|
|
41
|
+
python unittest/check_package_asserts.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CI runs on Python 3.11–3.14 on Linux and macOS. The AST gate scans the flat
|
|
45
|
+
package's root Python modules; tests and historical `unittest-v3` are excluded.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
include README.md LICENSE.txt RELEASING.md
|
|
2
|
+
include CHANGELOG.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
|
|
3
|
+
recursive-include tools *.py
|
|
4
|
+
include tools/setup_openssh_ci.sh
|
|
5
|
+
include .github/dependabot.yml
|
|
6
|
+
recursive-include .github/workflows *.yml
|
|
7
|
+
recursive-include src/sshscript/unittest *.py *.spy
|