Skip to content

Commit e94dd83

Browse files
feat(install): install.ps1 is published beside install.sh, and runs on Windows PowerShell 5.1 (#951)
* feat(install): plan for #948, install.ps1 is published and runs on Windows PowerShell 5.1 Claude-Session: https://claude.ai/code/session_01DuNfZFyfxBoD9pdaiDEMrY * feat(install): install.ps1 is published beside install.sh, and runs on Windows PowerShell 5.1 release.yaml's sync step copied install.sh to the site and nothing else, so install.ps1's own usage line -- irm https://devlore.noblefactor.com/install.ps1 | iex -- was a 404. It syncs both scripts now, and the release summary shows the Windows one-liner beside the Unix one. install.ps1 was written for PowerShell 7, which a fresh Windows machine does not have. Ruled 2026-09-24 (#798, ruling 3 reversed): the installer uses what is on the box, Windows PowerShell 5.1, and installs nothing but lore, star and writ. So: #Requires -Version 5.1; Get-OSName reads PSEdition before the $Is* automatic variables, which 5.1 lacks and strict mode would refuse; -UseBasicParsing on every web call, since 5.1's default engages the Internet Explorer parser; TLS 1.2 forced before the first request. The body is otherwise #914's. Verified under pwsh 7 on linux/arm64 against a scratch prefix with a real pre-release. The run under 5.1 on a Windows machine is the owner's, and the plan says so. Resolves #948 Claude-Session: https://claude.ai/code/session_01DuNfZFyfxBoD9pdaiDEMrY
1 parent d54626d commit e94dd83

3 files changed

Lines changed: 164 additions & 11 deletions

File tree

‎.github/workflows/release.yaml‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,17 @@ jobs:
101101
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
102102
echo "curl -sSL https://devlore.noblefactor.com/install.sh | GITHUB_TOKEN=<token> DEVLORE_VERSION=${{ steps.version.outputs.version }} bash" >> $GITHUB_STEP_SUMMARY
103103
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
104+
echo "\`\`\`powershell" >> $GITHUB_STEP_SUMMARY
105+
echo "\$env:GH_TOKEN = '<token>'; \$env:DEVLORE_VERSION = '${{ steps.version.outputs.version }}'; irm https://devlore.noblefactor.com/install.ps1 | iex" >> $GITHUB_STEP_SUMMARY
106+
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
104107
else
105108
echo "Full release (default latest):" >> $GITHUB_STEP_SUMMARY
106109
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
107110
echo "curl -sSL https://devlore.noblefactor.com/install.sh | GITHUB_TOKEN=<token> bash" >> $GITHUB_STEP_SUMMARY
108111
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
112+
echo "\`\`\`powershell" >> $GITHUB_STEP_SUMMARY
113+
echo "\$env:GH_TOKEN = '<token>'; irm https://devlore.noblefactor.com/install.ps1 | iex" >> $GITHUB_STEP_SUMMARY
114+
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
109115
fi
110116
echo "" >> $GITHUB_STEP_SUMMARY
111117
echo "### Artifacts" >> $GITHUB_STEP_SUMMARY
@@ -138,10 +144,12 @@ jobs:
138144
token: ${{ steps.app-token.outputs.token }}
139145
path: website
140146

141-
- name: Sync install script to website
147+
- name: Sync install scripts to website
142148
run: |
143-
# Copy install script only (binaries come from GitHub releases)
149+
# Copy the two install scripts only (binaries come from GitHub releases). Both, since #948: the
150+
# site published install.sh alone, and install.ps1's own usage line was a 404.
144151
cp install.sh website/public/install.sh
152+
cp install.ps1 website/public/install.ps1
145153
146154
cd website
147155
git config user.name "github-actions[bot]"
@@ -158,17 +166,17 @@ jobs:
158166
git checkout "$TARGET_BRANCH" || git checkout -b "$TARGET_BRANCH"
159167
160168
# Check if there are changes
161-
git add public/install.sh
169+
git add public/install.sh public/install.ps1
162170
if git diff --cached --quiet; then
163-
echo "No changes to install script"
171+
echo "No changes to the install scripts"
164172
exit 0
165173
fi
166174
167175
# Create a PR branch instead of pushing directly (respects branch protection)
168-
PR_BRANCH="sync-install-script-${{ github.sha }}"
176+
PR_BRANCH="sync-install-scripts-${{ github.sha }}"
169177
git checkout -b "$PR_BRANCH"
170178
171-
git commit -m "Update install script
179+
git commit -m "Update install scripts
172180
173181
Source: ${{ github.repository }}@${{ github.sha }}
174182
Ref: ${{ github.ref }}"
@@ -180,8 +188,8 @@ jobs:
180188
--repo NobleFactor/devlore.noblefactor.com \
181189
--base "$TARGET_BRANCH" \
182190
--head "$PR_BRANCH" \
183-
--title "Update install script from devlore-cli" \
184-
--body "Automated sync of install.sh from devlore-cli.
191+
--title "Update install scripts from devlore-cli" \
192+
--body "Automated sync of install.sh and install.ps1 from devlore-cli.
185193
186194
Source: ${{ github.repository }}@${{ github.sha }}
187195
Ref: ${{ github.ref }}")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: "install.ps1 is published beside install.sh, and runs on Windows PowerShell 5.1 — what a fresh Windows machine has"
3+
issue: https://github.com/NobleFactor/devlore-cli/issues/948
4+
status: active
5+
created: 2026-09-24
6+
updated: 2026-09-24
7+
---
8+
9+
# Plan: install.ps1 is published, and runs on 5.1
10+
11+
Lane 1 of [#949](https://github.com/NobleFactor/devlore-cli/issues/949). Design:
12+
[#798](https://github.com/NobleFactor/devlore-cli/issues/798), ruling 1 as it stands and ruling 3 as
13+
reversed below.
14+
15+
## Issue 948
16+
17+
`install.ps1` has done what `install.sh` does since #914: fetch the archive for the host, verify it, unpack it
18+
so the products sit in `bin/` beside `share/`, run each product's `self install <prefix> --unattended`. Two
19+
things stand between it and #798's acceptance:
20+
21+
- **Nobody can reach it.** `release.yaml`'s "Sync install script to website" step copies `install.sh` to the
22+
site's `public/` and `git add`s that one path. The header's own usage line, `irm
23+
https://devlore.noblefactor.com/install.ps1 | iex`, is a 404.
24+
- **It was never written for the shell it will run in.** A fresh Windows machine has Windows PowerShell 5.1
25+
and nothing else. The body assumes 7: `$IsWindows` under `Set-StrictMode -Version Latest` throws on 5.1,
26+
and `Invoke-WebRequest` without `-UseBasicParsing` engages 5.1's Internet Explorer parser.
27+
28+
**Ruling 3 reversed, 2026-09-24.** #798 had the body as `#Requires -Version 7` with a 5.1 preamble that
29+
installs pwsh and re-launches. Ruled instead: the installer uses what is known to be on the box, Windows
30+
PowerShell 5.1, and installs nothing but devlore. The shell-provider rewrite makes PowerShell 7 one shell
31+
among the ones a user may pick, not a prerequisite; the installer does not pre-empt that by installing it.
32+
33+
## Goals
34+
35+
1. `irm https://devlore.noblefactor.com/install.ps1 | iex` works, on the next release after the merge.
36+
2. The one script runs under Windows PowerShell 5.1 and PowerShell 7 alike, on Windows, macOS and Linux,
37+
one code path.
38+
3. Nothing is installed but lore, star and writ.
39+
40+
## Requirements
41+
42+
### Requirement 1: The sync publishes both scripts
43+
44+
In `.github/workflows/release.yaml`, the sync step copies `install.sh` and `install.ps1`, adds both paths,
45+
and its commit and pull-request text say "install scripts". The release summary (lines 102–107) gains the
46+
Windows one-liner beside the Unix one. Nothing else in the workflow changes.
47+
48+
### Requirement 2: The body runs on 5.1
49+
50+
The floor is declared and the 5.1 incompatibilities fixed; everything else in the body stays #914's.
51+
52+
- `#Requires -Version 5.1` as the first line after the header.
53+
- `Get-OSName`: `$IsWindows`, `$IsMacOS`, `$IsLinux` do not exist on 5.1, and under
54+
`Set-StrictMode -Version Latest` a missing variable is an error. On 5.1 the edition is `Desktop` and the
55+
platform is Windows; on 7 the automatic variables exist. The function reads `$PSVersionTable.PSEdition`
56+
first and the automatic variables only on Core.
57+
- `Invoke-WebRequest` and `Invoke-RestMethod` carry `-UseBasicParsing`: on 5.1 the default engages the
58+
Internet Explorer DOM parser, which hangs on a machine that has never opened IE. On 7 the switch is
59+
accepted and ignored.
60+
- `[Net.ServicePointManager]::SecurityProtocol` includes `Tls12` before the first request: 5.1 on a
61+
machine whose .NET default predates it cannot reach GitHub otherwise; on 7 the assignment is harmless.
62+
- Everything else the body uses is in 5.1: `Expand-Archive`, `Get-FileHash`, `Get-ChildItem -File`,
63+
`-in`/`-notin`, `[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture` (.NET Framework
64+
4.7.1+, which Windows 10 and 11 carry). The audit is Phase 3's first box.
65+
66+
### Requirement 3: The header says so
67+
68+
The header's Usage states the floor: "Runs on Windows PowerShell 5.1 and PowerShell 7." The
69+
`.\install.ps1 -Prefix` form notes that a Windows client's default execution policy refuses a downloaded
70+
script, and that the `irm | iex` form is the one that needs no policy change. The `# Parameters` block is
71+
unchanged (lane 2 adds the layer flags).
72+
73+
## Implementation Phases
74+
75+
### Phase 1: Commit the plan
76+
77+
- [x] This plan, before the change
78+
- [x] The reversal recorded on #798 as a comment, dated, in the owner's words
79+
80+
### Phase 2: The change
81+
82+
- [x] `release.yaml`: both scripts synced, both paths added, texts and summary updated (Requirement 1)
83+
- [x] `install.ps1`: the floor and the four fixes (Requirement 2)
84+
- [x] `install.ps1`: the header (Requirement 3)
85+
86+
### Phase 3: Verify
87+
88+
- [x] Every cmdlet, operator and .NET member the body uses is checked against 5.1's surface, listed in
89+
the pull request, and nothing outside it remains: the automatic `$Is*` variables are now read only
90+
on Core; `PSEdition` exists on 5.1; `Expand-Archive` (5.0), `Get-FileHash` (4.0), `-in` (3.0),
91+
`RuntimeInformation` (.NET Framework 4.7.1) are all inside it
92+
- [x] Under pwsh 7.x on DANOBLE-UD24-1 (linux/arm64): the body ran against a scratch prefix,
93+
`DEVLORE_TOOLS=writ`, pre-release `v0.1.0-dev.20260924074819`, checksum verified, and
94+
`<prefix>/bin/writ --version` reported build `d54626d2`; nothing outside the prefix was written
95+
- [ ] Under Windows PowerShell 5.1 on a Windows machine (DANOBLE-WD11-3), both invocation forms, against a
96+
scratch prefix: the products install and answer `--version`. This is rule 11's live-path test and it
97+
cannot run here; it is the owner's, or a Windows job's, and this box stays open until it has
98+
- [x] `release.yaml` parses as YAML; the sync step's shell block, extracted with the `${{ }}` expressions
99+
replaced, passes `bash -n`; shellcheck's one real finding is the pre-existing SC2164 on `cd website`,
100+
not this change's
101+
- [x] No gate in this repository reads `.ps1`: `star lint` has go, go-style, markdown and shell, and
102+
`ci.yaml` runs no PSScriptAnalyzer. Checked by hand with PSScriptAnalyzer 1.25.0 (CI's pin,
103+
installed to this host's CurrentUser scope that day) and the organization's settings: 33 findings,
104+
every one present on the base too (`PSAvoidUsingWriteHost` and kin), none this change's. The gap
105+
is noted for the lint-tooling schedule (noblefactor-ops#232)
106+
107+
### Phase 4: Merge, publish, converge
108+
109+
- [x] PR script written, shown, and handed over
110+
- [ ] After the merge: the next pre-release's sync opens the site PR carrying both scripts; when it merges,
111+
`curl -sI https://devlore.noblefactor.com/install.ps1` answers 200 and the body matches the
112+
repository's. Until a release runs, this box stays open and the plan `active`
113+
114+
## Out of Scope
115+
116+
- **The layer flags** — lane 2, #950.
117+
- **Installing PowerShell 7**, or any shell. Reversed above.
118+
- **What the products do under 5.1.** `self install` is each product's; if writ's PowerShell actions need
119+
pwsh on Windows, that is the shell-provider rewrite's question, not the installer's.
120+
- **A Windows CI job that runs the installer end to end.** #855's scenario jobs cover writ's journey, not
121+
the installer; a job that provisions a 5.1-only Windows is its own work.

‎install.ps1‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
# Usage: irm https://devlore.noblefactor.com/install.ps1 | iex
66
# .\install.ps1 -Prefix "C:\devlore"
77
#
8+
# Runs on Windows PowerShell 5.1 -- what a fresh Windows machine has -- and on PowerShell 7, on Windows,
9+
# macOS and Linux, one code path. It installs nothing but lore, star and writ (#798, ruled 2026-09-24).
10+
# The irm | iex form needs no execution-policy change; a Windows client's default policy refuses a
11+
# downloaded .ps1 run as a file, so the second form wants `-ExecutionPolicy Bypass` on the pwsh or
12+
# powershell command line.
13+
#
814
# For private repo (requires GitHub token):
915
# $env:GH_TOKEN = (gh auth token); irm https://devlore.noblefactor.com/install.ps1 | iex
1016
#
@@ -24,6 +30,8 @@
2430
# - GitHub Releases API: https://docs.github.com/en/rest/releases/releases
2531
# - GitHub Release Assets API: https://docs.github.com/en/rest/releases/assets
2632

33+
#Requires -Version 5.1
34+
2735
[CmdletBinding()]
2836
param(
2937
[string]$Prefix,
@@ -68,8 +76,15 @@ function Write-Fatal {
6876
}
6977

7078
# Detect OS
79+
#
80+
# Windows PowerShell 5.1 has no $IsWindows, $IsMacOS or $IsLinux, and under Set-StrictMode a variable that
81+
# does not exist is an error, so the edition is read first: Desktop is 5.1, which runs on Windows alone.
82+
# The automatic variables are consulted only on Core, where they exist.
7183
function Get-OSName {
72-
if ($IsWindows -or [System.Environment]::OSVersion.Platform -eq 'Win32NT') {
84+
if ($PSVersionTable.PSEdition -ne 'Core') {
85+
return "windows"
86+
}
87+
if ($IsWindows) {
7388
return "windows"
7489
} elseif ($IsMacOS) {
7590
return "darwin"
@@ -103,10 +118,14 @@ function Get-ApiHeaders {
103118

104119
# Make authenticated API request
105120
# Per https://docs.github.com/en/rest/releases/releases
121+
#
122+
# -UseBasicParsing on every web call: without it Windows PowerShell 5.1 parses responses with the Internet
123+
# Explorer engine, which hangs on a machine that has never run IE's first-launch dialog. PowerShell 7
124+
# accepts the switch and ignores it.
106125
function Invoke-ApiGet {
107126
param([string]$Url)
108127
$headers = Get-ApiHeaders
109-
Invoke-RestMethod -Uri $Url -Headers $headers -ErrorAction Stop
128+
Invoke-RestMethod -Uri $Url -Headers $headers -UseBasicParsing -ErrorAction Stop
110129
}
111130

112131
# Get latest release version from GitHub API
@@ -137,7 +156,7 @@ function Save-ReleaseAsset {
137156
param([string]$AssetId, [string]$Destination)
138157
$url = "$GitHubApi/releases/assets/$AssetId"
139158
$headers = Get-ApiHeaders -Accept "application/octet-stream"
140-
Invoke-WebRequest -Uri $url -Headers $headers -OutFile $Destination -ErrorAction Stop
159+
Invoke-WebRequest -Uri $url -Headers $headers -OutFile $Destination -UseBasicParsing -ErrorAction Stop
141160
}
142161

143162
# Verify checksum
@@ -157,6 +176,11 @@ function Main {
157176
Write-Info "DevLore CLI Installer"
158177
Write-Host ""
159178

179+
# GitHub speaks TLS 1.2 and later. Windows PowerShell 5.1 takes its protocols from the .NET Framework's
180+
# default, which on an older machine stops at 1.0, so every request fails before it starts. Adding 1.2
181+
# is a no-op wherever the default already includes it, PowerShell 7 included.
182+
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
183+
160184
# Check for auth token (required for private repo)
161185
if (-not $AuthToken) {
162186
Write-Warn "No GH_TOKEN set. This will fail for private repositories."

0 commit comments

Comments
 (0)