strictcli 0.30.0__tar.gz → 0.31.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strictcli-0.30.0/.github/workflows/ci-pypi.yml → strictcli-0.31.0/.github/workflows/ci.yml +5 -1
- {strictcli-0.30.0 → strictcli-0.31.0}/.github/workflows/publish.yml +74 -39
- strictcli-0.31.0/.gitignore +20 -0
- strictcli-0.31.0/.rlsbl/bases/.github/workflows/ci.yml +33 -0
- strictcli-0.31.0/.rlsbl/bases/.github/workflows/publish.yml +187 -0
- {strictcli-0.30.0 → strictcli-0.31.0/.rlsbl/bases}/.gitignore +3 -2
- strictcli-0.31.0/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/.rlsbl/config.json +1 -8
- strictcli-0.31.0/.rlsbl/lint/python.toml +25 -0
- strictcli-0.31.0/.rlsbl/managed-files.json +9 -0
- strictcli-0.31.0/.rlsbl/version +1 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/CLAUDE.md +1 -1
- {strictcli-0.30.0 → strictcli-0.31.0}/PKG-INFO +1 -1
- {strictcli-0.30.0 → strictcli-0.31.0}/pyproject.toml +1 -1
- {strictcli-0.30.0 → strictcli-0.31.0}/strictcli/__init__.py +12 -10
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_mcp.py +40 -2
- {strictcli-0.30.0 → strictcli-0.31.0}/uv.lock +1 -1
- strictcli-0.30.0/.github/workflows/ci.yml +0 -22
- strictcli-0.30.0/.npmignore +0 -34
- strictcli-0.30.0/.rlsbl/managed-files.json +0 -11
- strictcli-0.30.0/index.js +0 -5
- strictcli-0.30.0/package-lock.json +0 -14
- strictcli-0.30.0/package.json +0 -25
- strictcli-0.30.0/postinstall.js +0 -17
- {strictcli-0.30.0 → strictcli-0.31.0}/.claude/settings.json +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/.strictcli/schema.json +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/LICENSE +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/README.md +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/strictcli/py.typed +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/conftest.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_call.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_command.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_provider.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_check_types.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_choices.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_choices_none.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_config.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_context.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_coverage.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_e2e.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_env.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_float_format.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_float_type.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_float_vectors.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_help.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_hermetic.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_infra_env.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_int_type.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_invoke.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_mutex.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_nesting.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_parser.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_provenance.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_registration.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_unique.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_utilities.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_validate.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_variadic.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/tests/test_visibility.py +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.30.0 → strictcli-0.31.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -26,4 +26,8 @@ jobs:
|
|
|
26
26
|
- run: uv python install ${{ matrix.python-version }}
|
|
27
27
|
- run: uv sync --locked
|
|
28
28
|
- run: uv run python -c "import strictcli"
|
|
29
|
-
-
|
|
29
|
+
- name: Install gitleaks
|
|
30
|
+
run: |
|
|
31
|
+
GITLEAKS_VERSION=8.24.3
|
|
32
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
33
|
+
- run: uv run pytest --rootdir .
|
|
@@ -3,8 +3,18 @@ on:
|
|
|
3
3
|
release:
|
|
4
4
|
types: [published]
|
|
5
5
|
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
tag:
|
|
8
|
+
description: Release tag to publish (e.g. v1.2.3). Overrides the ref for
|
|
9
|
+
retry dispatch.
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
# One publish run per tag: a workflow_dispatch retry at the same tag
|
|
14
|
+
# queues behind the in-flight run instead of racing it. A publish is never
|
|
15
|
+
# cancelled mid-flight.
|
|
6
16
|
concurrency:
|
|
7
|
-
group:
|
|
17
|
+
group: publish-${{ inputs.tag || github.ref_name }}
|
|
8
18
|
cancel-in-progress: false
|
|
9
19
|
permissions:
|
|
10
20
|
contents: read
|
|
@@ -20,16 +30,57 @@ jobs:
|
|
|
20
30
|
GATE_TIMEOUT_MINUTES: '20'
|
|
21
31
|
GATE_GRACE_MINUTES: '5'
|
|
22
32
|
GATE_POLL_SECONDS: '15'
|
|
33
|
+
GATE_MARKER_ATTEMPTS: '5'
|
|
34
|
+
GATE_MARKER_RETRY_SECONDS: '5'
|
|
23
35
|
CI_CHECK_REGEX: ^(test)( \(.*\))?$
|
|
24
36
|
steps:
|
|
25
37
|
- name: Wait for CI to succeed on the release commit
|
|
26
38
|
run: |
|
|
27
39
|
set -euo pipefail
|
|
28
40
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
41
|
+
# Commit resolution -- explicit marker-first order (never reads the release
|
|
42
|
+
# event payload; dispatch retries have none, so this path is uniform for
|
|
43
|
+
# release events and dispatch retries alike):
|
|
44
|
+
# (1) Marker: rlsbl writes the exact commit CI ran on into the GitHub
|
|
45
|
+
# Release body as a machine-parseable line of the form
|
|
46
|
+
# <!-- rlsbl-ci-sha: <40-hex> -->
|
|
47
|
+
# Prefer it when present -- it pins the precise commit and is immune to
|
|
48
|
+
# ref races. A just-created release can lag on a GitHub API read replica
|
|
49
|
+
# (the marker is written at release creation but may not be visible on
|
|
50
|
+
# the first read), so RETRY the read GATE_MARKER_ATTEMPTS times,
|
|
51
|
+
# GATE_MARKER_RETRY_SECONDS apart, before concluding it is absent. The
|
|
52
|
+
# tag is inputs.tag (TAG_INPUT) when dispatched with an explicit
|
|
53
|
+
# override, else GITHUB_REF_NAME (matches the router resolver).
|
|
54
|
+
# (2) Fallback: older releases predate the marker, so fall back to
|
|
55
|
+
# $GITHUB_SHA, the tag's commit for both release and dispatch-at-tag.
|
|
56
|
+
extract_ci_sha() {
|
|
57
|
+
# Emit the first rlsbl-ci-sha marker SHA found on stdin, if any.
|
|
58
|
+
sed -n 's/.*<!-- rlsbl-ci-sha: \([0-9a-f]\{40\}\) -->.*/\1/p' | head -n1
|
|
59
|
+
}
|
|
60
|
+
tag="${TAG_INPUT:-$GITHUB_REF_NAME}"
|
|
61
|
+
marker_attempts="${GATE_MARKER_ATTEMPTS:-5}"
|
|
62
|
+
marker_retry_seconds="${GATE_MARKER_RETRY_SECONDS:-5}"
|
|
63
|
+
sha=""
|
|
64
|
+
attempt=1
|
|
65
|
+
while [ "$attempt" -le "$marker_attempts" ]; do
|
|
66
|
+
if body="$(gh release view "$tag" --json body --jq .body 2>/dev/null)"; then
|
|
67
|
+
sha="$(printf '%s\n' "$body" | extract_ci_sha)"
|
|
68
|
+
fi
|
|
69
|
+
if [ -n "$sha" ]; then
|
|
70
|
+
break
|
|
71
|
+
fi
|
|
72
|
+
if [ "$attempt" -lt "$marker_attempts" ]; then
|
|
73
|
+
echo "Publish gate: rlsbl-ci-sha marker not yet visible in the '$tag' release body (attempt $attempt/$marker_attempts); retrying in ${marker_retry_seconds}s..."
|
|
74
|
+
sleep "$marker_retry_seconds"
|
|
75
|
+
fi
|
|
76
|
+
attempt=$(( attempt + 1 ))
|
|
77
|
+
done
|
|
78
|
+
if [ -n "$sha" ]; then
|
|
79
|
+
echo "Publish gate: resolved release commit from rlsbl-ci-sha marker in the '$tag' release body."
|
|
80
|
+
else
|
|
81
|
+
sha="$GITHUB_SHA"
|
|
82
|
+
echo "Publish gate: no rlsbl-ci-sha marker after $marker_attempts attempt(s) on the '$tag' release body; falling back to \$GITHUB_SHA."
|
|
83
|
+
fi
|
|
33
84
|
echo "Publish gate: waiting for CI on $GITHUB_REF_NAME (commit $sha)"
|
|
34
85
|
echo "Check-run name filter: $CI_CHECK_REGEX"
|
|
35
86
|
# Timeout, grace window, and poll interval come from the job env above;
|
|
@@ -49,11 +100,19 @@ jobs:
|
|
|
49
100
|
# Match this project's CI check runs by name; exclude check runs that
|
|
50
101
|
# belong to THIS workflow run (the gate itself and the queued publish
|
|
51
102
|
# jobs would otherwise deadlock the poll loop).
|
|
103
|
+
# A retried CI run creates a BRAND-NEW check-run with the same name as the
|
|
104
|
+
# old one, so a stale failure would block the gate forever. Project id and
|
|
105
|
+
# started_at, then reduce to the latest check-run per name (max started_at,
|
|
106
|
+
# numeric id as tiebreak for retries within the same second) BEFORE the
|
|
107
|
+
# pending / not-success logic runs. This way the newest run decides: still
|
|
108
|
+
# running -> wait; genuinely red -> hard fail.
|
|
52
109
|
runs="$(jq -s --arg re "$CI_CHECK_REGEX" --arg run_id "$GITHUB_RUN_ID" '
|
|
53
110
|
[ .[].check_runs[]
|
|
54
111
|
| select(.name | test($re))
|
|
55
112
|
| select((.details_url // "") | contains("/actions/runs/" + $run_id + "/") | not)
|
|
56
|
-
| {name, status, conclusion} ]
|
|
113
|
+
| {name, status, conclusion, id, started_at} ]
|
|
114
|
+
| group_by(.name)
|
|
115
|
+
| map(sort_by(.started_at, .id) | last)' <<< "$resp")"
|
|
57
116
|
total="$(jq 'length' <<< "$runs")"
|
|
58
117
|
|
|
59
118
|
if [ "$total" -eq 0 ]; then
|
|
@@ -112,41 +171,17 @@ jobs:
|
|
|
112
171
|
runs-on: ubuntu-latest
|
|
113
172
|
steps:
|
|
114
173
|
- uses: actions/checkout@v6
|
|
174
|
+
with:
|
|
175
|
+
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
|
115
176
|
- uses: astral-sh/setup-uv@v7
|
|
177
|
+
- name: Install gitleaks
|
|
178
|
+
run: |
|
|
179
|
+
GITLEAKS_VERSION=8.24.3
|
|
180
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
116
181
|
- run: uv build --out-dir dist
|
|
182
|
+
- name: Scan artifacts for secrets
|
|
183
|
+
run: |
|
|
184
|
+
gitleaks dir dist/
|
|
117
185
|
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
118
186
|
with:
|
|
119
187
|
skip-existing: true
|
|
120
|
-
npm:
|
|
121
|
-
needs: gate
|
|
122
|
-
runs-on: ubuntu-latest
|
|
123
|
-
steps:
|
|
124
|
-
- uses: actions/checkout@v6
|
|
125
|
-
- uses: actions/setup-node@v6
|
|
126
|
-
with:
|
|
127
|
-
node-version: 24
|
|
128
|
-
registry-url: https://registry.npmjs.org
|
|
129
|
-
- name: Check if already published
|
|
130
|
-
id: check-npm
|
|
131
|
-
run: |
|
|
132
|
-
PKG_NAME=$(node -p "require('./package.json').name")
|
|
133
|
-
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
134
|
-
if npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null; then
|
|
135
|
-
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
136
|
-
echo "Already published: ${PKG_NAME}@${PKG_VERSION}"
|
|
137
|
-
fi
|
|
138
|
-
- name: Determine dist-tag
|
|
139
|
-
id: dist-tag
|
|
140
|
-
run: |
|
|
141
|
-
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
142
|
-
if echo "$PKG_VERSION" | grep -q '-'; then
|
|
143
|
-
PREID=$(echo "$PKG_VERSION" | sed 's/[^-]*-//' | sed 's/\.[^.]*$//')
|
|
144
|
-
echo "tag=--tag $PREID" >> "$GITHUB_OUTPUT"
|
|
145
|
-
else
|
|
146
|
-
echo "tag=" >> "$GITHUB_OUTPUT"
|
|
147
|
-
fi
|
|
148
|
-
- run: npm publish --provenance --access public ${{
|
|
149
|
-
steps.dist-tag.outputs.tag }}
|
|
150
|
-
if: steps.check-npm.outputs.skip != 'true'
|
|
151
|
-
env:
|
|
152
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.log
|
|
5
|
+
.DS_Store
|
|
6
|
+
coverage/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.rlsbl-notes-*.tmp
|
|
10
|
+
.rlsbl/lock
|
|
11
|
+
.credentials.json
|
|
12
|
+
.*-cache.json
|
|
13
|
+
.env
|
|
14
|
+
.env.local
|
|
15
|
+
*.local-only
|
|
16
|
+
|
|
17
|
+
.rlsbl-monorepo/lock
|
|
18
|
+
|
|
19
|
+
build/
|
|
20
|
+
target/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# Per-SHA group: re-runs of the same commit dedupe, but a new commit never
|
|
11
|
+
# cancels an earlier commit's in-flight run (release CI conclusions stay intact).
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow_ref }}-${{ github.sha }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
# requires-python: >= 3.11
|
|
22
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v6
|
|
25
|
+
- uses: astral-sh/setup-uv@v7
|
|
26
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
27
|
+
- run: uv sync --locked
|
|
28
|
+
- run: uv run python -c "import strictcli"
|
|
29
|
+
- name: Install gitleaks
|
|
30
|
+
run: |
|
|
31
|
+
GITLEAKS_VERSION=8.24.3
|
|
32
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
33
|
+
- run: uv run pytest --rootdir .
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
tag:
|
|
8
|
+
description: Release tag to publish (e.g. v1.2.3). Overrides the ref for
|
|
9
|
+
retry dispatch.
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
# One publish run per tag: a workflow_dispatch retry at the same tag
|
|
14
|
+
# queues behind the in-flight run instead of racing it. A publish is never
|
|
15
|
+
# cancelled mid-flight.
|
|
16
|
+
concurrency:
|
|
17
|
+
group: publish-${{ inputs.tag || github.ref_name }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
id-token: write
|
|
22
|
+
jobs:
|
|
23
|
+
gate:
|
|
24
|
+
name: Gate on CI
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
permissions:
|
|
27
|
+
checks: read
|
|
28
|
+
env:
|
|
29
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
30
|
+
GATE_TIMEOUT_MINUTES: '20'
|
|
31
|
+
GATE_GRACE_MINUTES: '5'
|
|
32
|
+
GATE_POLL_SECONDS: '15'
|
|
33
|
+
GATE_MARKER_ATTEMPTS: '5'
|
|
34
|
+
GATE_MARKER_RETRY_SECONDS: '5'
|
|
35
|
+
CI_CHECK_REGEX: ^(test)( \(.*\))?$
|
|
36
|
+
steps:
|
|
37
|
+
- name: Wait for CI to succeed on the release commit
|
|
38
|
+
run: |
|
|
39
|
+
set -euo pipefail
|
|
40
|
+
|
|
41
|
+
# Commit resolution -- explicit marker-first order (never reads the release
|
|
42
|
+
# event payload; dispatch retries have none, so this path is uniform for
|
|
43
|
+
# release events and dispatch retries alike):
|
|
44
|
+
# (1) Marker: rlsbl writes the exact commit CI ran on into the GitHub
|
|
45
|
+
# Release body as a machine-parseable line of the form
|
|
46
|
+
# <!-- rlsbl-ci-sha: <40-hex> -->
|
|
47
|
+
# Prefer it when present -- it pins the precise commit and is immune to
|
|
48
|
+
# ref races. A just-created release can lag on a GitHub API read replica
|
|
49
|
+
# (the marker is written at release creation but may not be visible on
|
|
50
|
+
# the first read), so RETRY the read GATE_MARKER_ATTEMPTS times,
|
|
51
|
+
# GATE_MARKER_RETRY_SECONDS apart, before concluding it is absent. The
|
|
52
|
+
# tag is inputs.tag (TAG_INPUT) when dispatched with an explicit
|
|
53
|
+
# override, else GITHUB_REF_NAME (matches the router resolver).
|
|
54
|
+
# (2) Fallback: older releases predate the marker, so fall back to
|
|
55
|
+
# $GITHUB_SHA, the tag's commit for both release and dispatch-at-tag.
|
|
56
|
+
extract_ci_sha() {
|
|
57
|
+
# Emit the first rlsbl-ci-sha marker SHA found on stdin, if any.
|
|
58
|
+
sed -n 's/.*<!-- rlsbl-ci-sha: \([0-9a-f]\{40\}\) -->.*/\1/p' | head -n1
|
|
59
|
+
}
|
|
60
|
+
tag="${TAG_INPUT:-$GITHUB_REF_NAME}"
|
|
61
|
+
marker_attempts="${GATE_MARKER_ATTEMPTS:-5}"
|
|
62
|
+
marker_retry_seconds="${GATE_MARKER_RETRY_SECONDS:-5}"
|
|
63
|
+
sha=""
|
|
64
|
+
attempt=1
|
|
65
|
+
while [ "$attempt" -le "$marker_attempts" ]; do
|
|
66
|
+
if body="$(gh release view "$tag" --json body --jq .body 2>/dev/null)"; then
|
|
67
|
+
sha="$(printf '%s\n' "$body" | extract_ci_sha)"
|
|
68
|
+
fi
|
|
69
|
+
if [ -n "$sha" ]; then
|
|
70
|
+
break
|
|
71
|
+
fi
|
|
72
|
+
if [ "$attempt" -lt "$marker_attempts" ]; then
|
|
73
|
+
echo "Publish gate: rlsbl-ci-sha marker not yet visible in the '$tag' release body (attempt $attempt/$marker_attempts); retrying in ${marker_retry_seconds}s..."
|
|
74
|
+
sleep "$marker_retry_seconds"
|
|
75
|
+
fi
|
|
76
|
+
attempt=$(( attempt + 1 ))
|
|
77
|
+
done
|
|
78
|
+
if [ -n "$sha" ]; then
|
|
79
|
+
echo "Publish gate: resolved release commit from rlsbl-ci-sha marker in the '$tag' release body."
|
|
80
|
+
else
|
|
81
|
+
sha="$GITHUB_SHA"
|
|
82
|
+
echo "Publish gate: no rlsbl-ci-sha marker after $marker_attempts attempt(s) on the '$tag' release body; falling back to \$GITHUB_SHA."
|
|
83
|
+
fi
|
|
84
|
+
echo "Publish gate: waiting for CI on $GITHUB_REF_NAME (commit $sha)"
|
|
85
|
+
echo "Check-run name filter: $CI_CHECK_REGEX"
|
|
86
|
+
# Timeout, grace window, and poll interval come from the job env above;
|
|
87
|
+
# edit them there if this repository's CI needs different limits.
|
|
88
|
+
|
|
89
|
+
now() { date +%s; }
|
|
90
|
+
start="$(now)"
|
|
91
|
+
deadline=$(( start + GATE_TIMEOUT_MINUTES * 60 ))
|
|
92
|
+
grace_deadline=$(( start + GATE_GRACE_MINUTES * 60 ))
|
|
93
|
+
|
|
94
|
+
while :; do
|
|
95
|
+
if ! resp="$(gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?per_page=100")"; then
|
|
96
|
+
echo "Checks API request failed; retrying in ${GATE_POLL_SECONDS}s..."
|
|
97
|
+
sleep "$GATE_POLL_SECONDS"
|
|
98
|
+
continue
|
|
99
|
+
fi
|
|
100
|
+
# Match this project's CI check runs by name; exclude check runs that
|
|
101
|
+
# belong to THIS workflow run (the gate itself and the queued publish
|
|
102
|
+
# jobs would otherwise deadlock the poll loop).
|
|
103
|
+
# A retried CI run creates a BRAND-NEW check-run with the same name as the
|
|
104
|
+
# old one, so a stale failure would block the gate forever. Project id and
|
|
105
|
+
# started_at, then reduce to the latest check-run per name (max started_at,
|
|
106
|
+
# numeric id as tiebreak for retries within the same second) BEFORE the
|
|
107
|
+
# pending / not-success logic runs. This way the newest run decides: still
|
|
108
|
+
# running -> wait; genuinely red -> hard fail.
|
|
109
|
+
runs="$(jq -s --arg re "$CI_CHECK_REGEX" --arg run_id "$GITHUB_RUN_ID" '
|
|
110
|
+
[ .[].check_runs[]
|
|
111
|
+
| select(.name | test($re))
|
|
112
|
+
| select((.details_url // "") | contains("/actions/runs/" + $run_id + "/") | not)
|
|
113
|
+
| {name, status, conclusion, id, started_at} ]
|
|
114
|
+
| group_by(.name)
|
|
115
|
+
| map(sort_by(.started_at, .id) | last)' <<< "$resp")"
|
|
116
|
+
total="$(jq 'length' <<< "$runs")"
|
|
117
|
+
|
|
118
|
+
if [ "$total" -eq 0 ]; then
|
|
119
|
+
if [ "$(now)" -ge "$grace_deadline" ]; then
|
|
120
|
+
echo "::error::Publish gate: no CI check runs matching $CI_CHECK_REGEX appeared on $sha within $GATE_GRACE_MINUTES minutes."
|
|
121
|
+
echo "A scaffolded repository always has a CI workflow, so the release commit must produce CI check runs."
|
|
122
|
+
echo "If CI jobs were renamed, update CI_CHECK_REGEX in this workflow's gate job to match the new names."
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
echo "No matching CI check runs yet; retrying in ${GATE_POLL_SECONDS}s..."
|
|
126
|
+
sleep "$GATE_POLL_SECONDS"
|
|
127
|
+
continue
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
pending="$(jq '[ .[] | select(.status != "completed") ] | length' <<< "$runs")"
|
|
131
|
+
if [ "$pending" -gt 0 ]; then
|
|
132
|
+
if [ "$(now)" -ge "$deadline" ]; then
|
|
133
|
+
echo "::error::Publish gate: timed out after $GATE_TIMEOUT_MINUTES minutes waiting for CI to complete on $sha."
|
|
134
|
+
jq -r '.[] | " \(.name): status=\(.status) conclusion=\(.conclusion // "none")"' <<< "$runs"
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
echo "$pending of $total matching CI check runs still running; retrying in ${GATE_POLL_SECONDS}s..."
|
|
138
|
+
sleep "$GATE_POLL_SECONDS"
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
not_success="$(jq '[ .[] | select(.conclusion != "success") ]' <<< "$runs")"
|
|
143
|
+
if [ "$(jq 'length' <<< "$not_success")" -gt 0 ]; then
|
|
144
|
+
echo "::error::Publish gate: CI did not pass on $sha -- refusing to publish."
|
|
145
|
+
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$not_success"
|
|
146
|
+
while IFS= read -r conclusion; do
|
|
147
|
+
case "$conclusion" in
|
|
148
|
+
failure|timed_out)
|
|
149
|
+
echo "CI concluded '$conclusion' on the release commit. Fix the failure, re-run the CI workflow to green on this exact commit (gh run rerun <run-id>), then re-dispatch this publish workflow at the tag ref: gh workflow run <publish workflow> --ref $GITHUB_REF_NAME"
|
|
150
|
+
;;
|
|
151
|
+
cancelled)
|
|
152
|
+
echo "A CI check run was CANCELLED. A cancelled run proves nothing about the commit, so the gate treats it as a hard failure instead of waiting for a conclusion that will never come. Re-run the cancelled CI workflow (gh run rerun <run-id>), then re-dispatch this publish workflow at the tag ref."
|
|
153
|
+
;;
|
|
154
|
+
skipped)
|
|
155
|
+
echo "A CI check run matching the filter was SKIPPED. The gate cannot treat a skipped check as passing: this project's own CI must actually run on the release commit. Check paths filters and job conditions, re-run CI on this commit, then re-dispatch this publish workflow at the tag ref."
|
|
156
|
+
;;
|
|
157
|
+
*)
|
|
158
|
+
echo "CI check concluded '$conclusion' (not success). The gate only proceeds when every matching check concluded success."
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
done <<< "$(jq -r '.[].conclusion' <<< "$not_success" | sort -u)"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
echo "Publish gate: all $total matching CI check runs succeeded."
|
|
166
|
+
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$runs"
|
|
167
|
+
exit 0
|
|
168
|
+
done
|
|
169
|
+
pypi:
|
|
170
|
+
needs: gate
|
|
171
|
+
runs-on: ubuntu-latest
|
|
172
|
+
steps:
|
|
173
|
+
- uses: actions/checkout@v6
|
|
174
|
+
with:
|
|
175
|
+
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
|
176
|
+
- uses: astral-sh/setup-uv@v7
|
|
177
|
+
- name: Install gitleaks
|
|
178
|
+
run: |
|
|
179
|
+
GITLEAKS_VERSION=8.24.3
|
|
180
|
+
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
181
|
+
- run: uv build --out-dir dist
|
|
182
|
+
- name: Scan artifacts for secrets
|
|
183
|
+
run: |
|
|
184
|
+
gitleaks dir dist/
|
|
185
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
186
|
+
with:
|
|
187
|
+
skip-existing: true
|
|
@@ -4,14 +4,15 @@ __pycache__/
|
|
|
4
4
|
*.log
|
|
5
5
|
.DS_Store
|
|
6
6
|
coverage/
|
|
7
|
+
build/
|
|
7
8
|
dist/
|
|
9
|
+
target/
|
|
8
10
|
*.egg-info/
|
|
9
11
|
.rlsbl-notes-*.tmp
|
|
10
12
|
.rlsbl/lock
|
|
13
|
+
.rlsbl-monorepo/lock
|
|
11
14
|
.credentials.json
|
|
12
15
|
.*-cache.json
|
|
13
16
|
.env
|
|
14
17
|
.env.local
|
|
15
18
|
*.local-only
|
|
16
|
-
|
|
17
|
-
.rlsbl-monorepo/lock
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[forbidden-imports]
|
|
2
|
+
modules = [
|
|
3
|
+
"argparse",
|
|
4
|
+
"click",
|
|
5
|
+
"typer",
|
|
6
|
+
"flask",
|
|
7
|
+
"fastapi",
|
|
8
|
+
"django",
|
|
9
|
+
"uvicorn",
|
|
10
|
+
"granian",
|
|
11
|
+
"starlette",
|
|
12
|
+
"tornado",
|
|
13
|
+
"bottle",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[stdout]
|
|
17
|
+
enabled = true
|
|
18
|
+
ignore = []
|
|
19
|
+
|
|
20
|
+
[entry-point]
|
|
21
|
+
enabled = true
|
|
22
|
+
ignore = []
|
|
23
|
+
|
|
24
|
+
[files]
|
|
25
|
+
exclude = []
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"targets": [
|
|
3
|
-
"pypi"
|
|
4
|
-
"npm"
|
|
3
|
+
"pypi"
|
|
5
4
|
],
|
|
6
5
|
"publish_mode": "ci",
|
|
7
6
|
"push_timeout": 300,
|
|
@@ -10,12 +9,6 @@
|
|
|
10
9
|
"type": "pypi",
|
|
11
10
|
"target": "pypi",
|
|
12
11
|
"local": false
|
|
13
|
-
},
|
|
14
|
-
"npm": {
|
|
15
|
-
"type": "npm",
|
|
16
|
-
"target": "npm",
|
|
17
|
-
"local": false,
|
|
18
|
-
"provenance": true
|
|
19
12
|
}
|
|
20
13
|
},
|
|
21
14
|
"coverage_unit": "commit"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[forbidden-imports]
|
|
2
|
+
modules = [
|
|
3
|
+
"argparse",
|
|
4
|
+
"click",
|
|
5
|
+
"typer",
|
|
6
|
+
"flask",
|
|
7
|
+
"fastapi",
|
|
8
|
+
"django",
|
|
9
|
+
"uvicorn",
|
|
10
|
+
"granian",
|
|
11
|
+
"starlette",
|
|
12
|
+
"tornado",
|
|
13
|
+
"bottle",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[stdout]
|
|
17
|
+
enabled = true
|
|
18
|
+
ignore = []
|
|
19
|
+
|
|
20
|
+
[entry-point]
|
|
21
|
+
enabled = true
|
|
22
|
+
ignore = []
|
|
23
|
+
|
|
24
|
+
[files]
|
|
25
|
+
exclude = []
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"files": {
|
|
4
|
+
".github/workflows/ci.yml": "bfcdec1fcd8ff944f7ba7eb114ab091d0c5158f499ed4632945c2c4ca2d868df",
|
|
5
|
+
".github/workflows/publish.yml": "9bc0b409f8c5c2a3c5795a3e7c66095b9479c1a03fb41250ff0461e1221b54f2",
|
|
6
|
+
".gitignore": "a102f466628d2d28aea6b2157c1b406b1911d788b596b5b84f4952f121539d42",
|
|
7
|
+
".rlsbl/lint/python.toml": "9cfbcef2e010d5fad243437bf2fd8df54a1fae70ee0762f9d11da70fe207501c"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.108.0
|
|
@@ -16,5 +16,5 @@ This project uses [rlsbl](https://github.com/smm-h/rlsbl) for release orchestrat
|
|
|
16
16
|
- No tokens or secrets in command-line arguments (use env vars or config files)
|
|
17
17
|
- All file writes to shared state should be atomic (write to tmp, then rename)
|
|
18
18
|
- External calls (APIs, CLI tools) must have timeouts and graceful fallbacks
|
|
19
|
-
- Use `
|
|
19
|
+
- Use `uv pip install -e .` for local development
|
|
20
20
|
- CI runs smoke tests on every push; manual testing for UI/UX changes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.31.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -2843,6 +2843,7 @@ class App:
|
|
|
2843
2843
|
if self._coverage_shard_path is None:
|
|
2844
2844
|
return
|
|
2845
2845
|
path = self._coverage_shard_path.format(n=self._coverage_shard_counter)
|
|
2846
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
2846
2847
|
with open(path, "a", encoding="utf-8") as f:
|
|
2847
2848
|
f.write(json.dumps({"command": cmd_path}) + "\n")
|
|
2848
2849
|
|
|
@@ -7716,26 +7717,27 @@ def _mcp_handle_tools_list(
|
|
|
7716
7717
|
|
|
7717
7718
|
def _mcp_handle_tools_call(
|
|
7718
7719
|
app: App,
|
|
7719
|
-
commands: dict[str, tuple[Command, str]],
|
|
7720
7720
|
req_id: object,
|
|
7721
7721
|
params: dict,
|
|
7722
7722
|
) -> dict:
|
|
7723
7723
|
"""Handle the MCP 'tools/call' request."""
|
|
7724
|
-
|
|
7725
|
-
if not isinstance(tool_name, str):
|
|
7724
|
+
if "name" not in params:
|
|
7726
7725
|
return _mcp_jsonrpc_error(
|
|
7727
|
-
req_id, -32602, "missing
|
|
7726
|
+
req_id, -32602, "missing required parameter: name",
|
|
7728
7727
|
)
|
|
7729
|
-
|
|
7730
|
-
if
|
|
7728
|
+
tool_name = params["name"]
|
|
7729
|
+
if not isinstance(tool_name, str):
|
|
7731
7730
|
return _mcp_jsonrpc_error(
|
|
7732
|
-
req_id, -32602,
|
|
7731
|
+
req_id, -32602, "parameter 'name' must be a string",
|
|
7733
7732
|
)
|
|
7734
7733
|
|
|
7734
|
+
# Unknown tools are NOT a -32602 protocol error: like Go, the name is
|
|
7735
|
+
# passed to app.call(), whose invocation error surfaces as tool-result
|
|
7736
|
+
# error content (isError) below.
|
|
7735
7737
|
arguments = params.get("arguments", {})
|
|
7736
7738
|
if not isinstance(arguments, dict):
|
|
7737
7739
|
return _mcp_jsonrpc_error(
|
|
7738
|
-
req_id, -32602, "'arguments' must be an object",
|
|
7740
|
+
req_id, -32602, "parameter 'arguments' must be an object",
|
|
7739
7741
|
)
|
|
7740
7742
|
|
|
7741
7743
|
try:
|
|
@@ -7793,7 +7795,7 @@ def _run_mcp_server(
|
|
|
7793
7795
|
app, commands, req_id,
|
|
7794
7796
|
),
|
|
7795
7797
|
"tools/call": lambda req_id, params: _mcp_handle_tools_call(
|
|
7796
|
-
app,
|
|
7798
|
+
app, req_id, params,
|
|
7797
7799
|
),
|
|
7798
7800
|
}
|
|
7799
7801
|
|
|
@@ -299,6 +299,11 @@ class TestMcpToolsCall:
|
|
|
299
299
|
assert parsed == {"migrated": True, "dry_run": True}
|
|
300
300
|
|
|
301
301
|
def test_call_unknown_tool(self):
|
|
302
|
+
"""Unknown tools surface as tool-result errors (isError), not -32602.
|
|
303
|
+
|
|
304
|
+
Matches Go: the name is passed to Call, whose invocation error
|
|
305
|
+
becomes error content in the result.
|
|
306
|
+
"""
|
|
302
307
|
app = _build_app()
|
|
303
308
|
|
|
304
309
|
@app.command("cmd", help="a command")
|
|
@@ -309,8 +314,12 @@ class TestMcpToolsCall:
|
|
|
309
314
|
"jsonrpc": "2.0", "id": 15, "method": "tools/call",
|
|
310
315
|
"params": {"name": "nonexistent", "arguments": {}},
|
|
311
316
|
})
|
|
312
|
-
assert
|
|
313
|
-
assert
|
|
317
|
+
assert "error" not in resp
|
|
318
|
+
assert resp["result"]["isError"] is True
|
|
319
|
+
content = resp["result"]["content"]
|
|
320
|
+
assert len(content) == 1
|
|
321
|
+
assert content[0]["type"] == "text"
|
|
322
|
+
assert content[0]["text"] == "unknown command 'nonexistent'"
|
|
314
323
|
|
|
315
324
|
def test_call_missing_required_flag(self):
|
|
316
325
|
app = _build_app()
|
|
@@ -342,6 +351,35 @@ class TestMcpToolsCall:
|
|
|
342
351
|
"params": {"arguments": {}},
|
|
343
352
|
})
|
|
344
353
|
assert resp["error"]["code"] == -32602
|
|
354
|
+
assert resp["error"]["message"] == "missing required parameter: name"
|
|
355
|
+
|
|
356
|
+
def test_call_non_string_name(self):
|
|
357
|
+
app = _build_app()
|
|
358
|
+
|
|
359
|
+
@app.command("cmd", help="a command")
|
|
360
|
+
def cmd(ctx):
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
resp = _send_one(app, {
|
|
364
|
+
"jsonrpc": "2.0", "id": 19, "method": "tools/call",
|
|
365
|
+
"params": {"name": 42, "arguments": {}},
|
|
366
|
+
})
|
|
367
|
+
assert resp["error"]["code"] == -32602
|
|
368
|
+
assert resp["error"]["message"] == "parameter 'name' must be a string"
|
|
369
|
+
|
|
370
|
+
def test_call_non_object_arguments(self):
|
|
371
|
+
app = _build_app()
|
|
372
|
+
|
|
373
|
+
@app.command("cmd", help="a command")
|
|
374
|
+
def cmd(ctx):
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
resp = _send_one(app, {
|
|
378
|
+
"jsonrpc": "2.0", "id": 20, "method": "tools/call",
|
|
379
|
+
"params": {"name": "cmd", "arguments": ["not", "an", "object"]},
|
|
380
|
+
})
|
|
381
|
+
assert resp["error"]["code"] == -32602
|
|
382
|
+
assert resp["error"]["message"] == "parameter 'arguments' must be an object"
|
|
345
383
|
|
|
346
384
|
def test_call_no_arguments_key(self):
|
|
347
385
|
"""When 'arguments' is omitted, defaults to empty dict."""
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
workflow_dispatch:
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
test:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
# requires-python: >= 3.11
|
|
16
|
-
python-version: ["3.12", "3.13", "3.14"]
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v6
|
|
19
|
-
- uses: astral-sh/setup-uv@v7
|
|
20
|
-
- run: uv python install ${{ matrix.python-version }}
|
|
21
|
-
- run: uv sync
|
|
22
|
-
- run: uv run python -c "import strictcli"
|
strictcli-0.30.0/.npmignore
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Development and CI
|
|
2
|
-
tests/
|
|
3
|
-
test/
|
|
4
|
-
__tests__/
|
|
5
|
-
.github/
|
|
6
|
-
scripts/
|
|
7
|
-
docs/
|
|
8
|
-
todo/
|
|
9
|
-
e2e/
|
|
10
|
-
|
|
11
|
-
# rlsbl metadata
|
|
12
|
-
.rlsbl/
|
|
13
|
-
.rlsbl-monorepo/
|
|
14
|
-
|
|
15
|
-
# Python artifacts (for multi-target projects)
|
|
16
|
-
__pycache__/
|
|
17
|
-
*.pyc
|
|
18
|
-
*.py
|
|
19
|
-
*.toml
|
|
20
|
-
.venv/
|
|
21
|
-
|
|
22
|
-
# Build/cache
|
|
23
|
-
coverage/
|
|
24
|
-
.nyc_output/
|
|
25
|
-
|
|
26
|
-
# Editor/OS
|
|
27
|
-
.DS_Store
|
|
28
|
-
*.log
|
|
29
|
-
.env
|
|
30
|
-
.env.local
|
|
31
|
-
|
|
32
|
-
# Selfdoc
|
|
33
|
-
.selfdoc/
|
|
34
|
-
selfdoc.json
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"files": {
|
|
4
|
-
".github/workflows/ci-pypi.yml": "6bde3f6dd33979636f6a8f51721f85519c4d2b6106cb9c9fa78e540df26471c4",
|
|
5
|
-
".npmignore": "7c0eea9785c0318aeb39bf8b670ce8384f4a5b812fde69ce23d7d018057325a3",
|
|
6
|
-
".github/workflows/publish.yml": "20db711b8a7745c3ab41814b827e946d60e937e82ee3984e86f39567717199b1",
|
|
7
|
-
".gitignore": "c0582f9b1c640f9f9a8acbbe02cd19fff50e66cc595f25e129b7cc9b6cd357db",
|
|
8
|
-
".rlsbl/lint/python.toml": "9cfbcef2e010d5fad243437bf2fd8df54a1fae70ee0762f9d11da70fe207501c",
|
|
9
|
-
".rlsbl/lint/npm.toml": "baa36014e65923d8cc5c8b83c77fa651da79351627570476402da66840027645"
|
|
10
|
-
}
|
|
11
|
-
}
|
strictcli-0.30.0/index.js
DELETED
strictcli-0.30.0/package.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "strictcli",
|
|
3
|
-
"version": "0.30.0",
|
|
4
|
-
"description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"postinstall": "node postinstall.js"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"strictcli",
|
|
11
|
-
"cli",
|
|
12
|
-
"argparse",
|
|
13
|
-
"command-line",
|
|
14
|
-
"framework",
|
|
15
|
-
"python",
|
|
16
|
-
"rlsbl"
|
|
17
|
-
],
|
|
18
|
-
"author": "S. M. Hosseini <m.hosseini@veliu.com>",
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "https://github.com/smm-h/strictcli.git"
|
|
23
|
-
},
|
|
24
|
-
"homepage": "https://github.com/smm-h/strictcli"
|
|
25
|
-
}
|
strictcli-0.30.0/postinstall.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const { execSync } = require("child_process");
|
|
2
|
-
|
|
3
|
-
const version = require("./package.json").version;
|
|
4
|
-
const pkg = `strictcli==${version}`;
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
execSync(`uv pip install ${pkg}`, { stdio: "inherit" });
|
|
8
|
-
} catch {
|
|
9
|
-
try {
|
|
10
|
-
execSync(`pip install ${pkg}`, { stdio: "inherit" });
|
|
11
|
-
} catch {
|
|
12
|
-
console.error(
|
|
13
|
-
`Failed to install Python package ${pkg}. ` +
|
|
14
|
-
"Ensure uv or pip is available, then run: uv add strictcli"
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|