sshscript 2.0.2__tar.gz → 3.1.5__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.5/.github/dependabot.yml +27 -0
- sshscript-3.1.5/.github/workflows/ci.yml +85 -0
- sshscript-3.1.5/.github/workflows/codeql.yml +30 -0
- sshscript-3.1.5/.github/workflows/release.yml +214 -0
- sshscript-3.1.5/CHANGELOG.md +101 -0
- sshscript-3.1.5/CODE_OF_CONDUCT.md +61 -0
- sshscript-3.1.5/CONTRIBUTING.md +87 -0
- sshscript-3.1.5/EXCEPTIONS.md +54 -0
- {sshscript-2.0.2 → sshscript-3.1.5}/LICENSE.txt +1 -1
- sshscript-3.1.5/MANIFEST.in +7 -0
- sshscript-3.1.5/PKG-INFO +375 -0
- sshscript-3.1.5/README.md +344 -0
- sshscript-3.1.5/RELEASING.md +29 -0
- sshscript-3.1.5/SECURITY.md +111 -0
- sshscript-3.1.5/SUPPORT.md +86 -0
- sshscript-3.1.5/pyproject.toml +60 -0
- sshscript-3.1.5/setup.cfg +4 -0
- sshscript-3.1.5/src/sshscript/__init__.py +86 -0
- sshscript-3.1.5/src/sshscript/_version.py +3 -0
- sshscript-3.1.5/src/sshscript/channelgeneric.py +2028 -0
- sshscript-3.1.5/src/sshscript/channelssh.py +289 -0
- sshscript-3.1.5/src/sshscript/channelsubprocess.py +294 -0
- sshscript-3.1.5/src/sshscript/channelutils.py +444 -0
- sshscript-3.1.5/src/sshscript/commandresult.py +28 -0
- sshscript-3.1.5/src/sshscript/dollar.py +590 -0
- sshscript-3.1.5/src/sshscript/dollarchanger.py +579 -0
- sshscript-3.1.5/src/sshscript/dollarparser.py +267 -0
- sshscript-3.1.5/src/sshscript/errorutils.py +486 -0
- sshscript-3.1.5/src/sshscript/patching.py +190 -0
- sshscript-3.1.5/src/sshscript/session.py +1606 -0
- sshscript-3.1.5/src/sshscript/sessionwrapper.py +300 -0
- sshscript-3.1.5/src/sshscript/spyimporter.py +157 -0
- sshscript-3.1.5/src/sshscript/sshconfig.py +127 -0
- sshscript-3.1.5/src/sshscript/sshscript.py +438 -0
- sshscript-3.1.5/src/sshscript/stdio.py +424 -0
- sshscript-3.1.5/src/sshscript/tokenparser.py +480 -0
- sshscript-3.1.5/src/sshscript/unittest/check_package_asserts.py +18 -0
- sshscript-3.1.5/src/sshscript/unittest/dollar_syntax.spy +247 -0
- sshscript-3.1.5/src/sshscript/unittest/dollar_syntax_fixture.spy +13 -0
- sshscript-3.1.5/src/sshscript/unittest/language.spy +533 -0
- sshscript-3.1.5/src/sshscript/unittest/language_fixture.spy +10 -0
- sshscript-3.1.5/src/sshscript/unittest/test_channelgeneric_expect.py +89 -0
- sshscript-3.1.5/src/sshscript/unittest/test_check_file.py +60 -0
- sshscript-3.1.5/src/sshscript/unittest/test_command_api.py +128 -0
- sshscript-3.1.5/src/sshscript/unittest/test_file_transfer.py +196 -0
- sshscript-3.1.5/src/sshscript/unittest/test_logger_api.py +232 -0
- sshscript-3.1.5/src/sshscript/unittest/test_logger_integration.py +230 -0
- sshscript-3.1.5/src/sshscript/unittest/test_openssh_integration.py +311 -0
- sshscript-3.1.5/src/sshscript/unittest/test_production_contract.py +371 -0
- sshscript-3.1.5/src/sshscript/unittest/test_session_close_reporting.py +183 -0
- sshscript-3.1.5/src/sshscript/unittest/test_session_proxy_cleanup.py +179 -0
- sshscript-3.1.5/src/sshscript/unittest/test_spy_source_mapping.py +297 -0
- sshscript-3.1.5/src/sshscript/unittest/test_spy_thread_session.py +230 -0
- sshscript-3.1.5/src/sshscript/unittest/test_ssh_config.py +128 -0
- sshscript-3.1.5/src/sshscript/unittest/test_ssh_security.py +282 -0
- sshscript-3.1.5/src/sshscript/unittest/test_sshscript_dollar_syntax.py +54 -0
- sshscript-3.1.5/src/sshscript/unittest/test_sshscript_module.py +479 -0
- sshscript-3.1.5/src/sshscript/unittest/test_stdio_dynamic_string.py +71 -0
- sshscript-3.1.5/src/sshscript/unittest/test_syntax_error.spy +21 -0
- sshscript-3.1.5/src/sshscript/unittest/test_update_check.py +76 -0
- sshscript-3.1.5/src/sshscript.egg-info/PKG-INFO +375 -0
- sshscript-3.1.5/src/sshscript.egg-info/SOURCES.txt +69 -0
- {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/dependency_links.txt +0 -0
- {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/entry_points.txt +0 -0
- sshscript-3.1.5/src/sshscript.egg-info/requires.txt +2 -0
- {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/top_level.txt +0 -0
- sshscript-3.1.5/tools/check_release.py +50 -0
- sshscript-3.1.5/tools/prepare_release.py +59 -0
- sshscript-3.1.5/tools/publish_release.py +26 -0
- sshscript-3.1.5/tools/run_checks.py +19 -0
- sshscript-3.1.5/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,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project records user-visible changes here. Release artifacts and their
|
|
4
|
+
provenance are available from the linked GitHub Release and PyPI pages.
|
|
5
|
+
|
|
6
|
+
## [3.1.5] - 2026-09-27
|
|
7
|
+
|
|
8
|
+
### Command API and SSH configuration
|
|
9
|
+
|
|
10
|
+
- Accept argv lists/tuples for a single command, with local direct execution
|
|
11
|
+
and POSIX quoting over SSH; shell mode remains explicit for strings.
|
|
12
|
+
- Return immutable CommandResult snapshots with stdout, stderr, exitcode,
|
|
13
|
+
host, duration, and command. Unpacking/indexing now yields three values:
|
|
14
|
+
stdout, stderr, exitcode. Two-value unpacking must be updated.
|
|
15
|
+
Output values are now strings rather than live buffer objects.
|
|
16
|
+
- Apply check=True consistently to local and remote one-shot commands after
|
|
17
|
+
preserving results; CalledProcessError carries text output and result.
|
|
18
|
+
- Resolve common ~/.ssh/config settings with explicit API overrides; add
|
|
19
|
+
config opt-out, alternate files, and a connection-free settings preview.
|
|
20
|
+
Support ProxyCommand and ProxyJump (the latter uses local OpenSSH).
|
|
21
|
+
- Reject unsupported Match/Include/canonicalization rules; warn about other
|
|
22
|
+
unapplied config settings. ProxyJump requires noninteractive authentication
|
|
23
|
+
and verified jump-host keys. Existing host-key checks remain enabled.
|
|
24
|
+
|
|
25
|
+
### Script validation
|
|
26
|
+
|
|
27
|
+
- Add check_file(path) and --check FILE to compile Python/dollar syntax without
|
|
28
|
+
executing user code, imports, or commands. The no-file --check update alias
|
|
29
|
+
remains compatible; --check-updates is the explicit update command.
|
|
30
|
+
- Map tokenizer indentation errors to the original source file; retain source
|
|
31
|
+
mapping coverage for nested commands, interpolation, multiline expressions,
|
|
32
|
+
and imported .spy modules. Editor integration is deferred.
|
|
33
|
+
|
|
34
|
+
### Documentation and community
|
|
35
|
+
|
|
36
|
+
- Reorganize the README around installation, secure first use, compatibility,
|
|
37
|
+
support, and release provenance.
|
|
38
|
+
- Add structured issue forms, a pull request template, and a Code of Conduct.
|
|
39
|
+
- Clarify supported environments, security boundaries, and public test paths.
|
|
40
|
+
|
|
41
|
+
## [3.1.4] - 2026-09-24
|
|
42
|
+
|
|
43
|
+
First Production/Stable release in the 3.1 line. Published through
|
|
44
|
+
[PyPI Trusted Publishing](https://pypi.org/project/sshscript/3.1.4/) with
|
|
45
|
+
verified artifacts in the
|
|
46
|
+
[GitHub Release](https://github.com/iapyeh/sshscript/releases/tag/v3.1.4).
|
|
47
|
+
|
|
48
|
+
### Production hardening
|
|
49
|
+
|
|
50
|
+
- Require Python 3.11 or newer; CI targets 3.11, 3.12, 3.13, and 3.14.
|
|
51
|
+
- Replace package runtime assertions with the stable exception contract in
|
|
52
|
+
EXCEPTIONS.md, including validation under `python -O`.
|
|
53
|
+
- Make listener removal, hijack/release, and last-layer checks atomic and
|
|
54
|
+
non-mutating on failure; fix positive session-stack indexing.
|
|
55
|
+
- Reject invalid command, PTY, pattern, and output arguments before execution
|
|
56
|
+
or buffer mutation. Disconnected SFTP access raises SSHScriptException.
|
|
57
|
+
- Move embedded stdio tests into the credential-free suite and add normal,
|
|
58
|
+
optimized, and package AST assertion release gates.
|
|
59
|
+
- AssertionError was never a supported API contract. User-written `.spy`
|
|
60
|
+
assertions are preserved, but production command-success handling must use
|
|
61
|
+
explicit checks or `check=True`, because optimized Python removes assertions.
|
|
62
|
+
- Validate closed and hijacked channel operations, AST invariants, persistent
|
|
63
|
+
commands, PTY modes, and output buffers with stable exception types.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### Security
|
|
67
|
+
|
|
68
|
+
- Verify SSH host keys by default; insecure automatic key acceptance now
|
|
69
|
+
requires an explicit Paramiko policy.
|
|
70
|
+
- Stop forwarding the complete local environment to interactive SSH sessions.
|
|
71
|
+
- Read remote private keys through SFTP instead of interpolated shell commands.
|
|
72
|
+
- Remove SSH hostnames and complete local/remote paths from upload/download
|
|
73
|
+
INFO logs.
|
|
74
|
+
- Add CodeQL, Dependabot, private vulnerability reporting guidance, and an
|
|
75
|
+
OIDC-based trusted publishing workflow.
|
|
76
|
+
- Pin release actions and build tooling, attest distributions, retain their
|
|
77
|
+
hash manifest, and keep the GitHub Release in draft until PyPI succeeds.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- Drain remote stdout and stderr concurrently and send stdin EOF.
|
|
82
|
+
- Make session cleanup failures observable and optionally strict.
|
|
83
|
+
- Preserve `$.break(code)` as the CLI exit status.
|
|
84
|
+
- Simplify `run_file()` to execute exactly one file; compose scripts through
|
|
85
|
+
include syntax or Python imports instead of directory/glob execution.
|
|
86
|
+
- Make `.spy` importing explicit with `sshscript.spy_imports()` and scope the
|
|
87
|
+
importer automatically while `run_file()` is executing.
|
|
88
|
+
- Stop patching `threading.Thread`, `warnings`, and `__main__` during import.
|
|
89
|
+
- Preserve `.spy` thread session inheritance with a context-aware Thread
|
|
90
|
+
created by the source transformer instead of a process-wide monkey patch.
|
|
91
|
+
- Make `Session()` construction side-effect free and scope stack activation to
|
|
92
|
+
`run()`, context managers, or an explicit unscoped `$.connect()` operation.
|
|
93
|
+
|
|
94
|
+
### Project
|
|
95
|
+
|
|
96
|
+
- Add packaging metadata, license, contributor guidance, and CI configuration.
|
|
97
|
+
- Add a disposable loopback OpenSSH integration gate covering host keys, SFTP,
|
|
98
|
+
PTY behavior, sudo/su, and timeout handling.
|
|
99
|
+
|
|
100
|
+
[Unreleased]: https://github.com/iapyeh/sshscript/compare/v3.1.4...release
|
|
101
|
+
[3.1.4]: https://github.com/iapyeh/sshscript/releases/tag/v3.1.4
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our commitment
|
|
4
|
+
|
|
5
|
+
SSHScript welcomes people of every background and experience level. We are
|
|
6
|
+
committed to a collaborative environment in which participation is respectful,
|
|
7
|
+
constructive, and free from harassment.
|
|
8
|
+
|
|
9
|
+
## Expected behavior
|
|
10
|
+
|
|
11
|
+
- Be respectful and assume good faith while still discussing technical risks
|
|
12
|
+
directly.
|
|
13
|
+
- Give actionable feedback about work rather than making personal judgments.
|
|
14
|
+
- Acknowledge different experience levels, environments, and communication
|
|
15
|
+
styles.
|
|
16
|
+
- Protect credentials, personal information, internal infrastructure details,
|
|
17
|
+
and embargoed vulnerability information.
|
|
18
|
+
- Accept moderation decisions and correct harmful behavior promptly.
|
|
19
|
+
|
|
20
|
+
## Unacceptable behavior
|
|
21
|
+
|
|
22
|
+
- Harassment, intimidation, threats, stalking, or discriminatory language.
|
|
23
|
+
- Sexualized language, imagery, or unwelcome attention.
|
|
24
|
+
- Personal attacks, deliberate humiliation, or sustained disruption.
|
|
25
|
+
- Publishing another person's private information without permission.
|
|
26
|
+
- Retaliation against someone who reports a concern in good faith.
|
|
27
|
+
- Using project spaces to distribute malware, credentials, or knowingly unsafe
|
|
28
|
+
instructions.
|
|
29
|
+
|
|
30
|
+
Reasonable technical disagreement, security review, and criticism of code are
|
|
31
|
+
not harassment when expressed professionally and focused on the work.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This policy applies to repository issues, pull requests, reviews, discussions,
|
|
36
|
+
release and documentation spaces, and other public interactions in which a
|
|
37
|
+
person represents the SSHScript project.
|
|
38
|
+
|
|
39
|
+
## Reporting conduct concerns
|
|
40
|
+
|
|
41
|
+
Do not post a conduct report publicly. Contact the maintainer at
|
|
42
|
+
[iapyeh@gmail.com](mailto:iapyeh@gmail.com) with links or screenshots, the
|
|
43
|
+
relevant dates, a factual description, and any immediate safety concern. Share
|
|
44
|
+
only information needed to evaluate the report.
|
|
45
|
+
|
|
46
|
+
Suspected software vulnerabilities belong in
|
|
47
|
+
[GitHub private vulnerability reporting](https://github.com/iapyeh/sshscript/security/advisories/new),
|
|
48
|
+
as described in [SECURITY.md](SECURITY.md).
|
|
49
|
+
|
|
50
|
+
Reports will be reviewed as privately as practical. A maintainer named in or
|
|
51
|
+
directly involved with a report should not make the enforcement decision when a
|
|
52
|
+
reasonable alternative reviewer is available. Knowingly false reports may
|
|
53
|
+
themselves violate this policy; an unproven good-faith report does not.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Project maintainers may edit or remove content, request a correction, issue a
|
|
58
|
+
warning, limit participation, or permanently exclude a participant. Responses
|
|
59
|
+
will be based on severity, impact, pattern of behavior, and willingness to
|
|
60
|
+
correct the problem. Illegal activity or a credible threat may be referred to
|
|
61
|
+
the appropriate service provider or authority.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping improve SSHScript. Participation is governed by the
|
|
4
|
+
[Code of Conduct](CODE_OF_CONDUCT.md). Please use a private security advisory,
|
|
5
|
+
not an issue or pull request, for a suspected vulnerability.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
Use Python 3.11 or newer. The public release checkout uses a `src/` package
|
|
10
|
+
layout. From the repository root:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
python3 -m venv .venv
|
|
14
|
+
. .venv/bin/activate
|
|
15
|
+
python3 -m pip install --upgrade pip
|
|
16
|
+
python3 -m pip install -e .
|
|
17
|
+
python3 -m pip install build twine
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Keep test credentials, host inventories, local configuration, build output,
|
|
21
|
+
and editor state untracked.
|
|
22
|
+
|
|
23
|
+
## Required checks
|
|
24
|
+
|
|
25
|
+
Run the complete credential-free gate before proposing a change:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python3 tools/run_checks.py
|
|
29
|
+
python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The output path must not already exist. The first command finds the public test
|
|
33
|
+
suite under `src/sshscript/unittest/` and runs normal and optimized tests,
|
|
34
|
+
compile checks, the package assertion scan, and dollar-syntax smoke tests. The
|
|
35
|
+
second command builds the allowlisted wheel and source distribution, checks
|
|
36
|
+
their metadata, installs the wheel in an isolated environment, and performs an
|
|
37
|
+
installed-package smoke test.
|
|
38
|
+
|
|
39
|
+
Tests under `src/sshscript/unittest/` must not require network access, SSH
|
|
40
|
+
agents, private keys, passwords, or host-specific configuration unless they are
|
|
41
|
+
part of the isolated OpenSSH fixture. A regression fix should add a
|
|
42
|
+
credential-free test whenever the behavior can be reproduced with a fake
|
|
43
|
+
Paramiko client or local subprocess.
|
|
44
|
+
|
|
45
|
+
## SSH integration tests
|
|
46
|
+
|
|
47
|
+
Public CI provisions a disposable loopback OpenSSH server and runs
|
|
48
|
+
`src/sshscript/unittest/test_openssh_integration.py` against the built wheel.
|
|
49
|
+
The setup uses ephemeral users, passwords, and keys generated inside the CI
|
|
50
|
+
runner; it must never target a persistent host. See
|
|
51
|
+
`tools/setup_openssh_ci.sh` for the exact environment contract.
|
|
52
|
+
|
|
53
|
+
Historical site-specific and credentialed tests are intentionally not shipped
|
|
54
|
+
in the public release checkout. Do not add production credentials or internal
|
|
55
|
+
host scenarios to a pull request. Convert a failure into a disposable fixture,
|
|
56
|
+
redacted fake, or credential-free unit test first.
|
|
57
|
+
|
|
58
|
+
## Compatibility and public APIs
|
|
59
|
+
|
|
60
|
+
Changes to `Session`, `run_file()`, CLI exit statuses, dollar syntax, logging,
|
|
61
|
+
or SSH security defaults require synchronized implementation, tests, README,
|
|
62
|
+
and changelog updates. Avoid silently accepting insecure behavior.
|
|
63
|
+
|
|
64
|
+
The module API is the primary public interface. Keep dollar syntax behavior
|
|
65
|
+
aligned with it, and document compatibility aliases or deprecations explicitly.
|
|
66
|
+
Tests should cover both normal Python and `python -O` where the exception
|
|
67
|
+
contract is involved. Do not use runtime `assert` statements for package input
|
|
68
|
+
or lifecycle validation.
|
|
69
|
+
|
|
70
|
+
Before release, run all normal, optimized, compile, and AST gates listed in
|
|
71
|
+
[EXCEPTIONS.md](EXCEPTIONS.md). CI targets Python 3.11 through 3.14.
|
|
72
|
+
|
|
73
|
+
## Pull requests
|
|
74
|
+
|
|
75
|
+
Target the protected `release` branch from a topic branch or fork. Keep each
|
|
76
|
+
pull request focused and include:
|
|
77
|
+
|
|
78
|
+
- the problem and intended behavior;
|
|
79
|
+
- tests that fail before the change and pass afterward when practical;
|
|
80
|
+
- documentation and changelog updates for user-visible behavior;
|
|
81
|
+
- the commands used to validate the change;
|
|
82
|
+
- any compatibility, security, logging, or migration impact.
|
|
83
|
+
|
|
84
|
+
Complete the pull request template and respond to review feedback. A passing CI
|
|
85
|
+
run is required but does not guarantee acceptance. By submitting a
|
|
86
|
+
contribution, you agree that it may be distributed under the repository's MIT
|
|
87
|
+
License.
|
|
@@ -0,0 +1,54 @@
|
|
|
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 by default. One-shot local and remote
|
|
20
|
+
`Session.exec_command(..., check=True)` raise `subprocess.CalledProcessError`
|
|
21
|
+
after saving `session.last_result` and channel output/status. The exception's
|
|
22
|
+
`output`/`stdout` and `stderr` are text snapshots on both backends; `cmd` is a
|
|
23
|
+
string or argument tuple, and `result` is the complete immutable CommandResult.
|
|
24
|
+
CommandResult unpacks as exactly three values: stdout, stderr, exitcode; indexing
|
|
25
|
+
and slicing use the same order. Persistent consoles retain two-buffer returns.
|
|
26
|
+
This replaces the local backend's former raw subprocess exception (byte
|
|
27
|
+
output and generated argv). `AssertionError` was never a supported API contract.
|
|
28
|
+
|
|
29
|
+
User-written `assert` in `.spy` files is preserved as Python syntax. Python's
|
|
30
|
+
optimized mode removes these statements, including any calls inside them.
|
|
31
|
+
Production scripts must not depend on `assert` for command-success handling.
|
|
32
|
+
For local or remote one-shot execution, use `session.exec_command(command, check=True)`, or
|
|
33
|
+
explicitly inspect `session.exitcode` and raise an application exception when
|
|
34
|
+
appropriate. Persistent consoles retain their separate command/prompt contract.
|
|
35
|
+
|
|
36
|
+
`for_with` must be strictly bool. `get_pty` must be None or bool. Commands must
|
|
37
|
+
be nonempty strings or nonempty lists/tuples of string arguments. Argument
|
|
38
|
+
sequences disallow NUL, an empty executable, and shell mode. Persistent
|
|
39
|
+
commands must contain only one line.
|
|
40
|
+
Compiled bytes regular expressions are not accepted by text-output matching.
|
|
41
|
+
Listener removal requires the identical top listener; failed removal, duplicate
|
|
42
|
+
hijack/release, and last-layer removal leave their associated state unchanged.
|
|
43
|
+
|
|
44
|
+
Run the canonical credential-free gates from the public release repository root
|
|
45
|
+
using a supported interpreter:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python3 tools/run_checks.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The runner locates the package and tests under `src/sshscript/`, then executes
|
|
52
|
+
normal and optimized test suites, the dollar-syntax smoke suite, compile checks,
|
|
53
|
+
and the package-assertion gate. CI runs these checks on Python 3.11–3.14 on
|
|
54
|
+
Linux and macOS. The AST gate scans package runtime modules and excludes tests.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
include README.md LICENSE.txt RELEASING.md
|
|
2
|
+
include CHANGELOG.md CODE_OF_CONDUCT.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
|