mirror of
https://github.com/github/spec-kit.git
synced 2026-03-21 12:53:08 +00:00
feat(cli): embed core pack in wheel for offline/air-gapped deployment (#1803)
* feat(cli): embed core pack in wheel + offline-first init (#1711, #1752) Bundle templates, commands, and scripts inside the specify-cli wheel so that `specify init` works without any network access by default. Changes: - pyproject.toml: add hatchling force-include for core_pack assets; bump version to 0.2.1 - __init__.py: add _locate_core_pack(), _generate_agent_commands() (Python port of generate_commands() shell function), and scaffold_from_core_pack(); modify init() to scaffold from bundled assets by default; add --from-github flag to opt back in to the GitHub download path - release.yml: build wheel during CI release job - create-github-release.sh: attach .whl as a release asset - docs/installation.md: add Enterprise/Air-Gapped Installation section - README.md: add Option 3 enterprise install with accurate offline story Closes #1711 Addresses #1752 * fix(tests): update kiro alias test for offline-first scaffold path * feat(cli): invoke bundled release script at runtime for offline scaffold - Embed release scripts (bash + PowerShell) in wheel via pyproject.toml - Replace Python _generate_agent_commands() with subprocess invocation of the canonical create-release-packages.sh, guaranteeing byte-for-byte parity between 'specify init --offline' and GitHub release ZIPs - Fix macOS bash 3.2 compat in release script: replace cp --parents, local -n (nameref), and mapfile with POSIX-safe alternatives - Fix _TOML_AGENTS: remove qwen (uses markdown per release script) - Rename --from-github to --offline (opt-in to bundled assets) - Add _locate_release_script() for cross-platform script discovery - Update tests: remove bash 4+/GNU coreutils requirements, handle Kimi directory-per-skill layout, 576 tests passing - Update CHANGELOG and docs/installation.md * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(offline): error out if --offline fails instead of falling back to network - _locate_core_pack() docstring now accurately describes that it only finds wheel-bundled core_pack/; source-checkout fallback lives in callers - init() --offline + no bundled assets now exits with a clear error (previously printed a warning and silently fell back to GitHub download) - init() scaffold failure under --offline now exits with an error instead of retrying via download_and_extract_template Addresses reviewer comment: https://github.com/github/spec-kit/pull/1803 * fix(offline): address PR review comments - fix(shell): harden validate_subset against glob injection in case patterns - fix(shell): make GENRELEASES_DIR overridable via env var for test isolation - fix(cli): probe pwsh then powershell on Windows instead of hardcoding pwsh - fix(cli): remove unreachable fallback branch when --offline fails - fix(cli): improve --offline error message with common failure causes - fix(release): move wheel build step after create-release-packages.sh - fix(docs): add --offline to installation.md air-gapped example - fix(tests): remove unused genreleases_dir param from _run_release_script - fix(tests): rewrite parity test to run one agent at a time with isolated temp dirs, preventing cross-agent interference from rm -rf * fix(offline): address second round of review comments - fix(shell): replace case-pattern membership with explicit loop + == check for unambiguous glob-safety in validate_subset() - fix(cli): require pwsh (PowerShell 7) only; drop powershell (PS5) fallback since the bundled script uses #requires -Version 7.0 - fix(cli): add bash and zip preflight checks in scaffold_from_core_pack() with clear error messages if either is missing - fix(build): list individual template files in pyproject.toml force-include to avoid duplicating templates/commands/ in the wheel * fix(offline): address third round of review comments - Add 120s timeout to subprocess.run in scaffold_from_core_pack to prevent indefinite hangs during offline scaffolding - Add test_pyproject_force_include_covers_all_templates to catch missing template files in wheel bundling - Tighten kiro alias test to assert specific scaffold path (download vs offline) * fix(offline): address Copilot review round 4 - fix(offline): use handle_vscode_settings() merge for --here --offline to prevent data loss on existing .vscode/settings.json - fix(release): glob wheel filename in create-github-release.sh instead of hardcoding version, preventing upload failures on version mismatch - docs(release): add comment noting pyproject.toml version is synced by release-trigger.yml before the tag is pushed * 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 * fix(tests): session-scoped scaffold cache + timeout + dead code removal - Add timeout=300 and returncode check to _run_release_script() to fail fast with clear output on script hangs or failures - Remove unused import specify_cli, _SOURCE_TEMPLATES, bundled_project fixture - Add session-scoped scaffolded_sh/scaffolded_ps fixtures that scaffold once per agent and reuse the output directory across all invariant tests - Reduces test_core_pack_scaffold runtime from ~175s to ~51s (3.4x faster) - Parity tests still scaffold independently for isolation * fix(offline): remove wheel from release, update air-gapped docs to use pip download * fix(tests): handle codex skills layout and iflow agent in scaffold tests Codex now uses create_skills() with hyphenated separator (speckit-plan/SKILL.md) instead of generate_commands(). Update _SKILL_AGENTS, _expected_ext, and _list_command_files to handle both codex ('-') and kimi ('.') skill agents. Also picks up iflow as a new testable agent automatically via AGENT_CONFIG. * fix(offline): require wheel core_pack for --offline, remove source-checkout fallback --offline now strictly requires _locate_core_pack() to find the wheel's bundled core_pack/ directory. Source-checkout fallbacks are no longer accepted at the init() level — if core_pack/ is missing, the CLI errors out with a clear message pointing to the installation docs. scaffold_from_core_pack() retains its internal source-checkout fallbacks so parity tests can call it directly from a source checkout. * fix(offline): remove stale [Unreleased] CHANGELOG section, scope httpx.Client to download path - Remove entire [Unreleased] section — CHANGELOG is auto-generated at release - Move httpx.Client into use_github branch with context manager so --offline path doesn't allocate an unused network client * fix(offline): remove dead --from-github flag, fix typer.Exit handling, add page templates validation - Remove unused --from-github CLI option and docstring example - Add (typer.Exit, SystemExit) re-raise before broad except Exception to prevent duplicate error panel on offline scaffold failure - Validate page templates directory exists in scaffold_from_core_pack() to fail fast on incomplete wheel installs - Fix ruff lint: remove unused shutil import, remove f-prefix on strings without placeholders in test_core_pack_scaffold.py * docs(offline): add v0.6.0 deprecation notice with rationale - Help text: note bundled assets become default in v0.6.0 - Docstring: explain why GitHub download is being retired (no network dependency, no proxy/firewall issues, guaranteed version match) - Runtime nudge: when bundled assets are available but user takes the GitHub download path, suggest --offline with rationale - docs/installation.md: add deprecation notice with full rationale * fix(offline): allow --offline in source checkouts, fix CHANGELOG truncation - Simplify use_github logic: use_github = not offline (let scaffold_from_core_pack handle fallback to source-checkout paths) - Remove hard-fail when core_pack/ is absent — scaffold_from_core_pack already falls back to repo-root templates/scripts/commands - Fix truncated 'skill…' → 'skills' in CHANGELOG.md * fix(offline): sandbox GENRELEASES_DIR and clean up on failure - Pin GENRELEASES_DIR to temp dir in scaffold_from_core_pack() so a user-exported value cannot redirect output or cause rm -rf outside the sandbox - Clean up partial project directory on --offline scaffold failure (same behavior as the GitHub-download failure path) * fix(tests): use shutil.which for bash discovery, add ps parity tests - _find_bash() now tries shutil.which('bash') first so non-standard install locations (Nix, custom CI images) are found - Parametrize parity test over both 'sh' and 'ps' script types to ensure PowerShell variant stays byte-for-byte identical to release script output (353 scaffold tests, 810 total) * fix(tests): parse pyproject.toml with tomllib, remove unused fixture - Use tomllib to parse force-include keys from the actual TOML table instead of raw substring search (avoids false positives) - Remove unused source_template_stems fixture from test_scaffold_command_dir_location * fix: guard GENRELEASES_DIR against unsafe values, update docstring - Add safety check in create-release-packages.sh: reject empty, '/', '.', '..' values for GENRELEASES_DIR before rm -rf - Strip trailing slash to avoid path surprises - Update scaffold_from_core_pack() docstring to accurately describe all failure modes (not just 'assets not found') * fix: harden GENRELEASES_DIR guard, cache parity tests, safe iterdir - Reject '..' path segments in GENRELEASES_DIR to prevent traversal - Session-cache both scaffold and release-script results in parity tests — runtime drops from ~74s to ~45s (40% faster) - Guard cmd_dir.iterdir() in assertion message against missing dirs * fix(tests): exclude YAML frontmatter source metadata from path rewrite check The codex and kimi SKILL.md files have 'source: templates/commands/...' in their YAML frontmatter — this is provenance metadata, not a runtime path that needs rewriting. Strip frontmatter before checking for bare scripts/ and templates/ paths. * fix(offline): surface scaffold failure detail in error output When --offline scaffold fails, look up the tracker's 'scaffold' step detail and print it alongside the generic error message so users see the specific root cause (e.g. missing zip/pwsh, script stderr). --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -26,9 +26,27 @@ fi
|
||||
echo "Building release packages for $NEW_VERSION"
|
||||
|
||||
# Create and use .genreleases directory for all build artifacts
|
||||
GENRELEASES_DIR=".genreleases"
|
||||
# Override via GENRELEASES_DIR env var (e.g. for tests writing to a temp dir)
|
||||
GENRELEASES_DIR="${GENRELEASES_DIR:-.genreleases}"
|
||||
|
||||
# Guard against unsafe GENRELEASES_DIR values before cleaning
|
||||
if [[ -z "$GENRELEASES_DIR" ]]; then
|
||||
echo "GENRELEASES_DIR must not be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$GENRELEASES_DIR" in
|
||||
'/'|'.'|'..')
|
||||
echo "Refusing to use unsafe GENRELEASES_DIR value: $GENRELEASES_DIR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [[ "$GENRELEASES_DIR" == *".."* ]]; then
|
||||
echo "Refusing to use GENRELEASES_DIR containing '..' path segments: $GENRELEASES_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$GENRELEASES_DIR"
|
||||
rm -rf "$GENRELEASES_DIR"/* || true
|
||||
rm -rf "${GENRELEASES_DIR%/}/"* || true
|
||||
|
||||
rewrite_paths() {
|
||||
sed -E \
|
||||
@@ -228,7 +246,7 @@ build_variant() {
|
||||
esac
|
||||
fi
|
||||
|
||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" | while IFS= read -r f; do d="$SPEC_DIR/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done; echo "Copied templates -> .specify/templates"; }
|
||||
|
||||
case $agent in
|
||||
claude)
|
||||
@@ -325,34 +343,35 @@ build_variant() {
|
||||
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow generic)
|
||||
ALL_SCRIPTS=(sh ps)
|
||||
|
||||
norm_list() {
|
||||
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
|
||||
}
|
||||
|
||||
validate_subset() {
|
||||
local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
|
||||
local type=$1; shift
|
||||
local allowed_str="$1"; shift
|
||||
local invalid=0
|
||||
for it in "${items[@]}"; do
|
||||
for it in "$@"; do
|
||||
local found=0
|
||||
for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done
|
||||
for a in $allowed_str; do
|
||||
if [[ "$it" == "$a" ]]; then found=1; break; fi
|
||||
done
|
||||
if [[ $found -eq 0 ]]; then
|
||||
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
|
||||
echo "Error: unknown $type '$it' (allowed: $allowed_str)" >&2
|
||||
invalid=1
|
||||
fi
|
||||
done
|
||||
return $invalid
|
||||
}
|
||||
|
||||
read_list() { tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i);out=1}}}END{printf("\n")}'; }
|
||||
|
||||
if [[ -n ${AGENTS:-} ]]; then
|
||||
mapfile -t AGENT_LIST < <(printf '%s' "$AGENTS" | norm_list)
|
||||
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
|
||||
read -ra AGENT_LIST <<< "$(printf '%s' "$AGENTS" | read_list)"
|
||||
validate_subset agent "${ALL_AGENTS[*]}" "${AGENT_LIST[@]}" || exit 1
|
||||
else
|
||||
AGENT_LIST=("${ALL_AGENTS[@]}")
|
||||
fi
|
||||
|
||||
if [[ -n ${SCRIPTS:-} ]]; then
|
||||
mapfile -t SCRIPT_LIST < <(printf '%s' "$SCRIPTS" | norm_list)
|
||||
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
|
||||
read -ra SCRIPT_LIST <<< "$(printf '%s' "$SCRIPTS" | read_list)"
|
||||
validate_subset script "${ALL_SCRIPTS[*]}" "${SCRIPT_LIST[@]}" || exit 1
|
||||
else
|
||||
SCRIPT_LIST=("${ALL_SCRIPTS[@]}")
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user