Skip to content

Commit 629257e

Browse files
committed
fix(offline): address review round 5 + offline bundle ZIP
- fix(offline): pwsh-only, no powershell.exe fallback; clarify error message - fix(offline): tighten _has_bundled to check scripts dir for source checkouts - feat(release): build specify-bundle-v*.zip with all deps at release time - feat(release): attach offline bundle ZIP to GitHub release assets - docs: simplify air-gapped install to single ZIP download from releases - docs: add Windows PowerShell 7+ (pwsh) requirement note
1 parent fb76d4c commit 629257e

5 files changed

Lines changed: 42 additions & 38 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ jobs:
4747
pip install build
4848
python -m build --wheel --outdir .genreleases/
4949
50+
- name: Bundle offline dependencies
51+
if: steps.check_release.outputs.exists == 'false'
52+
run: |
53+
pip download -d .genreleases/specify-bundle/ .genreleases/specify_cli-*.whl
54+
cd .genreleases && zip -r specify-bundle-${{ steps.version.outputs.tag }}.zip specify-bundle/
55+
5056
- name: Generate release notes
5157
if: steps.check_release.outputs.exists == 'false'
5258
id: release_notes

.github/workflows/scripts/create-github-release.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ fi
3333

3434
WHEEL_FILE="${wheel_files[0]}"
3535

36+
# Find the offline bundle ZIP
37+
bundle_files=(.genreleases/specify-bundle-"$VERSION".zip)
38+
BUNDLE_FILE=""
39+
if (( ${#bundle_files[@]} == 1 )); then
40+
BUNDLE_FILE="${bundle_files[0]}"
41+
fi
42+
3643
gh release create "$VERSION" \
3744
"$WHEEL_FILE" \
3845
.genreleases/spec-kit-template-copilot-sh-"$VERSION".zip \
@@ -83,5 +90,6 @@ gh release create "$VERSION" \
8390
.genreleases/spec-kit-template-pi-ps-"$VERSION".zip \
8491
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
8592
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
93+
${BUNDLE_FILE:+"$BUNDLE_FILE"} \
8694
--title "Spec Kit Templates - $VERSION_NO_V" \
8795
--notes-file release_notes.md

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@ uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai c
9898

9999
#### Option 3: Enterprise / Air-Gapped Installation
100100

101-
If your environment blocks PyPI access, download the pre-built `specify_cli-*.whl` wheel from the [releases page](https://github.com/github/spec-kit/releases/latest) and install it directly:
101+
Download `specify-bundle-v*.zip` from the [releases page](https://github.com/github/spec-kit/releases/latest) — it contains the CLI wheel and all dependencies in one file (~2.5 MB):
102102

103103
```bash
104-
pip install specify_cli-*.whl
105-
specify init my-project --ai claude --offline # runs without contacting api.github.com
104+
unzip specify-bundle-v*.zip
105+
pip install --no-index --find-links=./specify-bundle/ specify-cli
106+
specify init my-project --ai claude --offline
106107
```
107108

108-
The wheel bundles all templates, commands, and scripts, so `specify init` can run without any network access after install when you pass `--offline`. See the [Enterprise / Air-Gapped Installation](./docs/installation.md#enterprise--air-gapped-installation) section for fully offline (no-PyPI) instructions.
109+
See the [full air-gapped guide](./docs/installation.md#enterprise--air-gapped-installation) for details.
109110

110111
### 2. Establish project principles
111112

docs/installation.md

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -77,48 +77,30 @@ The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
7777

7878
### Enterprise / Air-Gapped Installation
7979

80-
If your environment blocks access to PyPI (you see 403 errors when running `uv tool install` or `pip install`), you can install Specify using the pre-built wheel from the GitHub releases page.
80+
For environments with no access to PyPI or GitHub, download the pre-built offline bundle from the [releases page](https://github.com/github/spec-kit/releases/latest).
8181

82-
**Step 1: Download the wheel**
82+
**On a connected machine:**
8383

84-
Go to the [Spec Kit releases page](https://github.com/github/spec-kit/releases/latest) and download the `specify_cli-*.whl` file.
84+
Download `specify-bundle-v*.zip` from the [Spec Kit releases page](https://github.com/github/spec-kit/releases/latest). This single ZIP contains the specify-cli wheel and all its runtime dependencies (~2.5 MB).
8585

86-
**Step 2: Install the wheel**
86+
**On the air-gapped machine:**
8787

8888
```bash
89-
pip install specify_cli-*.whl
90-
```
89+
# Unzip the bundle
90+
unzip specify-bundle-v*.zip
9191

92-
**Step 3: Initialize a project (no network required)**
92+
# Install — no network access needed
93+
pip install --no-index --find-links=./specify-bundle/ specify-cli
9394

94-
```bash
95+
# Initialize a project — no GitHub access needed
9596
specify init my-project --ai claude --offline
9697
```
9798

98-
The `--offline` flag tells the CLI to use the templates, commands, and scripts bundled inside the wheel instead of downloading from GitHub — no connection to `api.github.com` needed.
99+
The `--offline` flag tells the CLI to use the templates, commands, and scripts bundled inside the wheel instead of downloading from GitHub.
99100

100-
**If you also need runtime dependencies offline** (fully air-gapped machines with no access to any PyPI), use a connected machine with the same OS and Python version to download them first:
101-
102-
```bash
103-
# On a connected machine (same OS and Python version as the target):
104-
pip download -d vendor specify_cli-*.whl
105-
106-
# Transfer the wheel and vendor/ directory to the target machine
107-
108-
# On the target machine:
109-
pip install --no-index --find-links=./vendor specify_cli-*.whl
110-
```
101+
> **Note:** Python 3.11+ is required. All dependencies are pure-Python wheels, so the bundle works on any platform without recompilation.
111102
112-
> **Note:** Python 3.11+ is required. The wheel is a pure-Python artifact, so it works on any platform without recompilation.
113-
114-
**Using bundled assets (offline / air-gapped):**
115-
116-
If you want to scaffold from the templates bundled inside the specify-cli
117-
package instead of downloading from GitHub, use:
118-
119-
```bash
120-
specify init my-project --ai claude --offline
121-
```
103+
> **Windows note:** Offline scaffolding requires PowerShell 7+ (`pwsh`), not Windows PowerShell 5.x (`powershell.exe`). Install from https://aka.ms/powershell.
122104
123105
### Git Credential Manager on Linux
124106

src/specify_cli/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,8 +1114,9 @@ def _locate_release_script() -> tuple[Path, str]:
11141114
shell = shutil.which("pwsh")
11151115
if not shell:
11161116
raise FileNotFoundError(
1117-
"'pwsh' (PowerShell 7) not found on PATH. "
1118-
"The bundled release script requires PowerShell 7+. "
1117+
"'pwsh' (PowerShell 7+) not found on PATH. "
1118+
"The bundled release script requires PowerShell 7+ (pwsh), "
1119+
"not Windows PowerShell 5.x (powershell.exe). "
11191120
"Install from https://aka.ms/powershell to use offline scaffolding."
11201121
)
11211122
else:
@@ -1880,8 +1881,14 @@ def init(
18801881
# Determine whether to use bundled assets or download from GitHub (default).
18811882
# --offline opts in to bundled assets; without it, always use GitHub.
18821883
_core = _locate_core_pack()
1883-
_repo_commands = Path(__file__).parent.parent.parent / "templates" / "commands"
1884-
_has_bundled = (_core is not None) or _repo_commands.is_dir()
1884+
_repo_root = Path(__file__).parent.parent.parent
1885+
_repo_commands = _repo_root / "templates" / "commands"
1886+
_repo_scripts = _repo_root / "scripts"
1887+
# Treat bundled assets as available if we have a core_pack (wheel install),
1888+
# or, for a source checkout, when both commands and scripts are present.
1889+
_has_bundled = (_core is not None) or (
1890+
_repo_commands.is_dir() and _repo_scripts.is_dir()
1891+
)
18851892

18861893
if offline and not _has_bundled:
18871894
console.print(

0 commit comments

Comments
 (0)