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.
Files changed (87) hide show
  1. sshscript-3.1.5/.github/dependabot.yml +27 -0
  2. sshscript-3.1.5/.github/workflows/ci.yml +85 -0
  3. sshscript-3.1.5/.github/workflows/codeql.yml +30 -0
  4. sshscript-3.1.5/.github/workflows/release.yml +214 -0
  5. sshscript-3.1.5/CHANGELOG.md +101 -0
  6. sshscript-3.1.5/CODE_OF_CONDUCT.md +61 -0
  7. sshscript-3.1.5/CONTRIBUTING.md +87 -0
  8. sshscript-3.1.5/EXCEPTIONS.md +54 -0
  9. {sshscript-2.0.2 → sshscript-3.1.5}/LICENSE.txt +1 -1
  10. sshscript-3.1.5/MANIFEST.in +7 -0
  11. sshscript-3.1.5/PKG-INFO +375 -0
  12. sshscript-3.1.5/README.md +344 -0
  13. sshscript-3.1.5/RELEASING.md +29 -0
  14. sshscript-3.1.5/SECURITY.md +111 -0
  15. sshscript-3.1.5/SUPPORT.md +86 -0
  16. sshscript-3.1.5/pyproject.toml +60 -0
  17. sshscript-3.1.5/setup.cfg +4 -0
  18. sshscript-3.1.5/src/sshscript/__init__.py +86 -0
  19. sshscript-3.1.5/src/sshscript/_version.py +3 -0
  20. sshscript-3.1.5/src/sshscript/channelgeneric.py +2028 -0
  21. sshscript-3.1.5/src/sshscript/channelssh.py +289 -0
  22. sshscript-3.1.5/src/sshscript/channelsubprocess.py +294 -0
  23. sshscript-3.1.5/src/sshscript/channelutils.py +444 -0
  24. sshscript-3.1.5/src/sshscript/commandresult.py +28 -0
  25. sshscript-3.1.5/src/sshscript/dollar.py +590 -0
  26. sshscript-3.1.5/src/sshscript/dollarchanger.py +579 -0
  27. sshscript-3.1.5/src/sshscript/dollarparser.py +267 -0
  28. sshscript-3.1.5/src/sshscript/errorutils.py +486 -0
  29. sshscript-3.1.5/src/sshscript/patching.py +190 -0
  30. sshscript-3.1.5/src/sshscript/session.py +1606 -0
  31. sshscript-3.1.5/src/sshscript/sessionwrapper.py +300 -0
  32. sshscript-3.1.5/src/sshscript/spyimporter.py +157 -0
  33. sshscript-3.1.5/src/sshscript/sshconfig.py +127 -0
  34. sshscript-3.1.5/src/sshscript/sshscript.py +438 -0
  35. sshscript-3.1.5/src/sshscript/stdio.py +424 -0
  36. sshscript-3.1.5/src/sshscript/tokenparser.py +480 -0
  37. sshscript-3.1.5/src/sshscript/unittest/check_package_asserts.py +18 -0
  38. sshscript-3.1.5/src/sshscript/unittest/dollar_syntax.spy +247 -0
  39. sshscript-3.1.5/src/sshscript/unittest/dollar_syntax_fixture.spy +13 -0
  40. sshscript-3.1.5/src/sshscript/unittest/language.spy +533 -0
  41. sshscript-3.1.5/src/sshscript/unittest/language_fixture.spy +10 -0
  42. sshscript-3.1.5/src/sshscript/unittest/test_channelgeneric_expect.py +89 -0
  43. sshscript-3.1.5/src/sshscript/unittest/test_check_file.py +60 -0
  44. sshscript-3.1.5/src/sshscript/unittest/test_command_api.py +128 -0
  45. sshscript-3.1.5/src/sshscript/unittest/test_file_transfer.py +196 -0
  46. sshscript-3.1.5/src/sshscript/unittest/test_logger_api.py +232 -0
  47. sshscript-3.1.5/src/sshscript/unittest/test_logger_integration.py +230 -0
  48. sshscript-3.1.5/src/sshscript/unittest/test_openssh_integration.py +311 -0
  49. sshscript-3.1.5/src/sshscript/unittest/test_production_contract.py +371 -0
  50. sshscript-3.1.5/src/sshscript/unittest/test_session_close_reporting.py +183 -0
  51. sshscript-3.1.5/src/sshscript/unittest/test_session_proxy_cleanup.py +179 -0
  52. sshscript-3.1.5/src/sshscript/unittest/test_spy_source_mapping.py +297 -0
  53. sshscript-3.1.5/src/sshscript/unittest/test_spy_thread_session.py +230 -0
  54. sshscript-3.1.5/src/sshscript/unittest/test_ssh_config.py +128 -0
  55. sshscript-3.1.5/src/sshscript/unittest/test_ssh_security.py +282 -0
  56. sshscript-3.1.5/src/sshscript/unittest/test_sshscript_dollar_syntax.py +54 -0
  57. sshscript-3.1.5/src/sshscript/unittest/test_sshscript_module.py +479 -0
  58. sshscript-3.1.5/src/sshscript/unittest/test_stdio_dynamic_string.py +71 -0
  59. sshscript-3.1.5/src/sshscript/unittest/test_syntax_error.spy +21 -0
  60. sshscript-3.1.5/src/sshscript/unittest/test_update_check.py +76 -0
  61. sshscript-3.1.5/src/sshscript.egg-info/PKG-INFO +375 -0
  62. sshscript-3.1.5/src/sshscript.egg-info/SOURCES.txt +69 -0
  63. {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/dependency_links.txt +0 -0
  64. {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/entry_points.txt +0 -0
  65. sshscript-3.1.5/src/sshscript.egg-info/requires.txt +2 -0
  66. {sshscript-2.0.2 → sshscript-3.1.5}/src/sshscript.egg-info/top_level.txt +0 -0
  67. sshscript-3.1.5/tools/check_release.py +50 -0
  68. sshscript-3.1.5/tools/prepare_release.py +59 -0
  69. sshscript-3.1.5/tools/publish_release.py +26 -0
  70. sshscript-3.1.5/tools/run_checks.py +19 -0
  71. sshscript-3.1.5/tools/setup_openssh_ci.sh +192 -0
  72. sshscript-2.0.2/PKG-INFO +0 -134
  73. sshscript-2.0.2/README.rst +0 -106
  74. sshscript-2.0.2/setup.cfg +0 -13
  75. sshscript-2.0.2/setup.py +0 -44
  76. sshscript-2.0.2/src/sshscript/__init__.py +0 -33
  77. sshscript-2.0.2/src/sshscript/sshscript.py +0 -293
  78. sshscript-2.0.2/src/sshscript/sshscriptchannel.py +0 -1152
  79. sshscript-2.0.2/src/sshscript/sshscriptchannelutils.py +0 -506
  80. sshscript-2.0.2/src/sshscript/sshscriptdollar.py +0 -421
  81. sshscript-2.0.2/src/sshscript/sshscripterror.py +0 -70
  82. sshscript-2.0.2/src/sshscript/sshscriptparser.py +0 -1049
  83. sshscript-2.0.2/src/sshscript/sshscriptpatching.py +0 -110
  84. sshscript-2.0.2/src/sshscript/sshscriptsession.py +0 -988
  85. sshscript-2.0.2/src/sshscript.egg-info/PKG-INFO +0 -134
  86. sshscript-2.0.2/src/sshscript.egg-info/SOURCES.txt +0 -19
  87. 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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Hsin Yuan Yeh
3
+ Copyright (c) 2022-2026 Hsin Yuan Yeh
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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