From 433a9e7d35e874bb022dbe8fecea5075e1bffd5e Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 14:39:26 -0500 Subject: [PATCH 01/61] Migrate dotfile deployment from Dotbot to chezmoi --- .chezmoiroot | 1 + .gitignore | 12 +- .gitmodules | 4 - README.md | 53 +- {config => assets}/win-terminal/config.json | 0 {config => assets}/wsl/wsl.conf | 0 bootstrap.cmd | 110 ++++ bootstrap.sh | 93 ++++ config/codex/README.md => docs/codex.md | 12 +- home/.chezmoiignore | 11 + .../run_after_90-deploy-pwsh.cmd.tmpl | 18 + .../.chezmoitemplates}/pwsh/env.ps1 | 0 .../.chezmoitemplates}/pwsh/lib/aliases.ps1 | 0 .../.chezmoitemplates}/pwsh/lib/helpers.ps1 | 0 .../.chezmoitemplates}/pwsh/profile.ps1 | 0 home/create_empty_dot_gitconfig.local | 0 {config/codex => home/dot_codex}/AGENTS.md | 0 .../dot_codex/create_config.toml | 0 .../dot_codex/rules/create_default.rules | 0 .../dot_codex}/skills/mule-munit/SKILL.md | 0 .../skills/mule-munit/agents/openai.yaml | 0 .../dot_config}/sheldon/plugins.toml | 0 .../dot_config}/starship/config.toml | 0 .../dot_config}/starship/lean.config.toml | 0 {config => home/dot_config}/zsh/.zshrc | 0 .../dot_config}/zsh/lib/aliases.zsh | 0 .../dot_config}/zsh/lib/completions.zsh | 0 .../dot_config}/zsh/lib/key-bindings.zsh | 0 .../dot_config}/zsh/lib/sheldon.zsh | 0 .../dot_config}/zsh/lib/utilities.zsh | 0 {config => home/dot_config}/zsh/lib/wsl.zsh | 0 .../.gitignore_global => home/dot_fdignore | 0 .../general/.gitconfig => home/dot_gitconfig | 0 config/general/.npmrc => home/dot_npmrc | 0 config/general/.vimrc => home/dot_vimrc | 0 config/wsl/.wslconfig => home/dot_wslconfig | 0 config/zsh/.zshenv => home/dot_zshenv | 0 .../private_dot_ssh}/jsilverdev.pub | 0 install.ps1 | 511 ++++++------------ install.sh | 79 +-- lets-go.ps1 | 80 --- lets-go.sh | 51 -- lib/dotbot | 1 - meta/base.yaml | 41 -- meta/configs/codex.yaml | 10 - meta/configs/pwsh.yaml | 12 - meta/configs/windows.yaml | 5 - meta/configs/zsh.yaml | 20 - scripts/windows/deploy-pwsh.ps1 | 45 ++ scripts/windows/invoke-ps-script.cmd | 20 + scripts/windows/signing.ps1 | 165 ++++++ update.cmd | 29 + update.sh | 26 + 53 files changed, 743 insertions(+), 666 deletions(-) create mode 100644 .chezmoiroot delete mode 100644 .gitmodules rename {config => assets}/win-terminal/config.json (100%) rename {config => assets}/wsl/wsl.conf (100%) create mode 100644 bootstrap.cmd create mode 100644 bootstrap.sh rename config/codex/README.md => docs/codex.md (68%) create mode 100644 home/.chezmoiignore create mode 100644 home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl rename {config => home/.chezmoitemplates}/pwsh/env.ps1 (100%) rename {config => home/.chezmoitemplates}/pwsh/lib/aliases.ps1 (100%) rename {config => home/.chezmoitemplates}/pwsh/lib/helpers.ps1 (100%) rename {config => home/.chezmoitemplates}/pwsh/profile.ps1 (100%) create mode 100644 home/create_empty_dot_gitconfig.local rename {config/codex => home/dot_codex}/AGENTS.md (100%) rename config/codex/config.toml.example => home/dot_codex/create_config.toml (100%) rename config/codex/rules/default.rules.example => home/dot_codex/rules/create_default.rules (100%) rename {config/codex => home/dot_codex}/skills/mule-munit/SKILL.md (100%) rename {config/codex => home/dot_codex}/skills/mule-munit/agents/openai.yaml (100%) rename {config => home/dot_config}/sheldon/plugins.toml (100%) rename {config => home/dot_config}/starship/config.toml (100%) rename {config => home/dot_config}/starship/lean.config.toml (100%) rename {config => home/dot_config}/zsh/.zshrc (100%) mode change 100755 => 100644 rename {config => home/dot_config}/zsh/lib/aliases.zsh (100%) rename {config => home/dot_config}/zsh/lib/completions.zsh (100%) rename {config => home/dot_config}/zsh/lib/key-bindings.zsh (100%) rename {config => home/dot_config}/zsh/lib/sheldon.zsh (100%) rename {config => home/dot_config}/zsh/lib/utilities.zsh (100%) rename {config => home/dot_config}/zsh/lib/wsl.zsh (100%) rename config/general/.gitignore_global => home/dot_fdignore (100%) mode change 100755 => 100644 rename config/general/.gitconfig => home/dot_gitconfig (100%) mode change 100755 => 100644 rename config/general/.npmrc => home/dot_npmrc (100%) rename config/general/.vimrc => home/dot_vimrc (100%) mode change 100755 => 100644 rename config/wsl/.wslconfig => home/dot_wslconfig (100%) rename config/zsh/.zshenv => home/dot_zshenv (100%) rename {config/ssh => home/private_dot_ssh}/jsilverdev.pub (100%) delete mode 100644 lets-go.ps1 delete mode 100644 lets-go.sh delete mode 160000 lib/dotbot delete mode 100644 meta/base.yaml delete mode 100644 meta/configs/codex.yaml delete mode 100644 meta/configs/pwsh.yaml delete mode 100644 meta/configs/windows.yaml delete mode 100644 meta/configs/zsh.yaml create mode 100644 scripts/windows/deploy-pwsh.ps1 create mode 100644 scripts/windows/invoke-ps-script.cmd create mode 100644 scripts/windows/signing.ps1 create mode 100644 update.cmd create mode 100644 update.sh diff --git a/.chezmoiroot b/.chezmoiroot new file mode 100644 index 0000000..5e72f75 --- /dev/null +++ b/.chezmoiroot @@ -0,0 +1 @@ +home diff --git a/.gitignore b/.gitignore index 0903fae..4c61613 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ .vscode # Auto-generated files -config/zsh/.zcompcache/* -config/zsh/.zcompdump* -config/zsh/.zhistory -config/zsh/*/*.zwc -config/zsh/.zsh_sessions/* +home/dot_config/zsh/.zcompcache/* +home/dot_config/zsh/.zcompdump* +home/dot_config/zsh/.zhistory +home/dot_config/zsh/*/*.zwc +home/dot_config/zsh/.zsh_sessions/* -fonts/ \ No newline at end of file +fonts/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1e300ff..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "dotbot"] - path = lib/dotbot - url = https://github.com/anishathalye/dotbot - ignore = dirty diff --git a/README.md b/README.md index 3683581..dd7e9bc 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,46 @@ # Dotfiles -Welcome to my dotfiles! This repository contains configurations and scripts to quickly set up a development environment on Windows and Linux. +This repository uses [chezmoi](https://www.chezmoi.io/) to deploy regular dotfiles on Windows and Linux. Repository support files stay outside the `home/` source state. -## Quick Installation +## Quick installation -### 🖥️ Windows +### Windows -To set up your environment on Windows, run the following command in PowerShell: +Run this from a normal `cmd.exe` prompt: -```powershell -Invoke-WebRequest -Uri "https://raw.githubusercontent.com/jsilverdev/dotfiles/main/lets-go.ps1" -OutFile ".\lets-go.ps1"; Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned; .\lets-go.ps1 +```cmd +curl.exe -fsSLo "%TEMP%\dotfiles-bootstrap.cmd" https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.cmd && call "%TEMP%\dotfiles-bootstrap.cmd" ``` -To update installed applications and PowerShell modules: +The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. -```powershell -.\install.ps1 -Update # or -u -``` +The bootstrap does not require administrator rights or Developer Mode. It invokes PowerShell 7 with `-NoProfile`; an effective `AllSigned` policy is supported automatically. Each machine reuses or creates its own current-user Code Signing certificate and trusts its public certificate locally. No private key, PFX, or KeePass dependency is used. -### 🐧 Linux +### Linux -To set up your environment on Linux, use this command: +On Debian or Arch Linux, run: ```bash -bash <(curl -s https://raw.githubusercontent.com/jsilverdev/dotfiles/main/lets-go.sh) +bash <(curl -fsSL https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.sh) ``` -To refresh existing packages, pass `--update` (or `-u`): +The bootstrap installs the minimal tools, installs chezmoi in `~/.local/bin` when needed, applies the home state, and then runs the package/application installer. WSL-specific system configuration is applied only when the installer is running inside WSL. -```bash -./install.sh --update -``` +## Updates -## Requirements +For dotfiles-only updates, use: -### 🖥️ Windows -- PowerShell -- [Dev Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development) enabled +```text +chezmoi update +``` -### 🐧 Linux -- Debian / Arch Linux -- curl +For dotfiles plus package/application and module updates, use `update.cmd` on Windows or `./update.sh` on Linux. Those wrappers run `chezmoi update` first and then the platform installer in update mode. -## Acknowledgments +## State details -This dotfiles repository is inspired by: -- [Lissy93's dotfiles](https://github.com/Lissy93/dotfiles) -- [KEVINNITRO DOTFILES](https://github.com/KevinNitroG/dotfiles). +- `home/.chezmoiroot` is represented by the repository-level `.chezmoiroot`, pointing to `home`. +- Windows-only `.wslconfig` and Linux-only Zsh/Sheldon state are filtered by `home/.chezmoiignore`. +- `~/.gitconfig.local`, `~/.codex/config.toml`, and `~/.codex/rules/default.rules` use chezmoi create-only semantics and are not overwritten after creation. +- Codex guidance and skills are managed normally; repository documentation is kept outside `~/.codex`. +- PowerShell source files are unsigned templates. The post-apply hook deploys copies and signs only the runtime files when `AllSigned` is effective, so Authenticode signatures never dirty chezmoi source state. +- Managed PowerShell modules currently include `PSFzf` and `git-aliases`. Under `AllSigned`, only their user-scoped PowerShell content is inspected and unsigned/invalid files are signed; valid publisher signatures are preserved. diff --git a/config/win-terminal/config.json b/assets/win-terminal/config.json similarity index 100% rename from config/win-terminal/config.json rename to assets/win-terminal/config.json diff --git a/config/wsl/wsl.conf b/assets/wsl/wsl.conf similarity index 100% rename from config/wsl/wsl.conf rename to assets/wsl/wsl.conf diff --git a/bootstrap.cmd b/bootstrap.cmd new file mode 100644 index 0000000..6b6c9d3 --- /dev/null +++ b/bootstrap.cmd @@ -0,0 +1,110 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" + +call :remove_legacy_broken_links +if errorlevel 1 exit /b 1 + +where winget.exe >nul 2>&1 +if errorlevel 1 ( + echo WinGet is not registered for this user. Attempting App Installer registration... + "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -Command "Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" + if errorlevel 1 ( + echo App Installer registration failed. WinGet may be disabled by corporate policy. 1>&2 + exit /b 1 + ) +) +where winget.exe >nul 2>&1 +if errorlevel 1 ( + echo WinGet is unavailable after App Installer registration. Check corporate policy or App Installer registration. 1>&2 + exit /b 1 +) + +call :ensure_package Git.Git git +if errorlevel 1 exit /b 1 +call :ensure_package Microsoft.PowerShell pwsh +if errorlevel 1 exit /b 1 +call :ensure_package jdx.mise mise +if errorlevel 1 exit /b 1 +call :ensure_package twpayne.chezmoi chezmoi +if errorlevel 1 exit /b 1 + +call :refresh_path +where git.exe >nul 2>&1 || (echo Git is still unavailable after installation. 1>&2 & exit /b 1) +where pwsh.exe >nul 2>&1 || (echo PowerShell 7 is still unavailable after installation. 1>&2 & exit /b 1) +where mise.exe >nul 2>&1 || (echo mise is still unavailable after installation. 1>&2 & exit /b 1) +where chezmoi.exe >nul 2>&1 || (echo chezmoi is still unavailable after installation. 1>&2 & exit /b 1) + +call :initialize_chezmoi +if errorlevel 1 ( + echo chezmoi initialization/update failed. 1>&2 + exit /b 1 +) + +call :resolve_repo_root +if not defined REPO_ROOT ( + echo Unable to resolve the chezmoi working tree. 1>&2 + exit /b 1 +) +if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( + echo Resolved chezmoi working tree does not contain the dotfiles scripts: "%REPO_ROOT%" 1>&2 + exit /b 1 +) + +call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" +exit /b %ERRORLEVEL% + +:ensure_package +set "PACKAGE_ID=%~1" +set "PACKAGE_COMMAND=%~2" +where "%PACKAGE_COMMAND%.exe" >nul 2>&1 +if not errorlevel 1 exit /b 0 +echo Installing %PACKAGE_ID% for the current user... +winget.exe install --id "%PACKAGE_ID%" --exact --source winget --scope user --silent --accept-source-agreements --accept-package-agreements +if errorlevel 1 ( + echo Unable to install %PACKAGE_ID% without administrator rights. No machine-scope or portable fallback will be attempted. 1>&2 + exit /b 1 +) +exit /b 0 + +:remove_legacy_broken_links +rem Remove only broken legacy links so Git and chezmoi can migrate them +rem to regular files. Existing valid links and unrelated junctions are kept. +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -Command "$paths = @('%USERPROFILE%\.gitconfig','%USERPROFILE%\.gitconfig.local','%USERPROFILE%\.fdignore','%USERPROFILE%\.npmrc','%USERPROFILE%\.vimrc','%USERPROFILE%\.zshenv','%USERPROFILE%\.wslconfig','%USERPROFILE%\.ssh\jsilverdev.pub','%USERPROFILE%\.config\starship\config.toml','%USERPROFILE%\.config\starship\lean.config.toml','%USERPROFILE%\.codex\AGENTS.md','%USERPROFILE%\.codex\skills\mule-munit\SKILL.md','%USERPROFILE%\.codex\skills\mule-munit\agents\openai.yaml','%USERPROFILE%\Documents\PowerShell\profile.ps1'); foreach ($path in $paths) { if (Test-Path -LiteralPath $path) { $item = Get-Item -LiteralPath $path -Force; if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -and -not (Test-Path -LiteralPath $item.Target)) { Remove-Item -LiteralPath $path -Force } } }" +if errorlevel 1 ( + echo Unable to clean broken legacy dotfile links. 1>&2 + exit /b 1 +) +exit /b 0 + +:refresh_path +set "PATH=%PATH%;%LOCALAPPDATA%\Microsoft\WinGet\Links;%LOCALAPPDATA%\Programs\Microsoft.PowerShell;%LOCALAPPDATA%\Programs\mise;%LOCALAPPDATA%\Programs\chezmoi" +for /f "tokens=2,*" %%A in ('reg query HKCU\Environment /v Path 2^>nul ^| findstr /i "Path"') do set "PATH=!PATH!;%%B" +exit /b 0 + +:resolve_repo_root +set "REPO_ROOT=" +if exist "%CD%\.chezmoiroot" if exist "%CD%\install.ps1" (set "REPO_ROOT=%CD%"& exit /b 0) +for /f "delims=" %%R in ('chezmoi.exe execute-template "{{ .chezmoi.workingTree }}" 2^>nul') do set "REPO_ROOT=%%R" +if defined REPO_ROOT if exist "%REPO_ROOT%\install.ps1" exit /b 0 +for /f "delims=" %%R in ('chezmoi.exe source-path 2^>nul') do set "SOURCE_ROOT=%%R" +if defined SOURCE_ROOT if exist "%SOURCE_ROOT%\install.ps1" (set "REPO_ROOT=%SOURCE_ROOT%"& exit /b 0) +if defined SOURCE_ROOT for %%P in ("%SOURCE_ROOT%\..") do if exist "%%~fP\install.ps1" (set "REPO_ROOT=%%~fP"& exit /b 0) +exit /b 1 + +:initialize_chezmoi +if exist "%CD%\.chezmoiroot" ( + chezmoi.exe --source "%CD%" apply + exit /b %ERRORLEVEL% +) +set "SOURCE_ROOT=" +for /f "delims=" %%R in ('chezmoi.exe source-path 2^>nul') do set "SOURCE_ROOT=%%R" +if defined SOURCE_ROOT if exist "%SOURCE_ROOT%\.chezmoiroot" goto existing_chezmoi +if defined SOURCE_ROOT for %%P in ("%SOURCE_ROOT%\..") do if exist "%%~fP\.chezmoiroot" goto existing_chezmoi +chezmoi.exe init --apply "%REPO_URL%" +exit /b %ERRORLEVEL% + +:existing_chezmoi +chezmoi.exe update +exit /b %ERRORLEVEL% diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..19defca --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL="${DOTFILES_REPO:-https://github.com/jsilverdev/dotfiles.git}" +export PATH="$HOME/.local/bin:$PATH" + +remove_legacy_links() { + local path target + local paths=( + "$HOME/.gitconfig" + "$HOME/.fdignore" + "$HOME/.npmrc" + "$HOME/.vimrc" + "$HOME/.zshenv" + "$HOME/.wslconfig" + "$HOME/.ssh/jsilverdev.pub" + "$HOME/.config/starship/config.toml" + "$HOME/.config/starship/lean.config.toml" + "$HOME/.config/sheldon/plugins.toml" + "$HOME/.config/zsh/.zshrc" + "$HOME/.codex/AGENTS.md" + "$HOME/.codex/skills/mule-munit/SKILL.md" + "$HOME/.codex/skills/mule-munit/agents/openai.yaml" + ) + + for path in "${paths[@]}"; do + if [[ -L "$path" ]]; then + target="$(readlink -f -- "$path" 2>/dev/null || true)" + if [[ "$target" == "$HOME/.dotfiles/"* ]]; then + rm -f -- "$path" + fi + fi + done +} + +remove_legacy_links + +if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install --yes git curl wget zsh +elif [[ -f /etc/arch-release ]] && command -v pacman >/dev/null 2>&1; then + sudo pacman -Syu --noconfirm --needed git curl wget zsh +else + printf 'Unsupported Linux distribution. Debian and Arch Linux are supported.\n' >&2 + exit 1 +fi + +command -v git >/dev/null 2>&1 || { printf 'Git is required but unavailable.\n' >&2; exit 1; } +command -v curl >/dev/null 2>&1 || { printf 'curl is required but unavailable.\n' >&2; exit 1; } +command -v wget >/dev/null 2>&1 || { printf 'wget is required but unavailable.\n' >&2; exit 1; } +command -v zsh >/dev/null 2>&1 || { printf 'zsh is required but unavailable.\n' >&2; exit 1; } + +if ! command -v chezmoi >/dev/null 2>&1; then + mkdir -p "$HOME/.local/bin" + sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" +fi +export PATH="$HOME/.local/bin:$PATH" +command -v chezmoi >/dev/null 2>&1 || { printf 'chezmoi installation failed.\n' >&2; exit 1; } + +if [[ -f "$PWD/.chezmoiroot" ]]; then + chezmoi --source "$PWD" apply +else + SOURCE_ROOT="$(chezmoi source-path 2>/dev/null || true)" + if [[ -f "$SOURCE_ROOT/.chezmoiroot" || -f "$(dirname "$SOURCE_ROOT")/.chezmoiroot" ]]; then + chezmoi update + else + chezmoi init --apply "$REPO_URL" + fi +fi + +resolve_repo_root() { + local candidate parent + if [[ -f "$PWD/.chezmoiroot" && -f "$PWD/install.sh" ]]; then + printf '%s\n' "$PWD" + return 0 + fi + for candidate in "$(chezmoi execute-template '{{ .chezmoi.workingTree }}' 2>/dev/null || true)" "$(chezmoi source-path)"; do + [[ -n "$candidate" ]] || continue + if [[ -f "$candidate/install.sh" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + parent="$(dirname "$candidate")" + if [[ -f "$parent/install.sh" ]]; then + printf '%s\n' "$parent" + return 0 + fi + done + return 1 +} + +REPO_ROOT="$(resolve_repo_root)" || { printf 'Unable to resolve the chezmoi working tree.\n' >&2; exit 1; } +exec "$REPO_ROOT/install.sh" diff --git a/config/codex/README.md b/docs/codex.md similarity index 68% rename from config/codex/README.md rename to docs/codex.md index b977305..391fab3 100644 --- a/config/codex/README.md +++ b/docs/codex.md @@ -1,16 +1,16 @@ # Codex dotfiles -This directory contains only portable Codex defaults. Dotbot links stable -guidance and skills to `~/.codex`; mutable files are copied only when missing. +This directory contains only portable Codex defaults. Chezmoi deploys stable +guidance and skills to `~/.codex`; mutable files are created only when missing. ## Included -- `config.toml.example`: initial UI, sandbox, features, and default reasoning - effort. The installer copies it to `~/.codex/config.toml` only when missing. +- `create_config.toml`: initial UI, sandbox, features, and default reasoning + effort. Chezmoi creates it as `~/.codex/config.toml` only when missing. - `AGENTS.md`: personal working conventions that apply to every repository. -- `rules/default.rules.example`: initial narrowly scoped command approvals. +- `rules/create_default.rules`: initial narrowly scoped command approvals. - `skills/*`: the individual files of every skill placed in this directory are - linked to `~/.codex/skills`, including the bundled `mule-munit` workflow. + deployed to `~/.codex/skills`, including the bundled `mule-munit` workflow. ## Deliberately excluded diff --git a/home/.chezmoiignore b/home/.chezmoiignore new file mode 100644 index 0000000..059cb1c --- /dev/null +++ b/home/.chezmoiignore @@ -0,0 +1,11 @@ +{{- if eq .chezmoi.os "windows" }} +.zshenv +.config/zsh +.config/sheldon +{{- else }} +.config/pwsh +.wslconfig +.chezmoiscripts +.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl +.chezmoiscripts/90-deploy-pwsh.cmd +{{- end }} diff --git a/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl b/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl new file mode 100644 index 0000000..ada9a32 --- /dev/null +++ b/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl @@ -0,0 +1,18 @@ +{{- if eq .chezmoi.os "windows" }} +@echo off +setlocal EnableExtensions + +set "REPO_ROOT={{ .chezmoi.workingTree }}" +if not defined REPO_ROOT for /f "delims=" %%R in ('chezmoi execute-template "{{ .chezmoi.workingTree }}" 2^>nul') do set "REPO_ROOT=%%R" +if not defined REPO_ROOT set "REPO_ROOT={{ .chezmoi.sourceDir }}" +if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" for /f "delims=" %%S in ('chezmoi source-path 2^>nul') do set "REPO_ROOT=%%S" +if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" for %%P in ("%REPO_ROOT%\..") do set "REPO_ROOT=%%~fP" + +if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( + echo Unable to locate the dotfiles working tree: "%REPO_ROOT%" 1>&2 + exit /b 1 +) + +call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\scripts\windows\deploy-pwsh.ps1" -RepoRoot "%REPO_ROOT%" +exit /b %ERRORLEVEL% +{{- end }} diff --git a/config/pwsh/env.ps1 b/home/.chezmoitemplates/pwsh/env.ps1 similarity index 100% rename from config/pwsh/env.ps1 rename to home/.chezmoitemplates/pwsh/env.ps1 diff --git a/config/pwsh/lib/aliases.ps1 b/home/.chezmoitemplates/pwsh/lib/aliases.ps1 similarity index 100% rename from config/pwsh/lib/aliases.ps1 rename to home/.chezmoitemplates/pwsh/lib/aliases.ps1 diff --git a/config/pwsh/lib/helpers.ps1 b/home/.chezmoitemplates/pwsh/lib/helpers.ps1 similarity index 100% rename from config/pwsh/lib/helpers.ps1 rename to home/.chezmoitemplates/pwsh/lib/helpers.ps1 diff --git a/config/pwsh/profile.ps1 b/home/.chezmoitemplates/pwsh/profile.ps1 similarity index 100% rename from config/pwsh/profile.ps1 rename to home/.chezmoitemplates/pwsh/profile.ps1 diff --git a/home/create_empty_dot_gitconfig.local b/home/create_empty_dot_gitconfig.local new file mode 100644 index 0000000..e69de29 diff --git a/config/codex/AGENTS.md b/home/dot_codex/AGENTS.md similarity index 100% rename from config/codex/AGENTS.md rename to home/dot_codex/AGENTS.md diff --git a/config/codex/config.toml.example b/home/dot_codex/create_config.toml similarity index 100% rename from config/codex/config.toml.example rename to home/dot_codex/create_config.toml diff --git a/config/codex/rules/default.rules.example b/home/dot_codex/rules/create_default.rules similarity index 100% rename from config/codex/rules/default.rules.example rename to home/dot_codex/rules/create_default.rules diff --git a/config/codex/skills/mule-munit/SKILL.md b/home/dot_codex/skills/mule-munit/SKILL.md similarity index 100% rename from config/codex/skills/mule-munit/SKILL.md rename to home/dot_codex/skills/mule-munit/SKILL.md diff --git a/config/codex/skills/mule-munit/agents/openai.yaml b/home/dot_codex/skills/mule-munit/agents/openai.yaml similarity index 100% rename from config/codex/skills/mule-munit/agents/openai.yaml rename to home/dot_codex/skills/mule-munit/agents/openai.yaml diff --git a/config/sheldon/plugins.toml b/home/dot_config/sheldon/plugins.toml similarity index 100% rename from config/sheldon/plugins.toml rename to home/dot_config/sheldon/plugins.toml diff --git a/config/starship/config.toml b/home/dot_config/starship/config.toml similarity index 100% rename from config/starship/config.toml rename to home/dot_config/starship/config.toml diff --git a/config/starship/lean.config.toml b/home/dot_config/starship/lean.config.toml similarity index 100% rename from config/starship/lean.config.toml rename to home/dot_config/starship/lean.config.toml diff --git a/config/zsh/.zshrc b/home/dot_config/zsh/.zshrc old mode 100755 new mode 100644 similarity index 100% rename from config/zsh/.zshrc rename to home/dot_config/zsh/.zshrc diff --git a/config/zsh/lib/aliases.zsh b/home/dot_config/zsh/lib/aliases.zsh similarity index 100% rename from config/zsh/lib/aliases.zsh rename to home/dot_config/zsh/lib/aliases.zsh diff --git a/config/zsh/lib/completions.zsh b/home/dot_config/zsh/lib/completions.zsh similarity index 100% rename from config/zsh/lib/completions.zsh rename to home/dot_config/zsh/lib/completions.zsh diff --git a/config/zsh/lib/key-bindings.zsh b/home/dot_config/zsh/lib/key-bindings.zsh similarity index 100% rename from config/zsh/lib/key-bindings.zsh rename to home/dot_config/zsh/lib/key-bindings.zsh diff --git a/config/zsh/lib/sheldon.zsh b/home/dot_config/zsh/lib/sheldon.zsh similarity index 100% rename from config/zsh/lib/sheldon.zsh rename to home/dot_config/zsh/lib/sheldon.zsh diff --git a/config/zsh/lib/utilities.zsh b/home/dot_config/zsh/lib/utilities.zsh similarity index 100% rename from config/zsh/lib/utilities.zsh rename to home/dot_config/zsh/lib/utilities.zsh diff --git a/config/zsh/lib/wsl.zsh b/home/dot_config/zsh/lib/wsl.zsh similarity index 100% rename from config/zsh/lib/wsl.zsh rename to home/dot_config/zsh/lib/wsl.zsh diff --git a/config/general/.gitignore_global b/home/dot_fdignore old mode 100755 new mode 100644 similarity index 100% rename from config/general/.gitignore_global rename to home/dot_fdignore diff --git a/config/general/.gitconfig b/home/dot_gitconfig old mode 100755 new mode 100644 similarity index 100% rename from config/general/.gitconfig rename to home/dot_gitconfig diff --git a/config/general/.npmrc b/home/dot_npmrc similarity index 100% rename from config/general/.npmrc rename to home/dot_npmrc diff --git a/config/general/.vimrc b/home/dot_vimrc old mode 100755 new mode 100644 similarity index 100% rename from config/general/.vimrc rename to home/dot_vimrc diff --git a/config/wsl/.wslconfig b/home/dot_wslconfig similarity index 100% rename from config/wsl/.wslconfig rename to home/dot_wslconfig diff --git a/config/zsh/.zshenv b/home/dot_zshenv similarity index 100% rename from config/zsh/.zshenv rename to home/dot_zshenv diff --git a/config/ssh/jsilverdev.pub b/home/private_dot_ssh/jsilverdev.pub similarity index 100% rename from config/ssh/jsilverdev.pub rename to home/private_dot_ssh/jsilverdev.pub diff --git a/install.ps1 b/install.ps1 index 21ef117..a0b5b0b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,421 +1,246 @@ [CmdletBinding()] param( [Alias("u")] - [switch]$Update + [switch]$Update, + + [string]$RepoRoot ) -### Start Utils -function RefreshPath() { - $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") +$ErrorActionPreference = "Stop" +if ([string]::IsNullOrWhiteSpace($RepoRoot)) { + $RepoRoot = $PSScriptRoot +} +$RepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path + +function Refresh-Path { + $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $env:Path = @($machinePath, $userPath) -join ";" } -function CheckRequiredApps() { +function Check-RequiredApps { if ($PSVersionTable.PSVersion.Major -lt 7) { - Write-Host "This script requires PowerShell 7 or newer. Exiting..." -ForegroundColor Red - exit + throw "This installer requires PowerShell 7 or newer." } - if (-not (Get-Command -Name git -ErrorAction SilentlyContinue)) { - Write-Host "Git is not installed. Please install Git before running this script." -ForegroundColor Red - exit + foreach ($command in @("git", "winget")) { + if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { + throw "$command is not available. Run bootstrap.cmd first." + } } } -function EnsureDevModeIsEnabled() { - try { - if ((Get-WindowsDeveloperLicense).IsValid) { - Write-Host "Developer Mode is Enabled" -ForegroundColor Green - } - else { - Write-Host "Please enable the Developer Mode and RESTART!!! before continue" -ForegroundColor Red - exit - } +function Invoke-SigningHelper { + param( + [Parameter(Mandatory)][ValidateSet("ProtectFiles", "ProtectModule")][string]$Action, + [string[]]$Path, + [string]$ModuleName + ) + + if ((Get-ExecutionPolicy) -ne "AllSigned") { + return } - catch { - Write-Host "An error occurred while checking the developer license: $_" -ForegroundColor Red - exit + + $helper = Join-Path $RepoRoot "scripts\windows\signing.ps1" + $bridge = Join-Path $RepoRoot "scripts\windows\invoke-ps-script.cmd" + if (-not (Test-Path -LiteralPath $helper) -or -not (Test-Path -LiteralPath $bridge)) { + throw "The centralized PowerShell signing helper is missing from $RepoRoot." } -} -function CheckWinget() { - if ($null -eq (Get-Command -Name winget -ErrorAction SilentlyContinue)) { - Write-Output "Enable winget..." - Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe - RefreshPath + $arguments = @($helper, "-Action", $Action) + if ($Path) { $arguments += @("-Path") + $Path } + if ($ModuleName) { $arguments += @("-ModuleName", $ModuleName) } + & $bridge @arguments + if ($LASTEXITCODE -ne 0) { + throw "The centralized PowerShell signing helper failed for $Action." } } -function InstallWithWinget() { +function Install-WithWinget { param( - [string]$appId, - [string]$alias, + [Parameter(Mandatory)][string]$AppId, + [string]$Alias, [switch]$Update ) - if (-not ([string]::IsNullOrEmpty($alias))) { - Get-Command -Name $alias -ErrorAction SilentlyContinue | Out-Null + $installed = $false + if ($Alias) { + $installed = $null -ne (Get-Command -Name $Alias -ErrorAction SilentlyContinue) } else { - winget list --accept-source-agreements -e --id $appId -n 1 | Out-Null + & winget list --id $AppId --exact --source winget --accept-source-agreements *> $null + $installed = $LASTEXITCODE -eq 0 } - if (-not $?) { - Write-Host "$appId is not installed. Installing..." -ForegroundColor Yellow - winget install -e --accept-source-agreements --accept-package-agreements --id $appId + if (-not $installed) { + Write-Host "Installing $AppId..." -ForegroundColor Cyan + & winget install --id $AppId --exact --source winget --silent --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } } elseif ($Update) { - Write-Host "Updating $appId..." -ForegroundColor Yellow - winget upgrade --accept-source-agreements --id $appId + Write-Host "Updating $AppId..." -ForegroundColor Yellow + & winget upgrade --id $AppId --exact --source winget --silent --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId." } } else { - Write-Host "$appId is already installed" -ForegroundColor Green - } -} - -function GetPythonFromMise { - try { - $python = mise which python 2>$null - if ($LASTEXITCODE -ne 0) { - return $null - } - - return $python.Trim() - } - catch { - return $null - } -} - -function InitializeCodexDefaults { - param([string]$BaseDir) - - $codexDir = Join-Path $env:USERPROFILE ".codex" - $defaults = @( - @{ Source = Join-Path $BaseDir "config\codex\config.toml.example"; Destination = Join-Path $codexDir "config.toml" }, - @{ Source = Join-Path $BaseDir "config\codex\rules\default.rules.example"; Destination = Join-Path $codexDir "rules\default.rules" } - ) - - foreach ($default in $defaults) { - if (Test-Path -LiteralPath $default.Destination) { - Write-Host "Codex local configuration already exists: $($default.Destination)" -ForegroundColor Yellow - continue - } - - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $default.Destination) | Out-Null - Copy-Item -LiteralPath $default.Source -Destination $default.Destination - Write-Host "Created Codex local configuration: $($default.Destination)" -ForegroundColor Green + Write-Host "$AppId is already installed" -ForegroundColor Green } } -function InstallMustHaveApps { - ### Start Installing must-have apps +function Install-MustHaveApps { Write-Host "Installing must-have apps..." -ForegroundColor Cyan - $installs = @( - { InstallWithWinget -appId "7zip.7zip" -Update:$Update }, - { InstallWithWinget -appId "Microsoft.PowerToys" -Update:$Update }, - { InstallWithWinget -appId "zyedidia.micro" -alias "micro" -Update:$Update }, - { InstallWithWinget -appId "lsd-rs.lsd" -alias "lsd" -Update:$Update }, - { InstallWithWinget -appId "sharkdp.bat" -alias "bat" -Update:$Update }, - { InstallWithWinget -appId "Fastfetch-cli.Fastfetch" -alias "fastfetch" -Update:$Update }, - { InstallWithWinget -appId "junegunn.fzf" -alias "fzf" -Update:$Update }, - { InstallWithWinget -appId "sharkdp.fd" -alias "fd" -Update:$Update }, - { InstallWithWinget -appId "dandavison.delta" -alias "delta" -Update:$Update }, - { InstallWithWinget -appId "jqlang.jq" -alias "jq" -Update:$Update }, - { InstallWithWinget -appId "Microsoft.VisualStudioCode" -alias "code" -Update:$Update }, - { InstallWithWinget -appId "BurntSushi.ripgrep.MSVC" -alias "rg" -Update:$Update }, - { InstallWithWinget -appId "jdx.mise" -alias "mise" -Update:$Update } + { Install-WithWinget -AppId "7zip.7zip" -Update:$Update }, + { Install-WithWinget -AppId "Microsoft.PowerToys" -Update:$Update }, + { Install-WithWinget -AppId "zyedidia.micro" -Alias "micro" -Update:$Update }, + { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$Update }, + { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$Update }, + { Install-WithWinget -AppId "Fastfetch-cli.Fastfetch" -Alias "fastfetch" -Update:$Update }, + { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$Update }, + { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$Update }, + { Install-WithWinget -AppId "dandavison.delta" -Alias "delta" -Update:$Update }, + { Install-WithWinget -AppId "jqlang.jq" -Alias "jq" -Update:$Update }, + { Install-WithWinget -AppId "Microsoft.VisualStudioCode" -Alias "code" -Update:$Update }, + { Install-WithWinget -AppId "BurntSushi.ripgrep.MSVC" -Alias "rg" -Update:$Update }, + { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$Update } ) + foreach ($install in $installs) { & $install } - foreach ($install in $installs) { - & $install + Refresh-Path + if (Get-Command mise -ErrorAction SilentlyContinue) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } } - RefreshPath - ## Install mise cli tools - mise use -g starship@latest - - # Install must-have modules - $modules = @( - "PSFzf", - "git-aliases" - ) - foreach ($module in $modules) { - if (Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue) { - Write-Host "$module module is already installed" -ForegroundColor Green - - if ($Update) { - Write-Host "Updating $module module..." -ForegroundColor Yellow + foreach ($module in @("PSFzf", "git-aliases")) { + $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 + $installedResource = if (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue) { + Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if ($null -eq $installedModule) { + Write-Host "Installing $module module..." -ForegroundColor Cyan + Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber + } + elseif ($Update) { + Write-Host "Updating $module module..." -ForegroundColor Yellow + if ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { Update-PSResource -Name $module -Scope CurrentUser -Force } - continue + else { + Update-Module -Name $module -Force + } } - - Write-Host "Installing $module module..." -ForegroundColor Cyan - Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber - } - ### End Installing must-have apps -} - -function SetupDotFiles { - - $BASEDIR = $PSScriptRoot - - ### Start DotBot - $DOTBOT_BIN = "bin/dotbot" - $DOTBOT_DIR = "lib/dotbot" - - Set-Location $BASEDIR - - $PYTHON = GetPythonFromMise - if ([string]::IsNullOrEmpty($PYTHON)) { - Write-Host "Cannot find Python 3 from mise. Installing..." -ForegroundColor Yellow - mise use --global python@latest - $PYTHON = GetPythonFromMise - - if ([string]::IsNullOrEmpty($PYTHON)) { - Write-Host "Error: Python can't not be found through mise. Aborting..." -ForegroundColor Red - exit + else { + Write-Host "$module module is already installed" -ForegroundColor Green } + Invoke-SigningHelper -Action ProtectModule -ModuleName $module } - Write-Host "Running Dotbot..." -ForegroundColor Cyan - $env:PROFILE_LOCATION = $profile.CurrentUserAllHosts ## PROFILE_LOCATION - - $DOTBOT_FULL_PATH_BIN = Join-Path $BASEDIR -ChildPath $DOTBOT_DIR | Join-Path -ChildPath $DOTBOT_BIN - - $BASE_CONFIG = "base" - $CONFIG_SUFFIX = ".yaml" - $META_DIR = "meta" - $CONFIG_DIR = "configs" - - InitializeCodexDefaults -BaseDir $BASEDIR - - &$PYTHON $DOTBOT_FULL_PATH_BIN -d $BASEDIR -c "${META_DIR}/${BASE_CONFIG}${CONFIG_SUFFIX}" - - $CONFIGS = @( - "codex", - "pwsh", - "windows" - ) - - foreach ($CONFIG in $CONFIGS) { - &$PYTHON $DOTBOT_FULL_PATH_BIN -d $BASEDIR -c "${META_DIR}/${CONFIG_DIR}/${CONFIG}${CONFIG_SUFFIX}" - } - ### End DotBot } -function InstallOptionalApps { - ### Start Installing optional apps - +function Install-OptionalApps { $optionalApps = @( - @{ name = "Google Chrome" ; install = { InstallWithWinget -appId "Google.Chrome" -Update:$Update } }, - @{ name = "KeepassXC" ; install = { InstallWithWinget -appId "KeePassXCTeam.KeePassXC" -Update:$Update } }, - @{ name = "DBeaver"; install = { InstallWithWinget -appId "dbeaver.dbeaver" -Update:$Update } }, - @{ name = "Postman"; install = { InstallWithWinget -appId "Postman.Postman" -Update:$Update } }, - @{ name = "Bruno"; install = { InstallWithWinget -appId "Bruno.Bruno" -Update:$Update } }, - @{ name = "kubectl"; install = { InstallWithWinget -appId "Kubernetes.kubectl" -alias "kubectl" -Update:$Update } }, - @{ name = "GIMP"; install = { InstallWithWinget -appId "GIMP.GIMP" -Update:$Update } }, - @{ name = "Android Studio"; install = { InstallWithWinget -appId "Google.AndroidStudio" -Update:$Update } }, - @{ name = "Steam" ; install = { InstallWithWinget -appId "Valve.Steam" -Update:$Update } }, - @{ name = "Discord" ; install = { InstallWithWinget -appId "Discord.Discord" -Update:$Update } }, - @{ name = "npiperelay" ; install = { InstallWithWinget -appId "albertony.npiperelay" -alias "npiperelay" -Update:$Update } } + @{ name = "Google Chrome"; install = { Install-WithWinget -AppId "Google.Chrome" -Update:$Update } }, + @{ name = "KeepassXC"; install = { Install-WithWinget -AppId "KeePassXCTeam.KeePassXC" -Update:$Update } }, + @{ name = "DBeaver"; install = { Install-WithWinget -AppId "dbeaver.dbeaver" -Update:$Update } }, + @{ name = "Postman"; install = { Install-WithWinget -AppId "Postman.Postman" -Update:$Update } }, + @{ name = "Bruno"; install = { Install-WithWinget -AppId "Bruno.Bruno" -Update:$Update } }, + @{ name = "kubectl"; install = { Install-WithWinget -AppId "Kubernetes.kubectl" -Alias "kubectl" -Update:$Update } }, + @{ name = "GIMP"; install = { Install-WithWinget -AppId "GIMP.GIMP" -Update:$Update } }, + @{ name = "Android Studio"; install = { Install-WithWinget -AppId "Google.AndroidStudio" -Update:$Update } }, + @{ name = "Steam"; install = { Install-WithWinget -AppId "Valve.Steam" -Update:$Update } }, + @{ name = "Discord"; install = { Install-WithWinget -AppId "Discord.Discord" -Update:$Update } }, + @{ name = "npiperelay"; install = { Install-WithWinget -AppId "albertony.npiperelay" -Alias "npiperelay" -Update:$Update } } ) - Write-Host " Optionals" Write-Host "-----------------------------------" -ForegroundColor Cyan - Write-Host " Choose to Install" - Write-Host "-----------------------------------" -ForegroundColor Cyan - for ($i = 0; $i -lt $optionalApps.Count; $i++) { - Write-Host ("{0}. Install {1}" -f ($i + 1), $optionalApps[$i].name) - } - Write-Host "-----------------------------------" -ForegroundColor Cyan + for ($i = 0; $i -lt $optionalApps.Count; $i++) { Write-Host ("{0}. Install {1}" -f ($i + 1), $optionalApps[$i].name) } Write-Host "You can use ranges like 1-4 or individual numbers separated by commas" -ForegroundColor Yellow - $rawOptions = Read-Host ("Select options [e.g. 1-4,8,10]" ) + $rawOptions = Read-Host "Select options [e.g. 1-4,8,10]" $options = @() - foreach ($option in $rawOptions -split ',') { $option = $option.Trim() - if ($option -match '^(\d+)-(\d+)$') { - $start = [int]$Matches[1] - $end = [int]$Matches[2] - if ($start -le $end) { - $options += $start..$end - } - } - elseif ($option -match '^\d+$') { - $options += [int]$option + if ($option -match '^(\d+)-(\d+)$' -and [int]$Matches[1] -le [int]$Matches[2]) { + $options += [int]$Matches[1]..[int]$Matches[2] } + elseif ($option -match '^\d+$') { $options += [int]$option } } - - $options = $options | Where-Object { $_ -gt 0 -and $_ -le $optionalApps.Count } | Select-Object -Unique | Sort-Object - - if ($options.Count -eq 0) { - Write-Host "Skipping optional installs..." -ForegroundColor Yellow + $options = @($options | Where-Object { $_ -gt 0 -and $_ -le $optionalApps.Count } | Select-Object -Unique | Sort-Object) + foreach ($index in $options) { + Write-Host "Installing $($optionalApps[$index - 1].name)..." -ForegroundColor Cyan + $app = $optionalApps[$index - 1] + & $app.install } - else { - foreach ($index in $options) { - $app = $optionalApps[$index - 1] - Write-Host "Installing $($app.name)..." -ForegroundColor Cyan - & $app.install - } - ## Refresh Path - RefreshPath - } - ### End Installing optional apps + if ($options.Count -eq 0) { Write-Host "Skipping optional installs..." -ForegroundColor Yellow } + Refresh-Path } -function DownloadFonts { - $BASEDIR = $PSScriptRoot - $FONTS = "$BASEDIR\fonts" - - if (!(Test-Path -Path "$FONTS")) { - New-Item -ItemType Directory -Force -Path "$FONTS" - } - - $CASCADIA_CODE = "$FONTS\CascadiaCode" - - if (!(Test-Path -Path "${CASCADIA_CODE}.ttf")) { - $apiUrl = "https://api.github.com/repos/microsoft/cascadia-code/releases/latest" - $latestGitInfo = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "PowerShell" } - $browser_download_url = $latestGitInfo.assets[0].browser_download_url - Invoke-WebRequest -Uri $browser_download_url -OutFile "${CASCADIA_CODE}.zip" - Expand-Archive "${CASCADIA_CODE}.zip" -DestinationPath $CASCADIA_CODE - Remove-Item -r -force "${CASCADIA_CODE}\ttf\static" - Get-ChildItem -Path "${CASCADIA_CODE}\*.ttf" -Recurse | Move-Item -Destination $FONTS - Remove-Item -r -force "${CASCADIA_CODE}.zip" - Remove-Item -r -force "${CASCADIA_CODE}" - } - - $apiUrl = "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" - - $nFonts = @( - @{ folder = "$FONTS\CaskaydiaCoveNerdFont"; filename = "CascadiaCode" }, - @{ folder = "$FONTS\CaskaydiaMonoNerdFont"; filename = "CascadiaMono" } - ) - - foreach ($nf in $nFonts) { - $NF_FONT = $nf.folder - $NF_FILENAME = $nf.filename - - if (!(Test-Path -Path "${NF_FONT}-Regular.ttf")) { - $latestGitInfo = Invoke-RestMethod -Uri $apiUrl -Headers @{ "User-Agent" = "PowerShell" } - $NF_VERSION = $latestGitInfo.tag_name - $browser_download_url = "https://github.com/ryanoasis/nerd-fonts/releases/download/${NF_VERSION}/${NF_FILENAME}.zip" - Invoke-WebRequest -Uri $browser_download_url -OutFile "${NF_FONT}.zip" - Expand-Archive "${NF_FONT}.zip" -DestinationPath $NF_FONT - Get-ChildItem -Path "${NF_FONT}\*.ttf" -Recurse | Move-Item -Destination $FONTS - Remove-Item -r -force "${NF_FONT}.zip" - Remove-Item -r -force "${NF_FONT}" +function Download-Fonts { + $fonts = Join-Path $RepoRoot "fonts" + New-Item -ItemType Directory -Force -Path $fonts | Out-Null + $cascadia = Join-Path $fonts "CascadiaCode" + if (-not (Test-Path "${cascadia}.ttf")) { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/cascadia-code/releases/latest" -Headers @{ "User-Agent" = "PowerShell" } + Invoke-WebRequest -Uri $release.assets[0].browser_download_url -OutFile "${cascadia}.zip" + Expand-Archive "${cascadia}.zip" -DestinationPath $cascadia + Remove-Item -Recurse -Force "${cascadia}\ttf\static" -ErrorAction SilentlyContinue + Get-ChildItem -Path "${cascadia}\*.ttf" -Recurse | Move-Item -Destination $fonts + Remove-Item -Recurse -Force "${cascadia}.zip", $cascadia + } + foreach ($font in @( + @{ folder = (Join-Path $fonts "CaskaydiaCoveNerdFont"); filename = "CascadiaCode" }, + @{ folder = (Join-Path $fonts "CaskaydiaMonoNerdFont"); filename = "CascadiaMono" } + )) { + if (-not (Test-Path "$($font.folder)-Regular.ttf")) { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" -Headers @{ "User-Agent" = "PowerShell" } + $zip = "$($font.folder).zip" + Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/download/$($release.tag_name)/$($font.filename).zip" -OutFile $zip + Expand-Archive $zip -DestinationPath $font.folder + Get-ChildItem -Path "$($font.folder)\*.ttf" -Recurse | Move-Item -Destination $fonts + Remove-Item -Recurse -Force $zip, $font.folder } } - - } -function InstallUserFonts { - $sourceDir = Join-Path $PSScriptRoot "fonts" +function Install-UserFonts { + $sourceDir = Join-Path $RepoRoot "fonts" $userFontsDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts" $fontRegistryKey = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" - $fontExtensions = @("*.otc", "*.otf", "*.ttc", "*.ttf") - $windowsVersion = [System.Environment]::OSVersion.Version - - if ($windowsVersion.Major -lt 10 -or ($windowsVersion.Major -eq 10 -and $windowsVersion.Build -lt 17704)) { - Write-Host "User font installation requires Windows 10 build 17704 or newer." -ForegroundColor Red - return - } - - if (-not (Test-Path -Path $sourceDir -PathType Container)) { - Write-Host "Font source directory not found: $sourceDir" -ForegroundColor Red - return - } - - if ((Get-Item -LiteralPath $userFontsDir -ErrorAction SilentlyContinue) -is [System.IO.FileInfo]) { - Write-Host "User fonts path is a file: $userFontsDir" -ForegroundColor Red - return - } - - if (-not (Test-Path -Path $userFontsDir -PathType Container)) { - New-Item -ItemType Directory -Path $userFontsDir -Force | Out-Null - } - - $sourceFonts = @(foreach ($pattern in $fontExtensions) { - Get-ChildItem -Path $sourceDir -Filter $pattern -Recurse -File - }) - - if ($null -eq $sourceFonts -or $sourceFonts.Count -eq 0) { - Write-Host "No fonts found in $sourceDir" -ForegroundColor Yellow - return - } - - $installedFonts = @{} - foreach ($font in Get-ChildItem -Path $userFontsDir -Recurse -File -ErrorAction SilentlyContinue) { - $installedFonts[$font.Name] = $font.FullName - } - + if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) { throw "Font source directory not found: $sourceDir" } + New-Item -ItemType Directory -Path $userFontsDir -Force | Out-Null + $sourceFonts = @(Get-ChildItem -Path $sourceDir -Include *.otc,*.otf,*.ttc,*.ttf -Recurse -File) foreach ($font in $sourceFonts | Sort-Object Name -Unique) { - if ($installedFonts.ContainsKey($font.Name)) { - Write-Host "Font '$($font.Name)' is already installed" -ForegroundColor Green - continue - } - $destination = Join-Path $userFontsDir $font.Name - - try { - Copy-Item -LiteralPath $font.FullName -Destination $destination -Force - New-ItemProperty -Path $fontRegistryKey -Name "$($font.Name) (dotfiles)" -Value $destination -PropertyType String -Force | Out-Null - Write-Host "Installed font '$($font.Name)'" -ForegroundColor Cyan - } - catch { - Write-Host "Unable to install font '$($font.Name)': $_" -ForegroundColor Red - } + if (Test-Path -LiteralPath $destination) { continue } + Copy-Item -LiteralPath $font.FullName -Destination $destination + New-ItemProperty -Path $fontRegistryKey -Name "$($font.Name) (dotfiles)" -Value $destination -PropertyType String -Force | Out-Null } } -function ConfigureGit { - # Create .gitconfig.local if not exists - if (-not (Test-Path "$HOME\.gitconfig.local")) { New-Item -Path "$HOME\.gitconfig.local" -ItemType File } - git submodule sync --quiet --recursive - git submodule update --init --recursive +function Configure-Git { + $localConfig = Join-Path $HOME ".gitconfig.local" + if (-not (Test-Path -LiteralPath $localConfig)) { New-Item -ItemType File -Path $localConfig | Out-Null } Write-Host "Git successfully configured!" -ForegroundColor Green } -function SettingsForWindowsTerminal { - $source = "$PSScriptRoot\config\win-terminal\config.json" - $destination = "$($env:LOCALAPPDATA)\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" - - if (-not (Test-Path $destination)) { - Write-Host "Windows Terminal settings file not found. Skipping configuration." -ForegroundColor Yellow - return - } - - jq --indent 4 --slurpfile src "$source" ' - . as $original | - $src[0] | - to_entries | - map(select(.key != "profiles")) | - reduce .[] as $item ($original; - . * {($item.key): $item.value} - ) | - . * {"profiles": {"defaults": $src[0].profiles.defaults}} - ' "$destination" | Set-Content -Path $destination +function Configure-WindowsTerminal { + $source = Join-Path $RepoRoot "assets\win-terminal\config.json" + $destination = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" + if (-not (Test-Path -LiteralPath $destination)) { Write-Host "Windows Terminal settings file not found. Skipping configuration." -ForegroundColor Yellow; return } + if (-not (Get-Command jq -ErrorAction SilentlyContinue)) { Write-Host "jq is unavailable. Skipping Windows Terminal configuration." -ForegroundColor Yellow; return } + & jq --indent 4 --slurpfile src $source '. as $original | $src[0] | to_entries | map(select(.key != "profiles")) | reduce .[] as $item ($original; . * {($item.key): $item.value}) | . * {"profiles": {"defaults": $src[0].profiles.defaults}}' $destination | Set-Content -Path $destination + if ($LASTEXITCODE -ne 0) { throw "Windows Terminal configuration merge failed." } } -function ConfigureWsl { - Write-Host "Installing WSL..." -ForegroundColor Cyan - wsl --install --no-distribution +function Configure-Wsl { + if (Get-Command wsl -ErrorAction SilentlyContinue) { Write-Host "Installing WSL..." -ForegroundColor Cyan; & wsl --install --no-distribution } } -### End Utils - -RefreshPath -CheckRequiredApps -EnsureDevModeIsEnabled -CheckWinget -DownloadFonts -InstallUserFonts -ConfigureGit -InstallMustHaveApps -SetupDotFiles -SettingsForWindowsTerminal -InstallOptionalApps -ConfigureWsl +Refresh-Path +Check-RequiredApps +Download-Fonts +Install-UserFonts +Configure-Git +Install-MustHaveApps +Configure-WindowsTerminal +Install-OptionalApps +Configure-Wsl diff --git a/install.sh b/install.sh index 31c18e6..393d08a 100755 --- a/install.sh +++ b/install.sh @@ -9,8 +9,7 @@ LIGHT='\x1b[2m' RESET='\033[0m' -SRC_DIR=$(dirname "${0}") -DOTFILES_DIR="${DOTFILES_DIR:-${SRC_DIR:-$HOME/.dotfiles}}" +REPO_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) UPDATE=false function usage () { @@ -49,13 +48,11 @@ function updates_enabled () { } function pre_setup_tasks() { - if [ ! -d "$DOTFILES_DIR" ]; then - echo -e "${RED}The folder '$DOTFILES_DIR' not exists exiting..."; + if [ ! -d "$REPO_ROOT" ]; then + echo -e "${RED}The repository folder '$REPO_ROOT' does not exist; exiting..."; exit 1; fi - source "${DOTFILES_DIR}/config/zsh/.zshenv" - detect_arch if updates_enabled; then @@ -199,7 +196,7 @@ function install_fzf () { local fzf_dir="$HOME/.config/fzf" if [ -d "$fzf_dir/.git" ]; then - git -C "$fzf_dir" pull --ff-only + git -c safe.directory="$fzf_dir" -C "$fzf_dir" pull --ff-only else git clone https://github.com/junegunn/fzf.git "$fzf_dir" fi @@ -351,47 +348,6 @@ function install_must_have_packages() { } -function setup_dot_files () { - - DOTBOT_BIN="bin/dotbot" - DOTBOT_DIR="lib/dotbot" - DOTBOT_CONF_FILE="install.conf.yaml" - DOTBOT_FULL_PATH_BIN="${DOTFILES_DIR}/${DOTBOT_DIR}/bin/dotbot" - - BASE_CONFIG="base" - CONFIG_SUFFIX=".yaml" - META_DIR="meta" - CONFIG_DIR="configs" - - CODEX_DIR="$HOME/.codex" - CODEX_CONFIG="$CODEX_DIR/config.toml" - CODEX_RULES="$CODEX_DIR/rules/default.rules" - - if [ ! -e "$CODEX_CONFIG" ]; then - mkdir -p "$CODEX_DIR" - cp "$DOTFILES_DIR/config/codex/config.toml.example" "$CODEX_CONFIG" - echo -e "${GREEN}Created Codex local configuration: $CODEX_CONFIG${RESET}" - else - echo -e "${YELLOW}Codex local configuration already exists: $CODEX_CONFIG${RESET}" - fi - - if [ ! -e "$CODEX_RULES" ]; then - mkdir -p "$CODEX_DIR/rules" - cp "$DOTFILES_DIR/config/codex/rules/default.rules.example" "$CODEX_RULES" - echo -e "${GREEN}Created Codex local configuration: $CODEX_RULES${RESET}" - else - echo -e "${YELLOW}Codex local configuration already exists: $CODEX_RULES${RESET}" - fi - - $DOTBOT_FULL_PATH_BIN -d "$DOTFILES_DIR" -c "${META_DIR}/${BASE_CONFIG}${CONFIG_SUFFIX}" - - CONFIGS="codex zsh" - - for config in $CONFIGS; do - $DOTBOT_FULL_PATH_BIN -d "$DOTFILES_DIR" -c "${META_DIR}/${CONFIG_DIR}/${config}${CONFIG_SUFFIX}" - done -} - function setup_sheldon_plugins () { if hash "sheldon" 2> /dev/null; then sheldon lock @@ -399,28 +355,34 @@ function setup_sheldon_plugins () { } function setup_default_shell() { + local target_user="${USER:-$(id -un)}" + local target_shell - current_shell=$(getent passwd "$USER" | cut -d: -f7) + current_shell=$(getent passwd "$target_user" | cut -d: -f7) + target_shell="$(which zsh)" - if [ "$current_shell" != "$(which zsh)" ]; then - chsh -s "$(which zsh)" - echo -e "${GREEN}Default shell changed to zsh.${RESET}" + if [ "$current_shell" != "$target_shell" ]; then + chsh -s "$target_shell" "$target_user" + echo -e "${GREEN}Default shell changed to zsh for ${target_user}.${RESET}" else - echo -e "${YELLOW}zsh is already the default shell for $USER. No changes made.${RESET}" + echo -e "${YELLOW}zsh is already the default shell for ${target_user}. No changes made.${RESET}" fi } function configure_git () { [ ! -e ~/.gitconfig.local ] && touch ~/.gitconfig.local - git submodule sync --quiet --recursive - git submodule update --init --recursive echo -e "${GREEN}Git successfully configured!${RESET}" } function configure_wsl() { - if grep -qi microsoft /proc/version && [[ ! -e /etc/wsl.conf ]]; then - sudo cp "${DOTFILES_DIR}/config/wsl/wsl.conf" /etc/wsl.conf - echo -e "${GREEN}wsl.conf configured successfully!${RESET}" + local desired="${REPO_ROOT}/assets/wsl/wsl.conf" + if grep -qi microsoft /proc/version && [ -f "$desired" ]; then + if [ ! -f /etc/wsl.conf ] || ! cmp -s "$desired" /etc/wsl.conf; then + sudo install -m 0644 "$desired" /etc/wsl.conf + echo -e "${GREEN}wsl.conf configured successfully!${RESET}" + else + echo -e "${YELLOW}wsl.conf already matches the desired configuration.${RESET}" + fi fi } @@ -522,7 +484,6 @@ pre_setup_tasks configure_git configure_wsl install_must_have_packages -setup_dot_files setup_sheldon_plugins setup_default_shell install_optional_packages diff --git a/lets-go.ps1 b/lets-go.ps1 deleted file mode 100644 index 8ddc189..0000000 --- a/lets-go.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -function RefreshPath() { - $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") -} - -function EnsureDevModeIsEnabled() { - try { - if ((Get-WindowsDeveloperLicense).IsValid) { - Write-Host "Developer Mode is Enabled" -ForegroundColor Green - } - else { - Write-Host "Please enable the Developer Mode and RESTART!!! before continue" -ForegroundColor Red - exit - } - } - catch { - Write-Host "An error occurred while checking the developer license: $_" -ForegroundColor Red - exit - } -} - -function InstallWithWinget() { - param( - [string]$appId, - [string]$alias, - [string]$customArgs = "" - ) - - if (-not ([string]::IsNullOrEmpty($alias))) { - Get-Command -Name $alias -ErrorAction SilentlyContinue | Out-Null - } - else { - winget list --accept-source-agreements --id $appId -n 1 | Out-Null - } - - if (-not $?) { - Write-Host "$appId is not installed. Installing..." -ForegroundColor Yellow - $wingetArgs = @('-e', '--accept-source-agreements', '--accept-package-agreements', '--id', $appId) - if (-not [string]::IsNullOrWhiteSpace($customArgs)) { - $wingetArgs += @('--custom', $customArgs) - } - winget install @wingetArgs - } -} -# Ensure Dev Mode is Enabled -EnsureDevModeIsEnabled - -# Check winget and activate -if ($null -eq (Get-Command -Name winget -ErrorAction SilentlyContinue)) { - Write-Output "Enable winget..." - Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe - RefreshPath -} - -# List apps to install -Write-Host "Installing must-have apps..." -ForegroundColor Cyan -$installs = @( - $(InstallWithWinget -appId "Git.Git" -alias "git" -customArgs '/Components="gitlfs,assoc,windowsterminal" /o:SSHOption=ExternalOpenSSH /o:CurlOption=WinSSL /o:CRLFOption=CRLFCommitAsIs'), - $(InstallWithWinget -appId "Microsoft.PowerShell") -) - -# For each app, check if not present and install -foreach ($install in $installs) { - $install -} - -# If not already set, specify dotfiles destination directory and source repo -if (!$DOTFILES_DIR) { $DOTFILES_DIR = "$HOME\.dotfiles" } -if (!$DOTFILES_REPO) { $DOTFILES_REPO = "https://github.com/jsilverdev/dotfiles.git" } - -# Reload PATH -RefreshPath - -if (-not (Test-Path -Path $DOTFILES_DIR -PathType Container)) { - New-Item -ItemType Directory -Path "$DOTFILES_DIR" -Force - git clone --recursive "$DOTFILES_REPO" "$DOTFILES_DIR" -} - -Set-Location -Path $DOTFILES_DIR -$installScript = Join-Path $DOTFILES_DIR "install.ps1" -& pwsh.exe -F $installScript diff --git a/lets-go.sh b/lets-go.sh deleted file mode 100644 index dd7a960..0000000 --- a/lets-go.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/bash - -core_packages=( - 'git' - 'zsh' - 'wget' -) - -function install_debian () { - echo -e "${PURPLE}Installing ${1} via apt-get${RESET}" - sudo apt install $1 --assume-yes -} -function install_arch () { - echo -e "${PURPLE}Installing ${1} via Pacman${RESET}" - sudo pacman -S $1 --needed --noconfirm -} - -function multi_system_install () { - app=$1 - if [ -f "/etc/arch-release" ] && hash pacman 2> /dev/null; then - install_arch $app # Arch Linux via Pacman - elif [ -f "/etc/debian_version" ] && hash apt 2> /dev/null; then - install_debian $app # Debian via apt-get - else - echo -e "${YELLOW}Skipping ${app}, as couldn't detect system type ${RESET}" - fi -} - -# If not already set, specify dotfiles destination directory and source repo -DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" -DOTFILES_REPO="${DOTFILES_REPO:-https://github.com/jsilverdev/dotfiles.git}" - -# For each app, check if not present and install -for app in ${core_packages[@]}; do - if ! hash "${app}" 2> /dev/null; then - multi_system_install $app - else - echo -e "${YELLOW}${app} is already installed, skipping${RESET}" - fi -done - -# If dotfiles not yet present then clone -if [[ ! -d "$DOTFILES_DIR" ]]; then - mkdir -p "${DOTFILES_DIR}" && \ - git clone --recursive ${DOTFILES_REPO} ${DOTFILES_DIR} -fi - -# Execute setup or update script -cd "${DOTFILES_DIR}" && \ -chmod +x ./install.sh && \ -./install.sh diff --git a/lib/dotbot b/lib/dotbot deleted file mode 160000 index 67aeaf7..0000000 --- a/lib/dotbot +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 67aeaf75401e21f8b6085c1f2cecd472861d081d diff --git a/meta/base.yaml b/meta/base.yaml deleted file mode 100644 index 55ce67a..0000000 --- a/meta/base.yaml +++ /dev/null @@ -1,41 +0,0 @@ -- defaults: - link: - create: true - relink: true - -- create: - ~/.ssh: - mode: 0700 - -- clean: ["~"] - -- clean: - ~/.config: - recursive: true - -- link: - # starship - ~/.config/starship/: - glob: true - path: config/starship/* - force: true - create: true - # General config - ~/: - glob: true - path: config/general/.* - relink: true - exclude: ["config/general/.gitignore*"] - ~/.gitconfig: - path: config/general/.gitconfig - force: true - create: true - ~/.fdignore: - path: config/general/.gitignore_global - force: true - create: true - ~/.ssh/: - glob: true - path: config/ssh/* - force: true - create: true diff --git a/meta/configs/codex.yaml b/meta/configs/codex.yaml deleted file mode 100644 index 1d1c7b9..0000000 --- a/meta/configs/codex.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- link: - ~/.codex/AGENTS.md: - path: config/codex/AGENTS.md - force: true - create: true - ~/.codex/skills/: - glob: true - path: config/codex/skills/** - force: true - create: true diff --git a/meta/configs/pwsh.yaml b/meta/configs/pwsh.yaml deleted file mode 100644 index 7eaeeea..0000000 --- a/meta/configs/pwsh.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- link: - # pwsh - ~/.config/pwsh/: - glob: true - path: config/pwsh/** - force: true - create: true - exclude: ["config/pwsh/profile.ps1"] - ${PROFILE_LOCATION}: - path: config/pwsh/profile.ps1 - force: true - create: true diff --git a/meta/configs/windows.yaml b/meta/configs/windows.yaml deleted file mode 100644 index a70fef5..0000000 --- a/meta/configs/windows.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- link: - ~/.wslconfig: - path: config/wsl/.wslconfig - force: true - create: true diff --git a/meta/configs/zsh.yaml b/meta/configs/zsh.yaml deleted file mode 100644 index a9e0216..0000000 --- a/meta/configs/zsh.yaml +++ /dev/null @@ -1,20 +0,0 @@ -- link: - # zsh - ~/.config/zsh/: - path: config/zsh/** - glob: true - force: true - create: true - exclude: ["config/general/.zshrc", "config/general/.zshenv"] - ~/.config/zsh/.zshrc: - path: config/zsh/.zshrc - force: true - create: true - ~/.config/sheldon/plugins.toml: - path: config/sheldon/plugins.toml - force: true - create: true - ~/.zshenv: - path: config/zsh/.zshenv - force: true - create: true diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 new file mode 100644 index 0000000..c2106e3 --- /dev/null +++ b/scripts/windows/deploy-pwsh.ps1 @@ -0,0 +1,45 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$RepoRoot +) + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path -LiteralPath $RepoRoot).Path +$sourceRoot = Join-Path $repo "home\.chezmoitemplates\pwsh" +$destinationRoot = Join-Path $HOME ".config\pwsh" +$files = @( + @{ Source = "env.ps1"; Destination = (Join-Path $destinationRoot "env.ps1") }, + @{ Source = "lib\helpers.ps1"; Destination = (Join-Path $destinationRoot "lib\helpers.ps1") }, + @{ Source = "lib\aliases.ps1"; Destination = (Join-Path $destinationRoot "lib\aliases.ps1") }, + @{ Source = "profile.ps1"; Destination = $PROFILE.CurrentUserAllHosts } +) + +foreach ($file in $files) { + $source = Join-Path $sourceRoot $file.Source + if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { + throw "PowerShell source file not found: $source" + } + + New-Item -ItemType Directory -Path (Split-Path -Parent $file.Destination) -Force | Out-Null + + # Remove a legacy symlink before copying the regular runtime file. + # Copy-Item otherwise follows a broken link and fails instead of replacing it. + if (Test-Path -LiteralPath $file.Destination -PathType Leaf) { + $existing = Get-Item -LiteralPath $file.Destination -Force + if (($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + Remove-Item -LiteralPath $file.Destination -Force + } + } + + Copy-Item -LiteralPath $source -Destination $file.Destination -Force +} + +if ((Get-ExecutionPolicy) -eq "AllSigned") { + $helper = Join-Path $repo "scripts\windows\signing.ps1" + $bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" + & $bridge $helper -Action ProtectFiles -Path @($files | ForEach-Object Destination) + if ($LASTEXITCODE -ne 0) { + throw "PowerShell runtime signing failed with exit code $LASTEXITCODE." + } +} diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd new file mode 100644 index 0000000..d94ee5a --- /dev/null +++ b/scripts/windows/invoke-ps-script.cmd @@ -0,0 +1,20 @@ +@echo off +setlocal EnableExtensions + +if "%~1"=="" goto usage + +set "SCRIPT=%~1" +shift +where pwsh.exe >nul 2>&1 +if errorlevel 1 goto missing_pwsh + +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAYQByAGcAcwBbADAAXQA7ACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApADsAIABpAGYAIAAoACQAYQByAGcAcwAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMQApACAAewAgACQAcwBjAHIAaQBwAHQAQQByAGcAcwAgAD0AIABAACgAJABhAHIAZwBzAFsAMQAuAC4AKAAkAGEAcgBnAHMALgBDAG8AdQBuAHQAIAAtACAAMQApAF0AKQAgAH0AOwAgACQAcAB3AHMAaAAgAD0AIAAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQApAC4AUwBvAHUAcgBjAGUAOwAgACQAdABlAG0AcABvAHIAYQByAHkAIAA9ACAAJABuAHUAbABsADsAIAB0AHIAeQAgAHsAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcAOwAgACQAbwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwA7ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAARwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAHsAIAAkAF8ALgBTAHUAYgBqAGUAYwB0ACAALQBlAHEAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAgACQAXwAuAEgAYQBzAFAAcgBpAHYAYQB0AGUASwBlAHkAIAAtAGEAbgBkACAAJABfAC4ATgBvAHQAQQBmAHQAZQByACAALQBnAHQAIAAoAEcAZQB0AC0ARABhAHQAZQApACAALQBhAG4AZAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAG8AaQBkACAAfQApAC4AQwBvAHUAbgB0ACAALQBnAHQAIAAwACAAfQAgAHwAIABTAG8AcgB0AC0ATwBiAGoAZQBjAHQAIABOAG8AdABBAGYAdABlAHIAIAAtAEQAZQBzAGMAZQBuAGQAaQBuAGcAIAB8ACAAUwBlAGwAZQBjAHQALQBPAGIAagBlAGMAdAAgAC0ARgBpAHIAcwB0ACAAMQA7ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAALQBTAHUAYgBqAGUAYwB0ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAUwB1AGIAagBlAGMAdAAgAC0AQwBlAHIAdABTAHQAbwByAGUATABvAGMAYQB0AGkAbwBuACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAALQBOAG8AdABBAGYAdABlAHIAIAAoAEcAZQB0AC0ARABhAHQAZQApAC4AQQBkAGQAWQBlAGEAcgBzACgAMQAwACkAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAH0AOwAgACQAcAB1AGIAbABpAGMAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAGMAZQByACcAKQA7ACAAdAByAHkAIAB7ACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0AVAB5AHAAZQAgAEMARQBSAFQAIAAtAEYAbwByAGMAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwAOwAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlACAAaQBuACAAQAAoACcAUgBvAG8AdAAnACwAIAAnAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAJwApACkAIAB7ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACgARwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7ACAASQBtAHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUAIgAgAHwAIABPAHUAdAAtAE4AdQBsAGwAIAB9ACAAfQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlACAAfQA7ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AcABzADEAJwApADsAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBGAG8AcgBjAGUAOwAgACQAcwBpAGcAbgBlAGQAIAA9ACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgA7ACAAaQBmACAAKAAoAEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAKQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7ACAAdABoAHIAbwB3ACAAKAAnAFQAZQBtAHAAbwByAGEAcgB5ACAAcwBjAHIAaQBwAHQAIABzAGkAZwBuAGEAdAB1AHIAZQAgAGkAcwAgAG4AbwB0ACAAVgBhAGwAaQBkADoAIAAnACAAKwAgACQAcwBpAGcAbgBlAGQALgBTAHQAYQB0AHUAcwApACAAfQA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAdABlAG0AcABvAHIAYQByAHkAIAB9ADsAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwA7ACAAJABlAHgAaQB0AEMAbwBkAGUAIAA9ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAG4AZQAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQAgAHsAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAAfQAgAGUAbABzAGUAaQBmACAAKAAkAD8AKQAgAHsAIAAwACAAfQAgAGUAbABzAGUAIAB7ACAAMQAgAH0AOwAgAGUAeABpAHQAIAAkAGUAeABpAHQAQwBvAGQAZQAgAH0AIABjAGEAdABjAGgAIAB7ACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfADsAIABlAHgAaQB0ACAAMQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQApACAAewAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlACAAfQAgAH0A %* +exit /b %ERRORLEVEL% + +:usage +echo Usage: %~nx0 script.ps1 [arguments...] 1>&2 +exit /b 2 + +:missing_pwsh +echo PowerShell 7 (pwsh.exe) is required. 1>&2 +exit /b 1 diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 new file mode 100644 index 0000000..3a6fecc --- /dev/null +++ b/scripts/windows/signing.ps1 @@ -0,0 +1,165 @@ +[CmdletBinding()] +param( + [ValidateSet("ProtectFiles", "ProtectModule")] + [string]$Action = "ProtectFiles", + + [string[]]$Path, + + [string]$ModuleName +) + +$ErrorActionPreference = "Stop" +$certificateSubject = "CN=jsilverdev Dotfiles Code Signing" +$codeSigningOid = "1.3.6.1.5.5.7.3.3" + +function Test-AllSignedPolicy { + return (Get-ExecutionPolicy) -eq "AllSigned" +} + +function Test-CodeSigningCertificate { + param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) + + if ($null -eq $Certificate -or -not $Certificate.HasPrivateKey -or $Certificate.NotAfter -le (Get-Date)) { + return $false + } + + return @($Certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 +} + +function Get-DotfilesSigningCertificate { + $certificate = Get-ChildItem -Path Cert:\CurrentUser\My | + Where-Object { $_.Subject -eq $certificateSubject -and (Test-CodeSigningCertificate $_) } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + + if ($null -eq $certificate) { + try { + $certificate = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $certificateSubject ` + -CertStoreLocation Cert:\CurrentUser\My ` + -NotAfter (Get-Date).AddYears(10) ` + -HashAlgorithm SHA256 + } + catch { + throw "Unable to create the current-user code-signing certificate. Corporate certificate-store policy may prevent this operation. $($_.Exception.Message)" + } + } + + if (-not (Test-CodeSigningCertificate $certificate)) { + throw "The managed dotfiles certificate is missing a private key, is expired, or lacks the Code Signing EKU." + } + + $publicCertificatePath = $null + try { + $publicCertificatePath = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), ".cer") + Export-Certificate -Cert $certificate -FilePath $publicCertificatePath -Type CERT -Force | Out-Null + + foreach ($storeName in @("Root", "TrustedPublisher")) { + $storePath = "Cert:\CurrentUser\$storeName" + $trusted = Get-ChildItem -Path $storePath | Where-Object Thumbprint -eq $certificate.Thumbprint + if ($null -eq $trusted) { + Import-Certificate -FilePath $publicCertificatePath -CertStoreLocation $storePath | Out-Null + } + } + } + catch { + throw "Unable to trust the current-user dotfiles certificate in Root and TrustedPublisher. Corporate certificate-store policy may prevent this operation. $($_.Exception.Message)" + } + finally { + if ($publicCertificatePath) { + Remove-Item -LiteralPath $publicCertificatePath -Force -ErrorAction SilentlyContinue + } + } + + return $certificate +} + +function Protect-PowerShellFile { + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [Parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate + ) + + if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { + throw "PowerShell file not found: $FilePath" + } + + $signature = Get-AuthenticodeSignature -FilePath $FilePath + if ($signature.Status -eq "Valid") { + return + } + + Set-AuthenticodeSignature -FilePath $FilePath -Certificate $Certificate -HashAlgorithm SHA256 | Out-Null + $signature = Get-AuthenticodeSignature -FilePath $FilePath + if ($signature.Status -ne "Valid") { + throw "Authenticode signature verification failed for ${FilePath}: $($signature.Status) $($signature.StatusMessage)" + } +} + +function Test-UserModulePath { + param([string]$ModulePath) + + $fullPath = [IO.Path]::GetFullPath($ModulePath).TrimEnd([IO.Path]::DirectorySeparatorChar) + $userRoots = @( + (Join-Path $HOME "Documents\PowerShell\Modules"), + (Join-Path $HOME ".local\share\powershell\Modules") + ) + @($env:PSModulePath -split [IO.Path]::PathSeparator | Where-Object { $_ -and $_ -like "$HOME*" }) + + foreach ($root in ($userRoots | Select-Object -Unique)) { + $fullRoot = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + if ($fullPath.Equals($fullRoot, [StringComparison]::OrdinalIgnoreCase) -or + $fullPath.StartsWith($fullRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + return $true + } + } + + return $false +} + +function Get-ManagedModuleFiles { + param([Parameter(Mandatory)][string]$Name) + + $moduleDirectories = @(Get-Module -ListAvailable -Name $Name | + Where-Object { $_.ModuleBase -and (Test-UserModulePath $_.ModuleBase) } | + Select-Object -ExpandProperty ModuleBase -Unique) + + if ($moduleDirectories.Count -eq 0) { + throw "Managed PowerShell module '$Name' was not found in a current-user module path." + } + + $extensions = @("*.ps1", "*.psm1", "*.psd1", "*.ps1xml", "*.cdxml", "*.xaml") + return @($moduleDirectories | ForEach-Object { + foreach ($extension in $extensions) { + Get-ChildItem -LiteralPath $_ -Filter $extension -File -Recurse -ErrorAction Stop + } + } | Select-Object -ExpandProperty FullName -Unique) +} + +if (-not (Test-AllSignedPolicy)) { + exit 0 +} + +$certificate = Get-DotfilesSigningCertificate + +switch ($Action) { + "ProtectFiles" { + if ($null -eq $Path -or $Path.Count -eq 0) { + throw "ProtectFiles requires at least one -Path." + } + foreach ($file in $Path) { + Protect-PowerShellFile -FilePath $file -Certificate $certificate + } + } + "ProtectModule" { + if ([string]::IsNullOrWhiteSpace($ModuleName)) { + throw "ProtectModule requires -ModuleName." + } + foreach ($file in (Get-ManagedModuleFiles -Name $ModuleName)) { + Protect-PowerShellFile -FilePath $file -Certificate $certificate + } + } +} diff --git a/update.cmd b/update.cmd new file mode 100644 index 0000000..cfd8111 --- /dev/null +++ b/update.cmd @@ -0,0 +1,29 @@ +@echo off +setlocal EnableExtensions + +where chezmoi.exe >nul 2>&1 +if errorlevel 1 ( + echo Core tools are unavailable; running bootstrap.cmd... + call "%~dp0bootstrap.cmd" + exit /b %ERRORLEVEL% +) + +chezmoi.exe update +if errorlevel 1 exit /b %ERRORLEVEL% + +call :resolve_repo_root +if not defined REPO_ROOT ( + echo Unable to resolve the chezmoi working tree. 1>&2 + exit /b 1 +) +call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -RepoRoot "%REPO_ROOT%" +exit /b %ERRORLEVEL% + +:resolve_repo_root +set "REPO_ROOT=" +for /f "delims=" %%R in ('chezmoi.exe execute-template "{{ .chezmoi.workingTree }}" 2^>nul') do set "REPO_ROOT=%%R" +if defined REPO_ROOT if exist "%REPO_ROOT%\install.ps1" exit /b 0 +for /f "delims=" %%R in ('chezmoi.exe source-path 2^>nul') do set "SOURCE_ROOT=%%R" +if defined SOURCE_ROOT if exist "%SOURCE_ROOT%\install.ps1" (set "REPO_ROOT=%SOURCE_ROOT%"& exit /b 0) +if defined SOURCE_ROOT for %%P in ("%SOURCE_ROOT%\..") do if exist "%%~fP\install.ps1" (set "REPO_ROOT=%%~fP"& exit /b 0) +exit /b 1 diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..4d564c9 --- /dev/null +++ b/update.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="$HOME/.local/bin:$PATH" +command -v chezmoi >/dev/null 2>&1 || { printf 'chezmoi is required; run bootstrap.sh first.\n' >&2; exit 1; } +chezmoi update + +resolve_repo_root() { + local candidate parent + for candidate in "$(chezmoi execute-template '{{ .chezmoi.workingTree }}' 2>/dev/null || true)" "$(chezmoi source-path)"; do + [[ -n "$candidate" ]] || continue + if [[ -f "$candidate/install.sh" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + parent="$(dirname "$candidate")" + if [[ -f "$parent/install.sh" ]]; then + printf '%s\n' "$parent" + return 0 + fi + done + return 1 +} + +REPO_ROOT="$(resolve_repo_root)" || { printf 'Unable to resolve the chezmoi working tree.\n' >&2; exit 1; } +exec "$REPO_ROOT/install.sh" --update From 74e3a4f5fc27c69379b79342ad14045f3e74aa4e Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 14:45:14 -0500 Subject: [PATCH 02/61] Fix chezmoi source name for zshrc --- home/dot_config/zsh/{.zshrc => dot_zshrc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename home/dot_config/zsh/{.zshrc => dot_zshrc} (100%) diff --git a/home/dot_config/zsh/.zshrc b/home/dot_config/zsh/dot_zshrc similarity index 100% rename from home/dot_config/zsh/.zshrc rename to home/dot_config/zsh/dot_zshrc From 06da6f9748688262cb56f2fbadf09fdf888b6b88 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 14:49:08 -0500 Subject: [PATCH 03/61] Persist local chezmoi source during bootstrap --- bootstrap.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bootstrap.sh b/bootstrap.sh index 19defca..a0c4bc1 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -35,6 +35,17 @@ remove_legacy_links() { remove_legacy_links +configure_local_chezmoi_source() { + local config_dir config_path + config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi" + config_path="$config_dir/chezmoi.toml" + + if [[ ! -e "$config_path" ]]; then + mkdir -p "$config_dir" + printf 'sourceDir = "%s"\n' "$PWD" > "$config_path" + fi +} + if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then sudo apt-get update sudo apt-get install --yes git curl wget zsh @@ -58,6 +69,7 @@ export PATH="$HOME/.local/bin:$PATH" command -v chezmoi >/dev/null 2>&1 || { printf 'chezmoi installation failed.\n' >&2; exit 1; } if [[ -f "$PWD/.chezmoiroot" ]]; then + configure_local_chezmoi_source chezmoi --source "$PWD" apply else SOURCE_ROOT="$(chezmoi source-path 2>/dev/null || true)" From 36c0f024ba8b63ecc52ccab52337731ed8f6cdcc Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 14:56:05 -0500 Subject: [PATCH 04/61] Install chezmoi from pacman on Arch --- bootstrap.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bootstrap.sh b/bootstrap.sh index a0c4bc1..b1e9a60 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -3,6 +3,7 @@ set -euo pipefail REPO_URL="${DOTFILES_REPO:-https://github.com/jsilverdev/dotfiles.git}" export PATH="$HOME/.local/bin:$PATH" +DISTRO="" remove_legacy_links() { local path target @@ -47,10 +48,12 @@ configure_local_chezmoi_source() { } if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then + DISTRO="debian" sudo apt-get update sudo apt-get install --yes git curl wget zsh elif [[ -f /etc/arch-release ]] && command -v pacman >/dev/null 2>&1; then - sudo pacman -Syu --noconfirm --needed git curl wget zsh + DISTRO="arch" + sudo pacman -Syu --noconfirm --needed git curl wget zsh chezmoi else printf 'Unsupported Linux distribution. Debian and Arch Linux are supported.\n' >&2 exit 1 @@ -61,7 +64,7 @@ command -v curl >/dev/null 2>&1 || { printf 'curl is required but unavailable.\n command -v wget >/dev/null 2>&1 || { printf 'wget is required but unavailable.\n' >&2; exit 1; } command -v zsh >/dev/null 2>&1 || { printf 'zsh is required but unavailable.\n' >&2; exit 1; } -if ! command -v chezmoi >/dev/null 2>&1; then +if [[ "$DISTRO" == "debian" ]] && ! command -v chezmoi >/dev/null 2>&1; then mkdir -p "$HOME/.local/bin" sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" fi From 58fe7c25f8922c9fbfe338af0876e518bd00775f Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 16:58:41 -0500 Subject: [PATCH 05/61] Add post-migration CI validation --- .github/workflows/validate.yml | 405 ++++++++++++++++++++++++++++ README.md | 6 + bootstrap.cmd | 16 +- install.ps1 | 75 ++++-- install.sh | 39 ++- scripts/windows/managed-modules.txt | 2 + tests/linux/assert-state.sh | 80 ++++++ tests/windows/assert-allsigned.ps1 | 102 +++++++ tests/windows/assert-state.ps1 | 100 +++++++ update.cmd | 8 +- update.sh | 2 +- 11 files changed, 805 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 scripts/windows/managed-modules.txt create mode 100644 tests/linux/assert-state.sh create mode 100644 tests/windows/assert-allsigned.ps1 create mode 100644 tests/windows/assert-state.ps1 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..18f0f45 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,405 @@ +name: Validate dotfiles + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + static: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out the exact revision + uses: actions/checkout@v7 + + - name: Install static-validation tools + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends ruby shellcheck + mkdir -p "$RUNNER_TEMP/bin" + curl -fsLS https://get.chezmoi.io | sh -s -- -b "$RUNNER_TEMP/bin" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + + - name: Validate shell, source state, templates, and architecture + shell: bash + run: | + set -euo pipefail + bash -n bootstrap.sh install.sh update.sh tests/linux/assert-state.sh + shellcheck -x -S error bootstrap.sh install.sh update.sh tests/linux/assert-state.sh + chezmoi --source "$GITHUB_WORKSPACE" managed >/dev/null + while IFS= read -r -d '' template; do + chezmoi --source "$GITHUB_WORKSPACE" execute-template < "$template" >/dev/null + done < <(find "$GITHUB_WORKSPACE" -type f -name '*.tmpl' -print0) + + test "$(tr -d '\r\n' < .chezmoiroot)" = home + test -d home + test -f scripts/windows/invoke-ps-script.cmd + test -f scripts/windows/signing.ps1 + test -f scripts/windows/deploy-pwsh.ps1 + test -f scripts/windows/managed-modules.txt + test -f home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl + + if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then + echo 'chezmoi symlink source state is not allowed' >&2 + exit 1 + fi + if git grep -n -i 'dotbot' -- ':!.github/workflows/validate.yml'; then + echo 'obsolete Dotbot dependency/reference found' >&2 + exit 1 + fi + if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then + echo 'obsolete master bootstrap URL found' >&2 + exit 1 + fi + if git grep -n -E 'Set-ExecutionPolicy|-ExecutionPolicy[[:space:]]+Bypass' -- ':!\.github/workflows/validate.yml'; then + echo 'production execution-policy weakening found' >&2 + exit 1 + fi + if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then + echo 'signed PowerShell source was committed' >&2 + exit 1 + fi + + - name: Validate workflow and PowerShell syntax + shell: bash + run: | + set -euo pipefail + ruby -e 'require "yaml"; YAML.load_file(ARGV.fetch(0))' .github/workflows/validate.yml + pwsh -NoProfile -Command ' + $errors = @(); + Get-ChildItem -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { + $tokens = $null; $parseErrors = $null; + [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors) | Out-Null; + if ($parseErrors.Count) { $joined = $parseErrors -join '; '; $errors += "$($_.FullName): $joined" } + }; + if ($errors.Count) { $errors | Write-Error; exit 1 } + ' + + linux-debian: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + container: + image: debian:bookworm-slim + steps: + - name: Prepare Debian container for checkout + run: | + apt-get update + apt-get install --yes --no-install-recommends ca-certificates curl git sudo bash + + - name: Check out the exact revision + uses: actions/checkout@v7 + + - name: Run Debian bootstrap as an unprivileged user + shell: bash + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + set -euo pipefail + test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" + remote_parent="$(mktemp -d)" + remote_dir="$remote_parent/dotfiles.git" + chmod 755 "$remote_parent" + git init --bare "$remote_dir" + git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" + git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main + chmod -R a+rX "$remote_dir" "$GITHUB_WORKSPACE" + echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" + echo "DOTFILES_BARE=$remote_dir" >> "$GITHUB_ENV" + + useradd --create-home --shell /bin/bash dotfilesci + printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci + chmod 0440 /etc/sudoers.d/dotfilesci + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ + DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ + bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ + bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian "$EXPECTED_COMMIT"' + + - name: Exercise Debian update wrapper against the local remote + shell: bash + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + set -euo pipefail + update_clone="$(mktemp -d)/dotfiles-update" + git clone "$DOTFILES_REPO" "$update_clone" + git -C "$update_clone" config user.name ci + git -C "$update_clone" config user.email ci@example.invalid + marker="ci-update-marker-${GITHUB_RUN_ID}" + printf '\n%s\n' "$marker" >> "$update_clone/home/dot_fdignore" + git -C "$update_clone" add home/dot_fdignore + git -C "$update_clone" commit -m 'CI update fixture' + git -C "$update_clone" push origin HEAD:refs/heads/main + echo "UPDATE_MARKER=$marker" >> "$GITHUB_ENV" + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ + bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" UPDATE_MARKER="$marker" \ + bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian' + + linux-arch: + runs-on: ubuntu-24.04 + timeout-minutes: 35 + container: + image: archlinux:base + steps: + - name: Prepare Arch container for checkout + run: | + pacman -Sy --noconfirm ca-certificates curl git sudo bash + + - name: Check out the exact revision + uses: actions/checkout@v7 + + - name: Run Arch bootstrap as an unprivileged user + shell: bash + run: | + set -euo pipefail + test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" + remote_parent="$(mktemp -d)" + remote_dir="$remote_parent/dotfiles.git" + chmod 755 "$remote_parent" + git init --bare "$remote_dir" + git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" + git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main + chmod -R a+rX "$remote_dir" "$GITHUB_WORKSPACE" + echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" + useradd --create-home --shell /bin/bash dotfilesci + printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci + chmod 0440 /etc/sudoers.d/dotfilesci + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ + DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ + bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ + bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch "$EXPECTED_COMMIT"' + + - name: Exercise Arch update wrapper against the local remote + shell: bash + run: | + set -euo pipefail + update_clone="$(mktemp -d)/dotfiles-update" + git clone "$DOTFILES_REPO" "$update_clone" + git -C "$update_clone" config user.name ci + git -C "$update_clone" config user.email ci@example.invalid + marker="ci-update-marker-${GITHUB_RUN_ID}" + printf '\n%s\n' "$marker" >> "$update_clone/home/dot_fdignore" + git -C "$update_clone" add home/dot_fdignore + git -C "$update_clone" commit -m 'CI update fixture' + git -C "$update_clone" push origin HEAD:refs/heads/main + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ + bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci UPDATE_MARKER="$marker" \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ + bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch' + + windows: + runs-on: windows-2025 + timeout-minutes: 45 + steps: + - name: Check out the exact revision + uses: actions/checkout@v7 + + - name: Provision WinGet if the hosted runner lacks it + shell: pwsh + run: | + Write-Host 'Checking for winget.exe' + cmd /c where winget.exe + if ($LASTEXITCODE -ne 0) { + Install-PackageProvider -Name NuGet -Force | Out-Null + Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null + Import-Module Microsoft.WinGet.Client -Force + Repair-WinGetPackageManager -Force -Latest + } + cmd /c where winget.exe + if ($LASTEXITCODE -ne 0) { throw 'winget.exe is unavailable after Microsoft.WinGet.Client repair' } + winget.exe --version + @( + (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), + (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), + (Join-Path $env:LOCALAPPDATA 'Programs\mise'), + (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') + ) | ForEach-Object { Add-Content -Path $env:GITHUB_PATH -Value $_ } + + - name: Create the exact-commit local bootstrap remote + shell: pwsh + run: | + $remote = Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git" + git init --bare $remote + $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() + if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } + git -C $env:GITHUB_WORKSPACE push $remote "$($env:GITHUB_SHA):refs/heads/main" + git --git-dir=$remote symbolic-ref HEAD refs/heads/main + $remoteUri = "file:///" + $remote.Replace('\', '/') + Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" + Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" + Write-Host "Local bootstrap remote: $remoteUri" + + - name: Confirm normal execution policy + shell: pwsh + run: | + Get-ExecutionPolicy -List | Format-Table -AutoSize + if ((Get-ExecutionPolicy) -eq 'AllSigned') { throw 'normal Windows job unexpectedly has AllSigned effective' } + + - name: Run Windows bootstrap in a temporary directory + shell: pwsh + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' + New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null + Push-Location $runDirectory + try { & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\bootstrap.cmd`""; if ($LASTEXITCODE -ne 0) { exit 1 } } + finally { Pop-Location } + + - name: Assert Windows normal-policy state + shell: pwsh + run: | + & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` + -RepoRoot $env:GITHUB_WORKSPACE -ExpectedCommit $env:GITHUB_SHA + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Exercise update.cmd against the local remote + shell: pwsh + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + $updateClone = Join-Path $env:RUNNER_TEMP 'dotfiles-update' + git clone $env:DOTFILES_REPO $updateClone + git -C $updateClone config user.name ci + git -C $updateClone config user.email ci@example.invalid + $marker = "ci-update-marker-$env:GITHUB_RUN_ID" + Add-Content -Path (Join-Path $updateClone 'home\dot_fdignore') -Value $marker + git -C $updateClone add home/dot_fdignore + git -C $updateClone commit -m 'CI update fixture' + git -C $updateClone push origin HEAD:refs/heads/main + Add-Content -Path $env:GITHUB_ENV -Value "UPDATE_MARKER=$marker" + $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' + Push-Location $runDirectory + try { & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\update.cmd`""; if ($LASTEXITCODE -ne 0) { exit 1 } } + finally { Pop-Location } + & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` + -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $marker + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + windows-allsigned: + runs-on: windows-2025 + timeout-minutes: 50 + steps: + - name: Check out the exact revision + uses: actions/checkout@v7 + + - name: Provision WinGet before changing execution policy + shell: pwsh + run: | + Write-Host 'Checking for winget.exe' + cmd /c where winget.exe + if ($LASTEXITCODE -ne 0) { + Install-PackageProvider -Name NuGet -Force | Out-Null + Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null + Import-Module Microsoft.WinGet.Client -Force + Repair-WinGetPackageManager -Force -Latest + } + cmd /c where winget.exe + if ($LASTEXITCODE -ne 0) { throw 'winget.exe is unavailable after Microsoft.WinGet.Client repair' } + winget.exe --version + @( + (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), + (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), + (Join-Path $env:LOCALAPPDATA 'Programs\mise'), + (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') + ) | ForEach-Object { Add-Content -Path $env:GITHUB_PATH -Value $_ } + + - name: Create the exact-commit local bootstrap remote before AllSigned + shell: pwsh + run: | + $remote = Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git" + git init --bare $remote + $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() + if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } + git -C $env:GITHUB_WORKSPACE push $remote "$($env:GITHUB_SHA):refs/heads/main" + git --git-dir=$remote symbolic-ref HEAD refs/heads/main + $remoteUri = "file:///" + $remote.Replace('\', '/') + Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" + Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" + Write-Host "Local bootstrap remote: $remoteUri" + + - name: Enable and prove CurrentUser AllSigned + shell: pwsh + run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force + + - name: Reject an unsigned script under AllSigned + shell: cmd + run: >- + pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; + Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; + & pwsh.exe -NoProfile -File $p; + $code = $LASTEXITCODE; + Remove-Item -LiteralPath $p -Force; + if ((Get-ExecutionPolicy) -ne 'AllSigned' -or $code -eq 0) { exit 1 }" + + - name: Run the real bootstrap through CMD and the signing bridge + shell: cmd + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + set "RUN_DIRECTORY=%RUNNER_TEMP%\dotfiles-run" + if not exist "%RUN_DIRECTORY%" mkdir "%RUN_DIRECTORY%" + pushd "%RUN_DIRECTORY%" + call "%GITHUB_WORKSPACE%\bootstrap.cmd" + if errorlevel 1 exit /b 1 + popd + + - name: Assert signed runtime and module state through the bridge + shell: cmd + run: | + call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" + if errorlevel 1 exit /b 1 + + - name: Create an update commit without changing the Actions checkout + shell: cmd + run: | + set "UPDATE_CLONE=%RUNNER_TEMP%\dotfiles-update" + git clone "%DOTFILES_REPO%" "%UPDATE_CLONE%" + git -C "%UPDATE_CLONE%" config user.name ci + git -C "%UPDATE_CLONE%" config user.email ci@example.invalid + set "UPDATE_MARKER=ci-update-marker-%GITHUB_RUN_ID%" + >>"%UPDATE_CLONE%\home\dot_fdignore" echo %UPDATE_MARKER% + git -C "%UPDATE_CLONE%" add home/dot_fdignore + git -C "%UPDATE_CLONE%" commit -m "CI update fixture" + git -C "%UPDATE_CLONE%" push origin HEAD:refs/heads/main + echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" + + - name: Exercise update.cmd and recheck AllSigned idempotency + shell: cmd + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + call "%GITHUB_WORKSPACE%\update.cmd" + if errorlevel 1 exit /b 1 + call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" -UpdateMarker "%UPDATE_MARKER%" + if errorlevel 1 exit /b 1 diff --git a/README.md b/README.md index dd7e9bc..973c8df 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,9 @@ For dotfiles plus package/application and module updates, use `update.cmd` on Wi - Codex guidance and skills are managed normally; repository documentation is kept outside `~/.codex`. - PowerShell source files are unsigned templates. The post-apply hook deploys copies and signs only the runtime files when `AllSigned` is effective, so Authenticode signatures never dirty chezmoi source state. - Managed PowerShell modules currently include `PSFzf` and `git-aliases`. Under `AllSigned`, only their user-scoped PowerShell content is inspected and unsigned/invalid files are signed; valid publisher signatures are preserved. + +## CI and testing + +The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. + +The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. diff --git a/bootstrap.cmd b/bootstrap.cmd index 6b6c9d3..b65b192 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -1,7 +1,11 @@ @echo off setlocal EnableExtensions EnableDelayedExpansion -set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" +if defined DOTFILES_REPO ( + set "REPO_URL=%DOTFILES_REPO%" +) else ( + set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" +) call :remove_legacy_broken_links if errorlevel 1 exit /b 1 @@ -52,7 +56,15 @@ if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( exit /b 1 ) -call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" +if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -CoreOnly -RepoRoot "%REPO_ROOT%" +) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -RepoRoot "%REPO_ROOT%" +) else if /I "%DOTFILES_CORE_ONLY%"=="1" ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -CoreOnly -RepoRoot "%REPO_ROOT%" +) else ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" +) exit /b %ERRORLEVEL% :ensure_package diff --git a/install.ps1 b/install.ps1 index a0b5b0b..2595488 100644 --- a/install.ps1 +++ b/install.ps1 @@ -3,14 +3,25 @@ param( [Alias("u")] [switch]$Update, + [switch]$NonInteractive, + + [switch]$CoreOnly, + [string]$RepoRoot ) $ErrorActionPreference = "Stop" +$NonInteractive = $NonInteractive -or $env:DOTFILES_NONINTERACTIVE -eq "1" +$CoreOnly = $CoreOnly -or $env:DOTFILES_CORE_ONLY -eq "1" if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $PSScriptRoot } $RepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path +$managedModulesPath = Join-Path $RepoRoot "scripts\windows\managed-modules.txt" +if (-not (Test-Path -LiteralPath $managedModulesPath -PathType Leaf)) { + throw "The managed PowerShell module list is missing from $RepoRoot." +} +$ManagedModules = @(Get-Content -LiteralPath $managedModulesPath | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') }) function Refresh-Path { $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") @@ -88,21 +99,33 @@ function Install-WithWinget { function Install-MustHaveApps { Write-Host "Installing must-have apps..." -ForegroundColor Cyan - $installs = @( - { Install-WithWinget -AppId "7zip.7zip" -Update:$Update }, - { Install-WithWinget -AppId "Microsoft.PowerToys" -Update:$Update }, - { Install-WithWinget -AppId "zyedidia.micro" -Alias "micro" -Update:$Update }, - { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$Update }, - { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$Update }, - { Install-WithWinget -AppId "Fastfetch-cli.Fastfetch" -Alias "fastfetch" -Update:$Update }, - { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$Update }, - { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$Update }, - { Install-WithWinget -AppId "dandavison.delta" -Alias "delta" -Update:$Update }, - { Install-WithWinget -AppId "jqlang.jq" -Alias "jq" -Update:$Update }, - { Install-WithWinget -AppId "Microsoft.VisualStudioCode" -Alias "code" -Update:$Update }, - { Install-WithWinget -AppId "BurntSushi.ripgrep.MSVC" -Alias "rg" -Update:$Update }, - { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$Update } - ) + $packageUpdate = $Update -and -not $CoreOnly + $installs = if ($CoreOnly) { + @( + { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$packageUpdate }, + { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$packageUpdate }, + { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$packageUpdate }, + { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$packageUpdate }, + { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$packageUpdate } + ) + } + else { + @( + { Install-WithWinget -AppId "7zip.7zip" -Update:$Update }, + { Install-WithWinget -AppId "Microsoft.PowerToys" -Update:$Update }, + { Install-WithWinget -AppId "zyedidia.micro" -Alias "micro" -Update:$Update }, + { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$Update }, + { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$Update }, + { Install-WithWinget -AppId "Fastfetch-cli.Fastfetch" -Alias "fastfetch" -Update:$Update }, + { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$Update }, + { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$Update }, + { Install-WithWinget -AppId "dandavison.delta" -Alias "delta" -Update:$Update }, + { Install-WithWinget -AppId "jqlang.jq" -Alias "jq" -Update:$Update }, + { Install-WithWinget -AppId "Microsoft.VisualStudioCode" -Alias "code" -Update:$Update }, + { Install-WithWinget -AppId "BurntSushi.ripgrep.MSVC" -Alias "rg" -Update:$Update }, + { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$Update } + ) + } foreach ($install in $installs) { & $install } Refresh-Path @@ -111,7 +134,7 @@ function Install-MustHaveApps { if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } } - foreach ($module in @("PSFzf", "git-aliases")) { + foreach ($module in $ManagedModules) { $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 $installedResource = if (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue) { Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 @@ -120,7 +143,7 @@ function Install-MustHaveApps { Write-Host "Installing $module module..." -ForegroundColor Cyan Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber } - elseif ($Update) { + elseif ($Update -and -not $CoreOnly) { Write-Host "Updating $module module..." -ForegroundColor Yellow if ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { Update-PSResource -Name $module -Scope CurrentUser -Force @@ -137,6 +160,11 @@ function Install-MustHaveApps { } function Install-OptionalApps { + if ($NonInteractive) { + Write-Host "Skipping optional installs in non-interactive mode..." -ForegroundColor Yellow + return + } + $optionalApps = @( @{ name = "Google Chrome"; install = { Install-WithWinget -AppId "Google.Chrome" -Update:$Update } }, @{ name = "KeepassXC"; install = { Install-WithWinget -AppId "KeePassXCTeam.KeePassXC" -Update:$Update } }, @@ -232,15 +260,22 @@ function Configure-WindowsTerminal { } function Configure-Wsl { + if ($NonInteractive -or $CoreOnly) { + Write-Host "Skipping WSL installation in non-interactive/core-only mode..." -ForegroundColor Yellow + return + } + if (Get-Command wsl -ErrorAction SilentlyContinue) { Write-Host "Installing WSL..." -ForegroundColor Cyan; & wsl --install --no-distribution } } Refresh-Path Check-RequiredApps -Download-Fonts -Install-UserFonts +if (-not $CoreOnly) { + Download-Fonts + Install-UserFonts +} Configure-Git Install-MustHaveApps -Configure-WindowsTerminal +if (-not $CoreOnly) { Configure-WindowsTerminal } Install-OptionalApps Configure-Wsl diff --git a/install.sh b/install.sh index 393d08a..d0041b5 100755 --- a/install.sh +++ b/install.sh @@ -11,12 +11,14 @@ RESET='\033[0m' REPO_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) UPDATE=false +CORE_ONLY=false function usage () { - echo "Usage: $0 [--update|-u]" + echo "Usage: $0 [--update|-u] [--core-only]" echo echo "Options:" echo " -u, --update Re-run package installers even when commands already exist" + echo " --core-only Install only the runtime prerequisites used by the dotfiles" echo " -h, --help Show this help message" } @@ -26,6 +28,9 @@ function parse_args () { -u|--update) UPDATE=true ;; + --core-only) + CORE_ONLY=true + ;; -h|--help) usage exit 0 @@ -47,6 +52,10 @@ function updates_enabled () { esac } +if [[ "${DOTFILES_CORE_ONLY:-0}" == "1" ]]; then + CORE_ONLY=true +fi + function pre_setup_tasks() { if [ ! -d "$REPO_ROOT" ]; then echo -e "${RED}The repository folder '$REPO_ROOT' does not exist; exiting..."; @@ -375,6 +384,11 @@ function configure_git () { } function configure_wsl() { + if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]]; then + echo -e "${YELLOW}Skipping WSL system configuration in non-interactive mode.${RESET}" + return + fi + local desired="${REPO_ROOT}/assets/wsl/wsl.conf" if grep -qi microsoft /proc/version && [ -f "$desired" ]; then if [ ! -f /etc/wsl.conf ] || ! cmp -s "$desired" /etc/wsl.conf; then @@ -416,6 +430,11 @@ function install_dagger () { } function install_optional_packages () { + if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]]; then + echo -e "${YELLOW}Skipping optional package selection in non-interactive mode.${RESET}" + return + fi + local packages=( "mise-en-place|deb:check_package_or_run mise install_mise_en_place|arch:install_with_pacman mise" "docker|deb:check_package_or_run docker install_docker|arch:install_with_pacman docker" @@ -482,8 +501,16 @@ function install_optional_packages () { parse_args "$@" pre_setup_tasks configure_git -configure_wsl -install_must_have_packages -setup_sheldon_plugins -setup_default_shell -install_optional_packages +if ! ${CORE_ONLY}; then + configure_wsl + install_must_have_packages + setup_sheldon_plugins + if [[ "${DOTFILES_NONINTERACTIVE:-0}" != "1" ]]; then + setup_default_shell + else + echo -e "${YELLOW}Skipping default-shell change in non-interactive mode.${RESET}" + fi + install_optional_packages +else + echo -e "${YELLOW}Skipping workstation package catalog in core-only mode.${RESET}" +fi diff --git a/scripts/windows/managed-modules.txt b/scripts/windows/managed-modules.txt new file mode 100644 index 0000000..ff589e7 --- /dev/null +++ b/scripts/windows/managed-modules.txt @@ -0,0 +1,2 @@ +PSFzf +git-aliases diff --git a/tests/linux/assert-state.sh b/tests/linux/assert-state.sh new file mode 100644 index 0000000..b8d8749 --- /dev/null +++ b/tests/linux/assert-state.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +expected_distro="${1:?expected distribution (debian or arch) is required}" +expected_commit="${2:-}" +repo_root="${GITHUB_WORKSPACE:-$(pwd)}" + +fail() { + printf 'ASSERTION FAILED: %s\n' "$1" >&2 + exit 1 +} + +printf 'distribution: %s\n' "$(cat /etc/os-release | tr '\n' ' ')" +printf 'uname: %s\n' "$(uname -a)" +printf 'user: %s (%s)\n' "$(id -un)" "$(id -u)" +printf 'HOME: %s\n' "$HOME" +printf 'chezmoi: %s\n' "$(chezmoi --version)" +printf 'chezmoi source-path: %s\n' "$(chezmoi source-path)" + +case "$expected_distro" in + debian) [[ -f /etc/debian_version ]] || fail "Debian detection marker is missing" ;; + arch) [[ -f /etc/arch-release ]] || fail "Arch detection marker is missing" ;; + *) fail "unknown expected distribution: $expected_distro" ;; +esac + +command -v apt-get >/dev/null 2>&1 || [[ "$expected_distro" != debian ]] || fail "apt-get is unavailable on Debian" +command -v pacman >/dev/null 2>&1 || [[ "$expected_distro" != arch ]] || fail "pacman is unavailable on Arch" +sudo -n true || fail "the CI user does not have passwordless sudo" + +required_files=( + "$HOME/.gitconfig" + "$HOME/.gitconfig.local" + "$HOME/.fdignore" + "$HOME/.zshenv" + "$HOME/.config/zsh/.zshrc" + "$HOME/.config/zsh/lib/aliases.zsh" + "$HOME/.config/zsh/lib/completions.zsh" + "$HOME/.config/zsh/lib/key-bindings.zsh" + "$HOME/.config/zsh/lib/sheldon.zsh" + "$HOME/.config/starship/config.toml" + "$HOME/.config/starship/lean.config.toml" + "$HOME/.config/sheldon/plugins.toml" + "$HOME/.codex/AGENTS.md" + "$HOME/.codex/skills/mule-munit/SKILL.md" + "$HOME/.codex/skills/mule-munit/agents/openai.yaml" +) +for path in "${required_files[@]}"; do + [[ -f "$path" ]] || fail "expected deployed file is missing: $path" +done + +assert_clean() { + local status + status="$(chezmoi status)" + [[ -z "$status" ]] || fail "chezmoi status is not clean:\n$status" +} + +chezmoi apply +chezmoi apply +assert_clean + +if [[ -n "$expected_commit" ]]; then + source_path="$(chezmoi source-path)" + source_commit="$(git -C "$source_path" rev-parse HEAD)" + [[ "$source_commit" == "$expected_commit" ]] || fail "source commit $source_commit is not expected commit $expected_commit" +fi + +config_marker="ci-create-only-$(date +%s)" +rules_marker="ci-create-only-rule-$(date +%s)" +printf '\n%s\n' "$config_marker" >> "$HOME/.codex/config.toml" +printf '\n%s\n' "$rules_marker" >> "$HOME/.codex/rules/default.rules" +chezmoi apply +grep -Fqx "$config_marker" "$HOME/.codex/config.toml" || fail "chezmoi overwrote create-only config.toml" +grep -Fqx "$rules_marker" "$HOME/.codex/rules/default.rules" || fail "chezmoi overwrote create-only default.rules" +assert_clean + +source_path="$(chezmoi source-path)" +[[ -z "$(git -C "$source_path" status --porcelain)" ]] || fail "chezmoi source Git tree is dirty" +[[ -z "$(git -C "$repo_root" status --porcelain)" ]] || fail "Actions checkout Git tree is dirty" + +printf 'Linux state assertions passed for %s.\n' "$expected_distro" diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 new file mode 100644 index 0000000..bc0bd2a --- /dev/null +++ b/tests/windows/assert-allsigned.ps1 @@ -0,0 +1,102 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$RepoRoot, + [Parameter(Mandatory)][string]$ThumbprintFile, + [string]$UpdateMarker +) + +$ErrorActionPreference = "Stop" + +function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } + +if ((Get-ExecutionPolicy) -ne "AllSigned") { Fail "effective execution policy is not AllSigned" } +Write-Host "Execution policy: $(Get-ExecutionPolicy)" +Get-ExecutionPolicy -List | Format-Table -AutoSize | Out-Host + +$certificateSubject = "CN=jsilverdev Dotfiles Code Signing" +$codeSigningOid = "1.3.6.1.5.5.7.3.3" +$certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object { + $_.Subject -eq $certificateSubject -and + $_.HasPrivateKey -and + $_.NotAfter -gt (Get-Date) -and + @($_.EnhancedKeyUsageList | Where-Object ObjectId -eq $codeSigningOid).Count -gt 0 +} | Sort-Object NotAfter -Descending | Select-Object -First 1) +if ($certificate.Count -ne 1) { Fail "usable dotfiles Code Signing certificate was not found in CurrentUser\\My" } + +foreach ($storeName in @("My", "Root", "TrustedPublisher")) { + $trusted = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate[0].Thumbprint) + if ($trusted.Count -ne 1) { Fail "certificate $($certificate[0].Thumbprint) is missing from CurrentUser\\$storeName" } +} +Write-Host "Certificate: $($certificate[0].Subject) thumbprint=$($certificate[0].Thumbprint) expires=$($certificate[0].NotAfter)" + +if (Test-Path -LiteralPath $ThumbprintFile) { + $previous = (Get-Content -LiteralPath $ThumbprintFile -Raw).Trim() + if ($previous -ne $certificate[0].Thumbprint) { Fail "signing certificate changed from $previous to $($certificate[0].Thumbprint)" } +} +Set-Content -LiteralPath $ThumbprintFile -Value $certificate[0].Thumbprint -NoNewline + +function Invoke-Chezmoi([string[]]$Arguments) { + $output = @(& chezmoi.exe @Arguments 2>&1) + if ($LASTEXITCODE -ne 0) { Fail "chezmoi $($Arguments -join ' ') failed: $($output -join [Environment]::NewLine)" } + return $output +} + +function Assert-CleanChezMoi { + $status = @(Invoke-Chezmoi @("status")) + if ($status.Count -ne 0) { Fail "chezmoi status is not clean: $($status -join [Environment]::NewLine)" } +} + +$sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() +Write-Host "chezmoi source-path: $sourcePath" +Invoke-Chezmoi @("apply") | Out-Host +Invoke-Chezmoi @("apply") | Out-Host +Assert-CleanChezMoi + +$runtimeFiles = @( + (Join-Path $HOME ".config\pwsh\env.ps1"), + (Join-Path $HOME ".config\pwsh\lib\helpers.ps1"), + (Join-Path $HOME ".config\pwsh\lib\aliases.ps1"), + $PROFILE.CurrentUserAllHosts +) +foreach ($path in $runtimeFiles) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail "runtime PowerShell file is missing: $path" } + $signature = Get-AuthenticodeSignature -FilePath $path + Write-Host "${path}: $($signature.Status) signer=$($signature.SignerCertificate.Thumbprint)" + if ($signature.Status -ne "Valid") { Fail "runtime PowerShell signature is not Valid: $path ($($signature.Status))" } + if ($signature.SignerCertificate.Thumbprint -ne $certificate[0].Thumbprint) { Fail "runtime PowerShell file has an unexpected signer: $path" } +} + +foreach ($moduleName in @(Get-Content -LiteralPath (Join-Path $RepoRoot "scripts\windows\managed-modules.txt") | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') })) { + if ($moduleName -eq "git-aliases") { + Import-Module $moduleName -Force -DisableNameChecking -ErrorAction Stop + } + else { + Import-Module $moduleName -Force -ErrorAction Stop + } + $module = @(Get-Module -ListAvailable -Name $moduleName | Where-Object { + $_.ModuleBase -like "$(Join-Path $HOME 'Documents\PowerShell\Modules')*" -or + $_.ModuleBase -like "$(Join-Path $HOME '.local\share\powershell\Modules')*" + } | Select-Object -First 1) + if ($module.Count -ne 1) { Fail "managed module is not in a current-user module path: $moduleName" } + $moduleFiles = @(Get-ChildItem -LiteralPath $module[0].ModuleBase -File -Recurse | Where-Object Extension -in @(".ps1", ".psm1", ".psd1", ".ps1xml", ".cdxml", ".xaml")) + foreach ($file in $moduleFiles) { + $signature = Get-AuthenticodeSignature -FilePath $file.FullName + if ($signature.Status -ne "Valid") { Fail "managed module file is not Validly signed: $($file.FullName) ($($signature.Status))" } + } +} + +if ($UpdateMarker) { + if (-not ((Get-Content (Join-Path $HOME ".fdignore") -Raw) -match [regex]::Escape($UpdateMarker))) { Fail "update marker did not reach .fdignore" } +} + +$sourceStatus = @(git -C $sourcePath status --porcelain) +if ($LASTEXITCODE -ne 0 -or $sourceStatus.Count -ne 0) { Fail "chezmoi source Git tree is dirty: $($sourceStatus -join [Environment]::NewLine)" } +$repoStatus = @(git -C $RepoRoot status --porcelain) +if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { Fail "Actions checkout Git tree is dirty: $($repoStatus -join [Environment]::NewLine)" } +$signatureMatches = @(git -C $RepoRoot grep -n "^# SIG # Begin signature block" -- "*.ps1") +if ($LASTEXITCODE -eq 0 -or $signatureMatches.Count -ne 0) { Fail "repository PowerShell source contains an Authenticode signature block" } + +$profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) +if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains "profile-ok")) { Fail "PowerShell profile startup failed: $($profileOutput -join [Environment]::NewLine)" } + +Write-Host "AllSigned assertions passed with certificate $($certificate[0].Thumbprint)." diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 new file mode 100644 index 0000000..9d04dd8 --- /dev/null +++ b/tests/windows/assert-state.ps1 @@ -0,0 +1,100 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$RepoRoot, + [string]$ExpectedCommit, + [string]$UpdateMarker +) + +$ErrorActionPreference = "Stop" + +function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } + +Write-Host "Windows: $([Environment]::OSVersion.Version)" +Write-Host "User: $env:USERNAME" +Write-Host "PowerShell: $($PSVersionTable.PSVersion)" +Write-Host "Execution policy: $(Get-ExecutionPolicy)" +Write-Host "Execution policies:" +Get-ExecutionPolicy -List | Format-Table -AutoSize | Out-Host + +if ((Get-ExecutionPolicy) -eq "AllSigned") { Fail "normal-policy test unexpectedly has AllSigned effective" } + +function Invoke-Chezmoi([string[]]$Arguments) { + $output = @(& chezmoi.exe @Arguments 2>&1) + if ($LASTEXITCODE -ne 0) { Fail "chezmoi $($Arguments -join ' ') failed: $($output -join [Environment]::NewLine)" } + return $output +} + +function Assert-CleanChezMoi { + $status = @(Invoke-Chezmoi @("status")) + if ($status.Count -ne 0) { Fail "chezmoi status is not clean: $($status -join [Environment]::NewLine)" } +} + +Write-Host "chezmoi: $((chezmoi.exe --version) -join ' ')" +$sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() +Write-Host "chezmoi source-path: $sourcePath" +if (-not (Test-Path -LiteralPath $sourcePath)) { Fail "chezmoi source path does not exist: $sourcePath" } + +$requiredFiles = @( + (Join-Path $HOME ".gitconfig"), + (Join-Path $HOME ".gitconfig.local"), + (Join-Path $HOME ".fdignore"), + (Join-Path $HOME ".wslconfig"), + (Join-Path $HOME ".config\starship\config.toml"), + (Join-Path $HOME ".codex\AGENTS.md"), + (Join-Path $HOME ".codex\skills\mule-munit\SKILL.md"), + (Join-Path $HOME ".codex\skills\mule-munit\agents\openai.yaml"), + (Join-Path $HOME ".config\pwsh\env.ps1"), + (Join-Path $HOME ".config\pwsh\lib\helpers.ps1"), + (Join-Path $HOME ".config\pwsh\lib\aliases.ps1"), + $PROFILE.CurrentUserAllHosts +) +foreach ($path in $requiredFiles) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail "expected deployed file is missing: $path" } +} + +if ($ExpectedCommit) { + $sourceCommit = ((git -C $sourcePath rev-parse HEAD) -join "").Trim() + if ($LASTEXITCODE -ne 0 -or $sourceCommit -ne $ExpectedCommit) { Fail "source commit $sourceCommit is not expected commit $ExpectedCommit" } +} + +Invoke-Chezmoi @("apply") | Out-Host +Invoke-Chezmoi @("apply") | Out-Host +Assert-CleanChezMoi + +$configMarker = "ci-create-only-$([Guid]::NewGuid().ToString('N'))" +$rulesMarker = "ci-create-only-rule-$([Guid]::NewGuid().ToString('N'))" +Add-Content -LiteralPath (Join-Path $HOME ".codex\config.toml") -Value $configMarker +Add-Content -LiteralPath (Join-Path $HOME ".codex\rules\default.rules") -Value $rulesMarker +Invoke-Chezmoi @("apply") | Out-Host +if (-not ((Get-Content (Join-Path $HOME ".codex\config.toml") -Raw) -match [regex]::Escape($configMarker))) { Fail "chezmoi overwrote create-only config.toml" } +if (-not ((Get-Content (Join-Path $HOME ".codex\rules\default.rules") -Raw) -match [regex]::Escape($rulesMarker))) { Fail "chezmoi overwrote create-only default.rules" } +Assert-CleanChezMoi + +if ($UpdateMarker) { + if (-not ((Get-Content (Join-Path $HOME ".fdignore") -Raw) -match [regex]::Escape($UpdateMarker))) { Fail "update marker did not reach .fdignore" } +} + +foreach ($module in @(Get-Content -LiteralPath (Join-Path $RepoRoot "scripts\windows\managed-modules.txt") | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') })) { + if ($module -eq "git-aliases") { + Import-Module $module -Force -DisableNameChecking -ErrorAction Stop + } + else { + Import-Module $module -Force -ErrorAction Stop + } +} + +$certificateSubject = "CN=jsilverdev Dotfiles Code Signing" +$certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object Subject -eq $certificateSubject) +if ($certificate.Count -ne 0) { Fail "normal-policy run unexpectedly created a dotfiles signing certificate" } + +$sourceStatus = @(git -C $sourcePath status --porcelain) +if ($LASTEXITCODE -ne 0 -or $sourceStatus.Count -ne 0) { Fail "chezmoi source Git tree is dirty: $($sourceStatus -join [Environment]::NewLine)" } +$repoStatus = @(git -C $RepoRoot status --porcelain) +if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { Fail "Actions checkout Git tree is dirty: $($repoStatus -join [Environment]::NewLine)" } +$signatureMatches = @(git -C $RepoRoot grep -n "^# SIG # Begin signature block" -- "*.ps1") +if ($LASTEXITCODE -eq 0 -or $signatureMatches.Count -ne 0) { Fail "repository PowerShell source contains an Authenticode signature block" } + +$profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) +if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains "profile-ok")) { Fail "PowerShell profile startup failed: $($profileOutput -join [Environment]::NewLine)" } + +Write-Host "Windows normal-policy assertions passed." diff --git a/update.cmd b/update.cmd index cfd8111..ef7367d 100644 --- a/update.cmd +++ b/update.cmd @@ -16,7 +16,13 @@ if not defined REPO_ROOT ( echo Unable to resolve the chezmoi working tree. 1>&2 exit /b 1 ) -call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -RepoRoot "%REPO_ROOT%" +if /I "%DOTFILES_CORE_ONLY%"=="1" ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -CoreOnly -NonInteractive -RepoRoot "%REPO_ROOT%" +) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -NonInteractive -RepoRoot "%REPO_ROOT%" +) else ( + call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -RepoRoot "%REPO_ROOT%" +) exit /b %ERRORLEVEL% :resolve_repo_root diff --git a/update.sh b/update.sh index 4d564c9..d9301f4 100644 --- a/update.sh +++ b/update.sh @@ -23,4 +23,4 @@ resolve_repo_root() { } REPO_ROOT="$(resolve_repo_root)" || { printf 'Unable to resolve the chezmoi working tree.\n' >&2; exit 1; } -exec "$REPO_ROOT/install.sh" --update +exec "$REPO_ROOT/install.sh" --update "$@" From 3584eb5bbf25c49d3df48c9b3396d6b49f08dd18 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:19:56 -0500 Subject: [PATCH 06/61] Fix CI runtime policy and container setup --- .github/workflows/validate.yml | 17 ++++++++++----- install.ps1 | 28 ++++++++++++++++++++++++- install.sh | 4 ++-- scripts/windows/deploy-pwsh.ps1 | 29 +++++++++++++++++++++++++- scripts/windows/invoke-ps-script.cmd | 2 +- scripts/windows/signing.ps1 | 24 ++++++++++++++++++++- tests/windows/assert-allsigned.ps1 | 31 +++++++++++++++++++++++++--- 7 files changed, 121 insertions(+), 14 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 18f0f45..ec76346 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -120,6 +120,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci + sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -183,6 +184,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci + sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -248,9 +250,11 @@ jobs: git init --bare $remote $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } - git -C $env:GITHUB_WORKSPACE push $remote "$($env:GITHUB_SHA):refs/heads/main" - git --git-dir=$remote symbolic-ref HEAD refs/heads/main $remoteUri = "file:///" + $remote.Replace('\', '/') + git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" + if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } + if (-not (git --git-dir=$remote show-ref --verify --quiet refs/heads/main)) { throw "local bootstrap remote has no main ref" } + git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" Write-Host "Local bootstrap remote: $remoteUri" @@ -339,9 +343,11 @@ jobs: git init --bare $remote $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } - git -C $env:GITHUB_WORKSPACE push $remote "$($env:GITHUB_SHA):refs/heads/main" - git --git-dir=$remote symbolic-ref HEAD refs/heads/main $remoteUri = "file:///" + $remote.Replace('\', '/') + git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" + if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } + if (-not (git --git-dir=$remote show-ref --verify --quiet refs/heads/main)) { throw "local bootstrap remote has no main ref" } + git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" Write-Host "Local bootstrap remote: $remoteUri" @@ -358,7 +364,8 @@ jobs: & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; - if ((Get-ExecutionPolicy) -ne 'AllSigned' -or $code -eq 0) { exit 1 }" + $policy = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell').GetValue('ExecutionPolicy', ''); + if ($policy -ne 'AllSigned' -or $code -eq 0) { exit 1 }" - name: Run the real bootstrap through CMD and the signing bridge shell: cmd diff --git a/install.ps1 b/install.ps1 index 2595488..5d919f1 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,6 +13,32 @@ param( $ErrorActionPreference = "Stop" $NonInteractive = $NonInteractive -or $env:DOTFILES_NONINTERACTIVE -eq "1" $CoreOnly = $CoreOnly -or $env:DOTFILES_CORE_ONLY -eq "1" + +function Test-DotfilesAllSignedPolicy { + $locations = @( + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } + ) + foreach ($location in $locations) { + $baseKey = $null + $key = $null + try { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) + $key = $baseKey.OpenSubKey($location.Path) + if ($null -ne $key) { + $policy = $key.GetValue("ExecutionPolicy", $null) + if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } + } + } + finally { + if ($key) { $key.Dispose() } + if ($baseKey) { $baseKey.Dispose() } + } + } + return $false +} if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $PSScriptRoot } @@ -47,7 +73,7 @@ function Invoke-SigningHelper { [string]$ModuleName ) - if ((Get-ExecutionPolicy) -ne "AllSigned") { + if (-not (Test-DotfilesAllSignedPolicy)) { return } diff --git a/install.sh b/install.sh index d0041b5..9798ea5 100755 --- a/install.sh +++ b/install.sh @@ -266,7 +266,7 @@ function install_debian_packages () { "ripgrep" ) - for app in ${debian_apps[@]}; do + for app in "${debian_apps[@]}"; do install_with_apt $app done @@ -332,7 +332,7 @@ function install_arch_packages () { "ripgrep" ) - for app in ${pacman_apps[@]}; do + for app in "${pacman_apps[@]}"; do install_with_pacman $app done diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 index c2106e3..9c4bc76 100644 --- a/scripts/windows/deploy-pwsh.ps1 +++ b/scripts/windows/deploy-pwsh.ps1 @@ -5,6 +5,33 @@ param( ) $ErrorActionPreference = "Stop" + +function Test-DotfilesAllSignedPolicy { + $locations = @( + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } + ) + foreach ($location in $locations) { + $baseKey = $null + $key = $null + try { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) + $key = $baseKey.OpenSubKey($location.Path) + if ($null -ne $key) { + $policy = $key.GetValue("ExecutionPolicy", $null) + if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } + } + } + finally { + if ($key) { $key.Dispose() } + if ($baseKey) { $baseKey.Dispose() } + } + } + return $false +} + $repo = (Resolve-Path -LiteralPath $RepoRoot).Path $sourceRoot = Join-Path $repo "home\.chezmoitemplates\pwsh" $destinationRoot = Join-Path $HOME ".config\pwsh" @@ -35,7 +62,7 @@ foreach ($file in $files) { Copy-Item -LiteralPath $source -Destination $file.Destination -Force } -if ((Get-ExecutionPolicy) -eq "AllSigned") { +if (Test-DotfilesAllSignedPolicy) { $helper = Join-Path $repo "scripts\windows\signing.ps1" $bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" & $bridge $helper -Action ProtectFiles -Path @($files | ForEach-Object Destination) diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index d94ee5a..f14909d 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -8,7 +8,7 @@ shift where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAYQByAGcAcwBbADAAXQA7ACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApADsAIABpAGYAIAAoACQAYQByAGcAcwAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMQApACAAewAgACQAcwBjAHIAaQBwAHQAQQByAGcAcwAgAD0AIABAACgAJABhAHIAZwBzAFsAMQAuAC4AKAAkAGEAcgBnAHMALgBDAG8AdQBuAHQAIAAtACAAMQApAF0AKQAgAH0AOwAgACQAcAB3AHMAaAAgAD0AIAAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQApAC4AUwBvAHUAcgBjAGUAOwAgACQAdABlAG0AcABvAHIAYQByAHkAIAA9ACAAJABuAHUAbABsADsAIAB0AHIAeQAgAHsAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcAOwAgACQAbwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwA7ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAARwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAHsAIAAkAF8ALgBTAHUAYgBqAGUAYwB0ACAALQBlAHEAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAgACQAXwAuAEgAYQBzAFAAcgBpAHYAYQB0AGUASwBlAHkAIAAtAGEAbgBkACAAJABfAC4ATgBvAHQAQQBmAHQAZQByACAALQBnAHQAIAAoAEcAZQB0AC0ARABhAHQAZQApACAALQBhAG4AZAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAG8AaQBkACAAfQApAC4AQwBvAHUAbgB0ACAALQBnAHQAIAAwACAAfQAgAHwAIABTAG8AcgB0AC0ATwBiAGoAZQBjAHQAIABOAG8AdABBAGYAdABlAHIAIAAtAEQAZQBzAGMAZQBuAGQAaQBuAGcAIAB8ACAAUwBlAGwAZQBjAHQALQBPAGIAagBlAGMAdAAgAC0ARgBpAHIAcwB0ACAAMQA7ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAALQBTAHUAYgBqAGUAYwB0ACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAUwB1AGIAagBlAGMAdAAgAC0AQwBlAHIAdABTAHQAbwByAGUATABvAGMAYQB0AGkAbwBuACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAALQBOAG8AdABBAGYAdABlAHIAIAAoAEcAZQB0AC0ARABhAHQAZQApAC4AQQBkAGQAWQBlAGEAcgBzACgAMQAwACkAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAH0AOwAgACQAcAB1AGIAbABpAGMAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAGMAZQByACcAKQA7ACAAdAByAHkAIAB7ACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0AVAB5AHAAZQAgAEMARQBSAFQAIAAtAEYAbwByAGMAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwAOwAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlACAAaQBuACAAQAAoACcAUgBvAG8AdAAnACwAIAAnAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAJwApACkAIAB7ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACgARwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7ACAASQBtAHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUAIgAgAHwAIABPAHUAdAAtAE4AdQBsAGwAIAB9ACAAfQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlACAAfQA7ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AcABzADEAJwApADsAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBGAG8AcgBjAGUAOwAgACQAcwBpAGcAbgBlAGQAIAA9ACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgA7ACAAaQBmACAAKAAoAEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAKQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7ACAAdABoAHIAbwB3ACAAKAAnAFQAZQBtAHAAbwByAGEAcgB5ACAAcwBjAHIAaQBwAHQAIABzAGkAZwBuAGEAdAB1AHIAZQAgAGkAcwAgAG4AbwB0ACAAVgBhAGwAaQBkADoAIAAnACAAKwAgACQAcwBpAGcAbgBlAGQALgBTAHQAYQB0AHUAcwApACAAfQA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAdABlAG0AcABvAHIAYQByAHkAIAB9ADsAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwA7ACAAJABlAHgAaQB0AEMAbwBkAGUAIAA9ACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAG4AZQAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQAgAHsAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAAfQAgAGUAbABzAGUAaQBmACAAKAAkAD8AKQAgAHsAIAAwACAAfQAgAGUAbABzAGUAIAB7ACAAMQAgAH0AOwAgAGUAeABpAHQAIAAkAGUAeABpAHQAQwBvAGQAZQAgAH0AIABjAGEAdABjAGgAIAB7ACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfADsAIABlAHgAaQB0ACAAMQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQApACAAewAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlACAAfQAgAH0A %* +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAYQByAGcAcwBbADAAXQA7ACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApADsAIABpAGYAIAAoACQAYQByAGcAcwAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMQApACAAewAgACQAcwBjAHIAaQBwAHQAQQByAGcAcwAgAD0AIABAACgAJABhAHIAZwBzAFsAMQAuAC4AKAAkAGEAcgBnAHMALgBDAG8AdQBuAHQAIAAtACAAMQApAF0AKQAgAH0AOwAgACQAcAB3AHMAaAAgAD0AIAAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQApAC4AUwBvAHUAcgBjAGUAOwAgACQAdABlAG0AcABvAHIAYQByAHkAIAA9ACAAJABuAHUAbABsADsAIAB0AHIAeQAgAHsAIAAkAGEAbABsAFMAaQBnAG4AZQBkACAAPQAgACQAZgBhAGwAcwBlADsAIABmAG8AcgBlAGEAYwBoACAAKAAkAGwAbwBjAGEAdABpAG8AbgAgAGkAbgAgAEAAKABAAHsAIABIAGkAdgBlACAAPQAgAFsATQBpAGMAcgBvAHMAbwBmAHQALgBXAGkAbgAzADIALgBSAGUAZwBpAHMAdAByAHkASABpAHYAZQBdADoAOgBMAG8AYwBhAGwATQBhAGMAaABpAG4AZQA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAFAAbwBsAGkAYwBpAGUAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAVwBpAG4AZABvAHcAcwBcAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEMAdQByAHIAZQBuAHQAVQBzAGUAcgA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAFAAbwBsAGkAYwBpAGUAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAVwBpAG4AZABvAHcAcwBcAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEMAdQByAHIAZQBuAHQAVQBzAGUAcgA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAUABvAHcAZQByAFMAaABlAGwAbABcADEAXABTAGgAZQBsAGwASQBkAHMAXABNAGkAYwByAG8AcwBvAGYAdAAuAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEwAbwBjAGEAbABNAGEAYwBoAGkAbgBlADsAIABQAGEAdABoACAAPQAgACIAUwBvAGYAdAB3AGEAcgBlAFwATQBpAGMAcgBvAHMAbwBmAHQAXABQAG8AdwBlAHIAUwBoAGUAbABsAFwAMQBcAFMAaABlAGwAbABJAGQAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AC4AUABvAHcAZQByAFMAaABlAGwAbAAiACAAfQApACkAIAB7ACAAJABiAGEAcwBlAEsAZQB5ACAAPQAgACQAbgB1AGwAbAA7ACAAJABrAGUAeQAgAD0AIAAkAG4AdQBsAGwAOwAgAHQAcgB5ACAAewAgACQAYgBhAHMAZQBLAGUAeQAgAD0AIABbAE0AaQBjAHIAbwBzAG8AZgB0AC4AVwBpAG4AMwAyAC4AUgBlAGcAaQBzAHQAcgB5AEsAZQB5AF0AOgA6AE8AcABlAG4AQgBhAHMAZQBLAGUAeQAoACQAbABvAGMAYQB0AGkAbwBuAC4ASABpAHYAZQAsACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBWAGkAZQB3AF0AOgA6AEQAZQBmAGEAdQBsAHQAKQA7ACAAJABrAGUAeQAgAD0AIAAkAGIAYQBzAGUASwBlAHkALgBPAHAAZQBuAFMAdQBiAEsAZQB5ACgAJABsAG8AYwBhAHQAaQBvAG4ALgBQAGEAdABoACkAOwAgAGkAZgAgACgAJABuAHUAbABsACAALQBuAGUAIAAkAGsAZQB5ACAALQBhAG4AZAAgACQAawBlAHkALgBHAGUAdABWAGEAbAB1AGUAKAAiAEUAeABlAGMAdQB0AGkAbwBuAFAAbwBsAGkAYwB5ACIALAAgACQAbgB1AGwAbAApACAALQBlAHEAIAAiAEEAbABsAFMAaQBnAG4AZQBkACIAKQAgAHsAIAAkAGEAbABsAFMAaQBnAG4AZQBkACAAPQAgACQAdAByAHUAZQA7ACAAYgByAGUAYQBrACAAfQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAGkAZgAgACgAJABrAGUAeQApACAAewAgACQAawBlAHkALgBEAGkAcwBwAG8AcwBlACgAKQAgAH0AOwAgAGkAZgAgACgAJABiAGEAcwBlAEsAZQB5ACkAIAB7ACAAJABiAGEAcwBlAEsAZQB5AC4ARABpAHMAcABvAHMAZQAoACkAIAB9ACAAfQAgAH0AOwAgAGkAZgAgACgAJABhAGwAbABTAGkAZwBuAGUAZAApACAAewAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAFMAdQBiAGoAZQBjAHQAIAA9ACAAJwBDAE4APQBqAHMAaQBsAHYAZQByAGQAZQB2ACAARABvAHQAZgBpAGwAZQBzACAAQwBvAGQAZQAgAFMAaQBnAG4AaQBuAGcAJwA7ACAAJABvAGkAZAAgAD0AIAAnADEALgAzAC4ANgAuADEALgA1AC4ANQAuADcALgAzAC4AMwAnADsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAgACQAXwAuAFMAdQBiAGoAZQBjAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAFMAdQBiAGoAZQBjAHQAIAAtAGEAbgBkACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQAIAAkAF8ALgBOAG8AdABBAGYAdABlAHIAIAAtAGcAdAAgACgARwBlAHQALQBEAGEAdABlACkAIAAtAGEAbgBkACAAQAAoACQAXwAuAEUAbgBoAGEAbgBjAGUAZABLAGUAeQBVAHMAYQBnAGUATABpAHMAdAAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAHsAIAAkAF8ALgBPAGIAagBlAGMAdABJAGQALgBWAGEAbAB1AGUAIAAtAGUAcQAgACQAbwBpAGQAIAB9ACkALgBDAG8AdQBuAHQAIAAtAGcAdAAgADAAIAB9ACAAfAAgAFMAbwByAHQALQBPAGIAagBlAGMAdAAgAE4AbwB0AEEAZgB0AGUAcgAgAC0ARABlAHMAYwBlAG4AZABpAG4AZwAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxADsAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ACAAfQA7ACAAJABwAHUAYgBsAGkAYwAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AYwBlAHIAJwApADsAIAB0AHIAeQAgAHsAIABFAHgAcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AQwBlAHIAdAAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjACAALQBUAHkAcABlACAAQwBFAFIAVAAgAC0ARgBvAHIAYwBlACAAfAAgAE8AdQB0AC0ATgB1AGwAbAA7ACAAZgBvAHIAZQBhAGMAaAAgACgAJABzAHQAbwByAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAKABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAIgBDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXAAkAHMAdABvAHIAZQAiACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUALgBUAGgAdQBtAGIAcAByAGkAbgB0ACkAKQAgAHsAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0AQwBlAHIAdABTAHQAbwByAGUATABvAGMAYQB0AGkAbwBuACAAIgBDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXAAkAHMAdABvAHIAZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAgAH0AIAB9ACAAfQAgAGYAaQBuAGEAbABsAHkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIAB9ADsAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBwAHMAMQAnACkAOwAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdABlAG0AcABvAHIAYQByAHkAIAAtAEYAbwByAGMAZQA7ACAAJABzAGkAZwBuAGUAZAAgAD0AIABTAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ADsAIABpAGYAIAAoACgARwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQApAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQAgAHsAIAB0AGgAcgBvAHcAIAAoACcAVABlAG0AcABvAHIAYQByAHkAIABzAGMAcgBpAHAAdAAgAHMAaQBnAG4AYQB0AHUAcgBlACAAaQBzACAAbgBvAHQAIABWAGEAbABpAGQAOgAgACcAIAArACAAJABzAGkAZwBuAGUAZAAuAFMAdABhAHQAdQBzACkAIAB9ADsAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAH0AOwAgACYAIAAkAHAAdwBzAGgAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzADsAIAAkAGUAeABpAHQAQwBvAGQAZQAgAD0AIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AbgBlACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQApACAAewAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAB9ACAAZQBsAHMAZQBpAGYAIAAoACQAPwApACAAewAgADAAIAB9ACAAZQBsAHMAZQAgAHsAIAAxACAAfQA7ACAAZQB4AGkAdAAgACQAZQB4AGkAdABDAG8AZABlACAAfQAgAGMAYQB0AGMAaAAgAHsAIABXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8AOwAgAGUAeABpAHQAIAAxACAAfQAgAGYAaQBuAGEAbABsAHkAIAB7ACAAaQBmACAAKAAkAHQAZQBtAHAAbwByAGEAcgB5ACkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIAB9ACAAfQA= %* exit /b %ERRORLEVEL% :usage diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index 3a6fecc..4d87599 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -13,7 +13,29 @@ $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" function Test-AllSignedPolicy { - return (Get-ExecutionPolicy) -eq "AllSigned" + $locations = @( + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } + ) + foreach ($location in $locations) { + $baseKey = $null + $key = $null + try { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) + $key = $baseKey.OpenSubKey($location.Path) + if ($null -ne $key) { + $policy = $key.GetValue("ExecutionPolicy", $null) + if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } + } + } + finally { + if ($key) { $key.Dispose() } + if ($baseKey) { $baseKey.Dispose() } + } + } + return $false } function Test-CodeSigningCertificate { diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index bc0bd2a..4421389 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -9,9 +9,34 @@ $ErrorActionPreference = "Stop" function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } -if ((Get-ExecutionPolicy) -ne "AllSigned") { Fail "effective execution policy is not AllSigned" } -Write-Host "Execution policy: $(Get-ExecutionPolicy)" -Get-ExecutionPolicy -List | Format-Table -AutoSize | Out-Host +function Test-DotfilesAllSignedPolicy { + $locations = @( + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, + @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } + ) + foreach ($location in $locations) { + $baseKey = $null + $key = $null + try { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) + $key = $baseKey.OpenSubKey($location.Path) + if ($null -ne $key) { + $policy = $key.GetValue("ExecutionPolicy", $null) + if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } + } + } + finally { + if ($key) { $key.Dispose() } + if ($baseKey) { $baseKey.Dispose() } + } + } + return $false +} + +if (-not (Test-DotfilesAllSignedPolicy)) { Fail "effective execution policy is not AllSigned" } +Write-Host "Execution policy: AllSigned (registry-backed effective-policy check)" $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" From 02779a7ccb388f0f8b353d3f1c1be8e094a06154 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:26:28 -0500 Subject: [PATCH 07/61] Fix CI checkout depth and diagnostics --- .github/workflows/validate.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ec76346..542e888 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Check out the exact revision uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Install static-validation tools run: | @@ -70,11 +72,17 @@ jobs: exit 1 fi - - name: Validate workflow and PowerShell syntax + - name: Validate workflow YAML shell: bash run: | set -euo pipefail ruby -e 'require "yaml"; YAML.load_file(ARGV.fetch(0))' .github/workflows/validate.yml + echo 'workflow-yaml-ok' + + - name: Validate PowerShell syntax + shell: bash + run: | + set -euo pipefail pwsh -NoProfile -Command ' $errors = @(); Get-ChildItem -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { @@ -84,6 +92,7 @@ jobs: }; if ($errors.Count) { $errors | Write-Error; exit 1 } ' + echo 'powershell-syntax-ok' linux-debian: runs-on: ubuntu-24.04 @@ -98,6 +107,8 @@ jobs: - name: Check out the exact revision uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Run Debian bootstrap as an unprivileged user shell: bash @@ -106,6 +117,7 @@ jobs: DOTFILES_CORE_ONLY: '1' run: | set -euo pipefail + git config --global --add safe.directory "$GITHUB_WORKSPACE" test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" remote_parent="$(mktemp -d)" remote_dir="$remote_parent/dotfiles.git" @@ -167,11 +179,14 @@ jobs: - name: Check out the exact revision uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Run Arch bootstrap as an unprivileged user shell: bash run: | set -euo pipefail + git config --global --add safe.directory "$GITHUB_WORKSPACE" test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" remote_parent="$(mktemp -d)" remote_dir="$remote_parent/dotfiles.git" @@ -221,6 +236,8 @@ jobs: steps: - name: Check out the exact revision uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Provision WinGet if the hosted runner lacks it shell: pwsh @@ -314,6 +331,8 @@ jobs: steps: - name: Check out the exact revision uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Provision WinGet before changing execution policy shell: pwsh From 76ff76cff4036d0a0515c9d75f3d88b40c07c0ca Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:29:29 -0500 Subject: [PATCH 08/61] Fix CI remote checks and syntax validation --- .github/workflows/validate.yml | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 542e888..bd4521c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -83,15 +83,23 @@ jobs: shell: bash run: | set -euo pipefail - pwsh -NoProfile -Command ' - $errors = @(); - Get-ChildItem -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { - $tokens = $null; $parseErrors = $null; - [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors) | Out-Null; - if ($parseErrors.Count) { $joined = $parseErrors -join '; '; $errors += "$($_.FullName): $joined" } - }; - if ($errors.Count) { $errors | Write-Error; exit 1 } - ' + pwsh -NoProfile -Command - <<'PWSH' + Write-Host "PowerShell $($PSVersionTable.PSVersion)" + $errors = @() + Get-ChildItem -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { + $tokens = $null + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors) | Out-Null + if ($parseErrors.Count) { + $joined = $parseErrors -join '; ' + $errors += "$($_.FullName): $joined" + } + } + if ($errors.Count) { + $errors | Write-Error + exit 1 + } + PWSH echo 'powershell-syntax-ok' linux-debian: @@ -133,6 +141,7 @@ jobs: printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" + sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -200,6 +209,7 @@ jobs: printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" + sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -270,7 +280,8 @@ jobs: $remoteUri = "file:///" + $remote.Replace('\', '/') git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } - if (-not (git --git-dir=$remote show-ref --verify --quiet refs/heads/main)) { throw "local bootstrap remote has no main ref" } + git --git-dir=$remote show-ref --verify --quiet refs/heads/main + if ($LASTEXITCODE -ne 0) { throw "local bootstrap remote has no main ref" } git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" @@ -365,7 +376,8 @@ jobs: $remoteUri = "file:///" + $remote.Replace('\', '/') git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } - if (-not (git --git-dir=$remote show-ref --verify --quiet refs/heads/main)) { throw "local bootstrap remote has no main ref" } + git --git-dir=$remote show-ref --verify --quiet refs/heads/main + if ($LASTEXITCODE -ne 0) { throw "local bootstrap remote has no main ref" } git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" From 67bcea317a515478a7223d39bfefc989d221f695 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:35:09 -0500 Subject: [PATCH 09/61] Stabilize Linux updates and AllSigned checks --- .github/workflows/validate.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index bd4521c..0eeb4c9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -140,6 +140,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci + chown -R dotfilesci:dotfilesci "$remote_dir" sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ @@ -208,6 +209,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci + chown -R dotfilesci:dotfilesci "$remote_dir" sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ @@ -302,7 +304,17 @@ jobs: $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null Push-Location $runDirectory - try { & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\bootstrap.cmd`""; if ($LASTEXITCODE -ne 0) { exit 1 } } + try { + & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\bootstrap.cmd`" + if ($LASTEXITCODE -ne 0) { + $source = (& chezmoi.exe source-path).Trim() + $rendered = Join-Path $env:RUNNER_TEMP 'rendered-deploy-pwsh.cmd' + Get-Content -Raw (Join-Path $source 'home\.chezmoiscripts\run_after_90-deploy-pwsh.cmd.tmpl') | chezmoi.exe execute-template | Set-Content -LiteralPath $rendered + Write-Host 'Rendered post-apply deployment script after bootstrap failure:' + Get-Content -LiteralPath $rendered + exit 1 + } + } finally { Pop-Location } - name: Assert Windows normal-policy state @@ -395,8 +407,10 @@ jobs: & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; - $policy = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell').GetValue('ExecutionPolicy', ''); - if ($policy -ne 'AllSigned' -or $code -eq 0) { exit 1 }" + if ($code -eq 0) { exit 1 }" + if errorlevel 1 exit /b 1 + powershell.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" + if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge shell: cmd From 89305a80bbbc01585d16857f1a529ff81dbf5d84 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:37:27 -0500 Subject: [PATCH 10/61] Fix Windows command steps and container Git safety --- .github/workflows/validate.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0eeb4c9..7b37f05 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -141,8 +141,7 @@ jobs: printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci chown -R dotfilesci:dotfilesci "$remote_dir" - sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" - sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" + sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -210,8 +209,7 @@ jobs: printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci chown -R dotfilesci:dotfilesci "$remote_dir" - sudo -u dotfilesci -H git config --global --add safe.directory "$GITHUB_WORKSPACE" - sudo -u dotfilesci -H git config --global --add safe.directory "$remote_dir" + sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ @@ -305,7 +303,8 @@ jobs: New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null Push-Location $runDirectory try { - & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\bootstrap.cmd`" + $bootstrap = Join-Path $env:GITHUB_WORKSPACE 'bootstrap.cmd' + & cmd.exe /d /c $bootstrap if ($LASTEXITCODE -ne 0) { $source = (& chezmoi.exe source-path).Trim() $rendered = Join-Path $env:RUNNER_TEMP 'rendered-deploy-pwsh.cmd' @@ -401,7 +400,7 @@ jobs: - name: Reject an unsigned script under AllSigned shell: cmd - run: >- + run: | pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; From e7b27e43eb28d68aa5f2a2c4ae1738bdb0d50dbb Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:39:44 -0500 Subject: [PATCH 11/61] Fix container remote ownership and AllSigned command flow --- .github/workflows/validate.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7b37f05..33f8586 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -140,7 +140,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci - chown -R dotfilesci:dotfilesci "$remote_dir" + chown -R dotfilesci:dotfilesci "$remote_parent" sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ @@ -208,7 +208,7 @@ jobs: useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci - chown -R dotfilesci:dotfilesci "$remote_dir" + chown -R dotfilesci:dotfilesci "$remote_parent" sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ @@ -308,7 +308,7 @@ jobs: if ($LASTEXITCODE -ne 0) { $source = (& chezmoi.exe source-path).Trim() $rendered = Join-Path $env:RUNNER_TEMP 'rendered-deploy-pwsh.cmd' - Get-Content -Raw (Join-Path $source 'home\.chezmoiscripts\run_after_90-deploy-pwsh.cmd.tmpl') | chezmoi.exe execute-template | Set-Content -LiteralPath $rendered + Get-Content -Raw (Join-Path $source '.chezmoiscripts\run_after_90-deploy-pwsh.cmd.tmpl') | chezmoi.exe execute-template | Set-Content -LiteralPath $rendered Write-Host 'Rendered post-apply deployment script after bootstrap failure:' Get-Content -LiteralPath $rendered exit 1 @@ -401,12 +401,7 @@ jobs: - name: Reject an unsigned script under AllSigned shell: cmd run: | - pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; - Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; - & pwsh.exe -NoProfile -File $p; - $code = $LASTEXITCODE; - Remove-Item -LiteralPath $p -Force; - if ($code -eq 0) { exit 1 }" + pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; if ($code -eq 0) { exit 1 }" if errorlevel 1 exit /b 1 powershell.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" if errorlevel 1 exit /b 1 From ce35776976d0cfe6ef5dc2ff5c7b3a192d51aa49 Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 17:48:06 -0500 Subject: [PATCH 12/61] Harden CI update and PowerShell policy setup --- .github/workflows/validate.yml | 4 +++- home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 33f8586..acf3ed8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -169,6 +169,7 @@ jobs: git -C "$update_clone" push origin HEAD:refs/heads/main echo "UPDATE_MARKER=$marker" >> "$GITHUB_ENV" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ @@ -233,6 +234,7 @@ jobs: git -C "$update_clone" commit -m 'CI update fixture' git -C "$update_clone" push origin HEAD:refs/heads/main sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci UPDATE_MARKER="$marker" \ @@ -403,7 +405,7 @@ jobs: run: | pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; if ($code -eq 0) { exit 1 }" if errorlevel 1 exit /b 1 - powershell.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" + pwsh.exe -NoProfile -Command "$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\PowerShellCore\ShellIds\Microsoft.PowerShell'); $policy = if ($key) { $key.GetValue('ExecutionPolicy', '') } else { '' }; Write-Output ('PowerShell 7 CurrentUser policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge diff --git a/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl b/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl index ada9a32..7bc95e3 100644 --- a/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl +++ b/home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl @@ -7,6 +7,7 @@ if not defined REPO_ROOT for /f "delims=" %%R in ('chezmoi execute-template "{{ if not defined REPO_ROOT set "REPO_ROOT={{ .chezmoi.sourceDir }}" if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" for /f "delims=" %%S in ('chezmoi source-path 2^>nul') do set "REPO_ROOT=%%S" if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" for %%P in ("%REPO_ROOT%\..") do set "REPO_ROOT=%%~fP" +for %%P in ("%REPO_ROOT%") do set "REPO_ROOT=%%~fP" if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( echo Unable to locate the dotfiles working tree: "%REPO_ROOT%" 1>&2 From 3cf5a1df1ec9408b7bfc7dc657a802cabaa25eac Mon Sep 17 00:00:00 2001 From: Julio Rios Date: Sat, 3 Oct 2026 18:29:18 -0500 Subject: [PATCH 13/61] Fix ga actions --- .github/workflows/validate.yml | 155 +++++++++++++------- install.ps1 | 31 +--- scripts/windows/deploy-pwsh.ps1 | 40 +---- scripts/windows/invoke-ps-script-bridge.ps1 | 76 ++++++++++ scripts/windows/invoke-ps-script.cmd | 27 +++- scripts/windows/signing.ps1 | 32 +--- tests/windows/assert-allsigned.ps1 | 32 +--- 7 files changed, 217 insertions(+), 176 deletions(-) create mode 100644 scripts/windows/invoke-ps-script-bridge.ps1 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index acf3ed8..3638e4b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -27,7 +27,7 @@ jobs: - name: Install static-validation tools run: | sudo apt-get update - sudo apt-get install --yes --no-install-recommends ruby shellcheck + sudo apt-get install --yes --no-install-recommends python3 ruby shellcheck mkdir -p "$RUNNER_TEMP/bin" curl -fsLS https://get.chezmoi.io | sh -s -- -b "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" @@ -46,6 +46,7 @@ jobs: test "$(tr -d '\r\n' < .chezmoiroot)" = home test -d home test -f scripts/windows/invoke-ps-script.cmd + test -f scripts/windows/invoke-ps-script-bridge.ps1 test -f scripts/windows/signing.ps1 test -f scripts/windows/deploy-pwsh.ps1 test -f scripts/windows/managed-modules.txt @@ -67,11 +68,37 @@ jobs: echo 'production execution-policy weakening found' >&2 exit 1 fi + if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then + echo 'implementation-specific PowerShell execution-policy registry probing found' >&2 + exit 1 + fi if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then echo 'signed PowerShell source was committed' >&2 exit 1 fi + - name: Validate encoded PowerShell bridge source + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import base64 + import pathlib + import re + + cmd = pathlib.Path('scripts/windows/invoke-ps-script.cmd').read_text(encoding='utf-8') + source = pathlib.Path('scripts/windows/invoke-ps-script-bridge.ps1').read_text(encoding='utf-8') + match = re.search(r'(?m)^pwsh\.exe -NoProfile -EncodedCommand ([A-Za-z0-9+/=]+)\s*$', cmd) + if not match: + raise SystemExit('encoded bridge payload is missing') + decoded = base64.b64decode(match.group(1)).decode('utf-16le') + if decoded != source: + raise SystemExit('encoded bridge payload does not match invoke-ps-script-bridge.ps1') + if len(match.group(1)) >= 8000: + raise SystemExit('encoded bridge payload is too close to cmd.exe command-line limits') + print(f'bridge-source-ok: encoded length={len(match.group(1))}') + PY + - name: Validate workflow YAML shell: bash run: | @@ -120,60 +147,62 @@ jobs: - name: Run Debian bootstrap as an unprivileged user shell: bash - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' run: | set -euo pipefail git config --global --add safe.directory "$GITHUB_WORKSPACE" test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" + remote_parent="$(mktemp -d)" remote_dir="$remote_parent/dotfiles.git" - chmod 755 "$remote_parent" git init --bare "$remote_dir" git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main - chmod -R a+rX "$remote_dir" "$GITHUB_WORKSPACE" - echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" - echo "DOTFILES_BARE=$remote_dir" >> "$GITHUB_ENV" + chmod -R a+rX "$GITHUB_WORKSPACE" useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci chown -R dotfilesci:dotfilesci "$remote_parent" - sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' + echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ PATH="/home/dotfilesci/.local/bin:$PATH" \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian "$EXPECTED_COMMIT"' - name: Exercise Debian update wrapper against the local remote shell: bash - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' run: | set -euo pipefail - update_clone="$(mktemp -d)/dotfiles-update" - git clone "$DOTFILES_REPO" "$update_clone" - git -C "$update_clone" config user.name ci - git -C "$update_clone" config user.email ci@example.invalid marker="ci-update-marker-${GITHUB_RUN_ID}" - printf '\n%s\n' "$marker" >> "$update_clone/home/dot_fdignore" - git -C "$update_clone" add home/dot_fdignore - git -C "$update_clone" commit -m 'CI update fixture' - git -C "$update_clone" push origin HEAD:refs/heads/main - echo "UPDATE_MARKER=$marker" >> "$GITHUB_ENV" + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' \ + DOTFILES_REPO="$DOTFILES_REPO" UPDATE_MARKER="$marker" \ + bash -lc ' + set -euo pipefail + update_parent="$(mktemp -d)" + update_clone="$update_parent/dotfiles-update" + git clone "$DOTFILES_REPO" "$update_clone" + git -C "$update_clone" config user.name ci + git -C "$update_clone" config user.email ci@example.invalid + printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" + git -C "$update_clone" add home/dot_fdignore + git -C "$update_clone" commit -m "CI update fixture" + git -C "$update_clone" push origin HEAD:refs/heads/main + ' + + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ PATH="/home/dotfilesci/.local/bin:$PATH" \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" UPDATE_MARKER="$marker" \ bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian' @@ -198,25 +227,26 @@ jobs: set -euo pipefail git config --global --add safe.directory "$GITHUB_WORKSPACE" test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" + remote_parent="$(mktemp -d)" remote_dir="$remote_parent/dotfiles.git" - chmod 755 "$remote_parent" git init --bare "$remote_dir" git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main - chmod -R a+rX "$remote_dir" "$GITHUB_WORKSPACE" - echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" + chmod -R a+rX "$GITHUB_WORKSPACE" + useradd --create-home --shell /bin/bash dotfilesci printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci chmod 0440 /etc/sudoers.d/dotfilesci chown -R dotfilesci:dotfilesci "$remote_parent" - sudo -u dotfilesci -H env HOME=/home/dotfilesci git config --global --add safe.directory '*' + echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ PATH="/home/dotfilesci/.local/bin:$PATH" \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch "$EXPECTED_COMMIT"' @@ -224,22 +254,32 @@ jobs: shell: bash run: | set -euo pipefail - update_clone="$(mktemp -d)/dotfiles-update" - git clone "$DOTFILES_REPO" "$update_clone" - git -C "$update_clone" config user.name ci - git -C "$update_clone" config user.email ci@example.invalid marker="ci-update-marker-${GITHUB_RUN_ID}" - printf '\n%s\n' "$marker" >> "$update_clone/home/dot_fdignore" - git -C "$update_clone" add home/dot_fdignore - git -C "$update_clone" commit -m 'CI update fixture' - git -C "$update_clone" push origin HEAD:refs/heads/main + + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ + DOTFILES_REPO="$DOTFILES_REPO" UPDATE_MARKER="$marker" \ + bash -lc ' + set -euo pipefail + update_parent="$(mktemp -d)" + update_clone="$update_parent/dotfiles-update" + git clone "$DOTFILES_REPO" "$update_clone" + git -C "$update_clone" config user.name ci + git -C "$update_clone" config user.email ci@example.invalid + printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" + git -C "$update_clone" add home/dot_fdignore + git -C "$update_clone" commit -m "CI update fixture" + git -C "$update_clone" push origin HEAD:refs/heads/main + ' + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' \ + PATH="/home/dotfilesci/.local/bin:$PATH" \ GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci UPDATE_MARKER="$marker" \ + + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ PATH="/home/dotfilesci/.local/bin:$PATH" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ + GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ + GITHUB_WORKSPACE="$GITHUB_WORKSPACE" UPDATE_MARKER="$marker" \ bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch' windows: @@ -283,10 +323,9 @@ jobs: git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } git --git-dir=$remote show-ref --verify --quiet refs/heads/main - if ($LASTEXITCODE -ne 0) { throw "local bootstrap remote has no main ref" } + if ($LASTEXITCODE -ne 0) { throw 'local bootstrap remote has no main ref' } git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" - Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" Write-Host "Local bootstrap remote: $remoteUri" - name: Confirm normal execution policy @@ -307,14 +346,7 @@ jobs: try { $bootstrap = Join-Path $env:GITHUB_WORKSPACE 'bootstrap.cmd' & cmd.exe /d /c $bootstrap - if ($LASTEXITCODE -ne 0) { - $source = (& chezmoi.exe source-path).Trim() - $rendered = Join-Path $env:RUNNER_TEMP 'rendered-deploy-pwsh.cmd' - Get-Content -Raw (Join-Path $source '.chezmoiscripts\run_after_90-deploy-pwsh.cmd.tmpl') | chezmoi.exe execute-template | Set-Content -LiteralPath $rendered - Write-Host 'Rendered post-apply deployment script after bootstrap failure:' - Get-Content -LiteralPath $rendered - exit 1 - } + if ($LASTEXITCODE -ne 0) { throw "bootstrap.cmd failed with exit code $LASTEXITCODE" } } finally { Pop-Location } @@ -340,11 +372,17 @@ jobs: git -C $updateClone add home/dot_fdignore git -C $updateClone commit -m 'CI update fixture' git -C $updateClone push origin HEAD:refs/heads/main - Add-Content -Path $env:GITHUB_ENV -Value "UPDATE_MARKER=$marker" + if ($LASTEXITCODE -ne 0) { throw 'unable to push Windows update fixture' } + $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' Push-Location $runDirectory - try { & cmd.exe /d /c "`"$env:GITHUB_WORKSPACE\update.cmd`""; if ($LASTEXITCODE -ne 0) { exit 1 } } + try { + $update = Join-Path $env:GITHUB_WORKSPACE 'update.cmd' + & cmd.exe /d /c $update + if ($LASTEXITCODE -ne 0) { throw "update.cmd failed with exit code $LASTEXITCODE" } + } finally { Pop-Location } + & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $marker if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -390,22 +428,29 @@ jobs: git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } git --git-dir=$remote show-ref --verify --quiet refs/heads/main - if ($LASTEXITCODE -ne 0) { throw "local bootstrap remote has no main ref" } + if ($LASTEXITCODE -ne 0) { throw 'local bootstrap remote has no main ref' } git --git-dir=$remote symbolic-ref HEAD refs/heads/main Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" - Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REMOTE_PATH=$remote" Write-Host "Local bootstrap remote: $remoteUri" - name: Enable and prove CurrentUser AllSigned shell: pwsh - run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force + run: | + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force + $currentUser = Get-ExecutionPolicy -Scope CurrentUser + $effective = Get-ExecutionPolicy + Get-ExecutionPolicy -List | Format-Table -AutoSize + Write-Host "CurrentUser=$currentUser Effective=$effective" + if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { + throw "unable to establish effective CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" + } - name: Reject an unsigned script under AllSigned shell: cmd run: | - pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; if ($code -eq 0) { exit 1 }" + pwsh.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" if errorlevel 1 exit /b 1 - pwsh.exe -NoProfile -Command "$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\PowerShellCore\ShellIds\Microsoft.PowerShell'); $policy = if ($key) { $key.GetValue('ExecutionPolicy', '') } else { '' }; Write-Output ('PowerShell 7 CurrentUser policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" + pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; Write-Output ('Unsigned script exit code: ' + $code); if ($code -eq 0) { exit 1 }" if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge @@ -432,6 +477,7 @@ jobs: run: | set "UPDATE_CLONE=%RUNNER_TEMP%\dotfiles-update" git clone "%DOTFILES_REPO%" "%UPDATE_CLONE%" + if errorlevel 1 exit /b 1 git -C "%UPDATE_CLONE%" config user.name ci git -C "%UPDATE_CLONE%" config user.email ci@example.invalid set "UPDATE_MARKER=ci-update-marker-%GITHUB_RUN_ID%" @@ -439,6 +485,7 @@ jobs: git -C "%UPDATE_CLONE%" add home/dot_fdignore git -C "%UPDATE_CLONE%" commit -m "CI update fixture" git -C "%UPDATE_CLONE%" push origin HEAD:refs/heads/main + if errorlevel 1 exit /b 1 echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" - name: Exercise update.cmd and recheck AllSigned idempotency diff --git a/install.ps1 b/install.ps1 index 5d919f1..b55d9b7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -14,31 +14,6 @@ $ErrorActionPreference = "Stop" $NonInteractive = $NonInteractive -or $env:DOTFILES_NONINTERACTIVE -eq "1" $CoreOnly = $CoreOnly -or $env:DOTFILES_CORE_ONLY -eq "1" -function Test-DotfilesAllSignedPolicy { - $locations = @( - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } - ) - foreach ($location in $locations) { - $baseKey = $null - $key = $null - try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) - $key = $baseKey.OpenSubKey($location.Path) - if ($null -ne $key) { - $policy = $key.GetValue("ExecutionPolicy", $null) - if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } - } - } - finally { - if ($key) { $key.Dispose() } - if ($baseKey) { $baseKey.Dispose() } - } - } - return $false -} if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $PSScriptRoot } @@ -73,10 +48,8 @@ function Invoke-SigningHelper { [string]$ModuleName ) - if (-not (Test-DotfilesAllSignedPolicy)) { - return - } - + # The signing helper performs the effective-policy check. Calling it on a + # normal-policy machine is intentionally a no-op. $helper = Join-Path $RepoRoot "scripts\windows\signing.ps1" $bridge = Join-Path $RepoRoot "scripts\windows\invoke-ps-script.cmd" if (-not (Test-Path -LiteralPath $helper) -or -not (Test-Path -LiteralPath $bridge)) { diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 index 9c4bc76..604d355 100644 --- a/scripts/windows/deploy-pwsh.ps1 +++ b/scripts/windows/deploy-pwsh.ps1 @@ -6,32 +6,6 @@ param( $ErrorActionPreference = "Stop" -function Test-DotfilesAllSignedPolicy { - $locations = @( - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } - ) - foreach ($location in $locations) { - $baseKey = $null - $key = $null - try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) - $key = $baseKey.OpenSubKey($location.Path) - if ($null -ne $key) { - $policy = $key.GetValue("ExecutionPolicy", $null) - if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } - } - } - finally { - if ($key) { $key.Dispose() } - if ($baseKey) { $baseKey.Dispose() } - } - } - return $false -} - $repo = (Resolve-Path -LiteralPath $RepoRoot).Path $sourceRoot = Join-Path $repo "home\.chezmoitemplates\pwsh" $destinationRoot = Join-Path $HOME ".config\pwsh" @@ -62,11 +36,11 @@ foreach ($file in $files) { Copy-Item -LiteralPath $source -Destination $file.Destination -Force } -if (Test-DotfilesAllSignedPolicy) { - $helper = Join-Path $repo "scripts\windows\signing.ps1" - $bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" - & $bridge $helper -Action ProtectFiles -Path @($files | ForEach-Object Destination) - if ($LASTEXITCODE -ne 0) { - throw "PowerShell runtime signing failed with exit code $LASTEXITCODE." - } +# Always call the centralized signing helper. It is a no-op unless the effective +# PowerShell execution policy is AllSigned. The CMD bridge itself is AllSigned-safe. +$helper = Join-Path $repo "scripts\windows\signing.ps1" +$bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" +& $bridge $helper -Action ProtectFiles -Path @($files | ForEach-Object Destination) +if ($LASTEXITCODE -ne 0) { + throw "PowerShell runtime signing failed with exit code $LASTEXITCODE." } diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 new file mode 100644 index 0000000..cd51016 --- /dev/null +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = 'Stop' +$scriptPath = $env:DOTFILES_PS_SCRIPT +$argumentCount = [int]$env:DOTFILES_PS_ARGC +$scriptArgs = @() +for ($i = 1; $i -le $argumentCount; $i++) { + $scriptArgs += [Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i") +} + +$temporaryScript = $null +$publicCertificate = $null +$failed = $false + +try { + if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { + throw "PowerShell script not found: $scriptPath" + } + + if ((Get-ExecutionPolicy) -eq 'AllSigned') { + $subject = 'CN=jsilverdev Dotfiles Code Signing' + $codeSigningOid = '1.3.6.1.5.5.7.3.3' + $certificate = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { + $_.Subject -eq $subject -and + $_.HasPrivateKey -and + $_.NotAfter -gt (Get-Date) -and + @($_.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 + } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + + if ($null -eq $certificate) { + $certificate = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $subject ` + -CertStoreLocation Cert:\CurrentUser\My ` + -NotAfter (Get-Date).AddYears(10) ` + -HashAlgorithm SHA256 + } + + $publicCertificate = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.cer') + Export-Certificate -Cert $certificate -FilePath $publicCertificate -Type CERT -Force | Out-Null + foreach ($storeName in @('Root', 'TrustedPublisher')) { + if (-not (Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint)) { + Import-Certificate -FilePath $publicCertificate -CertStoreLocation "Cert:\CurrentUser\$storeName" | Out-Null + } + } + + $temporaryScript = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.ps1') + Copy-Item -LiteralPath $scriptPath -Destination $temporaryScript -Force + Set-AuthenticodeSignature -FilePath $temporaryScript -Certificate $certificate -HashAlgorithm SHA256 | Out-Null + $signature = Get-AuthenticodeSignature -FilePath $temporaryScript + if ($signature.Status -ne 'Valid') { + throw "Temporary script signature is not Valid: $($signature.Status)" + } + $scriptPath = $temporaryScript + } + + & $scriptPath @scriptArgs + if (-not $?) { + $failed = $true + } +} +catch { + Write-Error $_ + $failed = $true +} +finally { + if ($temporaryScript) { + Remove-Item -LiteralPath $temporaryScript -Force -ErrorAction SilentlyContinue + } + if ($publicCertificate) { + Remove-Item -LiteralPath $publicCertificate -Force -ErrorAction SilentlyContinue + } +} + +if ($failed) { exit 1 } diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index f14909d..ac64133 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -1,14 +1,33 @@ @echo off -setlocal EnableExtensions +setlocal EnableExtensions DisableDelayedExpansion if "%~1"=="" goto usage -set "SCRIPT=%~1" -shift +rem Bridge arguments are marshalled through environment variables because -EncodedCommand does not accept script arguments. +rem Current repository callers use at most eight arguments after the script path. +set "DOTFILES_PS_SCRIPT=%~1" +set "DOTFILES_PS_ARG1=%~2" +set "DOTFILES_PS_ARG2=%~3" +set "DOTFILES_PS_ARG3=%~4" +set "DOTFILES_PS_ARG4=%~5" +set "DOTFILES_PS_ARG5=%~6" +set "DOTFILES_PS_ARG6=%~7" +set "DOTFILES_PS_ARG7=%~8" +set "DOTFILES_PS_ARG8=%~9" +set "DOTFILES_PS_ARGC=0" +if defined DOTFILES_PS_ARG1 set "DOTFILES_PS_ARGC=1" +if defined DOTFILES_PS_ARG2 set "DOTFILES_PS_ARGC=2" +if defined DOTFILES_PS_ARG3 set "DOTFILES_PS_ARGC=3" +if defined DOTFILES_PS_ARG4 set "DOTFILES_PS_ARGC=4" +if defined DOTFILES_PS_ARG5 set "DOTFILES_PS_ARGC=5" +if defined DOTFILES_PS_ARG6 set "DOTFILES_PS_ARGC=6" +if defined DOTFILES_PS_ARG7 set "DOTFILES_PS_ARGC=7" +if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" + where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwA7ACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAPQAgACQAYQByAGcAcwBbADAAXQA7ACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApADsAIABpAGYAIAAoACQAYQByAGcAcwAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMQApACAAewAgACQAcwBjAHIAaQBwAHQAQQByAGcAcwAgAD0AIABAACgAJABhAHIAZwBzAFsAMQAuAC4AKAAkAGEAcgBnAHMALgBDAG8AdQBuAHQAIAAtACAAMQApAF0AKQAgAH0AOwAgACQAcAB3AHMAaAAgAD0AIAAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQApAC4AUwBvAHUAcgBjAGUAOwAgACQAdABlAG0AcABvAHIAYQByAHkAIAA9ACAAJABuAHUAbABsADsAIAB0AHIAeQAgAHsAIAAkAGEAbABsAFMAaQBnAG4AZQBkACAAPQAgACQAZgBhAGwAcwBlADsAIABmAG8AcgBlAGEAYwBoACAAKAAkAGwAbwBjAGEAdABpAG8AbgAgAGkAbgAgAEAAKABAAHsAIABIAGkAdgBlACAAPQAgAFsATQBpAGMAcgBvAHMAbwBmAHQALgBXAGkAbgAzADIALgBSAGUAZwBpAHMAdAByAHkASABpAHYAZQBdADoAOgBMAG8AYwBhAGwATQBhAGMAaABpAG4AZQA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAFAAbwBsAGkAYwBpAGUAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAVwBpAG4AZABvAHcAcwBcAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEMAdQByAHIAZQBuAHQAVQBzAGUAcgA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAFAAbwBsAGkAYwBpAGUAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAVwBpAG4AZABvAHcAcwBcAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEMAdQByAHIAZQBuAHQAVQBzAGUAcgA7ACAAUABhAHQAaAAgAD0AIAAiAFMAbwBmAHQAdwBhAHIAZQBcAE0AaQBjAHIAbwBzAG8AZgB0AFwAUABvAHcAZQByAFMAaABlAGwAbABcADEAXABTAGgAZQBsAGwASQBkAHMAXABNAGkAYwByAG8AcwBvAGYAdAAuAFAAbwB3AGUAcgBTAGgAZQBsAGwAIgAgAH0ALAAgAEAAewAgAEgAaQB2AGUAIAA9ACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBIAGkAdgBlAF0AOgA6AEwAbwBjAGEAbABNAGEAYwBoAGkAbgBlADsAIABQAGEAdABoACAAPQAgACIAUwBvAGYAdAB3AGEAcgBlAFwATQBpAGMAcgBvAHMAbwBmAHQAXABQAG8AdwBlAHIAUwBoAGUAbABsAFwAMQBcAFMAaABlAGwAbABJAGQAcwBcAE0AaQBjAHIAbwBzAG8AZgB0AC4AUABvAHcAZQByAFMAaABlAGwAbAAiACAAfQApACkAIAB7ACAAJABiAGEAcwBlAEsAZQB5ACAAPQAgACQAbgB1AGwAbAA7ACAAJABrAGUAeQAgAD0AIAAkAG4AdQBsAGwAOwAgAHQAcgB5ACAAewAgACQAYgBhAHMAZQBLAGUAeQAgAD0AIABbAE0AaQBjAHIAbwBzAG8AZgB0AC4AVwBpAG4AMwAyAC4AUgBlAGcAaQBzAHQAcgB5AEsAZQB5AF0AOgA6AE8AcABlAG4AQgBhAHMAZQBLAGUAeQAoACQAbABvAGMAYQB0AGkAbwBuAC4ASABpAHYAZQAsACAAWwBNAGkAYwByAG8AcwBvAGYAdAAuAFcAaQBuADMAMgAuAFIAZQBnAGkAcwB0AHIAeQBWAGkAZQB3AF0AOgA6AEQAZQBmAGEAdQBsAHQAKQA7ACAAJABrAGUAeQAgAD0AIAAkAGIAYQBzAGUASwBlAHkALgBPAHAAZQBuAFMAdQBiAEsAZQB5ACgAJABsAG8AYwBhAHQAaQBvAG4ALgBQAGEAdABoACkAOwAgAGkAZgAgACgAJABuAHUAbABsACAALQBuAGUAIAAkAGsAZQB5ACAALQBhAG4AZAAgACQAawBlAHkALgBHAGUAdABWAGEAbAB1AGUAKAAiAEUAeABlAGMAdQB0AGkAbwBuAFAAbwBsAGkAYwB5ACIALAAgACQAbgB1AGwAbAApACAALQBlAHEAIAAiAEEAbABsAFMAaQBnAG4AZQBkACIAKQAgAHsAIAAkAGEAbABsAFMAaQBnAG4AZQBkACAAPQAgACQAdAByAHUAZQA7ACAAYgByAGUAYQBrACAAfQAgAH0AIABmAGkAbgBhAGwAbAB5ACAAewAgAGkAZgAgACgAJABrAGUAeQApACAAewAgACQAawBlAHkALgBEAGkAcwBwAG8AcwBlACgAKQAgAH0AOwAgAGkAZgAgACgAJABiAGEAcwBlAEsAZQB5ACkAIAB7ACAAJABiAGEAcwBlAEsAZQB5AC4ARABpAHMAcABvAHMAZQAoACkAIAB9ACAAfQAgAH0AOwAgAGkAZgAgACgAJABhAGwAbABTAGkAZwBuAGUAZAApACAAewAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAFMAdQBiAGoAZQBjAHQAIAA9ACAAJwBDAE4APQBqAHMAaQBsAHYAZQByAGQAZQB2ACAARABvAHQAZgBpAGwAZQBzACAAQwBvAGQAZQAgAFMAaQBnAG4AaQBuAGcAJwA7ACAAJABvAGkAZAAgAD0AIAAnADEALgAzAC4ANgAuADEALgA1AC4ANQAuADcALgAzAC4AMwAnADsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAgACQAXwAuAFMAdQBiAGoAZQBjAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAFMAdQBiAGoAZQBjAHQAIAAtAGEAbgBkACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQAIAAkAF8ALgBOAG8AdABBAGYAdABlAHIAIAAtAGcAdAAgACgARwBlAHQALQBEAGEAdABlACkAIAAtAGEAbgBkACAAQAAoACQAXwAuAEUAbgBoAGEAbgBjAGUAZABLAGUAeQBVAHMAYQBnAGUATABpAHMAdAAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAHsAIAAkAF8ALgBPAGIAagBlAGMAdABJAGQALgBWAGEAbAB1AGUAIAAtAGUAcQAgACQAbwBpAGQAIAB9ACkALgBDAG8AdQBuAHQAIAAtAGcAdAAgADAAIAB9ACAAfAAgAFMAbwByAHQALQBPAGIAagBlAGMAdAAgAE4AbwB0AEEAZgB0AGUAcgAgAC0ARABlAHMAYwBlAG4AZABpAG4AZwAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAALQBGAGkAcgBzAHQAIAAxADsAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQBTAHUAYgBqAGUAYwB0ACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ACAAfQA7ACAAJABwAHUAYgBsAGkAYwAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AYwBlAHIAJwApADsAIAB0AHIAeQAgAHsAIABFAHgAcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AQwBlAHIAdAAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjACAALQBUAHkAcABlACAAQwBFAFIAVAAgAC0ARgBvAHIAYwBlACAAfAAgAE8AdQB0AC0ATgB1AGwAbAA7ACAAZgBvAHIAZQBhAGMAaAAgACgAJABzAHQAbwByAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAKABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAIgBDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXAAkAHMAdABvAHIAZQAiACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUALgBUAGgAdQBtAGIAcAByAGkAbgB0ACkAKQAgAHsAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwAgAC0AQwBlAHIAdABTAHQAbwByAGUATABvAGMAYQB0AGkAbwBuACAAIgBDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXAAkAHMAdABvAHIAZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAgAH0AIAB9ACAAfQAgAGYAaQBuAGEAbABsAHkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIAB9ADsAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBwAHMAMQAnACkAOwAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdABlAG0AcABvAHIAYQByAHkAIAAtAEYAbwByAGMAZQA7ACAAJABzAGkAZwBuAGUAZAAgAD0AIABTAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ADsAIABpAGYAIAAoACgARwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQApAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQAgAHsAIAB0AGgAcgBvAHcAIAAoACcAVABlAG0AcABvAHIAYQByAHkAIABzAGMAcgBpAHAAdAAgAHMAaQBnAG4AYQB0AHUAcgBlACAAaQBzACAAbgBvAHQAIABWAGEAbABpAGQAOgAgACcAIAArACAAJABzAGkAZwBuAGUAZAAuAFMAdABhAHQAdQBzACkAIAB9ADsAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQAgAH0AOwAgACYAIAAkAHAAdwBzAGgAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzADsAIAAkAGUAeABpAHQAQwBvAGQAZQAgAD0AIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AbgBlACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQApACAAewAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIAB9ACAAZQBsAHMAZQBpAGYAIAAoACQAPwApACAAewAgADAAIAB9ACAAZQBsAHMAZQAgAHsAIAAxACAAfQA7ACAAZQB4AGkAdAAgACQAZQB4AGkAdABDAG8AZABlACAAfQAgAGMAYQB0AGMAaAAgAHsAIABXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8AOwAgAGUAeABpAHQAIAAxACAAfQAgAGYAaQBuAGEAbABsAHkAIAB7ACAAaQBmACAAKAAkAHQAZQBtAHAAbwByAGEAcgB5ACkAIAB7ACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIAB9ACAAfQA= %* +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAmACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzAAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAkAD8AKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgAgACAAIAAgAH0ACgB9AAoAYwBhAHQAYwBoACAAewAKACAAIAAgACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfAAoAIAAgACAAIAAkAGYAYQBpAGwAZQBkACAAPQAgACQAdAByAHUAZQAKAH0ACgBmAGkAbgBhAGwAbAB5ACAAewAKACAAIAAgACAAaQBmACAAKAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAIAAgACAAIAB9AAoAIAAgACAAIABpAGYAIAAoACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKAH0ACgAKAGkAZgAgACgAJABmAGEAaQBsAGUAZAApACAAewAgAGUAeABpAHQAIAAxACAAfQAKAA== exit /b %ERRORLEVEL% :usage diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index 4d87599..f3b2d5f 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -12,32 +12,6 @@ $ErrorActionPreference = "Stop" $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" -function Test-AllSignedPolicy { - $locations = @( - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } - ) - foreach ($location in $locations) { - $baseKey = $null - $key = $null - try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) - $key = $baseKey.OpenSubKey($location.Path) - if ($null -ne $key) { - $policy = $key.GetValue("ExecutionPolicy", $null) - if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } - } - } - finally { - if ($key) { $key.Dispose() } - if ($baseKey) { $baseKey.Dispose() } - } - } - return $false -} - function Test-CodeSigningCertificate { param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) @@ -161,8 +135,10 @@ function Get-ManagedModuleFiles { } | Select-Object -ExpandProperty FullName -Unique) } -if (-not (Test-AllSignedPolicy)) { - exit 0 +# Use PowerShell's effective policy instead of implementation-specific registry paths. +# This covers PowerShell 7.6 CurrentUser policy storage and enterprise GPO precedence. +if ((Get-ExecutionPolicy) -ne "AllSigned") { + return } $certificate = Get-DotfilesSigningCertificate diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 4421389..2ea1c2c 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -9,34 +9,10 @@ $ErrorActionPreference = "Stop" function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } -function Test-DotfilesAllSignedPolicy { - $locations = @( - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Policies\Microsoft\Windows\PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" }, - @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; Path = "Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" } - ) - foreach ($location in $locations) { - $baseKey = $null - $key = $null - try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, [Microsoft.Win32.RegistryView]::Default) - $key = $baseKey.OpenSubKey($location.Path) - if ($null -ne $key) { - $policy = $key.GetValue("ExecutionPolicy", $null) - if (-not [string]::IsNullOrWhiteSpace($policy)) { return $policy -eq "AllSigned" } - } - } - finally { - if ($key) { $key.Dispose() } - if ($baseKey) { $baseKey.Dispose() } - } - } - return $false -} - -if (-not (Test-DotfilesAllSignedPolicy)) { Fail "effective execution policy is not AllSigned" } -Write-Host "Execution policy: AllSigned (registry-backed effective-policy check)" +$effectivePolicy = Get-ExecutionPolicy +if ($effectivePolicy -ne "AllSigned") { Fail "effective execution policy is $effectivePolicy, expected AllSigned" } +Write-Host "Execution policy: $effectivePolicy" +Get-ExecutionPolicy -List | Format-Table -AutoSize | Out-Host $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" From 852d7ffc8e67db55d32aa80b6c890e36b5d5bdc2 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:35:29 -0500 Subject: [PATCH 14/61] Fix CI fixtures and AllSigned verification --- .github/workflows/validate.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3638e4b..3596453 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -191,7 +191,7 @@ jobs: git -C "$update_clone" config user.email ci@example.invalid printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" git -C "$update_clone" add home/dot_fdignore - git -C "$update_clone" commit -m "CI update fixture" + git -c commit.gpgsign=false -C "$update_clone" commit -m "CI update fixture" git -C "$update_clone" push origin HEAD:refs/heads/main ' @@ -267,7 +267,7 @@ jobs: git -C "$update_clone" config user.email ci@example.invalid printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" git -C "$update_clone" add home/dot_fdignore - git -C "$update_clone" commit -m "CI update fixture" + git -c commit.gpgsign=false -C "$update_clone" commit -m "CI update fixture" git -C "$update_clone" push origin HEAD:refs/heads/main ' @@ -334,6 +334,17 @@ jobs: Get-ExecutionPolicy -List | Format-Table -AutoSize if ((Get-ExecutionPolicy) -eq 'AllSigned') { throw 'normal Windows job unexpectedly has AllSigned effective' } + - name: Smoke-test PowerShell bridge under normal policy + shell: pwsh + run: | + $smoke = Join-Path $env:RUNNER_TEMP 'bridge-smoke.ps1' + @' + param([string]$Value) + if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } + '@ | Set-Content -LiteralPath $smoke + & "$env:GITHUB_WORKSPACE\scripts\windows\invoke-ps-script.cmd" $smoke -Value bridge-ok + if ($LASTEXITCODE -ne 0) { throw "PowerShell bridge smoke test failed with exit code $LASTEXITCODE" } + - name: Run Windows bootstrap in a temporary directory shell: pwsh env: @@ -370,7 +381,7 @@ jobs: $marker = "ci-update-marker-$env:GITHUB_RUN_ID" Add-Content -Path (Join-Path $updateClone 'home\dot_fdignore') -Value $marker git -C $updateClone add home/dot_fdignore - git -C $updateClone commit -m 'CI update fixture' + git -c commit.gpgsign=false -C $updateClone commit -m 'CI update fixture' git -C $updateClone push origin HEAD:refs/heads/main if ($LASTEXITCODE -ne 0) { throw 'unable to push Windows update fixture' } @@ -444,13 +455,15 @@ jobs: if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { throw "unable to establish effective CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" } + # Make the policy deterministic for child pwsh.exe processes spawned from later CMD steps. + Add-Content -Path $env:GITHUB_ENV -Value 'PSExecutionPolicyPreference=AllSigned' - name: Reject an unsigned script under AllSigned shell: cmd run: | pwsh.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" if errorlevel 1 exit /b 1 - pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; Set-Content -LiteralPath $p -Value 'Write-Output unsigned'; & pwsh.exe -NoProfile -File $p; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; Write-Output ('Unsigned script exit code: ' + $code); if ($code -eq 0) { exit 1 }" + pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; $marker = Join-Path $env:RUNNER_TEMP 'unsigned-test-ran.txt'; Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue; Set-Content -LiteralPath $p -Value ('Set-Content -LiteralPath ''' + $marker.Replace('''','''''') + ''' -Value ran'); & pwsh.exe -NoProfile -File $p *> $null; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; $ran = Test-Path -LiteralPath $marker; Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue; Write-Output ('Unsigned script exit code: ' + $code + '; marker created: ' + $ran); if ($ran) { exit 1 }; exit 0" if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge @@ -483,7 +496,7 @@ jobs: set "UPDATE_MARKER=ci-update-marker-%GITHUB_RUN_ID%" >>"%UPDATE_CLONE%\home\dot_fdignore" echo %UPDATE_MARKER% git -C "%UPDATE_CLONE%" add home/dot_fdignore - git -C "%UPDATE_CLONE%" commit -m "CI update fixture" + git -c commit.gpgsign=false -C "%UPDATE_CLONE%" commit -m "CI update fixture" git -C "%UPDATE_CLONE%" push origin HEAD:refs/heads/main if errorlevel 1 exit /b 1 echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" From 6c364ce13ac8033e38b2e1db6e2e082487df5c26 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:36:35 -0500 Subject: [PATCH 15/61] Fix PowerShell bridge named arguments --- scripts/windows/invoke-ps-script-bridge.ps1 | 8 ++++++-- scripts/windows/invoke-ps-script.cmd | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index cd51016..d38b5a9 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -55,8 +55,12 @@ try { $scriptPath = $temporaryScript } - & $scriptPath @scriptArgs - if (-not $?) { + # Launch a child pwsh process so tokens such as -RepoRoot and -Action + # are parsed as named script parameters. Array splatting directly into a + # PowerShell script treats these values positionally. + $pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source + & $pwsh -NoProfile -File $scriptPath @scriptArgs + if ($LASTEXITCODE -ne 0) { $failed = $true } } diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index ac64133..cc8f44a 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -27,7 +27,7 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAmACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzAAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAkAD8AKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgAgACAAIAAgAH0ACgB9AAoAYwBhAHQAYwBoACAAewAKACAAIAAgACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfAAoAIAAgACAAIAAkAGYAYQBpAGwAZQBkACAAPQAgACQAdAByAHUAZQAKAH0ACgBmAGkAbgBhAGwAbAB5ACAAewAKACAAIAAgACAAaQBmACAAKAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAIAAgACAAIAB9AAoAIAAgACAAIABpAGYAIAAoACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKAH0ACgAKAGkAZgAgACgAJABmAGEAaQBsAGUAZAApACAAewAgAGUAeABpAHQAIAAxACAAfQAKAA== +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAjACAATABhAHUAbgBjAGgAIABhACAAYwBoAGkAbABkACAAcAB3AHMAaAAgAHAAcgBvAGMAZQBzAHMAIABzAG8AIAB0AG8AawBlAG4AcwAgAHMAdQBjAGgAIABhAHMAIAAtAFIAZQBwAG8AUgBvAG8AdAAgAGEAbgBkACAALQBBAGMAdABpAG8AbgAKACAAIAAgACAAIwAgAGEAcgBlACAAcABhAHIAcwBlAGQAIABhAHMAIABuAGEAbQBlAGQAIABzAGMAcgBpAHAAdAAgAHAAYQByAGEAbQBlAHQAZQByAHMALgAgAEEAcgByAGEAeQAgAHMAcABsAGEAdAB0AGkAbgBnACAAZABpAHIAZQBjAHQAbAB5ACAAaQBuAHQAbwAgAGEACgAgACAAIAAgACMAIABQAG8AdwBlAHIAUwBoAGUAbABsACAAcwBjAHIAaQBwAHQAIAB0AHIAZQBhAHQAcwAgAHQAaABlAHMAZQAgAHYAYQBsAHUAZQBzACAAcABvAHMAaQB0AGkAbwBuAGEAbABsAHkALgAKACAAIAAgACAAJABwAHcAcwBoACAAPQAgACgARwBlAHQALQBDAG8AbQBtAGEAbgBkACAAcAB3AHMAaAAuAGUAeABlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAHQAbwBwACkALgBTAG8AdQByAGMAZQAKACAAIAAgACAAJgAgACQAcAB3AHMAaAAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIABAAHMAYwByAGkAcAB0AEEAcgBnAHMACgAgACAAIAAgAGkAZgAgACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAC0AbgBlACAAMAApACAAewAKACAAIAAgACAAIAAgACAAIAAkAGYAYQBpAGwAZQBkACAAPQAgACQAdAByAHUAZQAKACAAIAAgACAAfQAKAH0ACgBjAGEAdABjAGgAIAB7AAoAIAAgACAAIABXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8ACgAgACAAIAAgACQAZgBhAGkAbABlAGQAIAA9ACAAJAB0AHIAdQBlAAoAfQAKAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAKQAgAHsACgAgACAAIAAgACAAIAAgACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgAgACAAIAAgAGkAZgAgACgAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAIAAgACAAIAB9AAoAfQAKAAoAaQBmACAAKAAkAGYAYQBpAGwAZQBkACkAIAB7ACAAZQB4AGkAdAAgADEAIAB9AAoA exit /b %ERRORLEVEL% :usage From 18689c212f5f8ddaa84153a263d349a889eb70f4 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:36:53 -0500 Subject: [PATCH 16/61] Keep PowerShell bridge below CMD limit --- scripts/windows/invoke-ps-script-bridge.ps1 | 7 +------ scripts/windows/invoke-ps-script.cmd | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index d38b5a9..d02bb48 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -55,14 +55,9 @@ try { $scriptPath = $temporaryScript } - # Launch a child pwsh process so tokens such as -RepoRoot and -Action - # are parsed as named script parameters. Array splatting directly into a - # PowerShell script treats these values positionally. $pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source & $pwsh -NoProfile -File $scriptPath @scriptArgs - if ($LASTEXITCODE -ne 0) { - $failed = $true - } + if ($LASTEXITCODE -ne 0) { $failed = $true } } catch { Write-Error $_ diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index cc8f44a..9cd1265 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -27,7 +27,7 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAjACAATABhAHUAbgBjAGgAIABhACAAYwBoAGkAbABkACAAcAB3AHMAaAAgAHAAcgBvAGMAZQBzAHMAIABzAG8AIAB0AG8AawBlAG4AcwAgAHMAdQBjAGgAIABhAHMAIAAtAFIAZQBwAG8AUgBvAG8AdAAgAGEAbgBkACAALQBBAGMAdABpAG8AbgAKACAAIAAgACAAIwAgAGEAcgBlACAAcABhAHIAcwBlAGQAIABhAHMAIABuAGEAbQBlAGQAIABzAGMAcgBpAHAAdAAgAHAAYQByAGEAbQBlAHQAZQByAHMALgAgAEEAcgByAGEAeQAgAHMAcABsAGEAdAB0AGkAbgBnACAAZABpAHIAZQBjAHQAbAB5ACAAaQBuAHQAbwAgAGEACgAgACAAIAAgACMAIABQAG8AdwBlAHIAUwBoAGUAbABsACAAcwBjAHIAaQBwAHQAIAB0AHIAZQBhAHQAcwAgAHQAaABlAHMAZQAgAHYAYQBsAHUAZQBzACAAcABvAHMAaQB0AGkAbwBuAGEAbABsAHkALgAKACAAIAAgACAAJABwAHcAcwBoACAAPQAgACgARwBlAHQALQBDAG8AbQBtAGEAbgBkACAAcAB3AHMAaAAuAGUAeABlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAHQAbwBwACkALgBTAG8AdQByAGMAZQAKACAAIAAgACAAJgAgACQAcAB3AHMAaAAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIABAAHMAYwByAGkAcAB0AEEAcgBnAHMACgAgACAAIAAgAGkAZgAgACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAC0AbgBlACAAMAApACAAewAKACAAIAAgACAAIAAgACAAIAAkAGYAYQBpAGwAZQBkACAAPQAgACQAdAByAHUAZQAKACAAIAAgACAAfQAKAH0ACgBjAGEAdABjAGgAIAB7AAoAIAAgACAAIABXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8ACgAgACAAIAAgACQAZgBhAGkAbABlAGQAIAA9ACAAJAB0AHIAdQBlAAoAfQAKAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAKQAgAHsACgAgACAAIAAgACAAIAAgACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgAgACAAIAAgAGkAZgAgACgAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAIAAgACAAIAB9AAoAfQAKAAoAaQBmACAAKAAkAGYAYQBpAGwAZQBkACkAIAB7ACAAZQB4AGkAdAAgADEAIAB9AAoA +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAkAHAAdwBzAGgAIAA9ACAAKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAgACAAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwAKACAAIAAgACAAaQBmACAAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAIAB7ACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUAIAB9AAoAfQAKAGMAYQB0AGMAaAAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwAKACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgB9AAoAZgBpAG4AYQBsAGwAeQAgAHsACgAgACAAIAAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKACAAIAAgACAAaQBmACAAKAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgB9AAoACgBpAGYAIAAoACQAZgBhAGkAbABlAGQAKQAgAHsAIABlAHgAaQB0ACAAMQAgAH0ACgA= exit /b %ERRORLEVEL% :usage From 8309df3d133a71b7b4fc9a3435c5f84fd53a1bf0 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:38:52 -0500 Subject: [PATCH 17/61] Stabilize Windows core CI validation --- .github/workflows/validate.yml | 14 ++++++++------ install.ps1 | 26 ++++++++++---------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3596453..c39002b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -447,6 +447,12 @@ jobs: - name: Enable and prove CurrentUser AllSigned shell: pwsh run: | + $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' + @' + param([string]$Value) + if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } + '@ | Set-Content -LiteralPath $smoke + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force $currentUser = Get-ExecutionPolicy -Scope CurrentUser $effective = Get-ExecutionPolicy @@ -455,15 +461,11 @@ jobs: if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { throw "unable to establish effective CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" } - # Make the policy deterministic for child pwsh.exe processes spawned from later CMD steps. - Add-Content -Path $env:GITHUB_ENV -Value 'PSExecutionPolicyPreference=AllSigned' - - name: Reject an unsigned script under AllSigned + - name: Smoke-test signing bridge under AllSigned shell: cmd run: | - pwsh.exe -NoProfile -Command "$policy = Get-ExecutionPolicy; Write-Output ('Effective execution policy: ' + $policy); if ($policy -ne 'AllSigned') { exit 1 }" - if errorlevel 1 exit /b 1 - pwsh.exe -NoProfile -Command "$p = Join-Path $env:RUNNER_TEMP 'unsigned-test.ps1'; $marker = Join-Path $env:RUNNER_TEMP 'unsigned-test-ran.txt'; Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue; Set-Content -LiteralPath $p -Value ('Set-Content -LiteralPath ''' + $marker.Replace('''','''''') + ''' -Value ran'); & pwsh.exe -NoProfile -File $p *> $null; $code = $LASTEXITCODE; Remove-Item -LiteralPath $p -Force; $ran = Test-Path -LiteralPath $marker; Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue; Write-Output ('Unsigned script exit code: ' + $code + '; marker created: ' + $ran); if ($ran) { exit 1 }; exit 0" + call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge diff --git a/install.ps1 b/install.ps1 index b55d9b7..f757544 100644 --- a/install.ps1 +++ b/install.ps1 @@ -98,18 +98,12 @@ function Install-WithWinget { function Install-MustHaveApps { Write-Host "Installing must-have apps..." -ForegroundColor Cyan - $packageUpdate = $Update -and -not $CoreOnly - $installs = if ($CoreOnly) { - @( - { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$packageUpdate }, - { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$packageUpdate }, - { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$packageUpdate }, - { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$packageUpdate }, - { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$packageUpdate } - ) + + if ($CoreOnly) { + Write-Host "Skipping workstation WinGet catalog and starship setup in core-only mode." -ForegroundColor Yellow } else { - @( + $installs = @( { Install-WithWinget -AppId "7zip.7zip" -Update:$Update }, { Install-WithWinget -AppId "Microsoft.PowerToys" -Update:$Update }, { Install-WithWinget -AppId "zyedidia.micro" -Alias "micro" -Update:$Update }, @@ -124,13 +118,13 @@ function Install-MustHaveApps { { Install-WithWinget -AppId "BurntSushi.ripgrep.MSVC" -Alias "rg" -Update:$Update }, { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$Update } ) - } - foreach ($install in $installs) { & $install } + foreach ($install in $installs) { & $install } - Refresh-Path - if (Get-Command mise -ErrorAction SilentlyContinue) { - & mise use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + Refresh-Path + if (Get-Command mise -ErrorAction SilentlyContinue) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + } } foreach ($module in $ManagedModules) { From 37c0a40b080e6774edbbbda5955c3c2f4b629c2e Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:40:46 -0500 Subject: [PATCH 18/61] Preserve Windows PATH and preprovision signing cert --- .github/workflows/validate.yml | 20 ++++++++++++++++++++ install.ps1 | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c39002b..006dad8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -453,6 +453,26 @@ jobs: if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } '@ | Set-Content -LiteralPath $smoke + $subject = 'CN=jsilverdev Dotfiles Code Signing' + $certificate = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { $_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + if ($null -eq $certificate) { + $certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 + } + foreach ($storeName in @('Root', 'TrustedPublisher')) { + $store = [Security.Cryptography.X509Certificates.X509Store]::new($storeName, 'CurrentUser') + try { + $store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + if (-not @($store.Certificates | Where-Object Thumbprint -eq $certificate.Thumbprint)) { + $store.Add($certificate) + } + } + finally { $store.Dispose() } + } + Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force $currentUser = Get-ExecutionPolicy -Scope CurrentUser $effective = Get-ExecutionPolicy diff --git a/install.ps1 b/install.ps1 index f757544..397b9d2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -27,7 +27,10 @@ $ManagedModules = @(Get-Content -LiteralPath $managedModulesPath | Where-Object function Refresh-Path { $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") $userPath = [Environment]::GetEnvironmentVariable("Path", "User") - $env:Path = @($machinePath, $userPath) -join ";" + $env:Path = @($env:Path, $machinePath, $userPath) | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique | + Join-String -Separator ";" } function Check-RequiredApps { From 8a3e20a2b018305b47b1010d4f23cde9752dde95 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:42:42 -0500 Subject: [PATCH 19/61] Split Windows bootstrap validation phases --- .github/workflows/validate.yml | 83 ++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 006dad8..57f6c11 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -345,14 +345,76 @@ jobs: & "$env:GITHUB_WORKSPACE\scripts\windows\invoke-ps-script.cmd" $smoke -Value bridge-ok if ($LASTEXITCODE -ne 0) { throw "PowerShell bridge smoke test failed with exit code $LASTEXITCODE" } - - name: Run Windows bootstrap in a temporary directory + - name: Ensure Windows bootstrap core tools + shell: pwsh + run: | + $packages = @( + @{ Id = 'Git.Git'; Command = 'git.exe' }, + @{ Id = 'Microsoft.PowerShell'; Command = 'pwsh.exe' }, + @{ Id = 'jdx.mise'; Command = 'mise.exe' }, + @{ Id = 'twpayne.chezmoi'; Command = 'chezmoi.exe' } + ) + foreach ($package in $packages) { + cmd /c "where $($package.Command) >nul 2>&1" + if ($LASTEXITCODE -ne 0) { + winget.exe install --id $package.Id --exact --source winget --scope user --silent --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "unable to install $($package.Id) for current user" } + } + } + @( + (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), + (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), + (Join-Path $env:LOCALAPPDATA 'Programs\mise'), + (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') + ) | ForEach-Object { + if ($env:Path -notlike "*$_*") { $env:Path += ";$_" } + Add-Content -Path $env:GITHUB_PATH -Value $_ + } + foreach ($command in @('git.exe','pwsh.exe','mise.exe','chezmoi.exe')) { + cmd /c "where $command" + if ($LASTEXITCODE -ne 0) { throw "$command is unavailable after provisioning" } + } + + - name: Initialize chezmoi from the exact-commit remote + shell: pwsh + run: | + $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' + New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null + Push-Location $runDirectory + try { + chezmoi.exe init --apply $env:DOTFILES_REPO + if ($LASTEXITCODE -ne 0) { throw "chezmoi init --apply failed with exit code $LASTEXITCODE" } + $source = (& chezmoi.exe source-path).Trim() + if (-not (Test-Path -LiteralPath $source)) { throw "chezmoi source path does not exist: $source" } + } + finally { Pop-Location } + + - name: Run Windows core installer through the bridge + shell: pwsh + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + $source = (& chezmoi.exe source-path).Trim() + $repoRoot = if (Test-Path -LiteralPath (Join-Path $source 'install.ps1')) { + $source + } + elseif (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $source) 'install.ps1')) { + Split-Path -Parent $source + } + else { + throw "unable to resolve repository root from chezmoi source path: $source" + } + & (Join-Path $repoRoot 'scripts\windows\invoke-ps-script.cmd') (Join-Path $repoRoot 'install.ps1') -NonInteractive -CoreOnly -RepoRoot $repoRoot + if ($LASTEXITCODE -ne 0) { throw "core installer failed with exit code $LASTEXITCODE" } + + - name: Run Windows bootstrap wrapper idempotently shell: pwsh env: DOTFILES_NONINTERACTIVE: '1' DOTFILES_CORE_ONLY: '1' run: | $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' - New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null Push-Location $runDirectory try { $bootstrap = Join-Path $env:GITHUB_WORKSPACE 'bootstrap.cmd' @@ -461,15 +523,18 @@ jobs: if ($null -eq $certificate) { $certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 } - foreach ($storeName in @('Root', 'TrustedPublisher')) { - $store = [Security.Cryptography.X509Certificates.X509Store]::new($storeName, 'CurrentUser') - try { - $store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - if (-not @($store.Certificates | Where-Object Thumbprint -eq $certificate.Thumbprint)) { - $store.Add($certificate) + $publicCertificate = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.cer') + try { + Export-Certificate -Cert $certificate -FilePath $publicCertificate -Type CERT -Force | Out-Null + foreach ($storeName in @('Root', 'TrustedPublisher')) { + $existing = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint) + if ($existing.Count -eq 0) { + Import-Certificate -FilePath $publicCertificate -CertStoreLocation "Cert:\CurrentUser\$storeName" -Confirm:$false | Out-Null } } - finally { $store.Dispose() } + } + finally { + Remove-Item -LiteralPath $publicCertificate -Force -ErrorAction SilentlyContinue } Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline From adbfeff8e5ab0da8b81e36e9aa9ca0d16e5ef185 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:43:08 -0500 Subject: [PATCH 20/61] Unblock revised CI validation run --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 57f6c11..182072d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ permissions: contents: read concurrency: - group: validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: validate-v2-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: From 1100843c715d8cd38f4c1af12e724c6a1b96af74 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:44:36 -0500 Subject: [PATCH 21/61] Make Windows CI fully non-interactive --- .github/workflows/validate.yml | 5 +++-- bootstrap.cmd | 2 +- install.ps1 | 4 ++-- scripts/windows/invoke-ps-script-bridge.ps1 | 13 +++---------- scripts/windows/invoke-ps-script.cmd | 2 +- scripts/windows/signing.ps1 | 3 ++- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 182072d..7da244a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -357,7 +357,7 @@ jobs: foreach ($package in $packages) { cmd /c "where $($package.Command) >nul 2>&1" if ($LASTEXITCODE -ne 0) { - winget.exe install --id $package.Id --exact --source winget --scope user --silent --accept-source-agreements --accept-package-agreements + winget.exe install --id $package.Id --exact --source winget --scope user --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -ne 0) { throw "unable to install $($package.Id) for current user" } } } @@ -529,7 +529,8 @@ jobs: foreach ($storeName in @('Root', 'TrustedPublisher')) { $existing = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint) if ($existing.Count -eq 0) { - Import-Certificate -FilePath $publicCertificate -CertStoreLocation "Cert:\CurrentUser\$storeName" -Confirm:$false | Out-Null + & certutil.exe -user -f -addstore $storeName $publicCertificate | Out-Null + if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\$storeName" } } } } diff --git a/bootstrap.cmd b/bootstrap.cmd index b65b192..74864e9 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -73,7 +73,7 @@ set "PACKAGE_COMMAND=%~2" where "%PACKAGE_COMMAND%.exe" >nul 2>&1 if not errorlevel 1 exit /b 0 echo Installing %PACKAGE_ID% for the current user... -winget.exe install --id "%PACKAGE_ID%" --exact --source winget --scope user --silent --accept-source-agreements --accept-package-agreements +winget.exe install --id "%PACKAGE_ID%" --exact --source winget --scope user --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if errorlevel 1 ( echo Unable to install %PACKAGE_ID% without administrator rights. No machine-scope or portable fallback will be attempted. 1>&2 exit /b 1 diff --git a/install.ps1 b/install.ps1 index 397b9d2..43da653 100644 --- a/install.ps1 +++ b/install.ps1 @@ -86,12 +86,12 @@ function Install-WithWinget { if (-not $installed) { Write-Host "Installing $AppId..." -ForegroundColor Cyan - & winget install --id $AppId --exact --source winget --silent --accept-source-agreements --accept-package-agreements + & winget install --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } } elseif ($Update) { Write-Host "Updating $AppId..." -ForegroundColor Yellow - & winget upgrade --id $AppId --exact --source winget --silent --accept-source-agreements --accept-package-agreements + & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId." } } else { diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index d02bb48..9f412c9 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -5,16 +5,13 @@ $scriptArgs = @() for ($i = 1; $i -le $argumentCount; $i++) { $scriptArgs += [Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i") } - $temporaryScript = $null $publicCertificate = $null $failed = $false - try { if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { throw "PowerShell script not found: $scriptPath" } - if ((Get-ExecutionPolicy) -eq 'AllSigned') { $subject = 'CN=jsilverdev Dotfiles Code Signing' $codeSigningOid = '1.3.6.1.5.5.7.3.3' @@ -27,7 +24,6 @@ try { } | Sort-Object NotAfter -Descending | Select-Object -First 1 - if ($null -eq $certificate) { $certificate = New-SelfSignedCertificate ` -Type CodeSigningCert ` @@ -36,15 +32,14 @@ try { -NotAfter (Get-Date).AddYears(10) ` -HashAlgorithm SHA256 } - $publicCertificate = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.cer') Export-Certificate -Cert $certificate -FilePath $publicCertificate -Type CERT -Force | Out-Null - foreach ($storeName in @('Root', 'TrustedPublisher')) { + foreach ($storeName in @('Root','TrustedPublisher')) { if (-not (Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint)) { - Import-Certificate -FilePath $publicCertificate -CertStoreLocation "Cert:\CurrentUser\$storeName" | Out-Null + & certutil.exe -user -f -addstore $storeName $publicCertificate | Out-Null + if ($LASTEXITCODE) { throw "certutil failed for $storeName" } } } - $temporaryScript = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.ps1') Copy-Item -LiteralPath $scriptPath -Destination $temporaryScript -Force Set-AuthenticodeSignature -FilePath $temporaryScript -Certificate $certificate -HashAlgorithm SHA256 | Out-Null @@ -54,7 +49,6 @@ try { } $scriptPath = $temporaryScript } - $pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source & $pwsh -NoProfile -File $scriptPath @scriptArgs if ($LASTEXITCODE -ne 0) { $failed = $true } @@ -71,5 +65,4 @@ finally { Remove-Item -LiteralPath $publicCertificate -Force -ErrorAction SilentlyContinue } } - if ($failed) { exit 1 } diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index 9cd1265..b821a45 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -27,7 +27,7 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAKACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAJABuAHUAbABsAAoAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAJABuAHUAbABsAAoAJABmAGEAaQBsAGUAZAAgAD0AIAAkAGYAYQBsAHMAZQAKAAoAdAByAHkAIAB7AAoAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApACAAewAKACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFAAbwB3AGUAcgBTAGgAZQBsAGwAIABzAGMAcgBpAHAAdAAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIgAKACAAIAAgACAAfQAKAAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAKACAAIAAgACAAIAAgACAAIABpAGYAIAAoACQAbgB1AGwAbAAgAC0AZQBxACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABOAGUAdwAtAFMAZQBsAGYAUwBpAGcAbgBlAGQAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBUAHkAcABlACAAQwBvAGQAZQBTAGkAZwBuAGkAbgBnAEMAZQByAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAFMAdQBiAGoAZQBjAHQAIAAkAHMAdQBiAGoAZQBjAHQAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcAE0AeQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ATgBvAHQAQQBmAHQAZQByACAAKABHAGUAdAAtAEQAYQB0AGUAKQAuAEEAZABkAFkAZQBhAHIAcwAoADEAMAApACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtACAAUwBIAEEAMgA1ADYACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgAFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAgACcALgBjAGUAcgAnACkACgAgACAAIAAgACAAIAAgACAARQB4AHAAbwByAHQALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAFQAeQBwAGUAIABDAEUAUgBUACAALQBGAG8AcgBjAGUAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAIAAgACAAIAAgACAAIAAgAGYAbwByAGUAYQBjAGgAIAAoACQAcwB0AG8AcgBlAE4AYQBtAGUAIABpAG4AIABAACgAJwBSAG8AbwB0ACcALAAgACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIABJAG0AcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEMAZQByAHQAUwB0AG8AcgBlAEwAbwBjAGEAdABpAG8AbgAgACIAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwAJABzAHQAbwByAGUATgBhAG0AZQAiACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAfQAKAAoAIAAgACAAIAAgACAAIAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAHAAcwAxACcAKQAKACAAIAAgACAAIAAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwBuAGEAdAB1AHIAZQAgAD0AIABHAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACAALQBuAGUAIAAnAFYAYQBsAGkAZAAnACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAdABoAHIAbwB3ACAAIgBUAGUAbQBwAG8AcgBhAHIAeQAgAHMAYwByAGkAcAB0ACAAcwBpAGcAbgBhAHQAdQByAGUAIABpAHMAIABuAG8AdAAgAFYAYQBsAGkAZAA6ACAAJAAoACQAcwBpAGcAbgBhAHQAdQByAGUALgBTAHQAYQB0AHUAcwApACIACgAgACAAIAAgACAAIAAgACAAfQAKACAAIAAgACAAIAAgACAAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAA9ACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAKACAAIAAgACAAfQAKAAoAIAAgACAAIAAkAHAAdwBzAGgAIAA9ACAAKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAgACAAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwAKACAAIAAgACAAaQBmACAAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAIAB7ACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUAIAB9AAoAfQAKAGMAYQB0AGMAaAAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwAKACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgB9AAoAZgBpAG4AYQBsAGwAeQAgAHsACgAgACAAIAAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKACAAIAAgACAAaQBmACAAKAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgB9AAoACgBpAGYAIAAoACQAZgBhAGkAbABlAGQAKQAgAHsAIABlAHgAaQB0ACAAMQAgAH0ACgA= +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAAPQAgACQAbgB1AGwAbAAKACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgACQAbgB1AGwAbAAKACQAZgBhAGkAbABlAGQAIAA9ACAAJABmAGEAbABzAGUACgB0AHIAeQAgAHsACgAgACAAIAAgAGkAZgAgACgALQBuAG8AdAAgACgAVABlAHMAdAAtAFAAYQB0AGgAIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAiAAoAIAAgACAAIAB9AAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBTAHUAYgBqAGUAYwB0ACAAJABzAHUAYgBqAGUAYwB0ACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAGMAZQByACcAKQAKACAAIAAgACAAIAAgACAAIABFAHgAcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AQwBlAHIAdAAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMARQBSAFQAIAAtAEYAbwByAGMAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAZgBvAHIAZQBhAGMAaAAgACgAJABzAHQAbwByAGUATgBhAG0AZQAgAGkAbgAgAEAAKAAnAFIAbwBvAHQAJwAsACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgACQAcwB0AG8AcgBlAE4AYQBtAGUAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQApACAAewAgAHQAaAByAG8AdwAgACIAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAH0ACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AcABzADEAJwApAAoAIAAgACAAIAAgACAAIAAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAKACAAIAAgACAAIAAgACAAIABTAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAkAHMAaQBnAG4AYQB0AHUAcgBlACAAPQAgAEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAHMAaQBnAG4AYQB0AHUAcgBlAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFQAZQBtAHAAbwByAGEAcgB5ACAAcwBjAHIAaQBwAHQAIABzAGkAZwBuAGEAdAB1AHIAZQAgAGkAcwAgAG4AbwB0ACAAVgBhAGwAaQBkADoAIAAkACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACkAIgAKACAAIAAgACAAIAAgACAAIAB9AAoAIAAgACAAIAAgACAAIAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAB9AAoAIAAgACAAIAAkAHAAdwBzAGgAIAA9ACAAKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAgACAAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwAKACAAIAAgACAAaQBmACAAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAIAB7ACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUAIAB9AAoAfQAKAGMAYQB0AGMAaAAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwAKACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgB9AAoAZgBpAG4AYQBsAGwAeQAgAHsACgAgACAAIAAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKACAAIAAgACAAaQBmACAAKAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgB9AAoAaQBmACAAKAAkAGYAYQBpAGwAZQBkACkAIAB7ACAAZQB4AGkAdAAgADEAIAB9AAoA exit /b %ERRORLEVEL% :usage diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index f3b2d5f..162430b 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -55,7 +55,8 @@ function Get-DotfilesSigningCertificate { $storePath = "Cert:\CurrentUser\$storeName" $trusted = Get-ChildItem -Path $storePath | Where-Object Thumbprint -eq $certificate.Thumbprint if ($null -eq $trusted) { - Import-Certificate -FilePath $publicCertificatePath -CertStoreLocation $storePath | Out-Null + & certutil.exe -user -f -addstore $storeName $publicCertificatePath | Out-Null + if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\$storeName." } } } } From 7f96857a97fe71da1edf6e0e3448e518cc0569ff Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:45:08 -0500 Subject: [PATCH 22/61] Fix post-apply PowerShell path binding --- scripts/windows/deploy-pwsh.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 index 604d355..63bcf39 100644 --- a/scripts/windows/deploy-pwsh.ps1 +++ b/scripts/windows/deploy-pwsh.ps1 @@ -40,7 +40,9 @@ foreach ($file in $files) { # PowerShell execution policy is AllSigned. The CMD bridge itself is AllSigned-safe. $helper = Join-Path $repo "scripts\windows\signing.ps1" $bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" -& $bridge $helper -Action ProtectFiles -Path @($files | ForEach-Object Destination) -if ($LASTEXITCODE -ne 0) { - throw "PowerShell runtime signing failed with exit code $LASTEXITCODE." +foreach ($destination in @($files | ForEach-Object Destination)) { + & $bridge $helper -Action ProtectFiles -Path $destination + if ($LASTEXITCODE -ne 0) { + throw "PowerShell runtime signing failed for $destination with exit code $LASTEXITCODE." + } } From 185213b081b923ac7672fc34c9437faadec5d538 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:46:45 -0500 Subject: [PATCH 23/61] Remove remaining Windows CI prompts --- .github/workflows/validate.yml | 29 ++++++++++++++--------------- install.ps1 | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7da244a..296ee02 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -506,7 +506,7 @@ jobs: Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" Write-Host "Local bootstrap remote: $remoteUri" - - name: Enable and prove CurrentUser AllSigned + - name: Create CI code-signing certificate shell: pwsh run: | $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' @@ -523,22 +523,21 @@ jobs: if ($null -eq $certificate) { $certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 } - $publicCertificate = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.cer') - try { - Export-Certificate -Cert $certificate -FilePath $publicCertificate -Type CERT -Force | Out-Null - foreach ($storeName in @('Root', 'TrustedPublisher')) { - $existing = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint) - if ($existing.Count -eq 0) { - & certutil.exe -user -f -addstore $storeName $publicCertificate | Out-Null - if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\$storeName" } - } - } - } - finally { - Remove-Item -LiteralPath $publicCertificate -Force -ErrorAction SilentlyContinue - } Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline + Export-Certificate -Cert $certificate -FilePath (Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer') -Type CERT -Force | Out-Null + - name: Trust CI code-signing certificate for current user + shell: cmd + timeout-minutes: 2 + run: | + certutil.exe -user -f -addstore TrustedPublisher "%RUNNER_TEMP%\dotfiles-signing.cer" + if errorlevel 1 exit /b 1 + certutil.exe -user -f -addstore Root "%RUNNER_TEMP%\dotfiles-signing.cer" + if errorlevel 1 exit /b 1 + + - name: Enable and prove CurrentUser AllSigned + shell: pwsh + run: | Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force $currentUser = Get-ExecutionPolicy -Scope CurrentUser $effective = Get-ExecutionPolicy diff --git a/install.ps1 b/install.ps1 index 43da653..8f38580 100644 --- a/install.ps1 +++ b/install.ps1 @@ -137,7 +137,7 @@ function Install-MustHaveApps { } if ($null -eq $installedModule) { Write-Host "Installing $module module..." -ForegroundColor Cyan - Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber + Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false } elseif ($Update -and -not $CoreOnly) { Write-Host "Updating $module module..." -ForegroundColor Yellow From 3eaa419558c9a170f106fa66aae56ab76b244b80 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:47:32 -0500 Subject: [PATCH 24/61] Keep Starship in Windows core setup --- install.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 8f38580..a765814 100644 --- a/install.ps1 +++ b/install.ps1 @@ -103,7 +103,13 @@ function Install-MustHaveApps { Write-Host "Installing must-have apps..." -ForegroundColor Cyan if ($CoreOnly) { - Write-Host "Skipping workstation WinGet catalog and starship setup in core-only mode." -ForegroundColor Yellow + Write-Host "Skipping workstation WinGet catalog in core-only mode." -ForegroundColor Yellow + Refresh-Path + if (-not (Get-Command starship -ErrorAction SilentlyContinue) -and (Get-Command mise -ErrorAction SilentlyContinue)) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + Refresh-Path + } } else { $installs = @( From 600a14ddd56207a1943788c687ab32ad1f26e014 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:49:52 -0500 Subject: [PATCH 25/61] Isolate Windows runtime and add AllSigned fallback --- .github/workflows/validate.yml | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 296ee02..6f7dc26 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -389,6 +389,38 @@ jobs: } finally { Pop-Location } + - name: Provision Starship for the core PowerShell profile + shell: pwsh + run: | + mise.exe use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not provision starship" } + + - name: Provision managed PowerShell modules + shell: pwsh + run: | + $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | + Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } + foreach ($module in $modules) { + if (-not (Get-Module -ListAvailable -Name $module | Select-Object -First 1)) { + Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false + } + if (-not (Get-Module -ListAvailable -Name $module | Select-Object -First 1)) { + throw "managed PowerShell module was not provisioned: $module" + } + } + + - name: Smoke-test module signing helper under normal policy + shell: pwsh + run: | + $helper = "$env:GITHUB_WORKSPACE\scripts\windows\signing.ps1" + $bridge = "$env:GITHUB_WORKSPACE\scripts\windows\invoke-ps-script.cmd" + $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | + Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } + foreach ($module in $modules) { + & $bridge $helper -Action ProtectModule -ModuleName $module + if ($LASTEXITCODE -ne 0) { throw "signing helper failed for $module under normal policy" } + } + - name: Run Windows core installer through the bridge shell: pwsh env: @@ -527,15 +559,48 @@ jobs: Export-Certificate -Cert $certificate -FilePath (Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer') -Type CERT -Force | Out-Null - name: Trust CI code-signing certificate for current user + id: trust_certificate + continue-on-error: true shell: cmd - timeout-minutes: 2 + timeout-minutes: 1 run: | certutil.exe -user -f -addstore TrustedPublisher "%RUNNER_TEMP%\dotfiles-signing.cer" if errorlevel 1 exit /b 1 certutil.exe -user -f -addstore Root "%RUNNER_TEMP%\dotfiles-signing.cer" if errorlevel 1 exit /b 1 + - name: Validate AllSigned prerequisites when Root trust is unavailable + if: steps.trust_certificate.outcome != 'success' + shell: pwsh + run: | + $thumbprint = (Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim() + $certificate = Get-ChildItem Cert:\CurrentUser\My | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 + if ($null -eq $certificate -or -not $certificate.HasPrivateKey) { + throw 'CI code-signing certificate is unavailable' + } + + $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' + $signed = Set-AuthenticodeSignature -FilePath $smoke -Certificate $certificate -HashAlgorithm SHA256 + $signature = Get-AuthenticodeSignature -FilePath $smoke + if ($null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $thumbprint) { + throw "AuthentiCode signing did not use the expected certificate: $($signed.Status)" + } + + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force + try { + $currentUser = Get-ExecutionPolicy -Scope CurrentUser + $effective = Get-ExecutionPolicy + if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { + throw "unable to establish CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" + } + Write-Warning 'Hosted runner blocked non-interactive CurrentUser Root trust. Full trusted-publisher execution is skipped; certificate creation, Authenticode signing, and AllSigned policy persistence were validated.' + } + finally { + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Undefined -Force + } + - name: Enable and prove CurrentUser AllSigned + if: steps.trust_certificate.outcome == 'success' shell: pwsh run: | Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force @@ -548,12 +613,14 @@ jobs: } - name: Smoke-test signing bridge under AllSigned + if: steps.trust_certificate.outcome == 'success' shell: cmd run: | call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge + if: steps.trust_certificate.outcome == 'success' shell: cmd env: DOTFILES_NONINTERACTIVE: '1' @@ -567,12 +634,14 @@ jobs: popd - name: Assert signed runtime and module state through the bridge + if: steps.trust_certificate.outcome == 'success' shell: cmd run: | call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" if errorlevel 1 exit /b 1 - name: Create an update commit without changing the Actions checkout + if: steps.trust_certificate.outcome == 'success' shell: cmd run: | set "UPDATE_CLONE=%RUNNER_TEMP%\dotfiles-update" @@ -589,6 +658,7 @@ jobs: echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" - name: Exercise update.cmd and recheck AllSigned idempotency + if: steps.trust_certificate.outcome == 'success' shell: cmd env: DOTFILES_NONINTERACTIVE: '1' From e21d73589e26af771a1721dcae22a217d3e660b0 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:52:01 -0500 Subject: [PATCH 26/61] Separate direct and bridged Windows installer checks --- .github/workflows/validate.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6f7dc26..d537334 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -421,6 +421,25 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "signing helper failed for $module under normal policy" } } + - name: Run Windows core installer directly + shell: pwsh + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | + $source = (& chezmoi.exe source-path).Trim() + $repoRoot = if (Test-Path -LiteralPath (Join-Path $source 'install.ps1')) { + $source + } + elseif (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $source) 'install.ps1')) { + Split-Path -Parent $source + } + else { + throw "unable to resolve repository root from chezmoi source path: $source" + } + & pwsh.exe -NoProfile -File (Join-Path $repoRoot 'install.ps1') -NonInteractive -CoreOnly -RepoRoot $repoRoot + if ($LASTEXITCODE -ne 0) { throw "direct core installer failed with exit code $LASTEXITCODE" } + - name: Run Windows core installer through the bridge shell: pwsh env: From 6cee063bb39866652fcfd5e67c17cc9ae0b7ef0a Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:53:47 -0500 Subject: [PATCH 27/61] Use mise state for core Starship check --- install.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index a765814..b594691 100644 --- a/install.ps1 +++ b/install.ps1 @@ -105,10 +105,12 @@ function Install-MustHaveApps { if ($CoreOnly) { Write-Host "Skipping workstation WinGet catalog in core-only mode." -ForegroundColor Yellow Refresh-Path - if (-not (Get-Command starship -ErrorAction SilentlyContinue) -and (Get-Command mise -ErrorAction SilentlyContinue)) { - & mise use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } - Refresh-Path + if (Get-Command mise -ErrorAction SilentlyContinue) { + & mise which starship *> $null + if ($LASTEXITCODE -ne 0) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + } } } else { From 518c4f7105917c047bcfc01eda055642583c3512 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:07:41 -0500 Subject: [PATCH 28/61] Fix Windows CI path and enforce AllSigned validation --- .github/workflows/validate.yml | 77 ++++++++++++++++++---------------- install.ps1 | 21 +++++++--- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d537334..83724a2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -577,49 +577,57 @@ jobs: Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline Export-Certificate -Cert $certificate -FilePath (Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer') -Type CERT -Force | Out-Null - - name: Trust CI code-signing certificate for current user - id: trust_certificate - continue-on-error: true - shell: cmd - timeout-minutes: 1 - run: | - certutil.exe -user -f -addstore TrustedPublisher "%RUNNER_TEMP%\dotfiles-signing.cer" - if errorlevel 1 exit /b 1 - certutil.exe -user -f -addstore Root "%RUNNER_TEMP%\dotfiles-signing.cer" - if errorlevel 1 exit /b 1 - - - name: Validate AllSigned prerequisites when Root trust is unavailable - if: steps.trust_certificate.outcome != 'success' + - name: Trust and verify CI code-signing certificate for current user shell: pwsh run: | + $certificatePath = Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer' $thumbprint = (Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim() - $certificate = Get-ChildItem Cert:\CurrentUser\My | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 - if ($null -eq $certificate -or -not $certificate.HasPrivateKey) { - throw 'CI code-signing certificate is unavailable' + $publicCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certificatePath) + if ($publicCertificate.Thumbprint -ne $thumbprint) { + throw "exported CI certificate thumbprint does not match: $($publicCertificate.Thumbprint)" } - $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' - $signed = Set-AuthenticodeSignature -FilePath $smoke -Certificate $certificate -HashAlgorithm SHA256 - $signature = Get-AuthenticodeSignature -FilePath $smoke - if ($null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $thumbprint) { - throw "AuthentiCode signing did not use the expected certificate: $($signed.Status)" - } + foreach ($storeName in @('Root', 'TrustedPublisher')) { + $store = [System.Security.Cryptography.X509Certificates.X509Store]::new( + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser + ) + try { + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $existing = $store.Certificates | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 + if ($null -eq $existing) { + $store.Add($publicCertificate) + } + } + finally { + $store.Close() + } - Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force - try { - $currentUser = Get-ExecutionPolicy -Scope CurrentUser - $effective = Get-ExecutionPolicy - if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { - throw "unable to establish CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" + $trusted = Get-ChildItem "Cert:\CurrentUser\$storeName" | + Where-Object Thumbprint -eq $thumbprint | + Select-Object -First 1 + if ($null -eq $trusted) { + throw "CI code-signing certificate was not trusted in CurrentUser\$storeName" } - Write-Warning 'Hosted runner blocked non-interactive CurrentUser Root trust. Full trusted-publisher execution is skipped; certificate creation, Authenticode signing, and AllSigned policy persistence were validated.' } - finally { - Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Undefined -Force + + $signingCertificate = Get-ChildItem Cert:\CurrentUser\My | + Where-Object Thumbprint -eq $thumbprint | + Select-Object -First 1 + if ($null -eq $signingCertificate -or -not $signingCertificate.HasPrivateKey) { + throw 'CI code-signing certificate with private key is unavailable' + } + + $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' + Set-AuthenticodeSignature -FilePath $smoke -Certificate $signingCertificate -HashAlgorithm SHA256 | Out-Null + $signature = Get-AuthenticodeSignature -FilePath $smoke + if ($signature.Status -ne 'Valid' -or + $null -eq $signature.SignerCertificate -or + $signature.SignerCertificate.Thumbprint -ne $thumbprint) { + throw "trusted CI Authenticode signature is not valid: $($signature.Status) $($signature.StatusMessage)" } - name: Enable and prove CurrentUser AllSigned - if: steps.trust_certificate.outcome == 'success' shell: pwsh run: | Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force @@ -632,14 +640,12 @@ jobs: } - name: Smoke-test signing bridge under AllSigned - if: steps.trust_certificate.outcome == 'success' shell: cmd run: | call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge - if: steps.trust_certificate.outcome == 'success' shell: cmd env: DOTFILES_NONINTERACTIVE: '1' @@ -653,14 +659,12 @@ jobs: popd - name: Assert signed runtime and module state through the bridge - if: steps.trust_certificate.outcome == 'success' shell: cmd run: | call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" if errorlevel 1 exit /b 1 - name: Create an update commit without changing the Actions checkout - if: steps.trust_certificate.outcome == 'success' shell: cmd run: | set "UPDATE_CLONE=%RUNNER_TEMP%\dotfiles-update" @@ -677,7 +681,6 @@ jobs: echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" - name: Exercise update.cmd and recheck AllSigned idempotency - if: steps.trust_certificate.outcome == 'success' shell: cmd env: DOTFILES_NONINTERACTIVE: '1' diff --git a/install.ps1 b/install.ps1 index b594691..254cefd 100644 --- a/install.ps1 +++ b/install.ps1 @@ -25,12 +25,21 @@ if (-not (Test-Path -LiteralPath $managedModulesPath -PathType Leaf)) { $ManagedModules = @(Get-Content -LiteralPath $managedModulesPath | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') }) function Refresh-Path { - $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") - $userPath = [Environment]::GetEnvironmentVariable("Path", "User") - $env:Path = @($env:Path, $machinePath, $userPath) | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - Select-Object -Unique | - Join-String -Separator ";" + $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $pathEntries = foreach ($pathValue in @( + $PSHOME + $env:Path + [Environment]::GetEnvironmentVariable("Path", "Machine") + [Environment]::GetEnvironmentVariable("Path", "User") + )) { + if ([string]::IsNullOrWhiteSpace($pathValue)) { continue } + foreach ($entry in $pathValue -split [IO.Path]::PathSeparator) { + $entry = $entry.Trim() + if ($entry -and $seen.Add($entry)) { $entry } + } + } + + $env:Path = $pathEntries -join [IO.Path]::PathSeparator } function Check-RequiredApps { From 0c9512bed91219a20ec53be87a912e0c5f986540 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:14:29 -0500 Subject: [PATCH 29/61] Fix Windows chezmoi status assertions --- .github/workflows/validate.yml | 22 ++++++++-------------- tests/windows/assert-allsigned.ps1 | 6 ++++-- tests/windows/assert-state.ps1 | 6 ++++-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 83724a2..3f2eb28 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -579,6 +579,7 @@ jobs: - name: Trust and verify CI code-signing certificate for current user shell: pwsh + timeout-minutes: 2 run: | $certificatePath = Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer' $thumbprint = (Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim() @@ -588,22 +589,15 @@ jobs: } foreach ($storeName in @('Root', 'TrustedPublisher')) { - $store = [System.Security.Cryptography.X509Certificates.X509Store]::new( - $storeName, - [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser - ) - try { - $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - $existing = $store.Certificates | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 - if ($null -eq $existing) { - $store.Add($publicCertificate) - } - } - finally { - $store.Close() + $storePath = "Cert:\CurrentUser\$storeName" + $existing = Get-ChildItem $storePath | + Where-Object Thumbprint -eq $thumbprint | + Select-Object -First 1 + if ($null -eq $existing) { + Import-Certificate -FilePath $certificatePath -CertStoreLocation $storePath -Confirm:$false | Out-Null } - $trusted = Get-ChildItem "Cert:\CurrentUser\$storeName" | + $trusted = Get-ChildItem $storePath | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 if ($null -eq $trusted) { diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 2ea1c2c..25efa6b 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -43,8 +43,10 @@ function Invoke-Chezmoi([string[]]$Arguments) { } function Assert-CleanChezMoi { - $status = @(Invoke-Chezmoi @("status")) - if ($status.Count -ne 0) { Fail "chezmoi status is not clean: $($status -join [Environment]::NewLine)" } + # Always-run scripts intentionally appear as "R" in chezmoi status. + # Exclude scripts so this assertion checks only declarative target drift. + $status = @(Invoke-Chezmoi @("status", "--exclude=scripts")) + if ($status.Count -ne 0) { Fail "chezmoi target state is not clean: $($status -join [Environment]::NewLine)" } } $sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 index 9d04dd8..11bad45 100644 --- a/tests/windows/assert-state.ps1 +++ b/tests/windows/assert-state.ps1 @@ -25,8 +25,10 @@ function Invoke-Chezmoi([string[]]$Arguments) { } function Assert-CleanChezMoi { - $status = @(Invoke-Chezmoi @("status")) - if ($status.Count -ne 0) { Fail "chezmoi status is not clean: $($status -join [Environment]::NewLine)" } + # Always-run scripts intentionally appear as "R" in chezmoi status. + # Exclude scripts so this assertion checks only declarative target drift. + $status = @(Invoke-Chezmoi @("status", "--exclude=scripts")) + if ($status.Count -ne 0) { Fail "chezmoi target state is not clean: $($status -join [Environment]::NewLine)" } } Write-Host "chezmoi: $((chezmoi.exe --version) -join ' ')" From 4f4f3c63dd060abac8365d517346236aa4231d5e Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:20:00 -0500 Subject: [PATCH 30/61] Fix Windows state semantics and AllSigned CI trust --- .github/workflows/validate.yml | 22 +++--- README.md | 2 +- scripts/windows/invoke-ps-script-bridge.ps1 | 88 +++++++++------------ scripts/windows/invoke-ps-script.cmd | 2 +- scripts/windows/signing.ps1 | 21 +++-- tests/windows/assert-allsigned.ps1 | 18 ++++- tests/windows/assert-state.ps1 | 11 ++- 7 files changed, 84 insertions(+), 80 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3f2eb28..6638172 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -577,31 +577,29 @@ jobs: Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline Export-Certificate -Cert $certificate -FilePath (Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer') -Type CERT -Force | Out-Null - - name: Trust and verify CI code-signing certificate for current user + - name: Trust and verify CI code-signing certificate shell: pwsh timeout-minutes: 2 run: | $certificatePath = Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer' $thumbprint = (Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim() - $publicCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certificatePath) - if ($publicCertificate.Thumbprint -ne $thumbprint) { - throw "exported CI certificate thumbprint does not match: $($publicCertificate.Thumbprint)" - } - foreach ($storeName in @('Root', 'TrustedPublisher')) { - $storePath = "Cert:\CurrentUser\$storeName" - $existing = Get-ChildItem $storePath | + $stores = @( + @{ Path = 'Cert:\LocalMachine\Root'; Name = 'LocalMachine\\Root' }, + @{ Path = 'Cert:\CurrentUser\TrustedPublisher'; Name = 'CurrentUser\\TrustedPublisher' } + ) + foreach ($store in $stores) { + $existing = Get-ChildItem $store.Path | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 if ($null -eq $existing) { - Import-Certificate -FilePath $certificatePath -CertStoreLocation $storePath -Confirm:$false | Out-Null + Import-Certificate -FilePath $certificatePath -CertStoreLocation $store.Path -Confirm:$false | Out-Null } - - $trusted = Get-ChildItem $storePath | + $trusted = Get-ChildItem $store.Path | Where-Object Thumbprint -eq $thumbprint | Select-Object -First 1 if ($null -eq $trusted) { - throw "CI code-signing certificate was not trusted in CurrentUser\$storeName" + throw "CI code-signing certificate was not trusted in $($store.Name)" } } diff --git a/README.md b/README.md index 973c8df..18a14a9 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ For dotfiles plus package/application and module updates, use `update.cmd` on Wi The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. -The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. +The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\\Root` plus `CurrentUser\\TrustedPublisher`; the runtime helpers also accept `CurrentUser\\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index 9f412c9..dde45cf 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -1,68 +1,52 @@ -$ErrorActionPreference = 'Stop' -$scriptPath = $env:DOTFILES_PS_SCRIPT -$argumentCount = [int]$env:DOTFILES_PS_ARGC -$scriptArgs = @() -for ($i = 1; $i -le $argumentCount; $i++) { +$ErrorActionPreference='Stop' +$scriptPath=$env:DOTFILES_PS_SCRIPT +$scriptArgs=@() +for($i=1;$i -le [int]$env:DOTFILES_PS_ARGC;$i++){ $scriptArgs += [Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i") } -$temporaryScript = $null -$publicCertificate = $null -$failed = $false +$temp=$null +$cer=$null +$failed=$false try { - if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { - throw "PowerShell script not found: $scriptPath" - } - if ((Get-ExecutionPolicy) -eq 'AllSigned') { - $subject = 'CN=jsilverdev Dotfiles Code Signing' - $codeSigningOid = '1.3.6.1.5.5.7.3.3' - $certificate = Get-ChildItem Cert:\CurrentUser\My | - Where-Object { - $_.Subject -eq $subject -and - $_.HasPrivateKey -and - $_.NotAfter -gt (Get-Date) -and - @($_.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 - } | + if(-not(Test-Path -LiteralPath $scriptPath -PathType Leaf)){throw "PowerShell script not found: $scriptPath"} + if((Get-ExecutionPolicy)-eq 'AllSigned'){ + $subject='CN=jsilverdev Dotfiles Code Signing' + $oid='1.3.6.1.5.5.7.3.3' + $cert=Get-ChildItem Cert:\CurrentUser\My | + Where-Object {$_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) -and @($_.EnhancedKeyUsageList | Where-Object {$_.ObjectId.Value -eq $oid}).Count -gt 0} | Sort-Object NotAfter -Descending | Select-Object -First 1 - if ($null -eq $certificate) { - $certificate = New-SelfSignedCertificate ` - -Type CodeSigningCert ` - -Subject $subject ` - -CertStoreLocation Cert:\CurrentUser\My ` - -NotAfter (Get-Date).AddYears(10) ` - -HashAlgorithm SHA256 + if($null -eq $cert){ + $cert=New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 } - $publicCertificate = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.cer') - Export-Certificate -Cert $certificate -FilePath $publicCertificate -Type CERT -Force | Out-Null - foreach ($storeName in @('Root','TrustedPublisher')) { - if (-not (Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate.Thumbprint)) { - & certutil.exe -user -f -addstore $storeName $publicCertificate | Out-Null - if ($LASTEXITCODE) { throw "certutil failed for $storeName" } - } + $cer=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.cer') + Export-Certificate -Cert $cert -FilePath $cer -Type CERT -Force | Out-Null + $thumb=$cert.Thumbprint + if(-not(Get-ChildItem -Path Cert:\CurrentUser\Root,Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $thumb)){ + & certutil.exe -user -f -addstore Root $cer | Out-Null + if($LASTEXITCODE){throw 'certutil failed for Root'} } - $temporaryScript = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), '.ps1') - Copy-Item -LiteralPath $scriptPath -Destination $temporaryScript -Force - Set-AuthenticodeSignature -FilePath $temporaryScript -Certificate $certificate -HashAlgorithm SHA256 | Out-Null - $signature = Get-AuthenticodeSignature -FilePath $temporaryScript - if ($signature.Status -ne 'Valid') { - throw "Temporary script signature is not Valid: $($signature.Status)" + if(-not(Get-ChildItem Cert:\CurrentUser\TrustedPublisher | Where-Object Thumbprint -eq $thumb)){ + & certutil.exe -user -f -addstore TrustedPublisher $cer | Out-Null + if($LASTEXITCODE){throw 'certutil failed for TrustedPublisher'} } - $scriptPath = $temporaryScript + $temp=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') + Copy-Item -LiteralPath $scriptPath -Destination $temp -Force + Set-AuthenticodeSignature -FilePath $temp -Certificate $cert -HashAlgorithm SHA256 | Out-Null + $sig=Get-AuthenticodeSignature -FilePath $temp + if($sig.Status -ne 'Valid'){throw "Temporary script signature is not Valid: $($sig.Status)"} + $scriptPath=$temp } - $pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source + $pwsh=(Get-Command pwsh.exe -ErrorAction Stop).Source & $pwsh -NoProfile -File $scriptPath @scriptArgs - if ($LASTEXITCODE -ne 0) { $failed = $true } + if($LASTEXITCODE -ne 0){$failed=$true} } catch { Write-Error $_ - $failed = $true + $failed=$true } finally { - if ($temporaryScript) { - Remove-Item -LiteralPath $temporaryScript -Force -ErrorAction SilentlyContinue - } - if ($publicCertificate) { - Remove-Item -LiteralPath $publicCertificate -Force -ErrorAction SilentlyContinue - } + if($temp){Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue} + if($cer){Remove-Item -LiteralPath $cer -Force -ErrorAction SilentlyContinue} } -if ($failed) { exit 1 } +if($failed){exit 1} diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index b821a45..1eb0599 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -27,7 +27,7 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAdABvAHAAJwAKACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AUwBDAFIASQBQAFQACgAkAGEAcgBnAHUAbQBlAG4AdABDAG8AdQBuAHQAIAA9ACAAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAPQAgAEAAKAApAAoAZgBvAHIAIAAoACQAaQAgAD0AIAAxADsAIAAkAGkAIAAtAGwAZQAgACQAYQByAGcAdQBtAGUAbgB0AEMAbwB1AG4AdAA7ACAAJABpACsAKwApACAAewAKACAAIAAgACAAJABzAGMAcgBpAHAAdABBAHIAZwBzACAAKwA9ACAAWwBFAG4AdgBpAHIAbwBuAG0AZQBuAHQAXQA6ADoARwBlAHQARQBuAHYAaQByAG8AbgBtAGUAbgB0AFYAYQByAGkAYQBiAGwAZQAoACIARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAJABpACIAKQAKAH0ACgAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAAPQAgACQAbgB1AGwAbAAKACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAPQAgACQAbgB1AGwAbAAKACQAZgBhAGkAbABlAGQAIAA9ACAAJABmAGEAbABzAGUACgB0AHIAeQAgAHsACgAgACAAIAAgAGkAZgAgACgALQBuAG8AdAAgACgAVABlAHMAdAAtAFAAYQB0AGgAIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgAHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAiAAoAIAAgACAAIAB9AAoAIAAgACAAIABpAGYAIAAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApACAALQBlAHEAIAAnAEEAbABsAFMAaQBnAG4AZQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAJABzAHUAYgBqAGUAYwB0ACAAPQAgACcAQwBOAD0AagBzAGkAbAB2AGUAcgBkAGUAdgAgAEQAbwB0AGYAaQBsAGUAcwAgAEMAbwBkAGUAIABTAGkAZwBuAGkAbgBnACcACgAgACAAIAAgACAAIAAgACAAJABjAG8AZABlAFMAaQBnAG4AaQBuAGcATwBpAGQAIAA9ACAAJwAxAC4AMwAuADYALgAxAC4ANQAuADUALgA3AC4AMwAuADMAJwAKACAAIAAgACAAIAAgACAAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAD0AIABHAGUAdAAtAEMAaABpAGwAZABJAHQAZQBtACAAQwBlAHIAdAA6AFwAQwB1AHIAcgBlAG4AdABVAHMAZQByAFwATQB5ACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4AUwB1AGIAagBlAGMAdAAgAC0AZQBxACAAJABzAHUAYgBqAGUAYwB0ACAALQBhAG4AZAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABfAC4ASABhAHMAUAByAGkAdgBhAHQAZQBLAGUAeQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAkAF8ALgBFAG4AaABhAG4AYwBlAGQASwBlAHkAVQBzAGEAZwBlAEwAaQBzAHQAIAB8ACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACAAJABfAC4ATwBiAGoAZQBjAHQASQBkAC4AVgBhAGwAdQBlACAALQBlAHEAIAAkAGMAbwBkAGUAUwBpAGcAbgBpAG4AZwBPAGkAZAAgAH0AKQAuAEMAbwB1AG4AdAAgAC0AZwB0ACAAMAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAG4AdQBsAGwAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABjAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBTAHUAYgBqAGUAYwB0ACAAJABzAHUAYgBqAGUAYwB0ACAAYAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIABgAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAGAACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJABwAHUAYgBsAGkAYwBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAA9ACAAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACAAJwAuAGMAZQByACcAKQAKACAAIAAgACAAIAAgACAAIABFAHgAcABvAHIAdAAtAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AQwBlAHIAdAAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMARQBSAFQAIAAtAEYAbwByAGMAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAZgBvAHIAZQBhAGMAaAAgACgAJABzAHQAbwByAGUATgBhAG0AZQAgAGkAbgAgAEAAKAAnAFIAbwBvAHQAJwAsACcAVAByAHUAcwB0AGUAZABQAHUAYgBsAGkAcwBoAGUAcgAnACkAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABpAGYAIAAoAC0AbgBvAHQAIAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAiAEMAZQByAHQAOgBcAEMAdQByAHIAZQBuAHQAVQBzAGUAcgBcACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAHwAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFQAaAB1AG0AYgBwAHIAaQBuAHQAIAAtAGUAcQAgACQAYwBlAHIAdABpAGYAaQBjAGEAdABlAC4AVABoAHUAbQBiAHAAcgBpAG4AdAApACkAIAB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgACQAcwB0AG8AcgBlAE4AYQBtAGUAIAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQApACAAewAgAHQAaAByAG8AdwAgACIAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgACQAcwB0AG8AcgBlAE4AYQBtAGUAIgAgAH0ACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAAgAD0AIABbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAIAAnAC4AcABzADEAJwApAAoAIAAgACAAIAAgACAAIAAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAYwByAGkAcAB0AFAAYQB0AGgAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAKACAAIAAgACAAIAAgACAAIABTAGUAdAAtAEEAdQB0AGgAZQBuAHQAaQBjAG8AZABlAFMAaQBnAG4AYQB0AHUAcgBlACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0ACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2ACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAkAHMAaQBnAG4AYQB0AHUAcgBlACAAPQAgAEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQACgAgACAAIAAgACAAIAAgACAAaQBmACAAKAAkAHMAaQBnAG4AYQB0AHUAcgBlAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQAgAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAB0AGgAcgBvAHcAIAAiAFQAZQBtAHAAbwByAGEAcgB5ACAAcwBjAHIAaQBwAHQAIABzAGkAZwBuAGEAdAB1AHIAZQAgAGkAcwAgAG4AbwB0ACAAVgBhAGwAaQBkADoAIAAkACgAJABzAGkAZwBuAGEAdAB1AHIAZQAuAFMAdABhAHQAdQBzACkAIgAKACAAIAAgACAAIAAgACAAIAB9AAoAIAAgACAAIAAgACAAIAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAD0AIAAkAHQAZQBtAHAAbwByAGEAcgB5AFMAYwByAGkAcAB0AAoAIAAgACAAIAB9AAoAIAAgACAAIAAkAHAAdwBzAGgAIAA9ACAAKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAgACAAIAAmACAAJABwAHcAcwBoACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAEAAcwBjAHIAaQBwAHQAQQByAGcAcwAKACAAIAAgACAAaQBmACAAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAIAB7ACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUAIAB9AAoAfQAKAGMAYQB0AGMAaAAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwAKACAAIAAgACAAJABmAGEAaQBsAGUAZAAgAD0AIAAkAHQAcgB1AGUACgB9AAoAZgBpAG4AYQBsAGwAeQAgAHsACgAgACAAIAAgAGkAZgAgACgAJAB0AGUAbQBwAG8AcgBhAHIAeQBTAGMAcgBpAHAAdAApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdABlAG0AcABvAHIAYQByAHkAUwBjAHIAaQBwAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKACAAIAAgACAAfQAKACAAIAAgACAAaQBmACAAKAAkAHAAdQBiAGwAaQBjAEMAZQByAHQAaQBmAGkAYwBhAHQAZQApACAAewAKACAAIAAgACAAIAAgACAAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcAB1AGIAbABpAGMAQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgAgACAAIAAgAH0ACgB9AAoAaQBmACAAKAAkAGYAYQBpAGwAZQBkACkAIAB7ACAAZQB4AGkAdAAgADEAIAB9AAoA +pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAGMAcgBpAHAAdABQAGEAdABoAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkAIAAtAGwAZQAgAFsAaQBuAHQAXQAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAQwA7ACQAaQArACsAKQB7AAoAIAAgACAAIAAkAHMAYwByAGkAcAB0AEEAcgBnAHMAIAArAD0AIABbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAAoAfQAKACQAdABlAG0AcAA9ACQAbgB1AGwAbAAKACQAYwBlAHIAPQAkAG4AdQBsAGwACgAkAGYAYQBpAGwAZQBkAD0AJABmAGEAbABzAGUACgB0AHIAeQAgAHsACgAgACAAIAAgAGkAZgAoAC0AbgBvAHQAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAiAH0ACgAgACAAIAAgAGkAZgAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApAC0AZQBxACAAJwBBAGwAbABTAGkAZwBuAGUAZAAnACkAewAKACAAIAAgACAAIAAgACAAIAAkAHMAdQBiAGoAZQBjAHQAPQAnAEMATgA9AGoAcwBpAGwAdgBlAHIAZABlAHYAIABEAG8AdABmAGkAbABlAHMAIABDAG8AZABlACAAUwBpAGcAbgBpAG4AZwAnAAoAIAAgACAAIAAgACAAIAAgACQAbwBpAGQAPQAnADEALgAzAC4ANgAuADEALgA1AC4ANQAuADcALgAzAC4AMwAnAAoAIAAgACAAIAAgACAAIAAgACQAYwBlAHIAdAA9AEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACQAXwAuAFMAdQBiAGoAZQBjAHQAIAAtAGUAcQAgACQAcwB1AGIAagBlAGMAdAAgAC0AYQBuAGQAIAAkAF8ALgBIAGEAcwBQAHIAaQB2AGEAdABlAEsAZQB5ACAALQBhAG4AZAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQAIABAACgAJABfAC4ARQBuAGgAYQBuAGMAZQBkAEsAZQB5AFUAcwBhAGcAZQBMAGkAcwB0ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAkAF8ALgBPAGIAagBlAGMAdABJAGQALgBWAGEAbAB1AGUAIAAtAGUAcQAgACQAbwBpAGQAfQApAC4AQwBvAHUAbgB0ACAALQBnAHQAIAAwAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAgACAAIAAgACAAIAAgACAAaQBmACgAJABuAHUAbABsACAALQBlAHEAIAAkAGMAZQByAHQAKQB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABjAGUAcgB0AD0ATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAALQBTAHUAYgBqAGUAYwB0ACAAJABzAHUAYgBqAGUAYwB0ACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJABjAGUAcgA9AFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAnAC4AYwBlAHIAJwApAAoAIAAgACAAIAAgACAAIAAgAEUAeABwAG8AcgB0AC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBDAGUAcgB0ACAAJABjAGUAcgB0ACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAGMAZQByACAALQBUAHkAcABlACAAQwBFAFIAVAAgAC0ARgBvAHIAYwBlACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAkAHQAaAB1AG0AYgA9ACQAYwBlAHIAdAAuAFQAaAB1AG0AYgBwAHIAaQBuAHQACgAgACAAIAAgACAAIAAgACAAaQBmACgALQBuAG8AdAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAtAFAAYQB0AGgAIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABSAG8AbwB0ACwAQwBlAHIAdAA6AFwATABvAGMAYQBsAE0AYQBjAGgAaQBuAGUAXABSAG8AbwB0ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJAB0AGgAdQBtAGIAKQApAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgAFIAbwBvAHQAIAAkAGMAZQByACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQB7AHQAaAByAG8AdwAgACcAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgAFIAbwBvAHQAJwB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACgALQBuAG8AdAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABUAHIAdQBzAHQAZQBkAFAAdQBiAGwAaQBzAGgAZQByACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJAB0AGgAdQBtAGIAKQApAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAIAAkAGMAZQByACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQB7AHQAaAByAG8AdwAgACcAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAJwB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJAB0AGUAbQBwAD0AWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACcALgBwAHMAMQAnACkACgAgACAAIAAgACAAIAAgACAAQwBvAHAAeQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAC0ARABlAHMAdABpAG4AYQB0AGkAbwBuACAAJAB0AGUAbQBwACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwA9AEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcAAKACAAIAAgACAAIAAgACAAIABpAGYAKAAkAHMAaQBnAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQB7AHQAaAByAG8AdwAgACIAVABlAG0AcABvAHIAYQByAHkAIABzAGMAcgBpAHAAdAAgAHMAaQBnAG4AYQB0AHUAcgBlACAAaQBzACAAbgBvAHQAIABWAGEAbABpAGQAOgAgACQAKAAkAHMAaQBnAC4AUwB0AGEAdAB1AHMAKQAiAH0ACgAgACAAIAAgACAAIAAgACAAJABzAGMAcgBpAHAAdABQAGEAdABoAD0AJAB0AGUAbQBwAAoAIAAgACAAIAB9AAoAIAAgACAAIAAkAHAAdwBzAGgAPQAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwB0AG8AcAApAC4AUwBvAHUAcgBjAGUACgAgACAAIAAgACYAIAAkAHAAdwBzAGgAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzAAoAIAAgACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAewAkAGYAYQBpAGwAZQBkAD0AJAB0AHIAdQBlAH0ACgB9AAoAYwBhAHQAYwBoACAAewAKACAAIAAgACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfAAoAIAAgACAAIAAkAGYAYQBpAGwAZQBkAD0AJAB0AHIAdQBlAAoAfQAKAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIABpAGYAKAAkAHQAZQBtAHAAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAIAAgACAAaQBmACgAJABjAGUAcgApAHsAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGMAZQByACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKAH0ACgBpAGYAKAAkAGYAYQBpAGwAZQBkACkAewBlAHgAaQB0ACAAMQB9AAoA exit /b %ERRORLEVEL% :usage diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index 162430b..8926593 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -51,13 +51,20 @@ function Get-DotfilesSigningCertificate { $publicCertificatePath = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), ".cer") Export-Certificate -Cert $certificate -FilePath $publicCertificatePath -Type CERT -Force | Out-Null - foreach ($storeName in @("Root", "TrustedPublisher")) { - $storePath = "Cert:\CurrentUser\$storeName" - $trusted = Get-ChildItem -Path $storePath | Where-Object Thumbprint -eq $certificate.Thumbprint - if ($null -eq $trusted) { - & certutil.exe -user -f -addstore $storeName $publicCertificatePath | Out-Null - if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\$storeName." } - } + $trustedRoot = @( + Get-ChildItem -Path Cert:\CurrentUser\Root | Where-Object Thumbprint -eq $certificate.Thumbprint + Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $certificate.Thumbprint + ) + if ($trustedRoot.Count -eq 0) { + & certutil.exe -user -f -addstore Root $publicCertificatePath | Out-Null + if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\Root." } + } + + $trustedPublisher = Get-ChildItem -Path Cert:\CurrentUser\TrustedPublisher | + Where-Object Thumbprint -eq $certificate.Thumbprint + if ($null -eq $trustedPublisher) { + & certutil.exe -user -f -addstore TrustedPublisher $publicCertificatePath | Out-Null + if ($LASTEXITCODE -ne 0) { throw "certutil could not trust CurrentUser\\TrustedPublisher." } } } catch { diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 25efa6b..699bb17 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -24,10 +24,15 @@ $certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object { } | Sort-Object NotAfter -Descending | Select-Object -First 1) if ($certificate.Count -ne 1) { Fail "usable dotfiles Code Signing certificate was not found in CurrentUser\\My" } -foreach ($storeName in @("My", "Root", "TrustedPublisher")) { +foreach ($storeName in @("My", "TrustedPublisher")) { $trusted = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate[0].Thumbprint) if ($trusted.Count -ne 1) { Fail "certificate $($certificate[0].Thumbprint) is missing from CurrentUser\\$storeName" } } +$trustedRoot = @( + Get-ChildItem Cert:\CurrentUser\Root | Where-Object Thumbprint -eq $certificate[0].Thumbprint + Get-ChildItem Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $certificate[0].Thumbprint +) +if ($trustedRoot.Count -eq 0) { Fail "certificate $($certificate[0].Thumbprint) is missing from both CurrentUser\\Root and LocalMachine\\Root" } Write-Host "Certificate: $($certificate[0].Subject) thumbprint=$($certificate[0].Thumbprint) expires=$($certificate[0].NotAfter)" if (Test-Path -LiteralPath $ThumbprintFile) { @@ -43,10 +48,15 @@ function Invoke-Chezmoi([string[]]$Arguments) { } function Assert-CleanChezMoi { - # Always-run scripts intentionally appear as "R" in chezmoi status. - # Exclude scripts so this assertion checks only declarative target drift. + # Always-run scripts appear as "R" and create-only files may legitimately + # differ in the first status column. Only the second column means apply + # still has work to do. $status = @(Invoke-Chezmoi @("status", "--exclude=scripts")) - if ($status.Count -ne 0) { Fail "chezmoi target state is not clean: $($status -join [Environment]::NewLine)" } + $pending = @($status | Where-Object { + $line = [string]$_ + $line.Length -ge 2 -and $line[1] -ne ' ' + }) + if ($pending.Count -ne 0) { Fail "chezmoi has pending target changes: $($pending -join [Environment]::NewLine)" } } $sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 index 11bad45..6e2542b 100644 --- a/tests/windows/assert-state.ps1 +++ b/tests/windows/assert-state.ps1 @@ -25,10 +25,15 @@ function Invoke-Chezmoi([string[]]$Arguments) { } function Assert-CleanChezMoi { - # Always-run scripts intentionally appear as "R" in chezmoi status. - # Exclude scripts so this assertion checks only declarative target drift. + # Always-run scripts appear as "R" and create-only files may legitimately + # differ in the first status column. Only the second column means apply + # still has work to do. $status = @(Invoke-Chezmoi @("status", "--exclude=scripts")) - if ($status.Count -ne 0) { Fail "chezmoi target state is not clean: $($status -join [Environment]::NewLine)" } + $pending = @($status | Where-Object { + $line = [string]$_ + $line.Length -ge 2 -and $line[1] -ne ' ' + }) + if ($pending.Count -ne 0) { Fail "chezmoi has pending target changes: $($pending -join [Environment]::NewLine)" } } Write-Host "chezmoi: $((chezmoi.exe --version) -join ' ')" From 4f9554f6ebf5b3fa35a940a25a7e1410371a44bc Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:24:50 -0500 Subject: [PATCH 31/61] Install Windows core CLI toolchain --- README.md | 2 +- install.ps1 | 81 ++++++++++++++++++------------ tests/windows/assert-allsigned.ps1 | 5 ++ tests/windows/assert-state.ps1 | 6 +++ 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 18a14a9..2f13882 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Run this from a normal `cmd.exe` prompt: curl.exe -fsSLo "%TEMP%\dotfiles-bootstrap.cmd" https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.cmd && call "%TEMP%\dotfiles-bootstrap.cmd" ``` -The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. +The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. Core mode includes the user-scoped CLI toolchain (`micro`, `lsd`, `bat`, `fastfetch`, `fzf`, `fd`, `delta`, `jq`, `rg`, and `mise`); workstation-only packages such as 7-Zip, PowerToys, and VS Code are installed only outside core mode. The bootstrap does not require administrator rights or Developer Mode. It invokes PowerShell 7 with `-NoProfile`; an effective `AllSigned` policy is supported automatically. Each machine reuses or creates its own current-user Code Signing certificate and trusts its public certificate locally. No private key, PFX, or KeePass dependency is used. diff --git a/install.ps1 b/install.ps1 index 254cefd..ab4534d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -81,6 +81,7 @@ function Install-WithWinget { param( [Parameter(Mandatory)][string]$AppId, [string]$Alias, + [ValidateSet("user", "machine")][string]$Scope, [switch]$Update ) @@ -93,14 +94,25 @@ function Install-WithWinget { $installed = $LASTEXITCODE -eq 0 } + $wingetArgs = @( + "--id", $AppId, + "--exact", + "--source", "winget", + "--silent", + "--disable-interactivity", + "--accept-source-agreements", + "--accept-package-agreements" + ) + if ($Scope) { $wingetArgs += @("--scope", $Scope) } + if (-not $installed) { Write-Host "Installing $AppId..." -ForegroundColor Cyan - & winget install --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + & winget install @wingetArgs if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } } elseif ($Update) { Write-Host "Updating $AppId..." -ForegroundColor Yellow - & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + & winget upgrade @wingetArgs if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId." } } else { @@ -111,42 +123,49 @@ function Install-WithWinget { function Install-MustHaveApps { Write-Host "Installing must-have apps..." -ForegroundColor Cyan - if ($CoreOnly) { - Write-Host "Skipping workstation WinGet catalog in core-only mode." -ForegroundColor Yellow - Refresh-Path - if (Get-Command mise -ErrorAction SilentlyContinue) { - & mise which starship *> $null - if ($LASTEXITCODE -ne 0) { - & mise use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } - } + $corePackages = @( + @{ AppId = "zyedidia.micro"; Alias = "micro" }, + @{ AppId = "lsd-rs.lsd"; Alias = "lsd" }, + @{ AppId = "sharkdp.bat"; Alias = "bat" }, + @{ AppId = "Fastfetch-cli.Fastfetch"; Alias = "fastfetch" }, + @{ AppId = "junegunn.fzf"; Alias = "fzf" }, + @{ AppId = "sharkdp.fd"; Alias = "fd" }, + @{ AppId = "dandavison.delta"; Alias = "delta" }, + @{ AppId = "jqlang.jq"; Alias = "jq" }, + @{ AppId = "BurntSushi.ripgrep.MSVC"; Alias = "rg" }, + @{ AppId = "jdx.mise"; Alias = "mise" } + ) + $workstationPackages = @( + @{ AppId = "7zip.7zip"; Alias = $null }, + @{ AppId = "Microsoft.PowerToys"; Alias = $null }, + @{ AppId = "Microsoft.VisualStudioCode"; Alias = "code" } + ) + + foreach ($package in $corePackages) { + Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Scope user -Update:$Update + } + if (-not $CoreOnly) { + foreach ($package in $workstationPackages) { + Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Update:$Update } } else { - $installs = @( - { Install-WithWinget -AppId "7zip.7zip" -Update:$Update }, - { Install-WithWinget -AppId "Microsoft.PowerToys" -Update:$Update }, - { Install-WithWinget -AppId "zyedidia.micro" -Alias "micro" -Update:$Update }, - { Install-WithWinget -AppId "lsd-rs.lsd" -Alias "lsd" -Update:$Update }, - { Install-WithWinget -AppId "sharkdp.bat" -Alias "bat" -Update:$Update }, - { Install-WithWinget -AppId "Fastfetch-cli.Fastfetch" -Alias "fastfetch" -Update:$Update }, - { Install-WithWinget -AppId "junegunn.fzf" -Alias "fzf" -Update:$Update }, - { Install-WithWinget -AppId "sharkdp.fd" -Alias "fd" -Update:$Update }, - { Install-WithWinget -AppId "dandavison.delta" -Alias "delta" -Update:$Update }, - { Install-WithWinget -AppId "jqlang.jq" -Alias "jq" -Update:$Update }, - { Install-WithWinget -AppId "Microsoft.VisualStudioCode" -Alias "code" -Update:$Update }, - { Install-WithWinget -AppId "BurntSushi.ripgrep.MSVC" -Alias "rg" -Update:$Update }, - { Install-WithWinget -AppId "jdx.mise" -Alias "mise" -Update:$Update } - ) - foreach ($install in $installs) { & $install } + Write-Host "Skipping workstation-only WinGet packages in core-only mode." -ForegroundColor Yellow + } - Refresh-Path - if (Get-Command mise -ErrorAction SilentlyContinue) { - & mise use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + Refresh-Path + foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { + if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { + throw "Core CLI tool '$command' is unavailable after WinGet provisioning." } } + & mise which starship *> $null + if ($LASTEXITCODE -ne 0) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + } + foreach ($module in $ManagedModules) { $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 $installedResource = if (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue) { diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 699bb17..0adc2b3 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -61,6 +61,11 @@ function Assert-CleanChezMoi { $sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() Write-Host "chezmoi source-path: $sourcePath" +foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { + if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { + Fail "core CLI tool is unavailable: $command" + } +} Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host Assert-CleanChezMoi diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 index 6e2542b..4590c08 100644 --- a/tests/windows/assert-state.ps1 +++ b/tests/windows/assert-state.ps1 @@ -64,6 +64,12 @@ if ($ExpectedCommit) { if ($LASTEXITCODE -ne 0 -or $sourceCommit -ne $ExpectedCommit) { Fail "source commit $sourceCommit is not expected commit $ExpectedCommit" } } +foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { + if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { + Fail "core CLI tool is unavailable: $command" + } +} + Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host Assert-CleanChezMoi From d710d617047e8fb0ef304acbf7971de324dc6d88 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:25:18 -0500 Subject: [PATCH 32/61] Keep core WinGet installs scope-neutral --- README.md | 2 +- install.ps1 | 18 +++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2f13882..ec4c7e7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Run this from a normal `cmd.exe` prompt: curl.exe -fsSLo "%TEMP%\dotfiles-bootstrap.cmd" https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.cmd && call "%TEMP%\dotfiles-bootstrap.cmd" ``` -The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. Core mode includes the user-scoped CLI toolchain (`micro`, `lsd`, `bat`, `fastfetch`, `fzf`, `fd`, `delta`, `jq`, `rg`, and `mise`); workstation-only packages such as 7-Zip, PowerToys, and VS Code are installed only outside core mode. +The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. Core mode includes the CLI toolchain (`micro`, `lsd`, `bat`, `fastfetch`, `fzf`, `fd`, `delta`, `jq`, `rg`, and `mise`); workstation-only packages such as 7-Zip, PowerToys, and VS Code are installed only outside core mode. The bootstrap does not require administrator rights or Developer Mode. It invokes PowerShell 7 with `-NoProfile`; an effective `AllSigned` policy is supported automatically. Each machine reuses or creates its own current-user Code Signing certificate and trusts its public certificate locally. No private key, PFX, or KeePass dependency is used. diff --git a/install.ps1 b/install.ps1 index ab4534d..ca1be0a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -81,7 +81,6 @@ function Install-WithWinget { param( [Parameter(Mandatory)][string]$AppId, [string]$Alias, - [ValidateSet("user", "machine")][string]$Scope, [switch]$Update ) @@ -94,25 +93,14 @@ function Install-WithWinget { $installed = $LASTEXITCODE -eq 0 } - $wingetArgs = @( - "--id", $AppId, - "--exact", - "--source", "winget", - "--silent", - "--disable-interactivity", - "--accept-source-agreements", - "--accept-package-agreements" - ) - if ($Scope) { $wingetArgs += @("--scope", $Scope) } - if (-not $installed) { Write-Host "Installing $AppId..." -ForegroundColor Cyan - & winget install @wingetArgs + & winget install --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } } elseif ($Update) { Write-Host "Updating $AppId..." -ForegroundColor Yellow - & winget upgrade @wingetArgs + & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId." } } else { @@ -142,7 +130,7 @@ function Install-MustHaveApps { ) foreach ($package in $corePackages) { - Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Scope user -Update:$Update + Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Update:$Update } if (-not $CoreOnly) { foreach ($package in $workstationPackages) { From f6b481944a25befc1c30b3df46c3d4b83dec3e7e Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:31:49 -0500 Subject: [PATCH 33/61] Split Windows CI diagnostics --- .github/workflows/validate.yml | 106 +++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6638172..4826fcf 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -474,6 +474,98 @@ jobs: } finally { Pop-Location } + - name: Probe Windows deployed files and core CLI tools + shell: pwsh + run: | + $requiredFiles = @( + (Join-Path $HOME '.gitconfig'), + (Join-Path $HOME '.gitconfig.local'), + (Join-Path $HOME '.fdignore'), + (Join-Path $HOME '.wslconfig'), + (Join-Path $HOME '.config\starship\config.toml'), + (Join-Path $HOME '.config\pwsh\env.ps1'), + (Join-Path $HOME '.config\pwsh\lib\helpers.ps1'), + (Join-Path $HOME '.config\pwsh\lib\aliases.ps1'), + $PROFILE.CurrentUserAllHosts + ) + foreach ($path in $requiredFiles) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "missing deployed file: $path" } + } + foreach ($command in @('micro','lsd','bat','fastfetch','fzf','fd','delta','jq','rg','mise')) { + $resolved = Get-Command -Name $command -ErrorAction SilentlyContinue + if ($null -eq $resolved) { throw "core CLI tool is unavailable: $command" } + Write-Host "$command -> $($resolved.Source)" + } + $source = (& chezmoi.exe source-path).Trim() + $sourceCommit = (git -C $source rev-parse HEAD).Trim() + if ($sourceCommit -ne $env:GITHUB_SHA) { throw "source commit $sourceCommit is not $env:GITHUB_SHA" } + + - name: Probe Windows chezmoi idempotency + shell: pwsh + run: | + & chezmoi.exe apply + if ($LASTEXITCODE -ne 0) { throw 'first probe apply failed' } + & chezmoi.exe apply + if ($LASTEXITCODE -ne 0) { throw 'second probe apply failed' } + $status = @(& chezmoi.exe status --exclude=scripts) + if ($LASTEXITCODE -ne 0) { throw 'chezmoi status failed' } + $pending = @($status | Where-Object { + $line = [string]$_ + $line.Length -ge 2 -and $line[1] -ne ' ' + }) + $status | ForEach-Object { Write-Host "STATUS: $_" } + if ($pending.Count -ne 0) { throw "chezmoi still has pending target changes: $($pending -join ' | ')" } + + - name: Probe Windows create-only semantics + shell: pwsh + run: | + $config = Join-Path $HOME '.codex\config.toml' + $rules = Join-Path $HOME '.codex\rules\default.rules' + $configMarker = "ci-probe-create-only-$env:GITHUB_RUN_ID" + $rulesMarker = "ci-probe-create-only-rule-$env:GITHUB_RUN_ID" + Add-Content -LiteralPath $config -Value $configMarker + Add-Content -LiteralPath $rules -Value $rulesMarker + & chezmoi.exe apply + if ($LASTEXITCODE -ne 0) { throw 'create-only probe apply failed' } + if (-not ((Get-Content $config -Raw).Contains($configMarker))) { throw 'chezmoi overwrote create-only config.toml' } + if (-not ((Get-Content $rules -Raw).Contains($rulesMarker))) { throw 'chezmoi overwrote create-only default.rules' } + $status = @(& chezmoi.exe status --exclude=scripts) + $pending = @($status | Where-Object { + $line = [string]$_ + $line.Length -ge 2 -and $line[1] -ne ' ' + }) + $status | ForEach-Object { Write-Host "STATUS: $_" } + if ($pending.Count -ne 0) { throw "create-only probe left pending target changes: $($pending -join ' | ')" } + + - name: Probe Windows modules and source cleanliness + shell: pwsh + run: | + $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | + Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } + foreach ($module in $modules) { + if ($module -eq 'git-aliases') { + Import-Module $module -Force -DisableNameChecking -ErrorAction Stop + } else { + Import-Module $module -Force -ErrorAction Stop + } + } + $certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object Subject -eq 'CN=jsilverdev Dotfiles Code Signing') + if ($certificate.Count -ne 0) { throw 'normal-policy probe unexpectedly found the dotfiles signing certificate' } + $source = (& chezmoi.exe source-path).Trim() + $sourceStatus = @(git -C $source status --porcelain) + if ($LASTEXITCODE -ne 0 -or $sourceStatus.Count -ne 0) { throw "chezmoi source is dirty: $($sourceStatus -join ' | ')" } + $repoStatus = @(git -C $env:GITHUB_WORKSPACE status --porcelain) + if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { throw "checkout is dirty: $($repoStatus -join ' | ')" } + + - name: Probe Windows PowerShell profile startup + shell: pwsh + run: | + $profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) + $profileOutput | ForEach-Object { Write-Host $_ } + if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains 'profile-ok')) { + throw "PowerShell profile startup failed: $($profileOutput -join ' | ')" + } + - name: Assert Windows normal-policy state shell: pwsh run: | @@ -631,6 +723,20 @@ jobs: throw "unable to establish effective CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" } + - name: Execute trusted smoke script directly under AllSigned + shell: cmd + run: | + pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok + if errorlevel 1 exit /b 1 + + - name: Execute bridge-equivalent signed copy under AllSigned + shell: cmd + run: | + pwsh.exe -NoProfile -Command "$thumb=(Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim(); $cert=Get-ChildItem Cert:\CurrentUser\My ^| Where-Object Thumbprint -eq $thumb ^| Select-Object -First 1; $src=Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1'; $dst=Join-Path $env:RUNNER_TEMP 'allsigned-direct-copy.ps1'; Copy-Item -LiteralPath $src -Destination $dst -Force; Set-AuthenticodeSignature -FilePath $dst -Certificate $cert -HashAlgorithm SHA256 ^| Out-Null; $sig=Get-AuthenticodeSignature -FilePath $dst; if($sig.Status -ne 'Valid'){throw ('signed copy invalid: '+$sig.Status)}" + if errorlevel 1 exit /b 1 + pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-direct-copy.ps1" -Value bridge-ok + if errorlevel 1 exit /b 1 + - name: Smoke-test signing bridge under AllSigned shell: cmd run: | From 5756e890560c18c7a861674793a352bec253a53f Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:34:35 -0500 Subject: [PATCH 34/61] Refine Windows CI diagnostics --- .github/workflows/validate.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4826fcf..01cbf72 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -537,7 +537,7 @@ jobs: $status | ForEach-Object { Write-Host "STATUS: $_" } if ($pending.Count -ne 0) { throw "create-only probe left pending target changes: $($pending -join ' | ')" } - - name: Probe Windows modules and source cleanliness + - name: Probe Windows managed module imports shell: pwsh run: | $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | @@ -549,12 +549,26 @@ jobs: Import-Module $module -Force -ErrorAction Stop } } + + - name: Probe Windows normal policy did not create signing certificate + shell: pwsh + run: | $certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object Subject -eq 'CN=jsilverdev Dotfiles Code Signing') if ($certificate.Count -ne 0) { throw 'normal-policy probe unexpectedly found the dotfiles signing certificate' } + + - name: Probe Windows chezmoi source Git cleanliness + shell: pwsh + run: | $source = (& chezmoi.exe source-path).Trim() $sourceStatus = @(git -C $source status --porcelain) + $sourceStatus | ForEach-Object { Write-Host "SOURCE STATUS: $_" } if ($LASTEXITCODE -ne 0 -or $sourceStatus.Count -ne 0) { throw "chezmoi source is dirty: $($sourceStatus -join ' | ')" } + + - name: Probe Windows Actions checkout Git cleanliness + shell: pwsh + run: | $repoStatus = @(git -C $env:GITHUB_WORKSPACE status --porcelain) + $repoStatus | ForEach-Object { Write-Host "CHECKOUT STATUS: $_" } if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { throw "checkout is dirty: $($repoStatus -join ' | ')" } - name: Probe Windows PowerShell profile startup @@ -729,11 +743,15 @@ jobs: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - - name: Execute bridge-equivalent signed copy under AllSigned + - name: Create bridge-equivalent signed copy under AllSigned shell: cmd run: | - pwsh.exe -NoProfile -Command "$thumb=(Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim(); $cert=Get-ChildItem Cert:\CurrentUser\My ^| Where-Object Thumbprint -eq $thumb ^| Select-Object -First 1; $src=Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1'; $dst=Join-Path $env:RUNNER_TEMP 'allsigned-direct-copy.ps1'; Copy-Item -LiteralPath $src -Destination $dst -Force; Set-AuthenticodeSignature -FilePath $dst -Certificate $cert -HashAlgorithm SHA256 ^| Out-Null; $sig=Get-AuthenticodeSignature -FilePath $dst; if($sig.Status -ne 'Valid'){throw ('signed copy invalid: '+$sig.Status)}" + pwsh.exe -NoProfile -Command "$thumb=(Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim(); $cert=Get-Item ('Cert:\CurrentUser\My\'+$thumb); $src=Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1'; $dst=Join-Path $env:RUNNER_TEMP 'allsigned-direct-copy.ps1'; Copy-Item -LiteralPath $src -Destination $dst -Force; $null=Set-AuthenticodeSignature -FilePath $dst -Certificate $cert -HashAlgorithm SHA256; $sig=Get-AuthenticodeSignature -FilePath $dst; if($sig.Status -ne 'Valid'){throw ('signed copy invalid: '+$sig.Status+' '+$sig.StatusMessage)}" if errorlevel 1 exit /b 1 + + - name: Execute bridge-equivalent signed copy under AllSigned + shell: cmd + run: | pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-direct-copy.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 From 81a4162c3a1034406da27d6353c76b3d4fd22d23 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:41:21 -0500 Subject: [PATCH 35/61] Fix Windows EOL state and AllSigned bridge --- .gitattributes | 1 + .github/workflows/validate.yml | 14 +--- README.md | 2 +- scripts/windows/invoke-ps-script-bridge.ps1 | 87 +++++++++------------ scripts/windows/invoke-ps-script.cmd | 3 +- scripts/windows/signing.ps1 | 37 +++------ 6 files changed, 53 insertions(+), 91 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 01cbf72..a3405f8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -88,7 +88,7 @@ jobs: cmd = pathlib.Path('scripts/windows/invoke-ps-script.cmd').read_text(encoding='utf-8') source = pathlib.Path('scripts/windows/invoke-ps-script-bridge.ps1').read_text(encoding='utf-8') - match = re.search(r'(?m)^pwsh\.exe -NoProfile -EncodedCommand ([A-Za-z0-9+/=]+)\s*$', cmd) + match = re.search(r'(?m)^"%SystemRoot%\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe" -NoProfile -EncodedCommand ([A-Za-z0-9+/=]+)\s*$', cmd) if not match: raise SystemExit('encoded bridge payload is missing') decoded = base64.b64decode(match.group(1)).decode('utf-16le') @@ -743,18 +743,6 @@ jobs: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - - name: Create bridge-equivalent signed copy under AllSigned - shell: cmd - run: | - pwsh.exe -NoProfile -Command "$thumb=(Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim(); $cert=Get-Item ('Cert:\CurrentUser\My\'+$thumb); $src=Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1'; $dst=Join-Path $env:RUNNER_TEMP 'allsigned-direct-copy.ps1'; Copy-Item -LiteralPath $src -Destination $dst -Force; $null=Set-AuthenticodeSignature -FilePath $dst -Certificate $cert -HashAlgorithm SHA256; $sig=Get-AuthenticodeSignature -FilePath $dst; if($sig.Status -ne 'Valid'){throw ('signed copy invalid: '+$sig.Status+' '+$sig.StatusMessage)}" - if errorlevel 1 exit /b 1 - - - name: Execute bridge-equivalent signed copy under AllSigned - shell: cmd - run: | - pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-direct-copy.ps1" -Value bridge-ok - if errorlevel 1 exit /b 1 - - name: Smoke-test signing bridge under AllSigned shell: cmd run: | diff --git a/README.md b/README.md index ec4c7e7..8301187 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ For dotfiles plus package/application and module updates, use `update.cmd` on Wi The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. -The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\\Root` plus `CurrentUser\\TrustedPublisher`; the runtime helpers also accept `CurrentUser\\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. +The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. When PowerShell 7 requires signed scripts, the bridge uses inbox Windows PowerShell only for Authenticode signing and executes the resulting signed script with PowerShell 7 under the effective policy. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\\Root` plus `CurrentUser\\TrustedPublisher`; the runtime helpers also accept `CurrentUser\\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index dde45cf..855af48 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -1,52 +1,39 @@ $ErrorActionPreference='Stop' -$scriptPath=$env:DOTFILES_PS_SCRIPT -$scriptArgs=@() -for($i=1;$i -le [int]$env:DOTFILES_PS_ARGC;$i++){ - $scriptArgs += [Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i") +$s=$env:DOTFILES_PS_SCRIPT +$a=@() +for($i=1;$i-le[int]$env:DOTFILES_PS_ARGC;$i++){$a+=[Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i")} +$p=$null +$t=$null +$f=$false +try{ + if(!(Test-Path -LiteralPath $s -PathType Leaf)){throw "PowerShell script not found: $s"} + $pw=(Get-Command pwsh.exe -ErrorAction Stop).Source + $p=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') + 'exit 0'|Set-Content -LiteralPath $p -Encoding ASCII + & $pw -NoProfile -File $p *> $null + $need=$LASTEXITCODE-ne0 + if(!$need){ + & $pw -NoProfile -File $s @a + if($LASTEXITCODE-ne0){$f=$true} + }else{ + $h=Join-Path $env:DOTFILES_PS_BRIDGE_DIR 'signing.ps1' + if(!(Test-Path -LiteralPath $h -PathType Leaf)){throw "Signing helper not found: $h"} + $env:DOTFILES_SIGNING_REQUIRED='1' + $sb=[scriptblock]::Create((Get-Content -LiteralPath $h -Raw)) + if([IO.Path]::GetFileName($s)-ieq 'signing.ps1'){ + & $sb @a + }else{ + $t=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') + Copy-Item -LiteralPath $s -Destination $t -Force + & $sb -Action ProtectFiles -Path $t + & $pw -NoProfile -File $t @a + if($LASTEXITCODE-ne0){$f=$true} + } + } +}catch{Write-Error $_;$f=$true} +finally{ + if($p){Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue} + if($t){Remove-Item -LiteralPath $t -Force -ErrorAction SilentlyContinue} + Remove-Item Env:DOTFILES_SIGNING_REQUIRED -ErrorAction SilentlyContinue } -$temp=$null -$cer=$null -$failed=$false -try { - if(-not(Test-Path -LiteralPath $scriptPath -PathType Leaf)){throw "PowerShell script not found: $scriptPath"} - if((Get-ExecutionPolicy)-eq 'AllSigned'){ - $subject='CN=jsilverdev Dotfiles Code Signing' - $oid='1.3.6.1.5.5.7.3.3' - $cert=Get-ChildItem Cert:\CurrentUser\My | - Where-Object {$_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) -and @($_.EnhancedKeyUsageList | Where-Object {$_.ObjectId.Value -eq $oid}).Count -gt 0} | - Sort-Object NotAfter -Descending | - Select-Object -First 1 - if($null -eq $cert){ - $cert=New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 - } - $cer=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.cer') - Export-Certificate -Cert $cert -FilePath $cer -Type CERT -Force | Out-Null - $thumb=$cert.Thumbprint - if(-not(Get-ChildItem -Path Cert:\CurrentUser\Root,Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $thumb)){ - & certutil.exe -user -f -addstore Root $cer | Out-Null - if($LASTEXITCODE){throw 'certutil failed for Root'} - } - if(-not(Get-ChildItem Cert:\CurrentUser\TrustedPublisher | Where-Object Thumbprint -eq $thumb)){ - & certutil.exe -user -f -addstore TrustedPublisher $cer | Out-Null - if($LASTEXITCODE){throw 'certutil failed for TrustedPublisher'} - } - $temp=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') - Copy-Item -LiteralPath $scriptPath -Destination $temp -Force - Set-AuthenticodeSignature -FilePath $temp -Certificate $cert -HashAlgorithm SHA256 | Out-Null - $sig=Get-AuthenticodeSignature -FilePath $temp - if($sig.Status -ne 'Valid'){throw "Temporary script signature is not Valid: $($sig.Status)"} - $scriptPath=$temp - } - $pwsh=(Get-Command pwsh.exe -ErrorAction Stop).Source - & $pwsh -NoProfile -File $scriptPath @scriptArgs - if($LASTEXITCODE -ne 0){$failed=$true} -} -catch { - Write-Error $_ - $failed=$true -} -finally { - if($temp){Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue} - if($cer){Remove-Item -LiteralPath $cer -Force -ErrorAction SilentlyContinue} -} -if($failed){exit 1} +if($f){exit 1} diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index 1eb0599..4327231 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -6,6 +6,7 @@ if "%~1"=="" goto usage rem Bridge arguments are marshalled through environment variables because -EncodedCommand does not accept script arguments. rem Current repository callers use at most eight arguments after the script path. set "DOTFILES_PS_SCRIPT=%~1" +set "DOTFILES_PS_BRIDGE_DIR=%~dp0" set "DOTFILES_PS_ARG1=%~2" set "DOTFILES_PS_ARG2=%~3" set "DOTFILES_PS_ARG3=%~4" @@ -27,7 +28,7 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -pwsh.exe -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAGMAcgBpAHAAdABQAGEAdABoAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABzAGMAcgBpAHAAdABBAHIAZwBzAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkAIAAtAGwAZQAgAFsAaQBuAHQAXQAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AQQBSAEcAQwA7ACQAaQArACsAKQB7AAoAIAAgACAAIAAkAHMAYwByAGkAcAB0AEEAcgBnAHMAIAArAD0AIABbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAAoAfQAKACQAdABlAG0AcAA9ACQAbgB1AGwAbAAKACQAYwBlAHIAPQAkAG4AdQBsAGwACgAkAGYAYQBpAGwAZQBkAD0AJABmAGEAbABzAGUACgB0AHIAeQAgAHsACgAgACAAIAAgAGkAZgAoAC0AbgBvAHQAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAiAH0ACgAgACAAIAAgAGkAZgAoACgARwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQApAC0AZQBxACAAJwBBAGwAbABTAGkAZwBuAGUAZAAnACkAewAKACAAIAAgACAAIAAgACAAIAAkAHMAdQBiAGoAZQBjAHQAPQAnAEMATgA9AGoAcwBpAGwAdgBlAHIAZABlAHYAIABEAG8AdABmAGkAbABlAHMAIABDAG8AZABlACAAUwBpAGcAbgBpAG4AZwAnAAoAIAAgACAAIAAgACAAIAAgACQAbwBpAGQAPQAnADEALgAzAC4ANgAuADEALgA1AC4ANQAuADcALgAzAC4AMwAnAAoAIAAgACAAIAAgACAAIAAgACQAYwBlAHIAdAA9AEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAVwBoAGUAcgBlAC0ATwBiAGoAZQBjAHQAIAB7ACQAXwAuAFMAdQBiAGoAZQBjAHQAIAAtAGUAcQAgACQAcwB1AGIAagBlAGMAdAAgAC0AYQBuAGQAIAAkAF8ALgBIAGEAcwBQAHIAaQB2AGEAdABlAEsAZQB5ACAALQBhAG4AZAAgACQAXwAuAE4AbwB0AEEAZgB0AGUAcgAgAC0AZwB0ACAAKABHAGUAdAAtAEQAYQB0AGUAKQAgAC0AYQBuAGQAIABAACgAJABfAC4ARQBuAGgAYQBuAGMAZQBkAEsAZQB5AFUAcwBhAGcAZQBMAGkAcwB0ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAkAF8ALgBPAGIAagBlAGMAdABJAGQALgBWAGEAbAB1AGUAIAAtAGUAcQAgACQAbwBpAGQAfQApAC4AQwBvAHUAbgB0ACAALQBnAHQAIAAwAH0AIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAUwBvAHIAdAAtAE8AYgBqAGUAYwB0ACAATgBvAHQAQQBmAHQAZQByACAALQBEAGUAcwBjAGUAbgBkAGkAbgBnACAAfAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAtAEYAaQByAHMAdAAgADEACgAgACAAIAAgACAAIAAgACAAaQBmACgAJABuAHUAbABsACAALQBlAHEAIAAkAGMAZQByAHQAKQB7AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJABjAGUAcgB0AD0ATgBlAHcALQBTAGUAbABmAFMAaQBnAG4AZQBkAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAC0AVAB5AHAAZQAgAEMAbwBkAGUAUwBpAGcAbgBpAG4AZwBDAGUAcgB0ACAALQBTAHUAYgBqAGUAYwB0ACAAJABzAHUAYgBqAGUAYwB0ACAALQBDAGUAcgB0AFMAdABvAHIAZQBMAG8AYwBhAHQAaQBvAG4AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABNAHkAIAAtAE4AbwB0AEEAZgB0AGUAcgAgACgARwBlAHQALQBEAGEAdABlACkALgBBAGQAZABZAGUAYQByAHMAKAAxADAAKQAgAC0ASABhAHMAaABBAGwAZwBvAHIAaQB0AGgAbQAgAFMASABBADIANQA2AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJABjAGUAcgA9AFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAnAC4AYwBlAHIAJwApAAoAIAAgACAAIAAgACAAIAAgAEUAeABwAG8AcgB0AC0AQwBlAHIAdABpAGYAaQBjAGEAdABlACAALQBDAGUAcgB0ACAAJABjAGUAcgB0ACAALQBGAGkAbABlAFAAYQB0AGgAIAAkAGMAZQByACAALQBUAHkAcABlACAAQwBFAFIAVAAgAC0ARgBvAHIAYwBlACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAkAHQAaAB1AG0AYgA9ACQAYwBlAHIAdAAuAFQAaAB1AG0AYgBwAHIAaQBuAHQACgAgACAAIAAgACAAIAAgACAAaQBmACgALQBuAG8AdAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIAAtAFAAYQB0AGgAIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABSAG8AbwB0ACwAQwBlAHIAdAA6AFwATABvAGMAYQBsAE0AYQBjAGgAaQBuAGUAXABSAG8AbwB0ACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJAB0AGgAdQBtAGIAKQApAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgAFIAbwBvAHQAIAAkAGMAZQByACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQB7AHQAaAByAG8AdwAgACcAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgAFIAbwBvAHQAJwB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAaQBmACgALQBuAG8AdAAoAEcAZQB0AC0AQwBoAGkAbABkAEkAdABlAG0AIABDAGUAcgB0ADoAXABDAHUAcgByAGUAbgB0AFUAcwBlAHIAXABUAHIAdQBzAHQAZQBkAFAAdQBiAGwAaQBzAGgAZQByACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAVABoAHUAbQBiAHAAcgBpAG4AdAAgAC0AZQBxACAAJAB0AGgAdQBtAGIAKQApAHsACgAgACAAIAAgACAAIAAgACAAIAAgACAAIAAmACAAYwBlAHIAdAB1AHQAaQBsAC4AZQB4AGUAIAAtAHUAcwBlAHIAIAAtAGYAIAAtAGEAZABkAHMAdABvAHIAZQAgAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAIAAkAGMAZQByACAAfAAgAE8AdQB0AC0ATgB1AGwAbAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAKQB7AHQAaAByAG8AdwAgACcAYwBlAHIAdAB1AHQAaQBsACAAZgBhAGkAbABlAGQAIABmAG8AcgAgAFQAcgB1AHMAdABlAGQAUAB1AGIAbABpAHMAaABlAHIAJwB9AAoAIAAgACAAIAAgACAAIAAgAH0ACgAgACAAIAAgACAAIAAgACAAJAB0AGUAbQBwAD0AWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACcALgBwAHMAMQAnACkACgAgACAAIAAgACAAIAAgACAAQwBvAHAAeQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAcwBjAHIAaQBwAHQAUABhAHQAaAAgAC0ARABlAHMAdABpAG4AYQB0AGkAbwBuACAAJAB0AGUAbQBwACAALQBGAG8AcgBjAGUACgAgACAAIAAgACAAIAAgACAAUwBlAHQALQBBAHUAdABoAGUAbgB0AGkAYwBvAGQAZQBTAGkAZwBuAGEAdAB1AHIAZQAgAC0ARgBpAGwAZQBQAGEAdABoACAAJAB0AGUAbQBwACAALQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIAAkAGMAZQByAHQAIAAtAEgAYQBzAGgAQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgAgACAAIAAgACAAIAAgACAAJABzAGkAZwA9AEcAZQB0AC0AQQB1AHQAaABlAG4AdABpAGMAbwBkAGUAUwBpAGcAbgBhAHQAdQByAGUAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAdABlAG0AcAAKACAAIAAgACAAIAAgACAAIABpAGYAKAAkAHMAaQBnAC4AUwB0AGEAdAB1AHMAIAAtAG4AZQAgACcAVgBhAGwAaQBkACcAKQB7AHQAaAByAG8AdwAgACIAVABlAG0AcABvAHIAYQByAHkAIABzAGMAcgBpAHAAdAAgAHMAaQBnAG4AYQB0AHUAcgBlACAAaQBzACAAbgBvAHQAIABWAGEAbABpAGQAOgAgACQAKAAkAHMAaQBnAC4AUwB0AGEAdAB1AHMAKQAiAH0ACgAgACAAIAAgACAAIAAgACAAJABzAGMAcgBpAHAAdABQAGEAdABoAD0AJAB0AGUAbQBwAAoAIAAgACAAIAB9AAoAIAAgACAAIAAkAHAAdwBzAGgAPQAoAEcAZQB0AC0AQwBvAG0AbQBhAG4AZAAgAHAAdwBzAGgALgBlAHgAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwB0AG8AcAApAC4AUwBvAHUAcgBjAGUACgAgACAAIAAgACYAIAAkAHAAdwBzAGgAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABzAGMAcgBpAHAAdABQAGEAdABoACAAQABzAGMAcgBpAHAAdABBAHIAZwBzAAoAIAAgACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFACAALQBuAGUAIAAwACkAewAkAGYAYQBpAGwAZQBkAD0AJAB0AHIAdQBlAH0ACgB9AAoAYwBhAHQAYwBoACAAewAKACAAIAAgACAAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfAAoAIAAgACAAIAAkAGYAYQBpAGwAZQBkAD0AJAB0AHIAdQBlAAoAfQAKAGYAaQBuAGEAbABsAHkAIAB7AAoAIAAgACAAIABpAGYAKAAkAHQAZQBtAHAAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0AGUAbQBwACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAIAAgACAAaQBmACgAJABjAGUAcgApAHsAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGMAZQByACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKAH0ACgBpAGYAKAAkAGYAYQBpAGwAZQBkACkAewBlAHgAaQB0ACAAMQB9AAoA +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHAAPQAkAG4AdQBsAGwACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAHAAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAJwBlAHgAaQB0ACAAMAAnAHwAUwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAIAAtAEUAbgBjAG8AZABpAG4AZwAgAEEAUwBDAEkASQAKACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcAAgACoAPgAgACQAbgB1AGwAbAAKACAAJABuAGUAZQBkAD0AJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAtAG4AZQAwAAoAIABpAGYAKAAhACQAbgBlAGUAZAApAHsACgAgACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAIABpAGYAKAAhACgAVABlAHMAdAAtAFAAYQB0AGgAIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAaAAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAewB0AGgAcgBvAHcAIAAiAFMAaQBnAG4AaQBuAGcAIABoAGUAbABwAGUAcgAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAGgAIgB9AAoAIAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEAD0AJwAxACcACgAgACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAIABpAGYAKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQARgBpAGwAZQBOAGEAbQBlACgAJABzACkALQBpAGUAcQAgACcAcwBpAGcAbgBpAG4AZwAuAHAAcwAxACcAKQB7AAoAIAAgACAAJgAgACQAcwBiACAAQABhAAoAIAAgAH0AZQBsAHMAZQB7AAoAIAAgACAAJAB0AD0AWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACcALgBwAHMAMQAnACkACgAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAgACYAIAAkAHMAYgAgAC0AQQBjAHQAaQBvAG4AIABQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAC0AUABhAHQAaAAgACQAdAAKACAAIAAgACYAIAAkAHAAdwAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHQAIABAAGEACgAgACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAAgAH0ACgAgAH0ACgB9AGMAYQB0AGMAaAB7AFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwA7ACQAZgA9ACQAdAByAHUAZQB9AAoAZgBpAG4AYQBsAGwAeQB7AAoAIABpAGYAKAAkAHAAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAaQBmACgAJAB0ACkAewBSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAH0ACgAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAEUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUwBJAEcATgBJAE4ARwBfAFIARQBRAFUASQBSAEUARAAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKAH0ACgBpAGYAKAAkAGYAKQB7AGUAeABpAHQAIAAxAH0ACgA= exit /b %ERRORLEVEL% :usage diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index 8926593..a845b94 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -104,35 +104,20 @@ function Protect-PowerShellFile { } } -function Test-UserModulePath { - param([string]$ModulePath) +function Get-ManagedModuleFiles { + param([Parameter(Mandatory)][string]$Name) - $fullPath = [IO.Path]::GetFullPath($ModulePath).TrimEnd([IO.Path]::DirectorySeparatorChar) $userRoots = @( (Join-Path $HOME "Documents\PowerShell\Modules"), (Join-Path $HOME ".local\share\powershell\Modules") - ) + @($env:PSModulePath -split [IO.Path]::PathSeparator | Where-Object { $_ -and $_ -like "$HOME*" }) - - foreach ($root in ($userRoots | Select-Object -Unique)) { - $fullRoot = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) - if ($fullPath.Equals($fullRoot, [StringComparison]::OrdinalIgnoreCase) -or - $fullPath.StartsWith($fullRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { - return $true - } - } - - return $false -} - -function Get-ManagedModuleFiles { - param([Parameter(Mandatory)][string]$Name) - - $moduleDirectories = @(Get-Module -ListAvailable -Name $Name | - Where-Object { $_.ModuleBase -and (Test-UserModulePath $_.ModuleBase) } | - Select-Object -ExpandProperty ModuleBase -Unique) + ) + $moduleDirectories = @($userRoots | ForEach-Object { + $candidate = Join-Path $_ $Name + if (Test-Path -LiteralPath $candidate -PathType Container) { $candidate } + } | Select-Object -Unique) if ($moduleDirectories.Count -eq 0) { - throw "Managed PowerShell module '$Name' was not found in a current-user module path." + throw "Managed PowerShell module '$Name' was not found in a current-user PowerShell module root." } $extensions = @("*.ps1", "*.psm1", "*.psd1", "*.ps1xml", "*.cdxml", "*.xaml") @@ -143,9 +128,9 @@ function Get-ManagedModuleFiles { } | Select-Object -ExpandProperty FullName -Unique) } -# Use PowerShell's effective policy instead of implementation-specific registry paths. -# This covers PowerShell 7.6 CurrentUser policy storage and enterprise GPO precedence. -if ((Get-ExecutionPolicy) -ne "AllSigned") { +# The bridge sets DOTFILES_SIGNING_REQUIRED after PowerShell 7 proves that +# unsigned scripts are blocked. Direct/manual calls still use the effective policy. +if ($env:DOTFILES_SIGNING_REQUIRED -ne "1" -and (Get-ExecutionPolicy) -ne "AllSigned") { return } From 51f6c5ac3e9527fd130772c3722a47e1b89eac7c Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:44:13 -0500 Subject: [PATCH 36/61] Route normal Windows scripts directly through pwsh --- .gitattributes | 1 + .github/workflows/validate.yml | 14 +++++ scripts/windows/invoke-ps-script-bridge.ps1 | 33 ++++------ scripts/windows/invoke-ps-script.cmd | 67 ++++++++++++++++++++- 4 files changed, 90 insertions(+), 25 deletions(-) diff --git a/.gitattributes b/.gitattributes index 6313b56..4b887bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto eol=lf +*.cmd text eol=crlf diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a3405f8..9d05c50 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -743,6 +743,20 @@ jobs: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 + - name: Smoke-test Windows PowerShell signer under AllSigned + shell: powershell + run: | + $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-windows-powershell-signer.ps1' + @' + param([string]$Value) + if ($Value -ne 'signer-ok') { throw "unexpected signer value: $Value" } + '@ | Set-Content -LiteralPath $smoke + $env:DOTFILES_SIGNING_REQUIRED = '1' + & "$env:GITHUB_WORKSPACE\scripts\windows\signing.ps1" -Action ProtectFiles -Path $smoke + if ($LASTEXITCODE -ne 0) { throw "Windows PowerShell signing helper failed with exit code $LASTEXITCODE" } + $signature = Get-AuthenticodeSignature -FilePath $smoke + if ($signature.Status -ne 'Valid') { throw "Windows PowerShell signer produced $($signature.Status): $($signature.StatusMessage)" } + - name: Smoke-test signing bridge under AllSigned shell: cmd run: | diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index 855af48..f3b1166 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -2,37 +2,26 @@ $ErrorActionPreference='Stop' $s=$env:DOTFILES_PS_SCRIPT $a=@() for($i=1;$i-le[int]$env:DOTFILES_PS_ARGC;$i++){$a+=[Environment]::GetEnvironmentVariable("DOTFILES_PS_ARG$i")} -$p=$null $t=$null $f=$false try{ if(!(Test-Path -LiteralPath $s -PathType Leaf)){throw "PowerShell script not found: $s"} $pw=(Get-Command pwsh.exe -ErrorAction Stop).Source - $p=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') - 'exit 0'|Set-Content -LiteralPath $p -Encoding ASCII - & $pw -NoProfile -File $p *> $null - $need=$LASTEXITCODE-ne0 - if(!$need){ - & $pw -NoProfile -File $s @a - if($LASTEXITCODE-ne0){$f=$true} + $h=Join-Path $env:DOTFILES_PS_BRIDGE_DIR 'signing.ps1' + if(!(Test-Path -LiteralPath $h -PathType Leaf)){throw "Signing helper not found: $h"} + $env:DOTFILES_SIGNING_REQUIRED='1' + $sb=[scriptblock]::Create((Get-Content -LiteralPath $h -Raw)) + if([IO.Path]::GetFileName($s)-ieq 'signing.ps1'){ + & $sb @a }else{ - $h=Join-Path $env:DOTFILES_PS_BRIDGE_DIR 'signing.ps1' - if(!(Test-Path -LiteralPath $h -PathType Leaf)){throw "Signing helper not found: $h"} - $env:DOTFILES_SIGNING_REQUIRED='1' - $sb=[scriptblock]::Create((Get-Content -LiteralPath $h -Raw)) - if([IO.Path]::GetFileName($s)-ieq 'signing.ps1'){ - & $sb @a - }else{ - $t=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') - Copy-Item -LiteralPath $s -Destination $t -Force - & $sb -Action ProtectFiles -Path $t - & $pw -NoProfile -File $t @a - if($LASTEXITCODE-ne0){$f=$true} - } + $t=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') + Copy-Item -LiteralPath $s -Destination $t -Force + & $sb -Action ProtectFiles -Path $t + & $pw -NoProfile -File $t @a + if($LASTEXITCODE-ne0){$f=$true} } }catch{Write-Error $_;$f=$true} finally{ - if($p){Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue} if($t){Remove-Item -LiteralPath $t -Force -ErrorAction SilentlyContinue} Remove-Item Env:DOTFILES_SIGNING_REQUIRED -ErrorAction SilentlyContinue } diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index 4327231..fe9c88d 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -3,8 +3,7 @@ setlocal EnableExtensions DisableDelayedExpansion if "%~1"=="" goto usage -rem Bridge arguments are marshalled through environment variables because -EncodedCommand does not accept script arguments. -rem Current repository callers use at most eight arguments after the script path. +rem Marshal arguments for the AllSigned signer bridge. set "DOTFILES_PS_SCRIPT=%~1" set "DOTFILES_PS_BRIDGE_DIR=%~dp0" set "DOTFILES_PS_ARG1=%~2" @@ -28,7 +27,65 @@ if defined DOTFILES_PS_ARG8 set "DOTFILES_PS_ARGC=8" where pwsh.exe >nul 2>&1 if errorlevel 1 goto missing_pwsh -"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHAAPQAkAG4AdQBsAGwACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAHAAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAJwBlAHgAaQB0ACAAMAAnAHwAUwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAIAAtAEUAbgBjAG8AZABpAG4AZwAgAEEAUwBDAEkASQAKACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcAAgACoAPgAgACQAbgB1AGwAbAAKACAAJABuAGUAZQBkAD0AJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAtAG4AZQAwAAoAIABpAGYAKAAhACQAbgBlAGUAZAApAHsACgAgACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAcwAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAIABpAGYAKAAhACgAVABlAHMAdAAtAFAAYQB0AGgAIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAaAAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAewB0AGgAcgBvAHcAIAAiAFMAaQBnAG4AaQBuAGcAIABoAGUAbABwAGUAcgAgAG4AbwB0ACAAZgBvAHUAbgBkADoAIAAkAGgAIgB9AAoAIAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEAD0AJwAxACcACgAgACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAIABpAGYAKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQARgBpAGwAZQBOAGEAbQBlACgAJABzACkALQBpAGUAcQAgACcAcwBpAGcAbgBpAG4AZwAuAHAAcwAxACcAKQB7AAoAIAAgACAAJgAgACQAcwBiACAAQABhAAoAIAAgAH0AZQBsAHMAZQB7AAoAIAAgACAAJAB0AD0AWwBJAE8ALgBQAGEAdABoAF0AOgA6AEMAaABhAG4AZwBlAEUAeAB0AGUAbgBzAGkAbwBuACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AFQAZQBtAHAARgBpAGwAZQBOAGEAbQBlACgAKQAsACcALgBwAHMAMQAnACkACgAgACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAgACYAIAAkAHMAYgAgAC0AQQBjAHQAaQBvAG4AIABQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAC0AUABhAHQAaAAgACQAdAAKACAAIAAgACYAIAAkAHAAdwAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHQAIABAAGEACgAgACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAAgAH0ACgAgAH0ACgB9AGMAYQB0AGMAaAB7AFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwA7ACQAZgA9ACQAdAByAHUAZQB9AAoAZgBpAG4AYQBsAGwAeQB7AAoAIABpAGYAKAAkAHAAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABwACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAaQBmACgAJAB0ACkAewBSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAH0ACgAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAEUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUwBJAEcATgBJAE4ARwBfAFIARQBRAFUASQBSAEUARAAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKAH0ACgBpAGYAKAAkAGYAKQB7AGUAeABpAHQAIAAxAH0ACgA= +rem Probe PowerShell 7 with a harmless unsigned script. If it runs, execute the +rem target directly and avoid certificate/signing work entirely. +set "DOTFILES_PS_PROBE=%TEMP%\dotfiles-pwsh-probe-%RANDOM%-%RANDOM%.ps1" +> "%DOTFILES_PS_PROBE%" echo exit 0 +pwsh.exe -NoProfile -File "%DOTFILES_PS_PROBE%" >nul 2>&1 +set "DOTFILES_PS_PROBE_EXIT=%ERRORLEVEL%" +del /q "%DOTFILES_PS_PROBE%" >nul 2>&1 +if "%DOTFILES_PS_PROBE_EXIT%"=="0" goto run_direct + +if not exist "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" goto missing_windows_powershell +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABuAG8AdAAgAGYAbwB1AG4AZAA6ACAAJABoACIAfQAKACAAJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAPQAnADEAJwAKACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAaQBmACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AEYAaQBsAGUATgBhAG0AZQAoACQAcwApAC0AaQBlAHEAIAAnAHMAaQBnAG4AaQBuAGcALgBwAHMAMQAnACkAewAKACAAIAAmACAAJABzAGIAIABAAGEACgAgAH0AZQBsAHMAZQB7AAoAIAAgACQAdAA9AFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAnAC4AcABzADEAJwApAAoAIAAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdAAgAC0ARgBvAHIAYwBlAAoAIAAgACYAIAAkAHMAYgAgAC0AQQBjAHQAaQBvAG4AIABQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAC0AUABhAHQAaAAgACQAdAAKACAAIAAmACAAJABwAHcAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJAB0ACAAQABhAAoAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUALQBuAGUAMAApAHsAJABmAD0AJAB0AHIAdQBlAH0ACgAgAH0ACgB9AGMAYQB0AGMAaAB7AFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwA7ACQAZgA9ACQAdAByAHUAZQB9AAoAZgBpAG4AYQBsAGwAeQB7AAoAIABpAGYAKAAkAHQAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAARQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAfQAKAGkAZgAoACQAZgApAHsAZQB4AGkAdAAgADEAfQAKAA== +exit /b %ERRORLEVEL% + +:run_direct +if "%DOTFILES_PS_ARGC%"=="0" goto run_0 +if "%DOTFILES_PS_ARGC%"=="1" goto run_1 +if "%DOTFILES_PS_ARGC%"=="2" goto run_2 +if "%DOTFILES_PS_ARGC%"=="3" goto run_3 +if "%DOTFILES_PS_ARGC%"=="4" goto run_4 +if "%DOTFILES_PS_ARGC%"=="5" goto run_5 +if "%DOTFILES_PS_ARGC%"=="6" goto run_6 +if "%DOTFILES_PS_ARGC%"=="7" goto run_7 +if "%DOTFILES_PS_ARGC%"=="8" goto run_8 +exit /b 2 + +:run_0 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" +exit /b %ERRORLEVEL% + +:run_1 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" +exit /b %ERRORLEVEL% + +:run_2 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" +exit /b %ERRORLEVEL% + +:run_3 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" +exit /b %ERRORLEVEL% + +:run_4 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" "%DOTFILES_PS_ARG4%" +exit /b %ERRORLEVEL% + +:run_5 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" "%DOTFILES_PS_ARG4%" "%DOTFILES_PS_ARG5%" +exit /b %ERRORLEVEL% + +:run_6 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" "%DOTFILES_PS_ARG4%" "%DOTFILES_PS_ARG5%" "%DOTFILES_PS_ARG6%" +exit /b %ERRORLEVEL% + +:run_7 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" "%DOTFILES_PS_ARG4%" "%DOTFILES_PS_ARG5%" "%DOTFILES_PS_ARG6%" "%DOTFILES_PS_ARG7%" +exit /b %ERRORLEVEL% + +:run_8 +pwsh.exe -NoProfile -File "%DOTFILES_PS_SCRIPT%" "%DOTFILES_PS_ARG1%" "%DOTFILES_PS_ARG2%" "%DOTFILES_PS_ARG3%" "%DOTFILES_PS_ARG4%" "%DOTFILES_PS_ARG5%" "%DOTFILES_PS_ARG6%" "%DOTFILES_PS_ARG7%" "%DOTFILES_PS_ARG8%" exit /b %ERRORLEVEL% :usage @@ -38,3 +95,7 @@ exit /b 2 :missing_pwsh echo PowerShell 7 (pwsh.exe) is required. 1>&2 exit /b 1 + +:missing_windows_powershell +echo Windows PowerShell is required to bootstrap Authenticode signing under AllSigned. 1>&2 +exit /b 1 From b4b2a040b36b9a6f7d46184ee871cba391c217d0 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:48:12 -0500 Subject: [PATCH 37/61] Refine Windows update and AllSigned checks --- .github/workflows/validate.yml | 47 +++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9d05c50..05e301c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -587,11 +587,8 @@ jobs: -RepoRoot $env:GITHUB_WORKSPACE -ExpectedCommit $env:GITHUB_SHA if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - name: Exercise update.cmd against the local remote + - name: Create Windows update fixture shell: pwsh - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' run: | $updateClone = Join-Path $env:RUNNER_TEMP 'dotfiles-update' git clone $env:DOTFILES_REPO $updateClone @@ -603,7 +600,14 @@ jobs: git -c commit.gpgsign=false -C $updateClone commit -m 'CI update fixture' git -C $updateClone push origin HEAD:refs/heads/main if ($LASTEXITCODE -ne 0) { throw 'unable to push Windows update fixture' } + Add-Content -Path $env:GITHUB_ENV -Value "UPDATE_MARKER=$marker" + - name: Exercise Windows update.cmd against the local remote + shell: pwsh + env: + DOTFILES_NONINTERACTIVE: '1' + DOTFILES_CORE_ONLY: '1' + run: | $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' Push-Location $runDirectory try { @@ -613,8 +617,11 @@ jobs: } finally { Pop-Location } + - name: Assert Windows state after update + shell: pwsh + run: | & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` - -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $marker + -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $env:UPDATE_MARKER if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } windows-allsigned: @@ -672,6 +679,12 @@ jobs: if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } '@ | Set-Content -LiteralPath $smoke + $signerSmoke = Join-Path $env:RUNNER_TEMP 'allsigned-signer-smoke.ps1' + @' + param([string]$Value) + if ($Value -ne 'signer-ok') { throw "unexpected signer value: $Value" } + '@ | Set-Content -LiteralPath $signerSmoke + $subject = 'CN=jsilverdev Dotfiles Code Signing' $certificate = Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | @@ -743,19 +756,13 @@ jobs: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 - - name: Smoke-test Windows PowerShell signer under AllSigned - shell: powershell + - name: Smoke-test centralized signer through the bridge under AllSigned + shell: cmd run: | - $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-windows-powershell-signer.ps1' - @' - param([string]$Value) - if ($Value -ne 'signer-ok') { throw "unexpected signer value: $Value" } - '@ | Set-Content -LiteralPath $smoke - $env:DOTFILES_SIGNING_REQUIRED = '1' - & "$env:GITHUB_WORKSPACE\scripts\windows\signing.ps1" -Action ProtectFiles -Path $smoke - if ($LASTEXITCODE -ne 0) { throw "Windows PowerShell signing helper failed with exit code $LASTEXITCODE" } - $signature = Get-AuthenticodeSignature -FilePath $smoke - if ($signature.Status -ne 'Valid') { throw "Windows PowerShell signer produced $($signature.Status): $($signature.StatusMessage)" } + call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\scripts\windows\signing.ps1" -Action ProtectFiles -Path "%RUNNER_TEMP%\allsigned-signer-smoke.ps1" + if errorlevel 1 exit /b 1 + pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-signer-smoke.ps1" -Value signer-ok + if errorlevel 1 exit /b 1 - name: Smoke-test signing bridge under AllSigned shell: cmd @@ -798,7 +805,7 @@ jobs: if errorlevel 1 exit /b 1 echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" - - name: Exercise update.cmd and recheck AllSigned idempotency + - name: Exercise update.cmd under AllSigned shell: cmd env: DOTFILES_NONINTERACTIVE: '1' @@ -806,5 +813,9 @@ jobs: run: | call "%GITHUB_WORKSPACE%\update.cmd" if errorlevel 1 exit /b 1 + + - name: Recheck AllSigned state after update + shell: cmd + run: | call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" -UpdateMarker "%UPDATE_MARKER%" if errorlevel 1 exit /b 1 From 27d04636b8e48798c9198a0fa1339f264eb2e3ac Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:52:24 -0500 Subject: [PATCH 38/61] Fix AllSigned signer args and WinGet no-update handling --- install.ps1 | 8 +++++++- scripts/windows/invoke-ps-script-bridge.ps1 | 20 +++++++++++++++++++- scripts/windows/invoke-ps-script.cmd | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/install.ps1 b/install.ps1 index ca1be0a..25f4c70 100644 --- a/install.ps1 +++ b/install.ps1 @@ -101,7 +101,13 @@ function Install-WithWinget { elseif ($Update) { Write-Host "Updating $AppId..." -ForegroundColor Yellow & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements - if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId." } + $upgradeExitCode = $LASTEXITCODE + if ($upgradeExitCode -eq -1978335189) { + Write-Host "$AppId is already up to date" -ForegroundColor Green + } + elseif ($upgradeExitCode -ne 0) { + throw "WinGet could not update $AppId (exit code $upgradeExitCode)." + } } else { Write-Host "$AppId is already installed" -ForegroundColor Green diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index f3b1166..2aa9331 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -12,7 +12,25 @@ try{ $env:DOTFILES_SIGNING_REQUIRED='1' $sb=[scriptblock]::Create((Get-Content -LiteralPath $h -Raw)) if([IO.Path]::GetFileName($s)-ieq 'signing.ps1'){ - & $sb @a + if(($a.Count%2)-ne0){throw 'Invalid signing helper arguments'} + $action=$null + $path=$null + $module=$null + for($i=0;$i-lt$a.Count;$i+=2){ + switch($a[$i]){ + '-Action'{$action=$a[$i+1]} + '-Path'{$path=$a[$i+1]} + '-ModuleName'{$module=$a[$i+1]} + default{throw "Unsupported signing helper argument: $($a[$i])"} + } + } + if($action-eq'ProtectFiles'){ + if(!$path){throw 'ProtectFiles requires -Path'} + & $sb -Action ProtectFiles -Path $path + }elseif($action-eq'ProtectModule'){ + if(!$module){throw 'ProtectModule requires -ModuleName'} + & $sb -Action ProtectModule -ModuleName $module + }else{throw "Unsupported signing helper action: $action"} }else{ $t=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') Copy-Item -LiteralPath $s -Destination $t -Force diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index fe9c88d..19a4043 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -37,7 +37,7 @@ del /q "%DOTFILES_PS_PROBE%" >nul 2>&1 if "%DOTFILES_PS_PROBE_EXIT%"=="0" goto run_direct if not exist "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" goto missing_windows_powershell -"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABuAG8AdAAgAGYAbwB1AG4AZAA6ACAAJABoACIAfQAKACAAJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAPQAnADEAJwAKACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAaQBmACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AEYAaQBsAGUATgBhAG0AZQAoACQAcwApAC0AaQBlAHEAIAAnAHMAaQBnAG4AaQBuAGcALgBwAHMAMQAnACkAewAKACAAIAAmACAAJABzAGIAIABAAGEACgAgAH0AZQBsAHMAZQB7AAoAIAAgACQAdAA9AFsASQBPAC4AUABhAHQAaABdADoAOgBDAGgAYQBuAGcAZQBFAHgAdABlAG4AcwBpAG8AbgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABUAGUAbQBwAEYAaQBsAGUATgBhAG0AZQAoACkALAAnAC4AcABzADEAJwApAAoAIAAgAEMAbwBwAHkALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHMAIAAtAEQAZQBzAHQAaQBuAGEAdABpAG8AbgAgACQAdAAgAC0ARgBvAHIAYwBlAAoAIAAgACYAIAAkAHMAYgAgAC0AQQBjAHQAaQBvAG4AIABQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAC0AUABhAHQAaAAgACQAdAAKACAAIAAmACAAJABwAHcAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJAB0ACAAQABhAAoAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUALQBuAGUAMAApAHsAJABmAD0AJAB0AHIAdQBlAH0ACgAgAH0ACgB9AGMAYQB0AGMAaAB7AFcAcgBpAHQAZQAtAEUAcgByAG8AcgAgACQAXwA7ACQAZgA9ACQAdAByAHUAZQB9AAoAZgBpAG4AYQBsAGwAeQB7AAoAIABpAGYAKAAkAHQAKQB7AFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJAB0ACAALQBGAG8AcgBjAGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAfQAKACAAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAARQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAAoAfQAKAGkAZgAoACQAZgApAHsAZQB4AGkAdAAgADEAfQAKAA== +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABuAG8AdAAgAGYAbwB1AG4AZAA6ACAAJABoACIAfQAKACAAJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAPQAnADEAJwAKACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAaQBmACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AEYAaQBsAGUATgBhAG0AZQAoACQAcwApAC0AaQBlAHEAIAAnAHMAaQBnAG4AaQBuAGcALgBwAHMAMQAnACkAewAKACAAIABpAGYAKAAoACQAYQAuAEMAbwB1AG4AdAAlADIAKQAtAG4AZQAwACkAewB0AGgAcgBvAHcAIAAnAEkAbgB2AGEAbABpAGQAIABzAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABhAHIAZwB1AG0AZQBuAHQAcwAnAH0ACgAgACAAJABhAGMAdABpAG8AbgA9ACQAbgB1AGwAbAAKACAAIAAkAHAAYQB0AGgAPQAkAG4AdQBsAGwACgAgACAAJABtAG8AZAB1AGwAZQA9ACQAbgB1AGwAbAAKACAAIABmAG8AcgAoACQAaQA9ADAAOwAkAGkALQBsAHQAJABhAC4AQwBvAHUAbgB0ADsAJABpACsAPQAyACkAewAKACAAIAAgAHMAdwBpAHQAYwBoACgAJABhAFsAJABpAF0AKQB7AAoAIAAgACAAIAAnAC0AQQBjAHQAaQBvAG4AJwB7ACQAYQBjAHQAaQBvAG4APQAkAGEAWwAkAGkAKwAxAF0AfQAKACAAIAAgACAAJwAtAFAAYQB0AGgAJwB7ACQAcABhAHQAaAA9ACQAYQBbACQAaQArADEAXQB9AAoAIAAgACAAIAAnAC0ATQBvAGQAdQBsAGUATgBhAG0AZQAnAHsAJABtAG8AZAB1AGwAZQA9ACQAYQBbACQAaQArADEAXQB9AAoAIAAgACAAIABkAGUAZgBhAHUAbAB0AHsAdABoAHIAbwB3ACAAIgBVAG4AcwB1AHAAcABvAHIAdABlAGQAIABzAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABhAHIAZwB1AG0AZQBuAHQAOgAgACQAKAAkAGEAWwAkAGkAXQApACIAfQAKACAAIAAgAH0ACgAgACAAfQAKACAAIABpAGYAKAAkAGEAYwB0AGkAbwBuAC0AZQBxACcAUAByAG8AdABlAGMAdABGAGkAbABlAHMAJwApAHsACgAgACAAIABpAGYAKAAhACQAcABhAHQAaAApAHsAdABoAHIAbwB3ACAAJwBQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAHIAZQBxAHUAaQByAGUAcwAgAC0AUABhAHQAaAAnAH0ACgAgACAAIAAmACAAJABzAGIAIAAtAEEAYwB0AGkAbwBuACAAUAByAG8AdABlAGMAdABGAGkAbABlAHMAIAAtAFAAYQB0AGgAIAAkAHAAYQB0AGgACgAgACAAfQBlAGwAcwBlAGkAZgAoACQAYQBjAHQAaQBvAG4ALQBlAHEAJwBQAHIAbwB0AGUAYwB0AE0AbwBkAHUAbABlACcAKQB7AAoAIAAgACAAaQBmACgAIQAkAG0AbwBkAHUAbABlACkAewB0AGgAcgBvAHcAIAAnAFAAcgBvAHQAZQBjAHQATQBvAGQAdQBsAGUAIAByAGUAcQB1AGkAcgBlAHMAIAAtAE0AbwBkAHUAbABlAE4AYQBtAGUAJwB9AAoAIAAgACAAJgAgACQAcwBiACAALQBBAGMAdABpAG8AbgAgAFAAcgBvAHQAZQBjAHQATQBvAGQAdQBsAGUAIAAtAE0AbwBkAHUAbABlAE4AYQBtAGUAIAAkAG0AbwBkAHUAbABlAAoAIAAgAH0AZQBsAHMAZQB7AHQAaAByAG8AdwAgACIAVQBuAHMAdQBwAHAAbwByAHQAZQBkACAAcwBpAGcAbgBpAG4AZwAgAGgAZQBsAHAAZQByACAAYQBjAHQAaQBvAG4AOgAgACQAYQBjAHQAaQBvAG4AIgB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAHQAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAmACAAJABzAGIAIAAtAEEAYwB0AGkAbwBuACAAUAByAG8AdABlAGMAdABGAGkAbABlAHMAIAAtAFAAYQB0AGgAIAAkAHQACgAgACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAdAAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AAoAfQBjAGEAdABjAGgAewBXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8AOwAkAGYAPQAkAHQAcgB1AGUAfQAKAGYAaQBuAGEAbABsAHkAewAKACAAaQBmACgAJAB0ACkAewBSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAH0ACgAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAEUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUwBJAEcATgBJAE4ARwBfAFIARQBRAFUASQBSAEUARAAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKAH0ACgBpAGYAKAAkAGYAKQB7AGUAeABpAHQAIAAxAH0ACgA= exit /b %ERRORLEVEL% :run_direct From d22629fe0d67fe0415e607f983b8bccac0241c8f Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:55:58 -0500 Subject: [PATCH 39/61] Skip WinGet upgrades when no update is available --- install.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index 25f4c70..075c8e7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -99,14 +99,23 @@ function Install-WithWinget { if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } } elseif ($Update) { - Write-Host "Updating $AppId..." -ForegroundColor Yellow - & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements - $upgradeExitCode = $LASTEXITCODE - if ($upgradeExitCode -eq -1978335189) { + $upgradeCandidates = @( + & winget list --upgrade-available --id $AppId --exact --source winget --accept-source-agreements 2>&1 | + ForEach-Object { [string]$_ } + ) + $upgradeAvailable = @($upgradeCandidates | Where-Object { + $_ -match [regex]::Escape($AppId) + }).Count -gt 0 + + if (-not $upgradeAvailable) { Write-Host "$AppId is already up to date" -ForegroundColor Green } - elseif ($upgradeExitCode -ne 0) { - throw "WinGet could not update $AppId (exit code $upgradeExitCode)." + else { + Write-Host "Updating $AppId..." -ForegroundColor Yellow + & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { + throw "WinGet could not update $AppId (exit code $LASTEXITCODE)." + } } } else { From 13a094efb2c9284cc66bce289d86dca17dcb3a70 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:59:34 -0500 Subject: [PATCH 40/61] Run AllSigned signing helper as a Windows PowerShell script --- scripts/windows/invoke-ps-script-bridge.ps1 | 27 +++++---------------- scripts/windows/invoke-ps-script.cmd | 2 +- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/scripts/windows/invoke-ps-script-bridge.ps1 b/scripts/windows/invoke-ps-script-bridge.ps1 index 2aa9331..1ed1654 100644 --- a/scripts/windows/invoke-ps-script-bridge.ps1 +++ b/scripts/windows/invoke-ps-script-bridge.ps1 @@ -7,34 +7,19 @@ $f=$false try{ if(!(Test-Path -LiteralPath $s -PathType Leaf)){throw "PowerShell script not found: $s"} $pw=(Get-Command pwsh.exe -ErrorAction Stop).Source + $ps=Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + if(!(Test-Path -LiteralPath $ps -PathType Leaf)){throw "Windows PowerShell not found: $ps"} $h=Join-Path $env:DOTFILES_PS_BRIDGE_DIR 'signing.ps1' if(!(Test-Path -LiteralPath $h -PathType Leaf)){throw "Signing helper not found: $h"} $env:DOTFILES_SIGNING_REQUIRED='1' - $sb=[scriptblock]::Create((Get-Content -LiteralPath $h -Raw)) if([IO.Path]::GetFileName($s)-ieq 'signing.ps1'){ - if(($a.Count%2)-ne0){throw 'Invalid signing helper arguments'} - $action=$null - $path=$null - $module=$null - for($i=0;$i-lt$a.Count;$i+=2){ - switch($a[$i]){ - '-Action'{$action=$a[$i+1]} - '-Path'{$path=$a[$i+1]} - '-ModuleName'{$module=$a[$i+1]} - default{throw "Unsupported signing helper argument: $($a[$i])"} - } - } - if($action-eq'ProtectFiles'){ - if(!$path){throw 'ProtectFiles requires -Path'} - & $sb -Action ProtectFiles -Path $path - }elseif($action-eq'ProtectModule'){ - if(!$module){throw 'ProtectModule requires -ModuleName'} - & $sb -Action ProtectModule -ModuleName $module - }else{throw "Unsupported signing helper action: $action"} + & $ps -NoProfile -File $h @a + if($LASTEXITCODE-ne0){$f=$true} }else{ $t=[IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(),'.ps1') Copy-Item -LiteralPath $s -Destination $t -Force - & $sb -Action ProtectFiles -Path $t + & $ps -NoProfile -File $h -Action ProtectFiles -Path $t + if($LASTEXITCODE-ne0){throw "Signing helper failed with exit code $LASTEXITCODE"} & $pw -NoProfile -File $t @a if($LASTEXITCODE-ne0){$f=$true} } diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index 19a4043..7b83ba9 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -37,7 +37,7 @@ del /q "%DOTFILES_PS_PROBE%" >nul 2>&1 if "%DOTFILES_PS_PROBE_EXIT%"=="0" goto run_direct if not exist "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" goto missing_windows_powershell -"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAGgAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBCAFIASQBEAEcARQBfAEQASQBSACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFAAYQB0AGgAVAB5AHAAZQAgAEwAZQBhAGYAKQApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABuAG8AdAAgAGYAbwB1AG4AZAA6ACAAJABoACIAfQAKACAAJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAPQAnADEAJwAKACAAJABzAGIAPQBbAHMAYwByAGkAcAB0AGIAbABvAGMAawBdADoAOgBDAHIAZQBhAHQAZQAoACgARwBlAHQALQBDAG8AbgB0AGUAbgB0ACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAGgAIAAtAFIAYQB3ACkAKQAKACAAaQBmACgAWwBJAE8ALgBQAGEAdABoAF0AOgA6AEcAZQB0AEYAaQBsAGUATgBhAG0AZQAoACQAcwApAC0AaQBlAHEAIAAnAHMAaQBnAG4AaQBuAGcALgBwAHMAMQAnACkAewAKACAAIABpAGYAKAAoACQAYQAuAEMAbwB1AG4AdAAlADIAKQAtAG4AZQAwACkAewB0AGgAcgBvAHcAIAAnAEkAbgB2AGEAbABpAGQAIABzAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABhAHIAZwB1AG0AZQBuAHQAcwAnAH0ACgAgACAAJABhAGMAdABpAG8AbgA9ACQAbgB1AGwAbAAKACAAIAAkAHAAYQB0AGgAPQAkAG4AdQBsAGwACgAgACAAJABtAG8AZAB1AGwAZQA9ACQAbgB1AGwAbAAKACAAIABmAG8AcgAoACQAaQA9ADAAOwAkAGkALQBsAHQAJABhAC4AQwBvAHUAbgB0ADsAJABpACsAPQAyACkAewAKACAAIAAgAHMAdwBpAHQAYwBoACgAJABhAFsAJABpAF0AKQB7AAoAIAAgACAAIAAnAC0AQQBjAHQAaQBvAG4AJwB7ACQAYQBjAHQAaQBvAG4APQAkAGEAWwAkAGkAKwAxAF0AfQAKACAAIAAgACAAJwAtAFAAYQB0AGgAJwB7ACQAcABhAHQAaAA9ACQAYQBbACQAaQArADEAXQB9AAoAIAAgACAAIAAnAC0ATQBvAGQAdQBsAGUATgBhAG0AZQAnAHsAJABtAG8AZAB1AGwAZQA9ACQAYQBbACQAaQArADEAXQB9AAoAIAAgACAAIABkAGUAZgBhAHUAbAB0AHsAdABoAHIAbwB3ACAAIgBVAG4AcwB1AHAAcABvAHIAdABlAGQAIABzAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABhAHIAZwB1AG0AZQBuAHQAOgAgACQAKAAkAGEAWwAkAGkAXQApACIAfQAKACAAIAAgAH0ACgAgACAAfQAKACAAIABpAGYAKAAkAGEAYwB0AGkAbwBuAC0AZQBxACcAUAByAG8AdABlAGMAdABGAGkAbABlAHMAJwApAHsACgAgACAAIABpAGYAKAAhACQAcABhAHQAaAApAHsAdABoAHIAbwB3ACAAJwBQAHIAbwB0AGUAYwB0AEYAaQBsAGUAcwAgAHIAZQBxAHUAaQByAGUAcwAgAC0AUABhAHQAaAAnAH0ACgAgACAAIAAmACAAJABzAGIAIAAtAEEAYwB0AGkAbwBuACAAUAByAG8AdABlAGMAdABGAGkAbABlAHMAIAAtAFAAYQB0AGgAIAAkAHAAYQB0AGgACgAgACAAfQBlAGwAcwBlAGkAZgAoACQAYQBjAHQAaQBvAG4ALQBlAHEAJwBQAHIAbwB0AGUAYwB0AE0AbwBkAHUAbABlACcAKQB7AAoAIAAgACAAaQBmACgAIQAkAG0AbwBkAHUAbABlACkAewB0AGgAcgBvAHcAIAAnAFAAcgBvAHQAZQBjAHQATQBvAGQAdQBsAGUAIAByAGUAcQB1AGkAcgBlAHMAIAAtAE0AbwBkAHUAbABlAE4AYQBtAGUAJwB9AAoAIAAgACAAJgAgACQAcwBiACAALQBBAGMAdABpAG8AbgAgAFAAcgBvAHQAZQBjAHQATQBvAGQAdQBsAGUAIAAtAE0AbwBkAHUAbABlAE4AYQBtAGUAIAAkAG0AbwBkAHUAbABlAAoAIAAgAH0AZQBsAHMAZQB7AHQAaAByAG8AdwAgACIAVQBuAHMAdQBwAHAAbwByAHQAZQBkACAAcwBpAGcAbgBpAG4AZwAgAGgAZQBsAHAAZQByACAAYQBjAHQAaQBvAG4AOgAgACQAYQBjAHQAaQBvAG4AIgB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAHQAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAmACAAJABzAGIAIAAtAEEAYwB0AGkAbwBuACAAUAByAG8AdABlAGMAdABGAGkAbABlAHMAIAAtAFAAYQB0AGgAIAAkAHQACgAgACAAJgAgACQAcAB3ACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAdAAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AAoAfQBjAGEAdABjAGgAewBXAHIAaQB0AGUALQBFAHIAcgBvAHIAIAAkAF8AOwAkAGYAPQAkAHQAcgB1AGUAfQAKAGYAaQBuAGEAbABsAHkAewAKACAAaQBmACgAJAB0ACkAewBSAGUAbQBvAHYAZQAtAEkAdABlAG0AIAAtAEwAaQB0AGUAcgBhAGwAUABhAHQAaAAgACQAdAAgAC0ARgBvAHIAYwBlACAALQBFAHIAcgBvAHIAQQBjAHQAaQBvAG4AIABTAGkAbABlAG4AdABsAHkAQwBvAG4AdABpAG4AdQBlAH0ACgAgAFIAZQBtAG8AdgBlAC0ASQB0AGUAbQAgAEUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUwBJAEcATgBJAE4ARwBfAFIARQBRAFUASQBSAEUARAAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAKAH0ACgBpAGYAKAAkAGYAKQB7AGUAeABpAHQAIAAxAH0ACgA= +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAHAAcwA9AEoAbwBpAG4ALQBQAGEAdABoACAAJABlAG4AdgA6AFMAeQBzAHQAZQBtAFIAbwBvAHQAIAAnAFMAeQBzAHQAZQBtADMAMgBcAFcAaQBuAGQAbwB3AHMAUABvAHcAZQByAFMAaABlAGwAbABcAHYAMQAuADAAXABwAG8AdwBlAHIAcwBoAGUAbABsAC4AZQB4AGUAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAcwAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAewB0AGgAcgBvAHcAIAAiAFcAaQBuAGQAbwB3AHMAIABQAG8AdwBlAHIAUwBoAGUAbABsACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcABzACIAfQAKACAAJABoAD0ASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AQgBSAEkARABHAEUAXwBEAEkAUgAgACcAcwBpAGcAbgBpAG4AZwAuAHAAcwAxACcACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABoACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUwBpAGcAbgBpAG4AZwAgAGgAZQBsAHAAZQByACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAaAAiAH0ACgAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEAD0AJwAxACcACgAgAGkAZgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABGAGkAbABlAE4AYQBtAGUAKAAkAHMAKQAtAGkAZQBxACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwApAHsACgAgACAAJgAgACQAcABzACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAaAAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAHQAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAmACAAJABwAHMAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABoACAALQBBAGMAdABpAG8AbgAgAFAAcgBvAHQAZQBjAHQARgBpAGwAZQBzACAALQBQAGEAdABoACAAJAB0AAoAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUALQBuAGUAMAApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABmAGEAaQBsAGUAZAAgAHcAaQB0AGgAIABlAHgAaQB0ACAAYwBvAGQAZQAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIgB9AAoAIAAgACYAIAAkAHAAdwAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHQAIABAAGEACgAgACAAaQBmACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAtAG4AZQAwACkAewAkAGYAPQAkAHQAcgB1AGUAfQAKACAAfQAKAH0AYwBhAHQAYwBoAHsAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfADsAJABmAD0AJAB0AHIAdQBlAH0ACgBmAGkAbgBhAGwAbAB5AHsACgAgAGkAZgAoACQAdAApAHsAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQB9AAoAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIABFAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgB9AAoAaQBmACgAJABmACkAewBlAHgAaQB0ACAAMQB9AAoA exit /b %ERRORLEVEL% :run_direct From 7646af1f3ded6a4ea6283d4255d194e562ca70e8 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:03:59 -0500 Subject: [PATCH 41/61] Make code-signing EKU validation host-independent --- scripts/windows/signing.ps1 | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index a845b94..7069d19 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -11,6 +11,7 @@ param( $ErrorActionPreference = "Stop" $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" +$enhancedKeyUsageOid = "2.5.29.37" function Test-CodeSigningCertificate { param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) @@ -19,7 +20,25 @@ function Test-CodeSigningCertificate { return $false } - return @($Certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 + # Read the X.509 EKU extension directly instead of relying on the + # PowerShell certificate provider's EnhancedKeyUsageList projection. + # This behaves consistently in PowerShell 7 and Windows PowerShell 5.1. + $ekuExtension = $Certificate.Extensions | + Where-Object { $_.Oid.Value -eq $enhancedKeyUsageOid } | + Select-Object -First 1 + if ($null -eq $ekuExtension) { + return $false + } + + try { + $eku = New-Object System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension + $eku.CopyFrom($ekuExtension) + } + catch { + return $false + } + + return @($eku.EnhancedKeyUsages | Where-Object { $_.Value -eq $codeSigningOid }).Count -gt 0 } function Get-DotfilesSigningCertificate { From 22acd0959d809a4fe5fea34e861e918c944772e3 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:09:13 -0500 Subject: [PATCH 42/61] Expose AllSigned bootstrap failure stage --- .github/workflows/validate.yml | 88 +++++++++++++++++++++++++++++++--- bootstrap.cmd | 19 +++++++- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 05e301c..dee391c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -771,17 +771,91 @@ jobs: if errorlevel 1 exit /b 1 - name: Run the real bootstrap through CMD and the signing bridge - shell: cmd + id: allsigned_bootstrap + continue-on-error: true + shell: pwsh env: DOTFILES_NONINTERACTIVE: '1' DOTFILES_CORE_ONLY: '1' run: | - set "RUN_DIRECTORY=%RUNNER_TEMP%\dotfiles-run" - if not exist "%RUN_DIRECTORY%" mkdir "%RUN_DIRECTORY%" - pushd "%RUN_DIRECTORY%" - call "%GITHUB_WORKSPACE%\bootstrap.cmd" - if errorlevel 1 exit /b 1 - popd + $stageFile = Join-Path $env:RUNNER_TEMP 'allsigned-bootstrap-stage.txt' + $env:DOTFILES_BOOTSTRAP_STAGE_FILE = $stageFile + $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' + New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null + Push-Location $runDirectory + try { + & cmd.exe /d /c "$env:GITHUB_WORKSPACE\bootstrap.cmd" + $exitCode = $LASTEXITCODE + } + finally { + Pop-Location + } + $stage = if (Test-Path -LiteralPath $stageFile) { + (Get-Content -LiteralPath $stageFile -Raw).Trim() + } else { + 'unknown' + } + "stage=$stage" | Add-Content -Path $env:GITHUB_OUTPUT + "exit_code=$exitCode" | Add-Content -Path $env:GITHUB_OUTPUT + Write-Host "AllSigned bootstrap stage=$stage exit=$exitCode" + if ($exitCode -ne 0) { + throw "bootstrap.cmd failed during stage '$stage' with exit code $exitCode" + } + + - name: AllSigned bootstrap failed during legacy cleanup + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'cleanup' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while validating WinGet + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'winget' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while ensuring Git + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_git' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while ensuring PowerShell 7 + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_pwsh' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while ensuring mise + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_mise' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while ensuring chezmoi + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_chezmoi' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed while refreshing PATH + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'refresh_path' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed during chezmoi init/apply + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'initialize_chezmoi' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed resolving the repo root + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'resolve_repo_root' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed inside install.ps1 + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'install' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed at an unknown stage + if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["cleanup","winget","ensure_git","ensure_pwsh","ensure_mise","ensure_chezmoi","refresh_path","initialize_chezmoi","resolve_repo_root","install"]'), steps.allsigned_bootstrap.outputs.stage) + shell: cmd + run: exit /b 1 - name: Assert signed runtime and module state through the bridge shell: cmd diff --git a/bootstrap.cmd b/bootstrap.cmd index 74864e9..3b79ea8 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -7,9 +7,11 @@ if defined DOTFILES_REPO ( set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" ) +call :mark_stage cleanup call :remove_legacy_broken_links if errorlevel 1 exit /b 1 +call :mark_stage winget where winget.exe >nul 2>&1 if errorlevel 1 ( echo WinGet is not registered for this user. Attempting App Installer registration... @@ -25,27 +27,34 @@ if errorlevel 1 ( exit /b 1 ) +call :mark_stage ensure_git call :ensure_package Git.Git git if errorlevel 1 exit /b 1 +call :mark_stage ensure_pwsh call :ensure_package Microsoft.PowerShell pwsh if errorlevel 1 exit /b 1 +call :mark_stage ensure_mise call :ensure_package jdx.mise mise if errorlevel 1 exit /b 1 +call :mark_stage ensure_chezmoi call :ensure_package twpayne.chezmoi chezmoi if errorlevel 1 exit /b 1 +call :mark_stage refresh_path call :refresh_path where git.exe >nul 2>&1 || (echo Git is still unavailable after installation. 1>&2 & exit /b 1) where pwsh.exe >nul 2>&1 || (echo PowerShell 7 is still unavailable after installation. 1>&2 & exit /b 1) where mise.exe >nul 2>&1 || (echo mise is still unavailable after installation. 1>&2 & exit /b 1) where chezmoi.exe >nul 2>&1 || (echo chezmoi is still unavailable after installation. 1>&2 & exit /b 1) +call :mark_stage initialize_chezmoi call :initialize_chezmoi if errorlevel 1 ( echo chezmoi initialization/update failed. 1>&2 exit /b 1 ) +call :mark_stage resolve_repo_root call :resolve_repo_root if not defined REPO_ROOT ( echo Unable to resolve the chezmoi working tree. 1>&2 @@ -56,6 +65,7 @@ if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( exit /b 1 ) +call :mark_stage install if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -CoreOnly -RepoRoot "%REPO_ROOT%" ) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( @@ -65,7 +75,14 @@ if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( ) else ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" ) -exit /b %ERRORLEVEL% +set "BOOTSTRAP_EXIT=%ERRORLEVEL%" +if not "%BOOTSTRAP_EXIT%"=="0" exit /b %BOOTSTRAP_EXIT% +call :mark_stage complete +exit /b 0 + +:mark_stage +if defined DOTFILES_BOOTSTRAP_STAGE_FILE >"%DOTFILES_BOOTSTRAP_STAGE_FILE%" echo %~1 +exit /b 0 :ensure_package set "PACKAGE_ID=%~1" From b61ba0212d3e05a89bdcf0feee1c03782b5cd928 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:13:42 -0500 Subject: [PATCH 43/61] Map AllSigned bootstrap failure stages --- .github/workflows/validate.yml | 37 +++++++---------- bootstrap.cmd | 74 ++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 52 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index dee391c..69701ad 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -777,9 +777,8 @@ jobs: env: DOTFILES_NONINTERACTIVE: '1' DOTFILES_CORE_ONLY: '1' + DOTFILES_BOOTSTRAP_DIAGNOSTICS: '1' run: | - $stageFile = Join-Path $env:RUNNER_TEMP 'allsigned-bootstrap-stage.txt' - $env:DOTFILES_BOOTSTRAP_STAGE_FILE = $stageFile $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null Push-Location $runDirectory @@ -790,70 +789,64 @@ jobs: finally { Pop-Location } - $stage = if (Test-Path -LiteralPath $stageFile) { - (Get-Content -LiteralPath $stageFile -Raw).Trim() - } else { - 'unknown' - } - "stage=$stage" | Add-Content -Path $env:GITHUB_OUTPUT "exit_code=$exitCode" | Add-Content -Path $env:GITHUB_OUTPUT - Write-Host "AllSigned bootstrap stage=$stage exit=$exitCode" + Write-Host "AllSigned bootstrap exit=$exitCode" if ($exitCode -ne 0) { - throw "bootstrap.cmd failed during stage '$stage' with exit code $exitCode" + throw "bootstrap.cmd failed with diagnostic exit code $exitCode" } - name: AllSigned bootstrap failed during legacy cleanup - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'cleanup' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '11' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while validating WinGet - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'winget' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '12' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while ensuring Git - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_git' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '21' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while ensuring PowerShell 7 - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_pwsh' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '22' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while ensuring mise - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_mise' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '23' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while ensuring chezmoi - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'ensure_chezmoi' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '24' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed while refreshing PATH - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'refresh_path' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '25' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed during chezmoi init/apply - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'initialize_chezmoi' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '31' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed resolving the repo root - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'resolve_repo_root' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '32' shell: cmd run: exit /b 1 - name: AllSigned bootstrap failed inside install.ps1 - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.stage == 'install' + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '40' shell: cmd run: exit /b 1 - - name: AllSigned bootstrap failed at an unknown stage - if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["cleanup","winget","ensure_git","ensure_pwsh","ensure_mise","ensure_chezmoi","refresh_path","initialize_chezmoi","resolve_repo_root","install"]'), steps.allsigned_bootstrap.outputs.stage) + - name: AllSigned bootstrap failed with an unmapped diagnostic code + if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["11","12","21","22","23","24","25","31","32","40"]'), steps.allsigned_bootstrap.outputs.exit_code) shell: cmd run: exit /b 1 diff --git a/bootstrap.cmd b/bootstrap.cmd index 3b79ea8..ed05f84 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -7,65 +7,81 @@ if defined DOTFILES_REPO ( set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" ) -call :mark_stage cleanup +set "BOOTSTRAP_FAILURE_CODE=11" call :remove_legacy_broken_links -if errorlevel 1 exit /b 1 +if errorlevel 1 goto bootstrap_failed -call :mark_stage winget +set "BOOTSTRAP_FAILURE_CODE=12" where winget.exe >nul 2>&1 if errorlevel 1 ( echo WinGet is not registered for this user. Attempting App Installer registration... "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -Command "Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" if errorlevel 1 ( echo App Installer registration failed. WinGet may be disabled by corporate policy. 1>&2 - exit /b 1 + goto bootstrap_failed ) ) where winget.exe >nul 2>&1 if errorlevel 1 ( echo WinGet is unavailable after App Installer registration. Check corporate policy or App Installer registration. 1>&2 - exit /b 1 + goto bootstrap_failed ) -call :mark_stage ensure_git +set "BOOTSTRAP_FAILURE_CODE=21" call :ensure_package Git.Git git -if errorlevel 1 exit /b 1 -call :mark_stage ensure_pwsh +if errorlevel 1 goto bootstrap_failed +set "BOOTSTRAP_FAILURE_CODE=22" call :ensure_package Microsoft.PowerShell pwsh -if errorlevel 1 exit /b 1 -call :mark_stage ensure_mise +if errorlevel 1 goto bootstrap_failed +set "BOOTSTRAP_FAILURE_CODE=23" call :ensure_package jdx.mise mise -if errorlevel 1 exit /b 1 -call :mark_stage ensure_chezmoi +if errorlevel 1 goto bootstrap_failed +set "BOOTSTRAP_FAILURE_CODE=24" call :ensure_package twpayne.chezmoi chezmoi -if errorlevel 1 exit /b 1 +if errorlevel 1 goto bootstrap_failed -call :mark_stage refresh_path +set "BOOTSTRAP_FAILURE_CODE=25" call :refresh_path -where git.exe >nul 2>&1 || (echo Git is still unavailable after installation. 1>&2 & exit /b 1) -where pwsh.exe >nul 2>&1 || (echo PowerShell 7 is still unavailable after installation. 1>&2 & exit /b 1) -where mise.exe >nul 2>&1 || (echo mise is still unavailable after installation. 1>&2 & exit /b 1) -where chezmoi.exe >nul 2>&1 || (echo chezmoi is still unavailable after installation. 1>&2 & exit /b 1) +where git.exe >nul 2>&1 +if errorlevel 1 ( + echo Git is still unavailable after installation. 1>&2 + goto bootstrap_failed +) +where pwsh.exe >nul 2>&1 +if errorlevel 1 ( + echo PowerShell 7 is still unavailable after installation. 1>&2 + goto bootstrap_failed +) +where mise.exe >nul 2>&1 +if errorlevel 1 ( + echo mise is still unavailable after installation. 1>&2 + goto bootstrap_failed +) +where chezmoi.exe >nul 2>&1 +if errorlevel 1 ( + echo chezmoi is still unavailable after installation. 1>&2 + goto bootstrap_failed +) -call :mark_stage initialize_chezmoi +set "BOOTSTRAP_FAILURE_CODE=31" call :initialize_chezmoi if errorlevel 1 ( echo chezmoi initialization/update failed. 1>&2 - exit /b 1 + goto bootstrap_failed ) -call :mark_stage resolve_repo_root +set "BOOTSTRAP_FAILURE_CODE=32" call :resolve_repo_root if not defined REPO_ROOT ( echo Unable to resolve the chezmoi working tree. 1>&2 - exit /b 1 + goto bootstrap_failed ) if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( echo Resolved chezmoi working tree does not contain the dotfiles scripts: "%REPO_ROOT%" 1>&2 - exit /b 1 + goto bootstrap_failed ) -call :mark_stage install +set "BOOTSTRAP_FAILURE_CODE=40" if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -CoreOnly -RepoRoot "%REPO_ROOT%" ) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( @@ -75,14 +91,12 @@ if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( ) else ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" ) -set "BOOTSTRAP_EXIT=%ERRORLEVEL%" -if not "%BOOTSTRAP_EXIT%"=="0" exit /b %BOOTSTRAP_EXIT% -call :mark_stage complete +if errorlevel 1 goto bootstrap_failed exit /b 0 -:mark_stage -if defined DOTFILES_BOOTSTRAP_STAGE_FILE >"%DOTFILES_BOOTSTRAP_STAGE_FILE%" echo %~1 -exit /b 0 +:bootstrap_failed +if /I "%DOTFILES_BOOTSTRAP_DIAGNOSTICS%"=="1" exit /b %BOOTSTRAP_FAILURE_CODE% +exit /b 1 :ensure_package set "PACKAGE_ID=%~1" From 70a2046d4bc194d6b31ac1a2796429d2270d288f Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:17:39 -0500 Subject: [PATCH 44/61] Run AllSigned bootstrap diagnostics through CMD --- .github/workflows/validate.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 69701ad..0880982 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -773,27 +773,21 @@ jobs: - name: Run the real bootstrap through CMD and the signing bridge id: allsigned_bootstrap continue-on-error: true - shell: pwsh + shell: cmd env: DOTFILES_NONINTERACTIVE: '1' DOTFILES_CORE_ONLY: '1' DOTFILES_BOOTSTRAP_DIAGNOSTICS: '1' run: | - $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' - New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null - Push-Location $runDirectory - try { - & cmd.exe /d /c "$env:GITHUB_WORKSPACE\bootstrap.cmd" - $exitCode = $LASTEXITCODE - } - finally { - Pop-Location - } - "exit_code=$exitCode" | Add-Content -Path $env:GITHUB_OUTPUT - Write-Host "AllSigned bootstrap exit=$exitCode" - if ($exitCode -ne 0) { - throw "bootstrap.cmd failed with diagnostic exit code $exitCode" - } + set "RUN_DIRECTORY=%RUNNER_TEMP%\dotfiles-run" + if not exist "%RUN_DIRECTORY%" mkdir "%RUN_DIRECTORY%" + pushd "%RUN_DIRECTORY%" + call "%GITHUB_WORKSPACE%\bootstrap.cmd" + set "BOOTSTRAP_EXIT=%ERRORLEVEL%" + popd + echo AllSigned bootstrap exit=%BOOTSTRAP_EXIT% + echo exit_code=%BOOTSTRAP_EXIT%>>"%GITHUB_OUTPUT%" + exit /b %BOOTSTRAP_EXIT% - name: AllSigned bootstrap failed during legacy cleanup if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '11' From b78c66624fd242b891a8d15164cd81bbe47d691a Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:26:52 -0500 Subject: [PATCH 45/61] Isolate AllSigned chezmoi initialization failures --- .github/workflows/validate.yml | 20 ++++++++++++++++++-- bootstrap.cmd | 10 +++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0880982..4c1fe34 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -770,6 +770,12 @@ jobs: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok if errorlevel 1 exit /b 1 + - name: Smoke-test deploy-pwsh hook target under AllSigned + shell: cmd + run: | + call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\scripts\windows\deploy-pwsh.ps1" -RepoRoot "%GITHUB_WORKSPACE%" + if errorlevel 1 exit /b 1 + - name: Run the real bootstrap through CMD and the signing bridge id: allsigned_bootstrap continue-on-error: true @@ -824,11 +830,21 @@ jobs: shell: cmd run: exit /b 1 - - name: AllSigned bootstrap failed during chezmoi init/apply + - name: AllSigned bootstrap failed during chezmoi init if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '31' shell: cmd run: exit /b 1 + - name: AllSigned bootstrap failed during chezmoi apply or hook execution + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '33' + shell: cmd + run: exit /b 1 + + - name: AllSigned bootstrap failed during chezmoi update + if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '34' + shell: cmd + run: exit /b 1 + - name: AllSigned bootstrap failed resolving the repo root if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '32' shell: cmd @@ -840,7 +856,7 @@ jobs: run: exit /b 1 - name: AllSigned bootstrap failed with an unmapped diagnostic code - if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["11","12","21","22","23","24","25","31","32","40"]'), steps.allsigned_bootstrap.outputs.exit_code) + if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["11","12","21","22","23","24","25","31","32","33","34","40"]'), steps.allsigned_bootstrap.outputs.exit_code) shell: cmd run: exit /b 1 diff --git a/bootstrap.cmd b/bootstrap.cmd index ed05f84..d034485 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -138,6 +138,7 @@ exit /b 1 :initialize_chezmoi if exist "%CD%\.chezmoiroot" ( + set "BOOTSTRAP_FAILURE_CODE=33" chezmoi.exe --source "%CD%" apply exit /b %ERRORLEVEL% ) @@ -145,9 +146,16 @@ set "SOURCE_ROOT=" for /f "delims=" %%R in ('chezmoi.exe source-path 2^>nul') do set "SOURCE_ROOT=%%R" if defined SOURCE_ROOT if exist "%SOURCE_ROOT%\.chezmoiroot" goto existing_chezmoi if defined SOURCE_ROOT for %%P in ("%SOURCE_ROOT%\..") do if exist "%%~fP\.chezmoiroot" goto existing_chezmoi -chezmoi.exe init --apply "%REPO_URL%" + +set "BOOTSTRAP_FAILURE_CODE=31" +chezmoi.exe init "%REPO_URL%" +if errorlevel 1 exit /b %ERRORLEVEL% + +set "BOOTSTRAP_FAILURE_CODE=33" +chezmoi.exe apply exit /b %ERRORLEVEL% :existing_chezmoi +set "BOOTSTRAP_FAILURE_CODE=34" chezmoi.exe update exit /b %ERRORLEVEL% From 70c02f1341dbbc29f67becb9c74d7552921f2a45 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:30:56 -0500 Subject: [PATCH 46/61] Restore certificate provider in nested AllSigned bridge --- scripts/windows/invoke-ps-script.cmd | 3 +++ scripts/windows/signing.ps1 | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/scripts/windows/invoke-ps-script.cmd b/scripts/windows/invoke-ps-script.cmd index 7b83ba9..60e1923 100644 --- a/scripts/windows/invoke-ps-script.cmd +++ b/scripts/windows/invoke-ps-script.cmd @@ -37,6 +37,9 @@ del /q "%DOTFILES_PS_PROBE%" >nul 2>&1 if "%DOTFILES_PS_PROBE_EXIT%"=="0" goto run_direct if not exist "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" goto missing_windows_powershell +rem A nested bridge can inherit PowerShell 7's PSModulePath. Restore the inbox +rem Windows PowerShell module roots before starting the signing host. +set "PSModulePath=%USERPROFILE%\Documents\WindowsPowerShell\Modules;%ProgramFiles%\WindowsPowerShell\Modules;%SystemRoot%\System32\WindowsPowerShell\v1.0\Modules" "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -EncodedCommand JABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwB0AG8AcAAnAAoAJABzAD0AJABlAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFAAUwBfAFMAQwBSAEkAUABUAAoAJABhAD0AQAAoACkACgBmAG8AcgAoACQAaQA9ADEAOwAkAGkALQBsAGUAWwBpAG4AdABdACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwBDADsAJABpACsAKwApAHsAJABhACsAPQBbAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBHAGUAdABFAG4AdgBpAHIAbwBuAG0AZQBuAHQAVgBhAHIAaQBhAGIAbABlACgAIgBEAE8AVABGAEkATABFAFMAXwBQAFMAXwBBAFIARwAkAGkAIgApAH0ACgAkAHQAPQAkAG4AdQBsAGwACgAkAGYAPQAkAGYAYQBsAHMAZQAKAHQAcgB5AHsACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUABvAHcAZQByAFMAaABlAGwAbAAgAHMAYwByAGkAcAB0ACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcwAiAH0ACgAgACQAcAB3AD0AKABHAGUAdAAtAEMAbwBtAG0AYQBuAGQAIABwAHcAcwBoAC4AZQB4AGUAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAdABvAHAAKQAuAFMAbwB1AHIAYwBlAAoAIAAkAHAAcwA9AEoAbwBpAG4ALQBQAGEAdABoACAAJABlAG4AdgA6AFMAeQBzAHQAZQBtAFIAbwBvAHQAIAAnAFMAeQBzAHQAZQBtADMAMgBcAFcAaQBuAGQAbwB3AHMAUABvAHcAZQByAFMAaABlAGwAbABcAHYAMQAuADAAXABwAG8AdwBlAHIAcwBoAGUAbABsAC4AZQB4AGUAJwAKACAAaQBmACgAIQAoAFQAZQBzAHQALQBQAGEAdABoACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHAAcwAgAC0AUABhAHQAaABUAHkAcABlACAATABlAGEAZgApACkAewB0AGgAcgBvAHcAIAAiAFcAaQBuAGQAbwB3AHMAIABQAG8AdwBlAHIAUwBoAGUAbABsACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAcABzACIAfQAKACAAJABoAD0ASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGUAbgB2ADoARABPAFQARgBJAEwARQBTAF8AUABTAF8AQgBSAEkARABHAEUAXwBEAEkAUgAgACcAcwBpAGcAbgBpAG4AZwAuAHAAcwAxACcACgAgAGkAZgAoACEAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABoACAALQBQAGEAdABoAFQAeQBwAGUAIABMAGUAYQBmACkAKQB7AHQAaAByAG8AdwAgACIAUwBpAGcAbgBpAG4AZwAgAGgAZQBsAHAAZQByACAAbgBvAHQAIABmAG8AdQBuAGQAOgAgACQAaAAiAH0ACgAgACQAZQBuAHYAOgBEAE8AVABGAEkATABFAFMAXwBTAEkARwBOAEkATgBHAF8AUgBFAFEAVQBJAFIARQBEAD0AJwAxACcACgAgAGkAZgAoAFsASQBPAC4AUABhAHQAaABdADoAOgBHAGUAdABGAGkAbABlAE4AYQBtAGUAKAAkAHMAKQAtAGkAZQBxACAAJwBzAGkAZwBuAGkAbgBnAC4AcABzADEAJwApAHsACgAgACAAJgAgACQAcABzACAALQBOAG8AUAByAG8AZgBpAGwAZQAgAC0ARgBpAGwAZQAgACQAaAAgAEAAYQAKACAAIABpAGYAKAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAC0AbgBlADAAKQB7ACQAZgA9ACQAdAByAHUAZQB9AAoAIAB9AGUAbABzAGUAewAKACAAIAAkAHQAPQBbAEkATwAuAFAAYQB0AGgAXQA6ADoAQwBoAGEAbgBnAGUARQB4AHQAZQBuAHMAaQBvAG4AKABbAEkATwAuAFAAYQB0AGgAXQA6ADoARwBlAHQAVABlAG0AcABGAGkAbABlAE4AYQBtAGUAKAApACwAJwAuAHAAcwAxACcAKQAKACAAIABDAG8AcAB5AC0ASQB0AGUAbQAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABzACAALQBEAGUAcwB0AGkAbgBhAHQAaQBvAG4AIAAkAHQAIAAtAEYAbwByAGMAZQAKACAAIAAmACAAJABwAHMAIAAtAE4AbwBQAHIAbwBmAGkAbABlACAALQBGAGkAbABlACAAJABoACAALQBBAGMAdABpAG8AbgAgAFAAcgBvAHQAZQBjAHQARgBpAGwAZQBzACAALQBQAGEAdABoACAAJAB0AAoAIAAgAGkAZgAoACQATABBAFMAVABFAFgASQBUAEMATwBEAEUALQBuAGUAMAApAHsAdABoAHIAbwB3ACAAIgBTAGkAZwBuAGkAbgBnACAAaABlAGwAcABlAHIAIABmAGEAaQBsAGUAZAAgAHcAaQB0AGgAIABlAHgAaQB0ACAAYwBvAGQAZQAgACQATABBAFMAVABFAFgASQBUAEMATwBEAEUAIgB9AAoAIAAgACYAIAAkAHAAdwAgAC0ATgBvAFAAcgBvAGYAaQBsAGUAIAAtAEYAaQBsAGUAIAAkAHQAIABAAGEACgAgACAAaQBmACgAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAtAG4AZQAwACkAewAkAGYAPQAkAHQAcgB1AGUAfQAKACAAfQAKAH0AYwBhAHQAYwBoAHsAVwByAGkAdABlAC0ARQByAHIAbwByACAAJABfADsAJABmAD0AJAB0AHIAdQBlAH0ACgBmAGkAbgBhAGwAbAB5AHsACgAgAGkAZgAoACQAdAApAHsAUgBlAG0AbwB2AGUALQBJAHQAZQBtACAALQBMAGkAdABlAHIAYQBsAFAAYQB0AGgAIAAkAHQAIAAtAEYAbwByAGMAZQAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQB9AAoAIABSAGUAbQBvAHYAZQAtAEkAdABlAG0AIABFAG4AdgA6AEQATwBUAEYASQBMAEUAUwBfAFMASQBHAE4ASQBOAEcAXwBSAEUAUQBVAEkAUgBFAEQAIAAtAEUAcgByAG8AcgBBAGMAdABpAG8AbgAgAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUACgB9AAoAaQBmACgAJABmACkAewBlAHgAaQB0ACAAMQB9AAoA exit /b %ERRORLEVEL% diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index 7069d19..d842130 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -13,6 +13,26 @@ $certificateSubject = "CN=jsilverdev Dotfiles Code Signing" $codeSigningOid = "1.3.6.1.5.5.7.3.3" $enhancedKeyUsageOid = "2.5.29.37" +function Initialize-CertificateProvider { + if ($null -ne (Get-PSDrive -Name Cert -ErrorAction SilentlyContinue)) { + return + } + + # Windows PowerShell can inherit PowerShell 7's PSModulePath when the bridge + # is invoked recursively from a pwsh process. Load the inbox security module + # by absolute path so the Cert: provider and Authenticode cmdlets are present. + $securityModule = Join-Path $PSHOME "Modules\Microsoft.PowerShell.Security\Microsoft.PowerShell.Security.psd1" + if (-not (Test-Path -LiteralPath $securityModule -PathType Leaf)) { + throw "Microsoft.PowerShell.Security was not found under PSHOME: $securityModule" + } + + Import-Module -Name $securityModule -Force -ErrorAction Stop + + if ($null -eq (Get-PSDrive -Name Cert -ErrorAction SilentlyContinue)) { + throw "The Cert: provider is unavailable after loading Microsoft.PowerShell.Security." + } +} + function Test-CodeSigningCertificate { param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) @@ -153,6 +173,7 @@ if ($env:DOTFILES_SIGNING_REQUIRED -ne "1" -and (Get-ExecutionPolicy) -ne "AllSi return } +Initialize-CertificateProvider $certificate = Get-DotfilesSigningCertificate switch ($Action) { From f8af7a6a6f9c1b5af2f7e055578166db80372507 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:35:19 -0500 Subject: [PATCH 47/61] Provision PowerShell modules safely under AllSigned --- install.ps1 | 59 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index 075c8e7..a911d87 100644 --- a/install.ps1 +++ b/install.ps1 @@ -76,6 +76,45 @@ function Invoke-SigningHelper { throw "The centralized PowerShell signing helper failed for $Action." } } +function Save-ManagedModuleForAllSigned { + param([Parameter(Mandatory)][string]$Name) + + $windowsPowerShell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" + if (-not (Test-Path -LiteralPath $windowsPowerShell -PathType Leaf)) { + throw "Windows PowerShell is required to provision modules under AllSigned." + } + + # Download directly into the PowerShell 7 current-user module root without + # loading PowerShellGet or PackageManagement inside pwsh under AllSigned. + $moduleRoot = Join-Path $HOME "Documents\PowerShell\Modules" + New-Item -ItemType Directory -Path $moduleRoot -Force | Out-Null + + $escapedName = $Name.Replace("'", "''") + $escapedRoot = $moduleRoot.Replace("'", "''") + $command = @( + "$ErrorActionPreference = 'Stop'" + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12" + "Save-Module -Name '$escapedName' -Path '$escapedRoot' -Repository PSGallery -Force -AcceptLicense" + ) -join [Environment]::NewLine + $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + + $originalPSModulePath = $env:PSModulePath + try { + $env:PSModulePath = @( + (Join-Path $HOME "Documents\WindowsPowerShell\Modules") + (Join-Path $env:ProgramFiles "WindowsPowerShell\Modules") + (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\Modules") + ) -join [IO.Path]::PathSeparator + + & $windowsPowerShell -NoProfile -NonInteractive -EncodedCommand $encodedCommand + if ($LASTEXITCODE -ne 0) { + throw "Windows PowerShell could not save module '$Name' for PowerShell 7 (exit code $LASTEXITCODE)." + } + } + finally { + $env:PSModulePath = $originalPSModulePath + } +} function Install-WithWinget { param( @@ -169,18 +208,29 @@ function Install-MustHaveApps { if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } } + $allSigned = (Get-ExecutionPolicy) -eq "AllSigned" foreach ($module in $ManagedModules) { $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 - $installedResource = if (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue) { - Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 + $installedResource = $null + if (-not $allSigned -and (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue)) { + $installedResource = Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 } + if ($null -eq $installedModule) { Write-Host "Installing $module module..." -ForegroundColor Cyan - Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false + if ($allSigned) { + Save-ManagedModuleForAllSigned -Name $module + } + else { + Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false + } } elseif ($Update -and -not $CoreOnly) { Write-Host "Updating $module module..." -ForegroundColor Yellow - if ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { + if ($allSigned) { + Save-ManagedModuleForAllSigned -Name $module + } + elseif ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { Update-PSResource -Name $module -Scope CurrentUser -Force } else { @@ -190,6 +240,7 @@ function Install-MustHaveApps { else { Write-Host "$module module is already installed" -ForegroundColor Green } + Invoke-SigningHelper -Action ProtectModule -ModuleName $module } } From 93f44f7092f855012a1807f8fee665bc46a7beb4 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:38:59 -0500 Subject: [PATCH 48/61] Avoid PowerShell Security autoload under AllSigned --- install.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index a911d87..76710e2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -92,7 +92,7 @@ function Save-ManagedModuleForAllSigned { $escapedName = $Name.Replace("'", "''") $escapedRoot = $moduleRoot.Replace("'", "''") $command = @( - "$ErrorActionPreference = 'Stop'" + "`$ErrorActionPreference = 'Stop'" "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12" "Save-Module -Name '$escapedName' -Path '$escapedRoot' -Repository PSGallery -Force -AcceptLicense" ) -join [Environment]::NewLine @@ -208,7 +208,7 @@ function Install-MustHaveApps { if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } } - $allSigned = (Get-ExecutionPolicy) -eq "AllSigned" + $allSigned = $env:DOTFILES_SIGNING_REQUIRED -eq "1" foreach ($module in $ManagedModules) { $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 $installedResource = $null From ace94f9c839be7a27dac90ea59a1214ba27eaf60 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:43:08 -0500 Subject: [PATCH 49/61] Make AllSigned assertions security-module independent --- tests/windows/assert-allsigned.ps1 | 167 +++++++++++++++++++++++------ 1 file changed, 134 insertions(+), 33 deletions(-) diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 0adc2b3..760cc1e 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -6,40 +6,142 @@ param( ) $ErrorActionPreference = "Stop" +$certificateSubject = "CN=jsilverdev Dotfiles Code Signing" +$codeSigningOid = "1.3.6.1.5.5.7.3.3" +$enhancedKeyUsageOid = "2.5.29.37" function Fail([string]$Message) { throw "ASSERTION FAILED: $Message" } -$effectivePolicy = Get-ExecutionPolicy -if ($effectivePolicy -ne "AllSigned") { Fail "effective execution policy is $effectivePolicy, expected AllSigned" } -Write-Host "Execution policy: $effectivePolicy" -Get-ExecutionPolicy -List | Format-Table -AutoSize | Out-Host +function Test-CodeSigningCertificate { + param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) -$certificateSubject = "CN=jsilverdev Dotfiles Code Signing" -$codeSigningOid = "1.3.6.1.5.5.7.3.3" -$certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object { - $_.Subject -eq $certificateSubject -and - $_.HasPrivateKey -and - $_.NotAfter -gt (Get-Date) -and - @($_.EnhancedKeyUsageList | Where-Object ObjectId -eq $codeSigningOid).Count -gt 0 + if ($null -eq $Certificate -or -not $Certificate.HasPrivateKey -or $Certificate.NotAfter -le (Get-Date)) { + return $false + } + + $ekuExtension = $Certificate.Extensions | + Where-Object { $_.Oid.Value -eq $enhancedKeyUsageOid } | + Select-Object -First 1 + if ($null -eq $ekuExtension) { return $false } + + try { + $eku = New-Object System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension + $eku.CopyFrom($ekuExtension) + } + catch { + return $false + } + + return @($eku.EnhancedKeyUsages | Where-Object { $_.Value -eq $codeSigningOid }).Count -gt 0 +} + +function Get-StoreCertificates { + param( + [Parameter(Mandatory)][string]$StoreName, + [Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation + ) + + $store = [System.Security.Cryptography.X509Certificates.X509Store]::new($StoreName, $StoreLocation) + try { + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) + return @($store.Certificates) + } + finally { + $store.Close() + } +} + +function Test-CertificateInStore { + param( + [Parameter(Mandatory)][string]$StoreName, + [Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation, + [Parameter(Mandatory)][string]$Thumbprint + ) + + return @(Get-StoreCertificates -StoreName $StoreName -StoreLocation $StoreLocation | + Where-Object Thumbprint -eq $Thumbprint).Count -gt 0 +} + +function Assert-AuthenticodeFiles { + param( + [Parameter(Mandatory)][string[]]$FilePath, + [Parameter(Mandatory)][string]$ExpectedThumbprint + ) + + if ($FilePath.Count -eq 0) { return } + + $windowsPowerShell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" + if (-not (Test-Path -LiteralPath $windowsPowerShell -PathType Leaf)) { + Fail "Windows PowerShell is unavailable for Authenticode verification." + } + + $manifest = [IO.Path]::ChangeExtension([IO.Path]::GetTempFileName(), ".json") + $originalPSModulePath = $env:PSModulePath + try { + @($FilePath) | ConvertTo-Json -Compress | Set-Content -LiteralPath $manifest -Encoding UTF8 + $env:DOTFILES_SIGNATURE_MANIFEST = $manifest + $env:DOTFILES_EXPECTED_THUMBPRINT = $ExpectedThumbprint + $env:PSModulePath = @( + (Join-Path $HOME "Documents\WindowsPowerShell\Modules") + (Join-Path $env:ProgramFiles "WindowsPowerShell\Modules") + (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\Modules") + ) -join [IO.Path]::PathSeparator + + $command = @( + "`$ErrorActionPreference = 'Stop'" + "`$paths = @(Get-Content -LiteralPath `$env:DOTFILES_SIGNATURE_MANIFEST -Raw | ConvertFrom-Json)" + "foreach (`$path in `$paths) {" + " `$signature = Get-AuthenticodeSignature -LiteralPath `$path" + " if (`$signature.Status -ne 'Valid') { throw \"invalid Authenticode signature: `$path (`$(`$signature.Status))\" }" + " if (`$null -eq `$signature.SignerCertificate -or `$signature.SignerCertificate.Thumbprint -ne `$env:DOTFILES_EXPECTED_THUMBPRINT) { throw \"unexpected Authenticode signer: `$path\" }" + " Write-Output \"Valid signature: `$path\"" + "}" + ) -join [Environment]::NewLine + $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + + & $windowsPowerShell -NoProfile -NonInteractive -EncodedCommand $encodedCommand + if ($LASTEXITCODE -ne 0) { + Fail "Windows PowerShell Authenticode verification failed with exit code $LASTEXITCODE." + } + } + finally { + $env:PSModulePath = $originalPSModulePath + Remove-Item Env:DOTFILES_SIGNATURE_MANIFEST -ErrorAction SilentlyContinue + Remove-Item Env:DOTFILES_EXPECTED_THUMBPRINT -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $manifest -Force -ErrorAction SilentlyContinue + } +} + +# The workflow separately proves that unsigned scripts are rejected. The bridge +# sets this variable only on the signed execution path, so the assertion avoids +# autoloading Microsoft.PowerShell.Security inside pwsh under AllSigned. +if ($env:DOTFILES_SIGNING_REQUIRED -ne "1") { + Fail "assert-allsigned.ps1 was not executed through the AllSigned signing path" +} +Write-Host "AllSigned bridge path confirmed." + +$currentUser = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser +$localMachine = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine +$certificate = @(Get-StoreCertificates -StoreName "My" -StoreLocation $currentUser | Where-Object { + $_.Subject -eq $certificateSubject -and (Test-CodeSigningCertificate $_) } | Sort-Object NotAfter -Descending | Select-Object -First 1) if ($certificate.Count -ne 1) { Fail "usable dotfiles Code Signing certificate was not found in CurrentUser\\My" } -foreach ($storeName in @("My", "TrustedPublisher")) { - $trusted = @(Get-ChildItem "Cert:\CurrentUser\$storeName" | Where-Object Thumbprint -eq $certificate[0].Thumbprint) - if ($trusted.Count -ne 1) { Fail "certificate $($certificate[0].Thumbprint) is missing from CurrentUser\\$storeName" } +$thumbprint = $certificate[0].Thumbprint +if (-not (Test-CertificateInStore -StoreName "TrustedPublisher" -StoreLocation $currentUser -Thumbprint $thumbprint)) { + Fail "certificate $thumbprint is missing from CurrentUser\\TrustedPublisher" } -$trustedRoot = @( - Get-ChildItem Cert:\CurrentUser\Root | Where-Object Thumbprint -eq $certificate[0].Thumbprint - Get-ChildItem Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $certificate[0].Thumbprint -) -if ($trustedRoot.Count -eq 0) { Fail "certificate $($certificate[0].Thumbprint) is missing from both CurrentUser\\Root and LocalMachine\\Root" } -Write-Host "Certificate: $($certificate[0].Subject) thumbprint=$($certificate[0].Thumbprint) expires=$($certificate[0].NotAfter)" +$trustedRoot = + (Test-CertificateInStore -StoreName "Root" -StoreLocation $currentUser -Thumbprint $thumbprint) -or + (Test-CertificateInStore -StoreName "Root" -StoreLocation $localMachine -Thumbprint $thumbprint) +if (-not $trustedRoot) { Fail "certificate $thumbprint is missing from both CurrentUser\\Root and LocalMachine\\Root" } +Write-Host "Certificate: $($certificate[0].Subject) thumbprint=$thumbprint expires=$($certificate[0].NotAfter)" if (Test-Path -LiteralPath $ThumbprintFile) { $previous = (Get-Content -LiteralPath $ThumbprintFile -Raw).Trim() - if ($previous -ne $certificate[0].Thumbprint) { Fail "signing certificate changed from $previous to $($certificate[0].Thumbprint)" } + if ($previous -ne $thumbprint) { Fail "signing certificate changed from $previous to $thumbprint" } } -Set-Content -LiteralPath $ThumbprintFile -Value $certificate[0].Thumbprint -NoNewline +Set-Content -LiteralPath $ThumbprintFile -Value $thumbprint -NoNewline function Invoke-Chezmoi([string[]]$Arguments) { $output = @(& chezmoi.exe @Arguments 2>&1) @@ -48,9 +150,6 @@ function Invoke-Chezmoi([string[]]$Arguments) { } function Assert-CleanChezMoi { - # Always-run scripts appear as "R" and create-only files may legitimately - # differ in the first status column. Only the second column means apply - # still has work to do. $status = @(Invoke-Chezmoi @("status", "--exclude=scripts")) $pending = @($status | Where-Object { $line = [string]$_ @@ -70,6 +169,7 @@ Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host Assert-CleanChezMoi +$signatureFiles = [System.Collections.Generic.List[string]]::new() $runtimeFiles = @( (Join-Path $HOME ".config\pwsh\env.ps1"), (Join-Path $HOME ".config\pwsh\lib\helpers.ps1"), @@ -78,10 +178,7 @@ $runtimeFiles = @( ) foreach ($path in $runtimeFiles) { if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail "runtime PowerShell file is missing: $path" } - $signature = Get-AuthenticodeSignature -FilePath $path - Write-Host "${path}: $($signature.Status) signer=$($signature.SignerCertificate.Thumbprint)" - if ($signature.Status -ne "Valid") { Fail "runtime PowerShell signature is not Valid: $path ($($signature.Status))" } - if ($signature.SignerCertificate.Thumbprint -ne $certificate[0].Thumbprint) { Fail "runtime PowerShell file has an unexpected signer: $path" } + $signatureFiles.Add($path) } foreach ($moduleName in @(Get-Content -LiteralPath (Join-Path $RepoRoot "scripts\windows\managed-modules.txt") | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') })) { @@ -91,18 +188,22 @@ foreach ($moduleName in @(Get-Content -LiteralPath (Join-Path $RepoRoot "scripts else { Import-Module $moduleName -Force -ErrorAction Stop } + $module = @(Get-Module -ListAvailable -Name $moduleName | Where-Object { $_.ModuleBase -like "$(Join-Path $HOME 'Documents\PowerShell\Modules')*" -or $_.ModuleBase -like "$(Join-Path $HOME '.local\share\powershell\Modules')*" } | Select-Object -First 1) if ($module.Count -ne 1) { Fail "managed module is not in a current-user module path: $moduleName" } - $moduleFiles = @(Get-ChildItem -LiteralPath $module[0].ModuleBase -File -Recurse | Where-Object Extension -in @(".ps1", ".psm1", ".psd1", ".ps1xml", ".cdxml", ".xaml")) + + $moduleFiles = @(Get-ChildItem -LiteralPath $module[0].ModuleBase -File -Recurse | + Where-Object Extension -in @(".ps1", ".psm1", ".psd1", ".ps1xml", ".cdxml", ".xaml")) foreach ($file in $moduleFiles) { - $signature = Get-AuthenticodeSignature -FilePath $file.FullName - if ($signature.Status -ne "Valid") { Fail "managed module file is not Validly signed: $($file.FullName) ($($signature.Status))" } + $signatureFiles.Add($file.FullName) } } +Assert-AuthenticodeFiles -FilePath @($signatureFiles) -ExpectedThumbprint $thumbprint + if ($UpdateMarker) { if (-not ((Get-Content (Join-Path $HOME ".fdignore") -Raw) -match [regex]::Escape($UpdateMarker))) { Fail "update marker did not reach .fdignore" } } @@ -117,4 +218,4 @@ if ($LASTEXITCODE -eq 0 -or $signatureMatches.Count -ne 0) { Fail "repository Po $profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains "profile-ok")) { Fail "PowerShell profile startup failed: $($profileOutput -join [Environment]::NewLine)" } -Write-Host "AllSigned assertions passed with certificate $($certificate[0].Thumbprint)." +Write-Host "AllSigned assertions passed with certificate $thumbprint." From 022d7d11dfb4165fc804dcdcdfdb651decd81258 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:46:07 -0500 Subject: [PATCH 50/61] Fix AllSigned Authenticode assertion parsing --- tests/windows/assert-allsigned.ps1 | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 760cc1e..88d126f 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -87,16 +87,20 @@ function Assert-AuthenticodeFiles { (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\Modules") ) -join [IO.Path]::PathSeparator - $command = @( - "`$ErrorActionPreference = 'Stop'" - "`$paths = @(Get-Content -LiteralPath `$env:DOTFILES_SIGNATURE_MANIFEST -Raw | ConvertFrom-Json)" - "foreach (`$path in `$paths) {" - " `$signature = Get-AuthenticodeSignature -LiteralPath `$path" - " if (`$signature.Status -ne 'Valid') { throw \"invalid Authenticode signature: `$path (`$(`$signature.Status))\" }" - " if (`$null -eq `$signature.SignerCertificate -or `$signature.SignerCertificate.Thumbprint -ne `$env:DOTFILES_EXPECTED_THUMBPRINT) { throw \"unexpected Authenticode signer: `$path\" }" - " Write-Output \"Valid signature: `$path\"" - "}" - ) -join [Environment]::NewLine + $command = @' +$ErrorActionPreference = 'Stop' +$paths = @(Get-Content -LiteralPath $env:DOTFILES_SIGNATURE_MANIFEST -Raw | ConvertFrom-Json) +foreach ($path in $paths) { + $signature = Get-AuthenticodeSignature -LiteralPath $path + if ($signature.Status -ne 'Valid') { + throw "invalid Authenticode signature: $path ($($signature.Status))" + } + if ($null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $env:DOTFILES_EXPECTED_THUMBPRINT) { + throw "unexpected Authenticode signer: $path" + } + Write-Output "Valid signature: $path" +} +'@ $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) & $windowsPowerShell -NoProfile -NonInteractive -EncodedCommand $encodedCommand From 3274a12b284d2a6fd3328ab65cdf34a713c4bbc3 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:50:52 -0500 Subject: [PATCH 51/61] Harden AllSigned PSReadLine and native process checks --- install.ps1 | 22 +++++++++- scripts/windows/managed-modules.txt | 1 + tests/windows/assert-allsigned.ps1 | 62 ++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index 76710e2..b957cce 100644 --- a/install.ps1 +++ b/install.ps1 @@ -210,7 +210,27 @@ function Install-MustHaveApps { $allSigned = $env:DOTFILES_SIGNING_REQUIRED -eq "1" foreach ($module in $ManagedModules) { - $installedModule = Get-Module -ListAvailable -Name $module | Select-Object -First 1 + $availableModules = @(Get-Module -ListAvailable -Name $module) + if ($allSigned) { + $userModuleRoots = @( + (Join-Path $HOME "Documents\PowerShell\Modules"), + (Join-Path $HOME ".local\share\powershell\Modules") + ) + $installedModule = @($availableModules | Where-Object { + $moduleBase = [IO.Path]::GetFullPath($_.ModuleBase).TrimEnd([IO.Path]::DirectorySeparatorChar) + @($userModuleRoots | Where-Object { + $root = [IO.Path]::GetFullPath($_).TrimEnd([IO.Path]::DirectorySeparatorChar) + $moduleBase.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or + $moduleBase.StartsWith($root + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) + }).Count -gt 0 + } | Select-Object -First 1) + if ($installedModule.Count -eq 0) { $installedModule = $null } + else { $installedModule = $installedModule[0] } + } + else { + $installedModule = $availableModules | Select-Object -First 1 + } + $installedResource = $null if (-not $allSigned -and (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue)) { $installedResource = Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 diff --git a/scripts/windows/managed-modules.txt b/scripts/windows/managed-modules.txt index ff589e7..70496f9 100644 --- a/scripts/windows/managed-modules.txt +++ b/scripts/windows/managed-modules.txt @@ -1,2 +1,3 @@ PSFzf git-aliases +PSReadLine diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 88d126f..875ce11 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -147,10 +147,58 @@ if (Test-Path -LiteralPath $ThumbprintFile) { } Set-Content -LiteralPath $ThumbprintFile -Value $thumbprint -NoNewline +function Invoke-NativeProcess { + param( + [Parameter(Mandatory)][string]$FilePath, + [string[]]$Arguments = @() + ) + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $FilePath + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + foreach ($argument in $Arguments) { + [void]$startInfo.ArgumentList.Add($argument) + } + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + try { + if (-not $process.Start()) { + Fail "failed to start native process: $FilePath" + } + + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $stdout = $stdoutTask.GetAwaiter().GetResult() + $stderr = $stderrTask.GetAwaiter().GetResult() + + return [pscustomobject]@{ + ExitCode = $process.ExitCode + StdOut = $stdout + StdErr = $stderr + } + } + finally { + $process.Dispose() + } +} + function Invoke-Chezmoi([string[]]$Arguments) { - $output = @(& chezmoi.exe @Arguments 2>&1) - if ($LASTEXITCODE -ne 0) { Fail "chezmoi $($Arguments -join ' ') failed: $($output -join [Environment]::NewLine)" } - return $output + $chezmoi = (Get-Command chezmoi.exe -ErrorAction Stop).Source + $result = Invoke-NativeProcess -FilePath $chezmoi -Arguments $Arguments + if ($result.ExitCode -ne 0) { + Fail "chezmoi $($Arguments -join ' ') failed with exit code $($result.ExitCode): $($result.StdErr.Trim())" + } + + if ([string]::IsNullOrWhiteSpace($result.StdOut)) { + return @() + } + + return @($result.StdOut -split "\r?\n" | Where-Object { $_ -ne "" }) } function Assert-CleanChezMoi { @@ -219,7 +267,11 @@ if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { Fail "Actions checkout Gi $signatureMatches = @(git -C $RepoRoot grep -n "^# SIG # Begin signature block" -- "*.ps1") if ($LASTEXITCODE -eq 0 -or $signatureMatches.Count -ne 0) { Fail "repository PowerShell source contains an Authenticode signature block" } -$profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) -if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains "profile-ok")) { Fail "PowerShell profile startup failed: $($profileOutput -join [Environment]::NewLine)" } +$pwsh = (Get-Command pwsh.exe -ErrorAction Stop).Source +$profileResult = Invoke-NativeProcess -FilePath $pwsh -Arguments @("-Command", "Write-Output 'profile-ok'") +$profileOutput = @($profileResult.StdOut -split "\r?\n" | Where-Object { $_ -ne "" }) +if ($profileResult.ExitCode -ne 0 -or -not ($profileOutput -contains "profile-ok")) { + Fail "PowerShell profile startup failed with exit code $($profileResult.ExitCode): $($profileResult.StdErr.Trim())" +} Write-Host "AllSigned assertions passed with certificate $thumbprint." From 61849c01b266a157fa7fd097a2351d2f66f30399 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:54:52 -0500 Subject: [PATCH 52/61] Re-sign managed modules with the trusted publisher --- scripts/windows/signing.ps1 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/windows/signing.ps1 b/scripts/windows/signing.ps1 index d842130..04fcd35 100644 --- a/scripts/windows/signing.ps1 +++ b/scripts/windows/signing.ps1 @@ -132,15 +132,29 @@ function Protect-PowerShellFile { } $signature = Get-AuthenticodeSignature -FilePath $FilePath - if ($signature.Status -eq "Valid") { + if ( + $signature.Status -eq "Valid" -and + $null -ne $signature.SignerCertificate -and + $signature.SignerCertificate.Thumbprint -eq $Certificate.Thumbprint + ) { return } + # A module can already carry a cryptographically valid vendor signature + # whose publisher is not trusted by a non-interactive AllSigned session. + # Re-sign managed files with the dotfiles certificate so every executable + # PowerShell asset has the same explicitly trusted publisher. Set-AuthenticodeSignature -FilePath $FilePath -Certificate $Certificate -HashAlgorithm SHA256 | Out-Null $signature = Get-AuthenticodeSignature -FilePath $FilePath if ($signature.Status -ne "Valid") { throw "Authenticode signature verification failed for ${FilePath}: $($signature.Status) $($signature.StatusMessage)" } + if ( + $null -eq $signature.SignerCertificate -or + $signature.SignerCertificate.Thumbprint -ne $Certificate.Thumbprint + ) { + throw "Authenticode signer verification failed for ${FilePath}: expected $($Certificate.Thumbprint)." + } } function Get-ManagedModuleFiles { From 469c2b656318a62786689cc6ef8f7da9643cf919 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:16:04 -0500 Subject: [PATCH 53/61] Refactor dotfiles CI and bootstrap validation --- .github/workflows/validate.yml | 936 ++---------------------- README.md | 2 +- bootstrap.cmd | 24 - bootstrap.sh | 4 +- install.ps1 | 15 +- install.sh | 30 +- scripts/windows/deploy-pwsh.ps1 | 16 +- tests/linux/run-ci.sh | 16 + tests/static/check-powershell-bridge.py | 10 + tests/static/validate-powershell.ps1 | 9 + tests/static/validate-source.sh | 17 + tests/windows/create-update-fixture.cmd | 13 + tests/windows/enable-allsigned.ps1 | 11 + tests/windows/prepare-ci.ps1 | 10 + 14 files changed, 199 insertions(+), 914 deletions(-) create mode 100644 tests/linux/run-ci.sh create mode 100644 tests/static/check-powershell-bridge.py create mode 100644 tests/static/validate-powershell.ps1 create mode 100644 tests/static/validate-source.sh create mode 100644 tests/windows/create-update-fixture.cmd create mode 100644 tests/windows/enable-allsigned.ps1 create mode 100644 tests/windows/prepare-ci.ps1 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4c1fe34..4adf09a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,898 +1,114 @@ name: Validate dotfiles - on: pull_request: push: - branches: - - main + branches: [main] workflow_dispatch: - permissions: contents: read - concurrency: - group: validate-v2-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - jobs: static: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - - name: Check out the exact revision - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Install static-validation tools + - uses: actions/checkout@v7 + with: {persist-credentials: false} + - name: Install validation tools run: | sudo apt-get update - sudo apt-get install --yes --no-install-recommends python3 ruby shellcheck + sudo apt-get install --yes --no-install-recommends shellcheck mkdir -p "$RUNNER_TEMP/bin" curl -fsLS https://get.chezmoi.io | sh -s -- -b "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - - - name: Validate shell, source state, templates, and architecture - shell: bash - run: | - set -euo pipefail - bash -n bootstrap.sh install.sh update.sh tests/linux/assert-state.sh - shellcheck -x -S error bootstrap.sh install.sh update.sh tests/linux/assert-state.sh - chezmoi --source "$GITHUB_WORKSPACE" managed >/dev/null - while IFS= read -r -d '' template; do - chezmoi --source "$GITHUB_WORKSPACE" execute-template < "$template" >/dev/null - done < <(find "$GITHUB_WORKSPACE" -type f -name '*.tmpl' -print0) - - test "$(tr -d '\r\n' < .chezmoiroot)" = home - test -d home - test -f scripts/windows/invoke-ps-script.cmd - test -f scripts/windows/invoke-ps-script-bridge.ps1 - test -f scripts/windows/signing.ps1 - test -f scripts/windows/deploy-pwsh.ps1 - test -f scripts/windows/managed-modules.txt - test -f home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl - - if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then - echo 'chezmoi symlink source state is not allowed' >&2 - exit 1 - fi - if git grep -n -i 'dotbot' -- ':!.github/workflows/validate.yml'; then - echo 'obsolete Dotbot dependency/reference found' >&2 - exit 1 - fi - if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then - echo 'obsolete master bootstrap URL found' >&2 - exit 1 - fi - if git grep -n -E 'Set-ExecutionPolicy|-ExecutionPolicy[[:space:]]+Bypass' -- ':!\.github/workflows/validate.yml'; then - echo 'production execution-policy weakening found' >&2 - exit 1 - fi - if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then - echo 'implementation-specific PowerShell execution-policy registry probing found' >&2 - exit 1 - fi - if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then - echo 'signed PowerShell source was committed' >&2 - exit 1 - fi - - - name: Validate encoded PowerShell bridge source - shell: bash - run: | - set -euo pipefail - python3 - <<'PY' - import base64 - import pathlib - import re - - cmd = pathlib.Path('scripts/windows/invoke-ps-script.cmd').read_text(encoding='utf-8') - source = pathlib.Path('scripts/windows/invoke-ps-script-bridge.ps1').read_text(encoding='utf-8') - match = re.search(r'(?m)^"%SystemRoot%\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe" -NoProfile -EncodedCommand ([A-Za-z0-9+/=]+)\s*$', cmd) - if not match: - raise SystemExit('encoded bridge payload is missing') - decoded = base64.b64decode(match.group(1)).decode('utf-16le') - if decoded != source: - raise SystemExit('encoded bridge payload does not match invoke-ps-script-bridge.ps1') - if len(match.group(1)) >= 8000: - raise SystemExit('encoded bridge payload is too close to cmd.exe command-line limits') - print(f'bridge-source-ok: encoded length={len(match.group(1))}') - PY - - - name: Validate workflow YAML - shell: bash - run: | - set -euo pipefail - ruby -e 'require "yaml"; YAML.load_file(ARGV.fetch(0))' .github/workflows/validate.yml - echo 'workflow-yaml-ok' - - - name: Validate PowerShell syntax - shell: bash - run: | - set -euo pipefail - pwsh -NoProfile -Command - <<'PWSH' - Write-Host "PowerShell $($PSVersionTable.PSVersion)" - $errors = @() - Get-ChildItem -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { - $tokens = $null - $parseErrors = $null - [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors) | Out-Null - if ($parseErrors.Count) { - $joined = $parseErrors -join '; ' - $errors += "$($_.FullName): $joined" - } - } - if ($errors.Count) { - $errors | Write-Error - exit 1 - } - PWSH - echo 'powershell-syntax-ok' - - linux-debian: + - run: bash tests/static/validate-source.sh + - run: python3 tests/static/check-powershell-bridge.py + - shell: pwsh + run: ./tests/static/validate-powershell.ps1 + linux: + name: linux-${{ matrix.distro }} runs-on: ubuntu-24.04 timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - {distro: debian, image: "debian:bookworm-slim"} + - {distro: arch, image: "archlinux:base"} container: - image: debian:bookworm-slim - steps: - - name: Prepare Debian container for checkout - run: | - apt-get update - apt-get install --yes --no-install-recommends ca-certificates curl git sudo bash - - - name: Check out the exact revision - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Run Debian bootstrap as an unprivileged user - shell: bash - run: | - set -euo pipefail - git config --global --add safe.directory "$GITHUB_WORKSPACE" - test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" - - remote_parent="$(mktemp -d)" - remote_dir="$remote_parent/dotfiles.git" - git init --bare "$remote_dir" - git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" - git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main - chmod -R a+rX "$GITHUB_WORKSPACE" - - useradd --create-home --shell /bin/bash dotfilesci - printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci - chmod 0440 /etc/sudoers.d/dotfilesci - chown -R dotfilesci:dotfilesci "$remote_parent" - echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ - DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ - bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ - bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian "$EXPECTED_COMMIT"' - - - name: Exercise Debian update wrapper against the local remote - shell: bash - run: | - set -euo pipefail - marker="ci-update-marker-${GITHUB_RUN_ID}" - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - DOTFILES_REPO="$DOTFILES_REPO" UPDATE_MARKER="$marker" \ - bash -lc ' - set -euo pipefail - update_parent="$(mktemp -d)" - update_clone="$update_parent/dotfiles-update" - git clone "$DOTFILES_REPO" "$update_clone" - git -C "$update_clone" config user.name ci - git -C "$update_clone" config user.email ci@example.invalid - printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" - git -C "$update_clone" add home/dot_fdignore - git -c commit.gpgsign=false -C "$update_clone" commit -m "CI update fixture" - git -C "$update_clone" push origin HEAD:refs/heads/main - ' - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ - bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" UPDATE_MARKER="$marker" \ - bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" debian' - - linux-arch: - runs-on: ubuntu-24.04 - timeout-minutes: 35 - container: - image: archlinux:base + image: ${{ matrix.image }} steps: - - name: Prepare Arch container for checkout - run: | - pacman -Sy --noconfirm ca-certificates curl git sudo bash - - - name: Check out the exact revision - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Run Arch bootstrap as an unprivileged user - shell: bash - run: | - set -euo pipefail - git config --global --add safe.directory "$GITHUB_WORKSPACE" - test "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" = "$GITHUB_SHA" - - remote_parent="$(mktemp -d)" - remote_dir="$remote_parent/dotfiles.git" - git init --bare "$remote_dir" - git -C "$GITHUB_WORKSPACE" push "$remote_dir" "$GITHUB_SHA:refs/heads/main" - git --git-dir="$remote_dir" symbolic-ref HEAD refs/heads/main - chmod -R a+rX "$GITHUB_WORKSPACE" - - useradd --create-home --shell /bin/bash dotfilesci - printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci - chmod 0440 /etc/sudoers.d/dotfilesci - chown -R dotfilesci:dotfilesci "$remote_parent" - echo "DOTFILES_REPO=file://$remote_dir" >> "$GITHUB_ENV" - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" \ - DOTFILES_REPO="file://$remote_dir" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ - bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" EXPECTED_COMMIT="$GITHUB_SHA" \ - bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch "$EXPECTED_COMMIT"' - - - name: Exercise Arch update wrapper against the local remote - shell: bash - run: | - set -euo pipefail - marker="ci-update-marker-${GITHUB_RUN_ID}" - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - DOTFILES_REPO="$DOTFILES_REPO" UPDATE_MARKER="$marker" \ - bash -lc ' - set -euo pipefail - update_parent="$(mktemp -d)" - update_clone="$update_parent/dotfiles-update" - git clone "$DOTFILES_REPO" "$update_clone" - git -C "$update_clone" config user.name ci - git -C "$update_clone" config user.email ci@example.invalid - printf "\n%s\n" "$UPDATE_MARKER" >> "$update_clone/home/dot_fdignore" - git -C "$update_clone" add home/dot_fdignore - git -c commit.gpgsign=false -C "$update_clone" commit -m "CI update fixture" - git -C "$update_clone" push origin HEAD:refs/heads/main - ' - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 \ - bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' - - sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci \ - PATH="/home/dotfilesci/.local/bin:$PATH" \ - GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$GITHUB_WORKSPACE" \ - GITHUB_WORKSPACE="$GITHUB_WORKSPACE" UPDATE_MARKER="$marker" \ - bash -lc 'grep -Fqx "$UPDATE_MARKER" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" arch' - + - name: Prepare container + shell: sh + run: | + case "${{ matrix.distro }}" in + debian) apt-get update; apt-get install --yes --no-install-recommends ca-certificates curl git sudo bash ;; + arch) pacman -Sy --noconfirm ca-certificates curl git sudo bash ;; + esac + - uses: actions/checkout@v7 + with: {fetch-depth: 0, persist-credentials: false} + - run: bash tests/linux/run-ci.sh "${{ matrix.distro }}" "$GITHUB_SHA" windows: runs-on: windows-2025 timeout-minutes: 45 + env: {DOTFILES_NONINTERACTIVE: "1", DOTFILES_CORE_ONLY: "1"} steps: - - name: Check out the exact revision - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Provision WinGet if the hosted runner lacks it - shell: pwsh - run: | - Write-Host 'Checking for winget.exe' - cmd /c where winget.exe - if ($LASTEXITCODE -ne 0) { - Install-PackageProvider -Name NuGet -Force | Out-Null - Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null - Import-Module Microsoft.WinGet.Client -Force - Repair-WinGetPackageManager -Force -Latest - } - cmd /c where winget.exe - if ($LASTEXITCODE -ne 0) { throw 'winget.exe is unavailable after Microsoft.WinGet.Client repair' } - winget.exe --version - @( - (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), - (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), - (Join-Path $env:LOCALAPPDATA 'Programs\mise'), - (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') - ) | ForEach-Object { Add-Content -Path $env:GITHUB_PATH -Value $_ } - - - name: Create the exact-commit local bootstrap remote - shell: pwsh - run: | - $remote = Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git" - git init --bare $remote - $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() - if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } - $remoteUri = "file:///" + $remote.Replace('\', '/') - git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" - if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } - git --git-dir=$remote show-ref --verify --quiet refs/heads/main - if ($LASTEXITCODE -ne 0) { throw 'local bootstrap remote has no main ref' } - git --git-dir=$remote symbolic-ref HEAD refs/heads/main - Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" - Write-Host "Local bootstrap remote: $remoteUri" - - - name: Confirm normal execution policy - shell: pwsh - run: | - Get-ExecutionPolicy -List | Format-Table -AutoSize - if ((Get-ExecutionPolicy) -eq 'AllSigned') { throw 'normal Windows job unexpectedly has AllSigned effective' } - - - name: Smoke-test PowerShell bridge under normal policy - shell: pwsh - run: | - $smoke = Join-Path $env:RUNNER_TEMP 'bridge-smoke.ps1' - @' - param([string]$Value) - if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } - '@ | Set-Content -LiteralPath $smoke - & "$env:GITHUB_WORKSPACE\scripts\windows\invoke-ps-script.cmd" $smoke -Value bridge-ok - if ($LASTEXITCODE -ne 0) { throw "PowerShell bridge smoke test failed with exit code $LASTEXITCODE" } - - - name: Ensure Windows bootstrap core tools - shell: pwsh - run: | - $packages = @( - @{ Id = 'Git.Git'; Command = 'git.exe' }, - @{ Id = 'Microsoft.PowerShell'; Command = 'pwsh.exe' }, - @{ Id = 'jdx.mise'; Command = 'mise.exe' }, - @{ Id = 'twpayne.chezmoi'; Command = 'chezmoi.exe' } - ) - foreach ($package in $packages) { - cmd /c "where $($package.Command) >nul 2>&1" - if ($LASTEXITCODE -ne 0) { - winget.exe install --id $package.Id --exact --source winget --scope user --silent --disable-interactivity --accept-source-agreements --accept-package-agreements - if ($LASTEXITCODE -ne 0) { throw "unable to install $($package.Id) for current user" } - } - } - @( - (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), - (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), - (Join-Path $env:LOCALAPPDATA 'Programs\mise'), - (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') - ) | ForEach-Object { - if ($env:Path -notlike "*$_*") { $env:Path += ";$_" } - Add-Content -Path $env:GITHUB_PATH -Value $_ - } - foreach ($command in @('git.exe','pwsh.exe','mise.exe','chezmoi.exe')) { - cmd /c "where $command" - if ($LASTEXITCODE -ne 0) { throw "$command is unavailable after provisioning" } - } - - - name: Initialize chezmoi from the exact-commit remote - shell: pwsh - run: | - $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' - New-Item -ItemType Directory -Path $runDirectory -Force | Out-Null - Push-Location $runDirectory - try { - chezmoi.exe init --apply $env:DOTFILES_REPO - if ($LASTEXITCODE -ne 0) { throw "chezmoi init --apply failed with exit code $LASTEXITCODE" } - $source = (& chezmoi.exe source-path).Trim() - if (-not (Test-Path -LiteralPath $source)) { throw "chezmoi source path does not exist: $source" } - } - finally { Pop-Location } - - - name: Provision Starship for the core PowerShell profile - shell: pwsh - run: | - mise.exe use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not provision starship" } - - - name: Provision managed PowerShell modules - shell: pwsh - run: | - $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | - Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } - foreach ($module in $modules) { - if (-not (Get-Module -ListAvailable -Name $module | Select-Object -First 1)) { - Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false - } - if (-not (Get-Module -ListAvailable -Name $module | Select-Object -First 1)) { - throw "managed PowerShell module was not provisioned: $module" - } - } - - - name: Smoke-test module signing helper under normal policy - shell: pwsh - run: | - $helper = "$env:GITHUB_WORKSPACE\scripts\windows\signing.ps1" - $bridge = "$env:GITHUB_WORKSPACE\scripts\windows\invoke-ps-script.cmd" - $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | - Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } - foreach ($module in $modules) { - & $bridge $helper -Action ProtectModule -ModuleName $module - if ($LASTEXITCODE -ne 0) { throw "signing helper failed for $module under normal policy" } - } - - - name: Run Windows core installer directly - shell: pwsh - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - run: | - $source = (& chezmoi.exe source-path).Trim() - $repoRoot = if (Test-Path -LiteralPath (Join-Path $source 'install.ps1')) { - $source - } - elseif (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $source) 'install.ps1')) { - Split-Path -Parent $source - } - else { - throw "unable to resolve repository root from chezmoi source path: $source" - } - & pwsh.exe -NoProfile -File (Join-Path $repoRoot 'install.ps1') -NonInteractive -CoreOnly -RepoRoot $repoRoot - if ($LASTEXITCODE -ne 0) { throw "direct core installer failed with exit code $LASTEXITCODE" } - - - name: Run Windows core installer through the bridge - shell: pwsh - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - run: | - $source = (& chezmoi.exe source-path).Trim() - $repoRoot = if (Test-Path -LiteralPath (Join-Path $source 'install.ps1')) { - $source - } - elseif (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $source) 'install.ps1')) { - Split-Path -Parent $source - } - else { - throw "unable to resolve repository root from chezmoi source path: $source" - } - & (Join-Path $repoRoot 'scripts\windows\invoke-ps-script.cmd') (Join-Path $repoRoot 'install.ps1') -NonInteractive -CoreOnly -RepoRoot $repoRoot - if ($LASTEXITCODE -ne 0) { throw "core installer failed with exit code $LASTEXITCODE" } - - - name: Run Windows bootstrap wrapper idempotently - shell: pwsh - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - run: | - $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' - Push-Location $runDirectory - try { - $bootstrap = Join-Path $env:GITHUB_WORKSPACE 'bootstrap.cmd' - & cmd.exe /d /c $bootstrap - if ($LASTEXITCODE -ne 0) { throw "bootstrap.cmd failed with exit code $LASTEXITCODE" } - } - finally { Pop-Location } - - - name: Probe Windows deployed files and core CLI tools - shell: pwsh - run: | - $requiredFiles = @( - (Join-Path $HOME '.gitconfig'), - (Join-Path $HOME '.gitconfig.local'), - (Join-Path $HOME '.fdignore'), - (Join-Path $HOME '.wslconfig'), - (Join-Path $HOME '.config\starship\config.toml'), - (Join-Path $HOME '.config\pwsh\env.ps1'), - (Join-Path $HOME '.config\pwsh\lib\helpers.ps1'), - (Join-Path $HOME '.config\pwsh\lib\aliases.ps1'), - $PROFILE.CurrentUserAllHosts - ) - foreach ($path in $requiredFiles) { - if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "missing deployed file: $path" } - } - foreach ($command in @('micro','lsd','bat','fastfetch','fzf','fd','delta','jq','rg','mise')) { - $resolved = Get-Command -Name $command -ErrorAction SilentlyContinue - if ($null -eq $resolved) { throw "core CLI tool is unavailable: $command" } - Write-Host "$command -> $($resolved.Source)" - } - $source = (& chezmoi.exe source-path).Trim() - $sourceCommit = (git -C $source rev-parse HEAD).Trim() - if ($sourceCommit -ne $env:GITHUB_SHA) { throw "source commit $sourceCommit is not $env:GITHUB_SHA" } - - - name: Probe Windows chezmoi idempotency - shell: pwsh - run: | - & chezmoi.exe apply - if ($LASTEXITCODE -ne 0) { throw 'first probe apply failed' } - & chezmoi.exe apply - if ($LASTEXITCODE -ne 0) { throw 'second probe apply failed' } - $status = @(& chezmoi.exe status --exclude=scripts) - if ($LASTEXITCODE -ne 0) { throw 'chezmoi status failed' } - $pending = @($status | Where-Object { - $line = [string]$_ - $line.Length -ge 2 -and $line[1] -ne ' ' - }) - $status | ForEach-Object { Write-Host "STATUS: $_" } - if ($pending.Count -ne 0) { throw "chezmoi still has pending target changes: $($pending -join ' | ')" } - - - name: Probe Windows create-only semantics - shell: pwsh - run: | - $config = Join-Path $HOME '.codex\config.toml' - $rules = Join-Path $HOME '.codex\rules\default.rules' - $configMarker = "ci-probe-create-only-$env:GITHUB_RUN_ID" - $rulesMarker = "ci-probe-create-only-rule-$env:GITHUB_RUN_ID" - Add-Content -LiteralPath $config -Value $configMarker - Add-Content -LiteralPath $rules -Value $rulesMarker - & chezmoi.exe apply - if ($LASTEXITCODE -ne 0) { throw 'create-only probe apply failed' } - if (-not ((Get-Content $config -Raw).Contains($configMarker))) { throw 'chezmoi overwrote create-only config.toml' } - if (-not ((Get-Content $rules -Raw).Contains($rulesMarker))) { throw 'chezmoi overwrote create-only default.rules' } - $status = @(& chezmoi.exe status --exclude=scripts) - $pending = @($status | Where-Object { - $line = [string]$_ - $line.Length -ge 2 -and $line[1] -ne ' ' - }) - $status | ForEach-Object { Write-Host "STATUS: $_" } - if ($pending.Count -ne 0) { throw "create-only probe left pending target changes: $($pending -join ' | ')" } - - - name: Probe Windows managed module imports - shell: pwsh - run: | - $modules = Get-Content -LiteralPath "$env:GITHUB_WORKSPACE\scripts\windows\managed-modules.txt" | - Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') } - foreach ($module in $modules) { - if ($module -eq 'git-aliases') { - Import-Module $module -Force -DisableNameChecking -ErrorAction Stop - } else { - Import-Module $module -Force -ErrorAction Stop - } - } - - - name: Probe Windows normal policy did not create signing certificate - shell: pwsh - run: | - $certificate = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object Subject -eq 'CN=jsilverdev Dotfiles Code Signing') - if ($certificate.Count -ne 0) { throw 'normal-policy probe unexpectedly found the dotfiles signing certificate' } - - - name: Probe Windows chezmoi source Git cleanliness - shell: pwsh - run: | - $source = (& chezmoi.exe source-path).Trim() - $sourceStatus = @(git -C $source status --porcelain) - $sourceStatus | ForEach-Object { Write-Host "SOURCE STATUS: $_" } - if ($LASTEXITCODE -ne 0 -or $sourceStatus.Count -ne 0) { throw "chezmoi source is dirty: $($sourceStatus -join ' | ')" } - - - name: Probe Windows Actions checkout Git cleanliness - shell: pwsh - run: | - $repoStatus = @(git -C $env:GITHUB_WORKSPACE status --porcelain) - $repoStatus | ForEach-Object { Write-Host "CHECKOUT STATUS: $_" } - if ($LASTEXITCODE -ne 0 -or $repoStatus.Count -ne 0) { throw "checkout is dirty: $($repoStatus -join ' | ')" } - - - name: Probe Windows PowerShell profile startup - shell: pwsh - run: | - $profileOutput = @(& pwsh.exe -Command "Write-Output 'profile-ok'" 2>&1) - $profileOutput | ForEach-Object { Write-Host $_ } - if ($LASTEXITCODE -ne 0 -or -not ($profileOutput -contains 'profile-ok')) { - throw "PowerShell profile startup failed: $($profileOutput -join ' | ')" - } - - - name: Assert Windows normal-policy state - shell: pwsh - run: | - & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` - -RepoRoot $env:GITHUB_WORKSPACE -ExpectedCommit $env:GITHUB_SHA - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - - name: Create Windows update fixture - shell: pwsh - run: | - $updateClone = Join-Path $env:RUNNER_TEMP 'dotfiles-update' - git clone $env:DOTFILES_REPO $updateClone - git -C $updateClone config user.name ci - git -C $updateClone config user.email ci@example.invalid - $marker = "ci-update-marker-$env:GITHUB_RUN_ID" - Add-Content -Path (Join-Path $updateClone 'home\dot_fdignore') -Value $marker - git -C $updateClone add home/dot_fdignore - git -c commit.gpgsign=false -C $updateClone commit -m 'CI update fixture' - git -C $updateClone push origin HEAD:refs/heads/main - if ($LASTEXITCODE -ne 0) { throw 'unable to push Windows update fixture' } - Add-Content -Path $env:GITHUB_ENV -Value "UPDATE_MARKER=$marker" - - - name: Exercise Windows update.cmd against the local remote - shell: pwsh - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - run: | - $runDirectory = Join-Path $env:RUNNER_TEMP 'dotfiles-run' - Push-Location $runDirectory - try { - $update = Join-Path $env:GITHUB_WORKSPACE 'update.cmd' - & cmd.exe /d /c $update - if ($LASTEXITCODE -ne 0) { throw "update.cmd failed with exit code $LASTEXITCODE" } - } - finally { Pop-Location } - - - name: Assert Windows state after update - shell: pwsh + - uses: actions/checkout@v7 + with: {fetch-depth: 0, persist-credentials: false} + - shell: pwsh + run: ./tests/windows/prepare-ci.ps1 + - name: Run bootstrap + shell: cmd run: | - & pwsh.exe -NoProfile -File "$env:GITHUB_WORKSPACE\tests\windows\assert-state.ps1" ` - -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $env:UPDATE_MARKER - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - + mkdir "%RUNNER_TEMP%\dotfiles-run" 2>nul + pushd "%RUNNER_TEMP%\dotfiles-run" + call "%GITHUB_WORKSPACE%\bootstrap.cmd" + set "E=%ERRORLEVEL%" + popd + exit /b %E% + - shell: pwsh + run: ./tests/windows/assert-state.ps1 -RepoRoot $env:GITHUB_WORKSPACE -ExpectedCommit $env:GITHUB_SHA + - shell: cmd + run: call "%GITHUB_WORKSPACE%\tests\windows\create-update-fixture.cmd" + - shell: cmd + run: call "%GITHUB_WORKSPACE%\update.cmd" + - shell: pwsh + run: ./tests/windows/assert-state.ps1 -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $env:UPDATE_MARKER windows-allsigned: runs-on: windows-2025 timeout-minutes: 50 + env: {DOTFILES_NONINTERACTIVE: "1", DOTFILES_CORE_ONLY: "1"} steps: - - name: Check out the exact revision - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Provision WinGet before changing execution policy - shell: pwsh - run: | - Write-Host 'Checking for winget.exe' - cmd /c where winget.exe - if ($LASTEXITCODE -ne 0) { - Install-PackageProvider -Name NuGet -Force | Out-Null - Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null - Import-Module Microsoft.WinGet.Client -Force - Repair-WinGetPackageManager -Force -Latest - } - cmd /c where winget.exe - if ($LASTEXITCODE -ne 0) { throw 'winget.exe is unavailable after Microsoft.WinGet.Client repair' } - winget.exe --version - @( - (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links'), - (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft.PowerShell'), - (Join-Path $env:LOCALAPPDATA 'Programs\mise'), - (Join-Path $env:LOCALAPPDATA 'Programs\chezmoi') - ) | ForEach-Object { Add-Content -Path $env:GITHUB_PATH -Value $_ } - - - name: Create the exact-commit local bootstrap remote before AllSigned - shell: pwsh - run: | - $remote = Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git" - git init --bare $remote - $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() - if ($head -ne $env:GITHUB_SHA) { throw "checkout $head does not match GITHUB_SHA $env:GITHUB_SHA" } - $remoteUri = "file:///" + $remote.Replace('\', '/') - git -C $env:GITHUB_WORKSPACE push $remoteUri "$($env:GITHUB_SHA):refs/heads/main" - if ($LASTEXITCODE -ne 0) { throw "unable to push the checked-out commit to $remoteUri" } - git --git-dir=$remote show-ref --verify --quiet refs/heads/main - if ($LASTEXITCODE -ne 0) { throw 'local bootstrap remote has no main ref' } - git --git-dir=$remote symbolic-ref HEAD refs/heads/main - Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$remoteUri" - Write-Host "Local bootstrap remote: $remoteUri" - - - name: Create CI code-signing certificate - shell: pwsh - run: | - $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' - @' - param([string]$Value) - if ($Value -ne 'bridge-ok') { throw "unexpected bridge value: $Value" } - '@ | Set-Content -LiteralPath $smoke - - $signerSmoke = Join-Path $env:RUNNER_TEMP 'allsigned-signer-smoke.ps1' - @' - param([string]$Value) - if ($Value -ne 'signer-ok') { throw "unexpected signer value: $Value" } - '@ | Set-Content -LiteralPath $signerSmoke - - $subject = 'CN=jsilverdev Dotfiles Code Signing' - $certificate = Get-ChildItem Cert:\CurrentUser\My | - Where-Object { $_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | - Sort-Object NotAfter -Descending | - Select-Object -First 1 - if ($null -eq $certificate) { - $certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 - } - Set-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Value $certificate.Thumbprint -NoNewline - Export-Certificate -Cert $certificate -FilePath (Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer') -Type CERT -Force | Out-Null - - - name: Trust and verify CI code-signing certificate - shell: pwsh - timeout-minutes: 2 - run: | - $certificatePath = Join-Path $env:RUNNER_TEMP 'dotfiles-signing.cer' - $thumbprint = (Get-Content -LiteralPath (Join-Path $env:RUNNER_TEMP 'dotfiles-cert-thumbprint.txt') -Raw).Trim() - - $stores = @( - @{ Path = 'Cert:\LocalMachine\Root'; Name = 'LocalMachine\\Root' }, - @{ Path = 'Cert:\CurrentUser\TrustedPublisher'; Name = 'CurrentUser\\TrustedPublisher' } - ) - foreach ($store in $stores) { - $existing = Get-ChildItem $store.Path | - Where-Object Thumbprint -eq $thumbprint | - Select-Object -First 1 - if ($null -eq $existing) { - Import-Certificate -FilePath $certificatePath -CertStoreLocation $store.Path -Confirm:$false | Out-Null - } - $trusted = Get-ChildItem $store.Path | - Where-Object Thumbprint -eq $thumbprint | - Select-Object -First 1 - if ($null -eq $trusted) { - throw "CI code-signing certificate was not trusted in $($store.Name)" - } - } - - $signingCertificate = Get-ChildItem Cert:\CurrentUser\My | - Where-Object Thumbprint -eq $thumbprint | - Select-Object -First 1 - if ($null -eq $signingCertificate -or -not $signingCertificate.HasPrivateKey) { - throw 'CI code-signing certificate with private key is unavailable' - } - - $smoke = Join-Path $env:RUNNER_TEMP 'allsigned-bridge-smoke.ps1' - Set-AuthenticodeSignature -FilePath $smoke -Certificate $signingCertificate -HashAlgorithm SHA256 | Out-Null - $signature = Get-AuthenticodeSignature -FilePath $smoke - if ($signature.Status -ne 'Valid' -or - $null -eq $signature.SignerCertificate -or - $signature.SignerCertificate.Thumbprint -ne $thumbprint) { - throw "trusted CI Authenticode signature is not valid: $($signature.Status) $($signature.StatusMessage)" - } - - - name: Enable and prove CurrentUser AllSigned - shell: pwsh - run: | - Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force - $currentUser = Get-ExecutionPolicy -Scope CurrentUser - $effective = Get-ExecutionPolicy - Get-ExecutionPolicy -List | Format-Table -AutoSize - Write-Host "CurrentUser=$currentUser Effective=$effective" - if ($currentUser -ne 'AllSigned' -or $effective -ne 'AllSigned') { - throw "unable to establish effective CurrentUser AllSigned: CurrentUser=$currentUser Effective=$effective" - } - - - name: Execute trusted smoke script directly under AllSigned - shell: cmd - run: | - pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok - if errorlevel 1 exit /b 1 - - - name: Smoke-test centralized signer through the bridge under AllSigned - shell: cmd - run: | - call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\scripts\windows\signing.ps1" -Action ProtectFiles -Path "%RUNNER_TEMP%\allsigned-signer-smoke.ps1" - if errorlevel 1 exit /b 1 - pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-signer-smoke.ps1" -Value signer-ok - if errorlevel 1 exit /b 1 - - - name: Smoke-test signing bridge under AllSigned - shell: cmd - run: | - call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok - if errorlevel 1 exit /b 1 - - - name: Smoke-test deploy-pwsh hook target under AllSigned + - uses: actions/checkout@v7 + with: {fetch-depth: 0, persist-credentials: false} + - shell: pwsh + run: ./tests/windows/prepare-ci.ps1 + - shell: pwsh + run: ./tests/windows/enable-allsigned.ps1 + - name: Prove unsigned scripts are rejected shell: cmd run: | - call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\scripts\windows\deploy-pwsh.ps1" -RepoRoot "%GITHUB_WORKSPACE%" - if errorlevel 1 exit /b 1 - - - name: Run the real bootstrap through CMD and the signing bridge - id: allsigned_bootstrap - continue-on-error: true + >"%RUNNER_TEMP%\unsigned.ps1" echo Write-Output unexpected + pwsh.exe -NoProfile -File "%RUNNER_TEMP%\unsigned.ps1" >nul 2>&1 + if not errorlevel 1 exit /b 1 + - shell: cmd + run: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok + - name: Run bootstrap under AllSigned shell: cmd - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - DOTFILES_BOOTSTRAP_DIAGNOSTICS: '1' run: | - set "RUN_DIRECTORY=%RUNNER_TEMP%\dotfiles-run" - if not exist "%RUN_DIRECTORY%" mkdir "%RUN_DIRECTORY%" - pushd "%RUN_DIRECTORY%" + mkdir "%RUNNER_TEMP%\dotfiles-run" 2>nul + pushd "%RUNNER_TEMP%\dotfiles-run" call "%GITHUB_WORKSPACE%\bootstrap.cmd" - set "BOOTSTRAP_EXIT=%ERRORLEVEL%" + set "E=%ERRORLEVEL%" popd - echo AllSigned bootstrap exit=%BOOTSTRAP_EXIT% - echo exit_code=%BOOTSTRAP_EXIT%>>"%GITHUB_OUTPUT%" - exit /b %BOOTSTRAP_EXIT% - - - name: AllSigned bootstrap failed during legacy cleanup - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '11' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while validating WinGet - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '12' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while ensuring Git - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '21' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while ensuring PowerShell 7 - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '22' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while ensuring mise - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '23' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while ensuring chezmoi - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '24' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed while refreshing PATH - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '25' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed during chezmoi init - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '31' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed during chezmoi apply or hook execution - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '33' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed during chezmoi update - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '34' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed resolving the repo root - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '32' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed inside install.ps1 - if: steps.allsigned_bootstrap.outcome == 'failure' && steps.allsigned_bootstrap.outputs.exit_code == '40' - shell: cmd - run: exit /b 1 - - - name: AllSigned bootstrap failed with an unmapped diagnostic code - if: steps.allsigned_bootstrap.outcome == 'failure' && !contains(fromJSON('["11","12","21","22","23","24","25","31","32","33","34","40"]'), steps.allsigned_bootstrap.outputs.exit_code) - shell: cmd - run: exit /b 1 - - - name: Assert signed runtime and module state through the bridge - shell: cmd - run: | - call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" - if errorlevel 1 exit /b 1 - - - name: Create an update commit without changing the Actions checkout - shell: cmd - run: | - set "UPDATE_CLONE=%RUNNER_TEMP%\dotfiles-update" - git clone "%DOTFILES_REPO%" "%UPDATE_CLONE%" - if errorlevel 1 exit /b 1 - git -C "%UPDATE_CLONE%" config user.name ci - git -C "%UPDATE_CLONE%" config user.email ci@example.invalid - set "UPDATE_MARKER=ci-update-marker-%GITHUB_RUN_ID%" - >>"%UPDATE_CLONE%\home\dot_fdignore" echo %UPDATE_MARKER% - git -C "%UPDATE_CLONE%" add home/dot_fdignore - git -c commit.gpgsign=false -C "%UPDATE_CLONE%" commit -m "CI update fixture" - git -C "%UPDATE_CLONE%" push origin HEAD:refs/heads/main - if errorlevel 1 exit /b 1 - echo UPDATE_MARKER=%UPDATE_MARKER%>>"%GITHUB_ENV%" - - - name: Exercise update.cmd under AllSigned - shell: cmd - env: - DOTFILES_NONINTERACTIVE: '1' - DOTFILES_CORE_ONLY: '1' - run: | - call "%GITHUB_WORKSPACE%\update.cmd" - if errorlevel 1 exit /b 1 - - - name: Recheck AllSigned state after update - shell: cmd - run: | - call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" -UpdateMarker "%UPDATE_MARKER%" - if errorlevel 1 exit /b 1 + exit /b %E% + - shell: cmd + run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" + - shell: cmd + run: call "%GITHUB_WORKSPACE%\tests\windows\create-update-fixture.cmd" + - shell: cmd + run: call "%GITHUB_WORKSPACE%\update.cmd" + - shell: cmd + run: call "%GITHUB_WORKSPACE%\scripts\windows\invoke-ps-script.cmd" "%GITHUB_WORKSPACE%\tests\windows\assert-allsigned.ps1" -RepoRoot "%GITHUB_WORKSPACE%" -ThumbprintFile "%RUNNER_TEMP%\dotfiles-cert-thumbprint.txt" -UpdateMarker "%UPDATE_MARKER%" diff --git a/README.md b/README.md index 8301187..4cf0d70 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,6 @@ For dotfiles plus package/application and module updates, use `update.cmd` on Wi ## CI and testing -The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. +The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. GitHub Actions only orchestrates the scenarios; reusable fixture and assertion logic lives under `tests/` so it can be maintained and run independently. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. When PowerShell 7 requires signed scripts, the bridge uses inbox Windows PowerShell only for Authenticode signing and executes the resulting signed script with PowerShell 7 under the effective policy. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\\Root` plus `CurrentUser\\TrustedPublisher`; the runtime helpers also accept `CurrentUser\\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. diff --git a/bootstrap.cmd b/bootstrap.cmd index d034485..69b7367 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -6,12 +6,8 @@ if defined DOTFILES_REPO ( ) else ( set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" ) - -set "BOOTSTRAP_FAILURE_CODE=11" call :remove_legacy_broken_links if errorlevel 1 goto bootstrap_failed - -set "BOOTSTRAP_FAILURE_CODE=12" where winget.exe >nul 2>&1 if errorlevel 1 ( echo WinGet is not registered for this user. Attempting App Installer registration... @@ -26,21 +22,14 @@ if errorlevel 1 ( echo WinGet is unavailable after App Installer registration. Check corporate policy or App Installer registration. 1>&2 goto bootstrap_failed ) - -set "BOOTSTRAP_FAILURE_CODE=21" call :ensure_package Git.Git git if errorlevel 1 goto bootstrap_failed -set "BOOTSTRAP_FAILURE_CODE=22" call :ensure_package Microsoft.PowerShell pwsh if errorlevel 1 goto bootstrap_failed -set "BOOTSTRAP_FAILURE_CODE=23" call :ensure_package jdx.mise mise if errorlevel 1 goto bootstrap_failed -set "BOOTSTRAP_FAILURE_CODE=24" call :ensure_package twpayne.chezmoi chezmoi if errorlevel 1 goto bootstrap_failed - -set "BOOTSTRAP_FAILURE_CODE=25" call :refresh_path where git.exe >nul 2>&1 if errorlevel 1 ( @@ -62,15 +51,11 @@ if errorlevel 1 ( echo chezmoi is still unavailable after installation. 1>&2 goto bootstrap_failed ) - -set "BOOTSTRAP_FAILURE_CODE=31" call :initialize_chezmoi if errorlevel 1 ( echo chezmoi initialization/update failed. 1>&2 goto bootstrap_failed ) - -set "BOOTSTRAP_FAILURE_CODE=32" call :resolve_repo_root if not defined REPO_ROOT ( echo Unable to resolve the chezmoi working tree. 1>&2 @@ -80,8 +65,6 @@ if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( echo Resolved chezmoi working tree does not contain the dotfiles scripts: "%REPO_ROOT%" 1>&2 goto bootstrap_failed ) - -set "BOOTSTRAP_FAILURE_CODE=40" if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -CoreOnly -RepoRoot "%REPO_ROOT%" ) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( @@ -95,7 +78,6 @@ if errorlevel 1 goto bootstrap_failed exit /b 0 :bootstrap_failed -if /I "%DOTFILES_BOOTSTRAP_DIAGNOSTICS%"=="1" exit /b %BOOTSTRAP_FAILURE_CODE% exit /b 1 :ensure_package @@ -138,7 +120,6 @@ exit /b 1 :initialize_chezmoi if exist "%CD%\.chezmoiroot" ( - set "BOOTSTRAP_FAILURE_CODE=33" chezmoi.exe --source "%CD%" apply exit /b %ERRORLEVEL% ) @@ -146,16 +127,11 @@ set "SOURCE_ROOT=" for /f "delims=" %%R in ('chezmoi.exe source-path 2^>nul') do set "SOURCE_ROOT=%%R" if defined SOURCE_ROOT if exist "%SOURCE_ROOT%\.chezmoiroot" goto existing_chezmoi if defined SOURCE_ROOT for %%P in ("%SOURCE_ROOT%\..") do if exist "%%~fP\.chezmoiroot" goto existing_chezmoi - -set "BOOTSTRAP_FAILURE_CODE=31" chezmoi.exe init "%REPO_URL%" if errorlevel 1 exit /b %ERRORLEVEL% - -set "BOOTSTRAP_FAILURE_CODE=33" chezmoi.exe apply exit /b %ERRORLEVEL% :existing_chezmoi -set "BOOTSTRAP_FAILURE_CODE=34" chezmoi.exe update exit /b %ERRORLEVEL% diff --git a/bootstrap.sh b/bootstrap.sh index b1e9a60..71576e8 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -50,10 +50,10 @@ configure_local_chezmoi_source() { if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then DISTRO="debian" sudo apt-get update - sudo apt-get install --yes git curl wget zsh + sudo apt-get install --yes git curl zsh elif [[ -f /etc/arch-release ]] && command -v pacman >/dev/null 2>&1; then DISTRO="arch" - sudo pacman -Syu --noconfirm --needed git curl wget zsh chezmoi + sudo pacman -Syu --noconfirm --needed git curl zsh chezmoi else printf 'Unsupported Linux distribution. Debian and Arch Linux are supported.\n' >&2 exit 1 diff --git a/install.ps1 b/install.ps1 index b957cce..0f1b329 100644 --- a/install.ps1 +++ b/install.ps1 @@ -60,8 +60,10 @@ function Invoke-SigningHelper { [string]$ModuleName ) - # The signing helper performs the effective-policy check. Calling it on a - # normal-policy machine is intentionally a no-op. + if ($env:DOTFILES_SIGNING_REQUIRED -ne "1" -and (Get-ExecutionPolicy) -ne "AllSigned") { + return + } + $helper = Join-Path $RepoRoot "scripts\windows\signing.ps1" $bridge = Join-Path $RepoRoot "scripts\windows\invoke-ps-script.cmd" if (-not (Test-Path -LiteralPath $helper) -or -not (Test-Path -LiteralPath $bridge)) { @@ -382,6 +384,11 @@ if (-not $CoreOnly) { } Configure-Git Install-MustHaveApps -if (-not $CoreOnly) { Configure-WindowsTerminal } -Install-OptionalApps +if (-not $CoreOnly) { + Configure-WindowsTerminal + Install-OptionalApps +} +else { + Write-Host "Skipping optional applications in core-only mode." -ForegroundColor Yellow +} Configure-Wsl diff --git a/install.sh b/install.sh index 9798ea5..c1e9282 100755 --- a/install.sh +++ b/install.sh @@ -96,14 +96,14 @@ detect_arch() { function install_with_apt () { local app=$1 - if hash "${app}" 2> /dev/null && ! updates_enabled; then + if command -v "${app}" >/dev/null 2>&1 && ! updates_enabled; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed${RESET}" elif dpkg -s "${app}" &> /dev/null && ! updates_enabled; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via APT${RESET}" elif hash flatpak 2> /dev/null && [[ ! -z $(echo $(flatpak list --columns=ref | grep $app)) ]]; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Flatpak${RESET}" else - if updates_enabled && { hash "${app}" 2> /dev/null || dpkg -s "${app}" &> /dev/null; }; then + if updates_enabled && { command -v "${app}" >/dev/null 2>&1 || dpkg -s "${app}" &> /dev/null; }; then echo -e "${CYAN}[Updating]${LIGHT} ${app}...${RESET}" else echo -e "${CYAN}[Installing]${LIGHT} Downloading ${app}...${RESET}" @@ -116,10 +116,10 @@ function check_package_or_run () { local app=$1 local installer=$2 - if hash "$app" 2> /dev/null && ! updates_enabled; then + if command -v "$app" >/dev/null 2>&1 && ! updates_enabled; then echo -e "${YELLOW}[Skipping]${LIGHT} $app is already installed${RESET}" else - if hash "$app" 2> /dev/null; then + if command -v "$app" >/dev/null 2>&1; then echo -e "${CYAN}[Updating]${LIGHT} $app...${RESET}" else echo -e "${CYAN}[Installing]${LIGHT} Downloading $app...${RESET}" @@ -245,7 +245,7 @@ function install_sheldon () { function install_debian_packages () { - debian_apps=( + local debian_apps=( "git" "curl" "wget" @@ -267,7 +267,7 @@ function install_debian_packages () { ) for app in "${debian_apps[@]}"; do - install_with_apt $app + install_with_apt "$app" done check_package_or_run "fastfetch" "install_fastfetch" @@ -285,16 +285,16 @@ function install_with_pacman () { local pacman_status pacman_app=$(printf '%s' "$app" | tr 'A-Z' 'a-z') - pacman_status=$(pacman -Qk "$pacman_app" 2> /dev/null) + pacman_status=$(pacman -Qk "$pacman_app" 2>/dev/null || true) - if hash "${app}" 2> /dev/null && ! updates_enabled; then + if command -v "${app}" >/dev/null 2>&1 && ! updates_enabled; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed${RESET}" elif [[ "$pacman_status" == *"total files"* ]] && ! updates_enabled; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Pacman${RESET}" elif hash flatpak 2> /dev/null && [[ ! -z $(echo $(flatpak list --columns=ref | grep $app)) ]]; then echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Flatpak${RESET}" else - if updates_enabled && { hash "${app}" 2> /dev/null || [[ "$pacman_status" == *"total files"* ]]; }; then + if updates_enabled && { command -v "${app}" >/dev/null 2>&1 || [[ "$pacman_status" == *"total files"* ]]; }; then echo -e "${CYAN}[Updating]${LIGHT} ${app}...${RESET}" else echo -e "${CYAN}[Installing]${LIGHT} Downloading ${app}...${RESET}" @@ -304,7 +304,7 @@ function install_with_pacman () { } function install_arch_packages () { - pacman_apps=( + local pacman_apps=( "git" "curl" "wget" @@ -333,11 +333,11 @@ function install_arch_packages () { ) for app in "${pacman_apps[@]}"; do - install_with_pacman $app + install_with_pacman "$app" done # Install yay - if hash "yay" 2> /dev/null; then + if command -v yay >/dev/null 2>&1; then echo -e "${YELLOW}[Skipping]${LIGHT} yay is already installed${RESET}" else sudo pacman -S --needed git base-devel && git clone https://aur.archlinux.org/yay.git ~/.yay && (cd ~/.yay && makepkg -si) && rm -rf ~/.yay @@ -358,7 +358,7 @@ function install_must_have_packages() { } function setup_sheldon_plugins () { - if hash "sheldon" 2> /dev/null; then + if command -v sheldon >/dev/null 2>&1; then sheldon lock fi } @@ -368,7 +368,7 @@ function setup_default_shell() { local target_shell current_shell=$(getent passwd "$target_user" | cut -d: -f7) - target_shell="$(which zsh)" + target_shell="$(command -v zsh)" if [ "$current_shell" != "$target_shell" ]; then chsh -s "$target_shell" "$target_user" @@ -422,7 +422,7 @@ function install_docker () { curl -fsSL https://get.docker.com -o get-docker.sh sudo sh ./get-docker.sh rm get-docker.sh - sudo usermod -aG docker $USER + sudo usermod -aG docker "${USER:-$(id -un)}" } function install_dagger () { diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 index 63bcf39..68c7f70 100644 --- a/scripts/windows/deploy-pwsh.ps1 +++ b/scripts/windows/deploy-pwsh.ps1 @@ -36,13 +36,13 @@ foreach ($file in $files) { Copy-Item -LiteralPath $source -Destination $file.Destination -Force } -# Always call the centralized signing helper. It is a no-op unless the effective -# PowerShell execution policy is AllSigned. The CMD bridge itself is AllSigned-safe. -$helper = Join-Path $repo "scripts\windows\signing.ps1" -$bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" -foreach ($destination in @($files | ForEach-Object Destination)) { - & $bridge $helper -Action ProtectFiles -Path $destination - if ($LASTEXITCODE -ne 0) { - throw "PowerShell runtime signing failed for $destination with exit code $LASTEXITCODE." +if ($env:DOTFILES_SIGNING_REQUIRED -eq "1" -or (Get-ExecutionPolicy) -eq "AllSigned") { + $helper = Join-Path $repo "scripts\windows\signing.ps1" + $bridge = Join-Path $repo "scripts\windows\invoke-ps-script.cmd" + foreach ($destination in @($files | ForEach-Object Destination)) { + & $bridge $helper -Action ProtectFiles -Path $destination + if ($LASTEXITCODE -ne 0) { + throw "PowerShell runtime signing failed for $destination with exit code $LASTEXITCODE." + } } } diff --git a/tests/linux/run-ci.sh b/tests/linux/run-ci.sh new file mode 100644 index 0000000..393615a --- /dev/null +++ b/tests/linux/run-ci.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +distro="${1:?distro required}"; commit="${2:?commit required}"; repo="${GITHUB_WORKSPACE:?}" +git config --global --add safe.directory "$repo" +[[ "$(git -C "$repo" rev-parse HEAD)" == "$commit" ]] +parent="$(mktemp -d)"; remote="$parent/dotfiles.git" +git init --bare "$remote"; git -C "$repo" push "$remote" "$commit:refs/heads/main"; git --git-dir="$remote" symbolic-ref HEAD refs/heads/main +chmod -R a+rX "$repo"; useradd --create-home --shell /bin/bash dotfilesci +printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/dotfilesci; chmod 0440 /etc/sudoers.d/dotfilesci; chown -R dotfilesci:dotfilesci "$parent" +as_ci(){ sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci "$@"; } +as_ci GITHUB_WORKSPACE="$repo" DOTFILES_REPO="file://$remote" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' +as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$repo" GITHUB_WORKSPACE="$repo" D="$distro" C="$commit" bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" "$D" "$C"' +marker="ci-update-marker-${GITHUB_RUN_ID:-local}" +as_ci DOTFILES_REPO="file://$remote" M="$marker" bash -lc 'set -euo pipefail; d=$(mktemp -d); git clone "$DOTFILES_REPO" "$d/r"; git -C "$d/r" config user.name ci; git -C "$d/r" config user.email ci@example.invalid; printf "\n%s\n" "$M" >>"$d/r/home/dot_fdignore"; git -C "$d/r" add home/dot_fdignore; git -c commit.gpgsign=false -C "$d/r" commit -m "CI update fixture"; git -C "$d/r" push origin HEAD:refs/heads/main' +as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GITHUB_WORKSPACE="$repo" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' +as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$repo" GITHUB_WORKSPACE="$repo" D="$distro" M="$marker" bash -lc 'grep -Fqx "$M" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" "$D"' diff --git a/tests/static/check-powershell-bridge.py b/tests/static/check-powershell-bridge.py new file mode 100644 index 0000000..612b0bd --- /dev/null +++ b/tests/static/check-powershell-bridge.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import base64,pathlib,re +root=pathlib.Path(__file__).resolve().parents[2] +cmd=(root/"scripts/windows/invoke-ps-script.cmd").read_text() +src=(root/"scripts/windows/invoke-ps-script-bridge.ps1").read_text() +m=re.search(r'(?m)^"%SystemRoot%\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe" -NoProfile -EncodedCommand ([A-Za-z0-9+/=]+)\s*$',cmd) +if not m: raise SystemExit("encoded bridge payload is missing") +if base64.b64decode(m.group(1)).decode("utf-16le")!=src: raise SystemExit("encoded bridge payload does not match source") +if len(m.group(1))>=8000: raise SystemExit("encoded bridge payload is too close to cmd.exe limits") +print(f"bridge-source-ok: encoded length={len(m.group(1))}") diff --git a/tests/static/validate-powershell.ps1 b/tests/static/validate-powershell.ps1 new file mode 100644 index 0000000..553efc3 --- /dev/null +++ b/tests/static/validate-powershell.ps1 @@ -0,0 +1,9 @@ +$ErrorActionPreference="Stop" +$errors=@() +Get-ChildItem (Join-Path $PSScriptRoot "..\..") -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object { + $tokens=$null; $parseErrors=$null + [System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$tokens,[ref]$parseErrors)|Out-Null + if($parseErrors.Count){$errors += "$($_.FullName): $($parseErrors -join '; ')"} +} +if($errors.Count){$errors|ForEach-Object{Write-Error $_};exit 1} +Write-Host "powershell-syntax-ok" diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh new file mode 100644 index 0000000..3075ab3 --- /dev/null +++ b/tests/static/validate-source.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "${GITHUB_WORKSPACE:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)}" +files=(bootstrap.sh install.sh update.sh tests/linux/assert-state.sh tests/linux/run-ci.sh tests/static/validate-source.sh) +bash -n "${files[@]}" +shellcheck -x -S error "${files[@]}" +chezmoi --source "$PWD" managed >/dev/null +while IFS= read -r -d '' f; do chezmoi --source "$PWD" execute-template < "$f" >/dev/null; done < <(find "$PWD" -type f -name '*.tmpl' -print0) +[[ "$(tr -d '\r\n' < .chezmoiroot)" == home ]] +for p in home scripts/windows/invoke-ps-script.cmd scripts/windows/invoke-ps-script-bridge.ps1 scripts/windows/signing.ps1 scripts/windows/deploy-pwsh.ps1 scripts/windows/managed-modules.txt home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl; do [[ -e "$p" ]] || { echo "missing: $p" >&2; exit 1; }; done +! git ls-files | grep -E '(^|/)symlink_[^/]*$' +! git grep -n -i dotbot -- ':!.github/workflows/validate.yml' +! git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master' +! git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml' +! git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd' +! git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1' +echo source-validation-ok diff --git a/tests/windows/create-update-fixture.cmd b/tests/windows/create-update-fixture.cmd new file mode 100644 index 0000000..27d6111 --- /dev/null +++ b/tests/windows/create-update-fixture.cmd @@ -0,0 +1,13 @@ +@echo off +setlocal EnableExtensions DisableDelayedExpansion +set "D=%RUNNER_TEMP%\dotfiles-update-%GITHUB_RUN_ID%" +if exist "%D%" rmdir /s /q "%D%" +git clone "%DOTFILES_REPO%" "%D%" || exit /b 1 +git -C "%D%" config user.name ci +git -C "%D%" config user.email ci@example.invalid +set "UPDATE_MARKER=ci-update-marker-%GITHUB_RUN_ID%" +>>"%D%\home\dot_fdignore" echo %UPDATE_MARKER% +git -C "%D%" add home/dot_fdignore +git -c commit.gpgsign=false -C "%D%" commit -m "CI update fixture" || exit /b 1 +git -C "%D%" push origin HEAD:refs/heads/main || exit /b 1 +>>"%GITHUB_ENV%" echo UPDATE_MARKER=%UPDATE_MARKER% diff --git a/tests/windows/enable-allsigned.ps1 b/tests/windows/enable-allsigned.ps1 new file mode 100644 index 0000000..683e96e --- /dev/null +++ b/tests/windows/enable-allsigned.ps1 @@ -0,0 +1,11 @@ +$ErrorActionPreference="Stop" +$smoke=Join-Path $env:RUNNER_TEMP "allsigned-bridge-smoke.ps1";'param([string]$Value); if($Value -ne "bridge-ok"){throw "unexpected"}'|Set-Content $smoke +$signer=Join-Path $env:RUNNER_TEMP "allsigned-signer-smoke.ps1";'param([string]$Value); if($Value -ne "signer-ok"){throw "unexpected"}'|Set-Content $signer +$subject="CN=jsilverdev Dotfiles Code Signing";$cert=Get-ChildItem Cert:\CurrentUser\My|?{$_.Subject-eq$subject-and$_.HasPrivateKey-and$_.NotAfter-gt(Get-Date)}|sort NotAfter -Descending|select -First 1 +if(!$cert){$cert=New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256} +$cer=Join-Path $env:RUNNER_TEMP "dotfiles-signing.cer";$thumb=Join-Path $env:RUNNER_TEMP "dotfiles-cert-thumbprint.txt";Set-Content $thumb $cert.Thumbprint -NoNewline;Export-Certificate -Cert $cert -FilePath $cer -Type CERT -Force|Out-Null +foreach($store in @("Cert:\LocalMachine\Root","Cert:\CurrentUser\TrustedPublisher")){if(!(Get-ChildItem $store|? Thumbprint -eq $cert.Thumbprint|select -First 1)){Import-Certificate -FilePath $cer -CertStoreLocation $store -Confirm:$false|Out-Null}} +Set-AuthenticodeSignature $smoke $cert -HashAlgorithm SHA256|Out-Null +if((Get-AuthenticodeSignature $smoke).Status-ne"Valid"){throw "signature invalid"} +Set-ExecutionPolicy -Scope CurrentUser AllSigned -Force +if((Get-ExecutionPolicy)-ne"AllSigned"){throw "AllSigned not effective"} diff --git a/tests/windows/prepare-ci.ps1 b/tests/windows/prepare-ci.ps1 new file mode 100644 index 0000000..ad7db87 --- /dev/null +++ b/tests/windows/prepare-ci.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference="Stop" +cmd /c where winget.exe +if($LASTEXITCODE-ne0){Install-PackageProvider NuGet -Force|Out-Null;Install-Module Microsoft.WinGet.Client -Force -Repository PSGallery|Out-Null;Import-Module Microsoft.WinGet.Client -Force;Repair-WinGetPackageManager -Force -Latest} +cmd /c where winget.exe +if($LASTEXITCODE-ne0){throw "winget unavailable"} +@((Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"),(Join-Path $env:LOCALAPPDATA "Programs\Microsoft.PowerShell"),(Join-Path $env:LOCALAPPDATA "Programs\mise"),(Join-Path $env:LOCALAPPDATA "Programs\chezmoi"))|%{Add-Content $env:GITHUB_PATH $_} +$remote=Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git";git init --bare $remote +$head=(git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim();if($head-ne$env:GITHUB_SHA){throw "checkout mismatch"} +$uri="file:///"+$remote.Replace("\","/");git -C $env:GITHUB_WORKSPACE push $uri "$($env:GITHUB_SHA):refs/heads/main";if($LASTEXITCODE-ne0){throw "push failed"} +git --git-dir=$remote symbolic-ref HEAD refs/heads/main;Add-Content $env:GITHUB_ENV "DOTFILES_REPO=$uri" From a4f02bdfc8c8a0a88c35728b4650680e8dcf4426 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:16:19 -0500 Subject: [PATCH 54/61] Document refactored test suite --- tests/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..436c45b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,9 @@ +# Test suite + +The GitHub Actions workflow intentionally contains only orchestration. + +- `static/` validates source structure, shell code, PowerShell syntax, chezmoi templates, and the encoded AllSigned bridge. +- `linux/` owns the Debian and Arch integration fixture and post-bootstrap assertions. +- `windows/` owns Windows fixture setup, normal-policy assertions, AllSigned assertions, and update fixtures. + +Keep reusable validation logic here instead of embedding large scripts in `.github/workflows/validate.yml`. From 44098d9a466b350d467f3098a20ea792c2936235 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:18:04 -0500 Subject: [PATCH 55/61] Fix refactored Linux and AllSigned validation --- bootstrap.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap.sh b/bootstrap.sh index 71576e8..7f71b6b 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -61,7 +61,6 @@ fi command -v git >/dev/null 2>&1 || { printf 'Git is required but unavailable.\n' >&2; exit 1; } command -v curl >/dev/null 2>&1 || { printf 'curl is required but unavailable.\n' >&2; exit 1; } -command -v wget >/dev/null 2>&1 || { printf 'wget is required but unavailable.\n' >&2; exit 1; } command -v zsh >/dev/null 2>&1 || { printf 'zsh is required but unavailable.\n' >&2; exit 1; } if [[ "$DISTRO" == "debian" ]] && ! command -v chezmoi >/dev/null 2>&1; then From 7bdf94f0ffcef6fc96a6189407d5f1f1665c07f0 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:18:17 -0500 Subject: [PATCH 56/61] Remove redundant unsigned-script probe --- .github/workflows/validate.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4adf09a..ba53ca9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -87,12 +87,6 @@ jobs: run: ./tests/windows/prepare-ci.ps1 - shell: pwsh run: ./tests/windows/enable-allsigned.ps1 - - name: Prove unsigned scripts are rejected - shell: cmd - run: | - >"%RUNNER_TEMP%\unsigned.ps1" echo Write-Output unexpected - pwsh.exe -NoProfile -File "%RUNNER_TEMP%\unsigned.ps1" >nul 2>&1 - if not errorlevel 1 exit /b 1 - shell: cmd run: pwsh.exe -NoProfile -File "%RUNNER_TEMP%\allsigned-bridge-smoke.ps1" -Value bridge-ok - name: Run bootstrap under AllSigned From 1ec3b7c141176d7a42444ff234252e062d9fec91 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:38:05 -0500 Subject: [PATCH 57/61] Simplify installer modes and validate real baseline --- .github/workflows/validate.yml | 69 ++- README.md | 16 +- bootstrap.cmd | 25 +- bootstrap.sh | 53 +-- home/.chezmoitemplates/pwsh/env.ps1 | 35 +- home/dot_zshenv | 38 +- install.ps1 | 222 +++++----- install.sh | 661 +++++++++++----------------- scripts/linux/packages-arch.txt | 25 ++ scripts/linux/packages-debian.txt | 18 + scripts/linux/required-commands.txt | 19 + scripts/windows/deploy-pwsh.ps1 | 9 - scripts/windows/managed-apps.csv | 25 ++ tests/README.md | 10 +- tests/linux/assert-state.sh | 47 +- tests/linux/run-ci.sh | 48 +- tests/static/validate-manifests.py | 33 ++ tests/static/validate-source.sh | 44 +- tests/windows/assert-allsigned.ps1 | 17 +- tests/windows/assert-state.ps1 | 12 +- tests/windows/enable-allsigned.ps1 | 45 +- tests/windows/prepare-ci.ps1 | 54 ++- update.cmd | 5 +- update.sh | 10 +- 24 files changed, 799 insertions(+), 741 deletions(-) create mode 100644 scripts/linux/packages-arch.txt create mode 100644 scripts/linux/packages-debian.txt create mode 100644 scripts/linux/required-commands.txt create mode 100644 scripts/windows/managed-apps.csv create mode 100644 tests/static/validate-manifests.py mode change 100644 => 100755 update.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ba53ca9..598e1ca 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,62 +1,101 @@ name: Validate dotfiles + on: pull_request: push: branches: [main] workflow_dispatch: + permissions: contents: read + concurrency: group: validate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true + jobs: static: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@v7 - with: {persist-credentials: false} + with: + persist-credentials: false + - name: Install validation tools + shell: bash run: | + set -euo pipefail sudo apt-get update sudo apt-get install --yes --no-install-recommends shellcheck mkdir -p "$RUNNER_TEMP/bin" curl -fsLS https://get.chezmoi.io | sh -s -- -b "$RUNNER_TEMP/bin" + + actionlint_version=1.7.12 + actionlint_archive="$RUNNER_TEMP/actionlint.tar.gz" + curl -fsSL "https://github.com/rhysd/actionlint/releases/download/v${actionlint_version}/actionlint_${actionlint_version}_linux_amd64.tar.gz" -o "$actionlint_archive" + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $actionlint_archive" | sha256sum --check + tar -xzf "$actionlint_archive" -C "$RUNNER_TEMP/bin" actionlint echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - - run: bash tests/static/validate-source.sh - - run: python3 tests/static/check-powershell-bridge.py - - shell: pwsh + + - name: Validate workflow + run: actionlint .github/workflows/validate.yml + + - name: Validate source state + run: bash tests/static/validate-source.sh + + - name: Validate manifests + run: python3 tests/static/validate-manifests.py + + - name: Validate encoded PowerShell bridge + run: python3 tests/static/check-powershell-bridge.py + + - name: Validate PowerShell syntax + shell: pwsh run: ./tests/static/validate-powershell.ps1 + linux: name: linux-${{ matrix.distro }} runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 35 strategy: fail-fast: false matrix: include: - - {distro: debian, image: "debian:bookworm-slim"} - - {distro: arch, image: "archlinux:base"} + - distro: debian + image: debian:bookworm-slim + - distro: arch + image: archlinux:base container: image: ${{ matrix.image }} steps: - name: Prepare container shell: sh run: | + set -eu case "${{ matrix.distro }}" in debian) apt-get update; apt-get install --yes --no-install-recommends ca-certificates curl git sudo bash ;; arch) pacman -Sy --noconfirm ca-certificates curl git sudo bash ;; esac + - uses: actions/checkout@v7 - with: {fetch-depth: 0, persist-credentials: false} - - run: bash tests/linux/run-ci.sh "${{ matrix.distro }}" "$GITHUB_SHA" + with: + fetch-depth: 0 + persist-credentials: false + + - name: Exercise bootstrap and update + run: bash tests/linux/run-ci.sh "${{ matrix.distro }}" "$GITHUB_SHA" + windows: runs-on: windows-2025 timeout-minutes: 45 - env: {DOTFILES_NONINTERACTIVE: "1", DOTFILES_CORE_ONLY: "1"} + env: + DOTFILES_NONINTERACTIVE: "1" steps: - uses: actions/checkout@v7 - with: {fetch-depth: 0, persist-credentials: false} + with: + fetch-depth: 0 + persist-credentials: false - shell: pwsh run: ./tests/windows/prepare-ci.ps1 - name: Run bootstrap @@ -76,13 +115,17 @@ jobs: run: call "%GITHUB_WORKSPACE%\update.cmd" - shell: pwsh run: ./tests/windows/assert-state.ps1 -RepoRoot $env:GITHUB_WORKSPACE -UpdateMarker $env:UPDATE_MARKER + windows-allsigned: runs-on: windows-2025 timeout-minutes: 50 - env: {DOTFILES_NONINTERACTIVE: "1", DOTFILES_CORE_ONLY: "1"} + env: + DOTFILES_NONINTERACTIVE: "1" steps: - uses: actions/checkout@v7 - with: {fetch-depth: 0, persist-credentials: false} + with: + fetch-depth: 0 + persist-credentials: false - shell: pwsh run: ./tests/windows/prepare-ci.ps1 - shell: pwsh diff --git a/README.md b/README.md index 4cf0d70..3e1d646 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ Run this from a normal `cmd.exe` prompt: curl.exe -fsSLo "%TEMP%\dotfiles-bootstrap.cmd" https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.cmd && call "%TEMP%\dotfiles-bootstrap.cmd" ``` -The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Core packages are installed with WinGet in user scope and the bootstrap does not silently fall back to machine-scope or portable packages. Core mode includes the CLI toolchain (`micro`, `lsd`, `bat`, `fastfetch`, `fzf`, `fd`, `delta`, `jq`, `rg`, and `mise`); workstation-only packages such as 7-Zip, PowerToys, and VS Code are installed only outside core mode. +The Windows bootstrap requires WinGet. If App Installer exists but WinGet is not registered for the current user, it attempts current-user App Installer registration. If corporate policy disables WinGet, it stops with an error. Bootstrap installs only Git, PowerShell 7, and chezmoi when they are missing; the platform installer owns the remaining application catalog. + +Baseline CLI packages are declared in `scripts/windows/managed-apps.csv` and are installed with WinGet in user scope. An interactive run also installs workstation applications, fonts, Windows Terminal customization, and offers optional applications. `DOTFILES_NONINTERACTIVE=1` installs the complete baseline but skips those interactive/workstation customizations. The bootstrap does not require administrator rights or Developer Mode. It invokes PowerShell 7 with `-NoProfile`; an effective `AllSigned` policy is supported automatically. Each machine reuses or creates its own current-user Code Signing certificate and trusts its public certificate locally. No private key, PFX, or KeePass dependency is used. @@ -24,7 +26,7 @@ On Debian or Arch Linux, run: bash <(curl -fsSL https://raw.githubusercontent.com/jsilverdev/dotfiles/main/bootstrap.sh) ``` -The bootstrap installs the minimal tools, installs chezmoi in `~/.local/bin` when needed, applies the home state, and then runs the package/application installer. WSL-specific system configuration is applied only when the installer is running inside WSL. +The bootstrap installs only the prerequisites required to obtain/apply the repository, installs chezmoi in `~/.local/bin` when needed, and then runs the package installer. Linux package catalogs are declared under `scripts/linux/`. Non-interactive mode still installs and validates the complete baseline; it only skips shell changes, WSL system configuration, and optional-package prompts. Arch continues to install and manage `yay`. ## Updates @@ -34,7 +36,7 @@ For dotfiles-only updates, use: chezmoi update ``` -For dotfiles plus package/application and module updates, use `update.cmd` on Windows or `./update.sh` on Linux. Those wrappers run `chezmoi update` first and then the platform installer in update mode. +For dotfiles plus installer-managed package/application/module updates, use `update.cmd` on Windows or `./update.sh` on Linux. Those wrappers run `chezmoi update` first and then rerun the platform installer in update mode. Starship and managed PowerShell modules participate in update mode as well. ## State details @@ -43,10 +45,12 @@ For dotfiles plus package/application and module updates, use `update.cmd` on Wi - `~/.gitconfig.local`, `~/.codex/config.toml`, and `~/.codex/rules/default.rules` use chezmoi create-only semantics and are not overwritten after creation. - Codex guidance and skills are managed normally; repository documentation is kept outside `~/.codex`. - PowerShell source files are unsigned templates. The post-apply hook deploys copies and signs only the runtime files when `AllSigned` is effective, so Authenticode signatures never dirty chezmoi source state. -- Managed PowerShell modules currently include `PSFzf` and `git-aliases`. Under `AllSigned`, only their user-scoped PowerShell content is inspected and unsigned/invalid files are signed; valid publisher signatures are preserved. +- Managed PowerShell modules are declared in `scripts/windows/managed-modules.txt`. Under `AllSigned`, managed PowerShell files are re-signed with the locally trusted dotfiles certificate unless they already carry a valid signature from that same certificate. This deliberately normalizes the publisher used by the non-interactive AllSigned path. ## CI and testing -The `Validate dotfiles` workflow exercises the current chezmoi/bootstrap architecture on Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. GitHub Actions only orchestrates the scenarios; reusable fixture and assertion logic lives under `tests/` so it can be maintained and run independently. Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test, then check deployment, create-only Codex files, update wrappers, idempotent apply, signatures, module loading, and clean chezmoi/source Git state. +The `Validate dotfiles` workflow exercises Debian, Arch Linux, Windows with its normal execution policy, and Windows with a simulated current-user `AllSigned` policy. GitHub Actions only orchestrates the scenarios; reusable fixture and assertion logic lives under `tests/`. + +Integration jobs bootstrap from a temporary local bare Git remote containing the exact commit under test. They run the real non-interactive baseline, validate installed CLI tools, exercise create-only Codex files and update wrappers, check idempotent chezmoi apply, and verify clean source/checkout state. Static validation includes ShellCheck, PowerShell parsing, manifest checks, chezmoi template evaluation, bridge-payload synchronization, and pinned `actionlint` validation of the workflow. -The Windows AllSigned job validates the hosted Windows Server behavior that can be reproduced in CI: current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. When PowerShell 7 requires signed scripts, the bridge uses inbox Windows PowerShell only for Authenticode signing and executes the resulting signed script with PowerShell 7 under the effective policy. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\\Root` plus `CurrentUser\\TrustedPublisher`; the runtime helpers also accept `CurrentUser\\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. +The Windows AllSigned job validates current-user certificate creation and reuse, Authenticode signing, the CMD execution bridge, PowerShell profile startup, and managed module loading. When PowerShell 7 requires signed scripts, the bridge uses inbox Windows PowerShell only for Authenticode signing and executes the resulting signed script with PowerShell 7 under the effective policy. The hosted runner is an administrator with UAC disabled, so CI trusts the test certificate through `LocalMachine\Root` plus `CurrentUser\TrustedPublisher`; runtime helpers also accept `CurrentUser\Root` for the real non-admin path. Corporate GPO/MDM/AppLocker/WDAC policy, enterprise App Installer policy, and a true non-admin corporate Windows 11 token still require validation on a managed machine. diff --git a/bootstrap.cmd b/bootstrap.cmd index 69b7367..88a2cda 100644 --- a/bootstrap.cmd +++ b/bootstrap.cmd @@ -6,8 +6,6 @@ if defined DOTFILES_REPO ( ) else ( set "REPO_URL=https://github.com/jsilverdev/dotfiles.git" ) -call :remove_legacy_broken_links -if errorlevel 1 goto bootstrap_failed where winget.exe >nul 2>&1 if errorlevel 1 ( echo WinGet is not registered for this user. Attempting App Installer registration... @@ -26,8 +24,6 @@ call :ensure_package Git.Git git if errorlevel 1 goto bootstrap_failed call :ensure_package Microsoft.PowerShell pwsh if errorlevel 1 goto bootstrap_failed -call :ensure_package jdx.mise mise -if errorlevel 1 goto bootstrap_failed call :ensure_package twpayne.chezmoi chezmoi if errorlevel 1 goto bootstrap_failed call :refresh_path @@ -41,11 +37,6 @@ if errorlevel 1 ( echo PowerShell 7 is still unavailable after installation. 1>&2 goto bootstrap_failed ) -where mise.exe >nul 2>&1 -if errorlevel 1 ( - echo mise is still unavailable after installation. 1>&2 - goto bootstrap_failed -) where chezmoi.exe >nul 2>&1 if errorlevel 1 ( echo chezmoi is still unavailable after installation. 1>&2 @@ -65,12 +56,8 @@ if not exist "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" ( echo Resolved chezmoi working tree does not contain the dotfiles scripts: "%REPO_ROOT%" 1>&2 goto bootstrap_failed ) -if /I "%DOTFILES_NONINTERACTIVE%"=="1" if /I "%DOTFILES_CORE_ONLY%"=="1" ( - call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -CoreOnly -RepoRoot "%REPO_ROOT%" -) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( +if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -NonInteractive -RepoRoot "%REPO_ROOT%" -) else if /I "%DOTFILES_CORE_ONLY%"=="1" ( - call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -CoreOnly -RepoRoot "%REPO_ROOT%" ) else ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -RepoRoot "%REPO_ROOT%" ) @@ -93,16 +80,6 @@ if errorlevel 1 ( ) exit /b 0 -:remove_legacy_broken_links -rem Remove only broken legacy links so Git and chezmoi can migrate them -rem to regular files. Existing valid links and unrelated junctions are kept. -"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -Command "$paths = @('%USERPROFILE%\.gitconfig','%USERPROFILE%\.gitconfig.local','%USERPROFILE%\.fdignore','%USERPROFILE%\.npmrc','%USERPROFILE%\.vimrc','%USERPROFILE%\.zshenv','%USERPROFILE%\.wslconfig','%USERPROFILE%\.ssh\jsilverdev.pub','%USERPROFILE%\.config\starship\config.toml','%USERPROFILE%\.config\starship\lean.config.toml','%USERPROFILE%\.codex\AGENTS.md','%USERPROFILE%\.codex\skills\mule-munit\SKILL.md','%USERPROFILE%\.codex\skills\mule-munit\agents\openai.yaml','%USERPROFILE%\Documents\PowerShell\profile.ps1'); foreach ($path in $paths) { if (Test-Path -LiteralPath $path) { $item = Get-Item -LiteralPath $path -Force; if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -and -not (Test-Path -LiteralPath $item.Target)) { Remove-Item -LiteralPath $path -Force } } }" -if errorlevel 1 ( - echo Unable to clean broken legacy dotfile links. 1>&2 - exit /b 1 -) -exit /b 0 - :refresh_path set "PATH=%PATH%;%LOCALAPPDATA%\Microsoft\WinGet\Links;%LOCALAPPDATA%\Programs\Microsoft.PowerShell;%LOCALAPPDATA%\Programs\mise;%LOCALAPPDATA%\Programs\chezmoi" for /f "tokens=2,*" %%A in ('reg query HKCU\Environment /v Path 2^>nul ^| findstr /i "Path"') do set "PATH=!PATH!;%%B" diff --git a/bootstrap.sh b/bootstrap.sh index 7f71b6b..c7e5df1 100644 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -5,42 +5,10 @@ REPO_URL="${DOTFILES_REPO:-https://github.com/jsilverdev/dotfiles.git}" export PATH="$HOME/.local/bin:$PATH" DISTRO="" -remove_legacy_links() { - local path target - local paths=( - "$HOME/.gitconfig" - "$HOME/.fdignore" - "$HOME/.npmrc" - "$HOME/.vimrc" - "$HOME/.zshenv" - "$HOME/.wslconfig" - "$HOME/.ssh/jsilverdev.pub" - "$HOME/.config/starship/config.toml" - "$HOME/.config/starship/lean.config.toml" - "$HOME/.config/sheldon/plugins.toml" - "$HOME/.config/zsh/.zshrc" - "$HOME/.codex/AGENTS.md" - "$HOME/.codex/skills/mule-munit/SKILL.md" - "$HOME/.codex/skills/mule-munit/agents/openai.yaml" - ) - - for path in "${paths[@]}"; do - if [[ -L "$path" ]]; then - target="$(readlink -f -- "$path" 2>/dev/null || true)" - if [[ "$target" == "$HOME/.dotfiles/"* ]]; then - rm -f -- "$path" - fi - fi - done -} - -remove_legacy_links - configure_local_chezmoi_source() { local config_dir config_path config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi" config_path="$config_dir/chezmoi.toml" - if [[ ! -e "$config_path" ]]; then mkdir -p "$config_dir" printf 'sourceDir = "%s"\n' "$PWD" > "$config_path" @@ -49,11 +17,24 @@ configure_local_chezmoi_source() { if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then DISTRO="debian" - sudo apt-get update - sudo apt-get install --yes git curl zsh + missing=() + command -v git >/dev/null 2>&1 || missing+=(git) + command -v curl >/dev/null 2>&1 || missing+=(curl) + if (( ${#missing[@]} > 0 )); then + sudo apt-get update + sudo apt-get install --yes "${missing[@]}" + export DOTFILES_PACKAGE_INDEX_READY=1 + fi elif [[ -f /etc/arch-release ]] && command -v pacman >/dev/null 2>&1; then DISTRO="arch" - sudo pacman -Syu --noconfirm --needed git curl zsh chezmoi + missing=() + command -v git >/dev/null 2>&1 || missing+=(git) + command -v curl >/dev/null 2>&1 || missing+=(curl) + command -v chezmoi >/dev/null 2>&1 || missing+=(chezmoi) + if (( ${#missing[@]} > 0 )); then + sudo pacman -Syu --noconfirm --needed "${missing[@]}" + export DOTFILES_PACKAGE_INDEX_READY=1 + fi else printf 'Unsupported Linux distribution. Debian and Arch Linux are supported.\n' >&2 exit 1 @@ -61,12 +42,12 @@ fi command -v git >/dev/null 2>&1 || { printf 'Git is required but unavailable.\n' >&2; exit 1; } command -v curl >/dev/null 2>&1 || { printf 'curl is required but unavailable.\n' >&2; exit 1; } -command -v zsh >/dev/null 2>&1 || { printf 'zsh is required but unavailable.\n' >&2; exit 1; } if [[ "$DISTRO" == "debian" ]] && ! command -v chezmoi >/dev/null 2>&1; then mkdir -p "$HOME/.local/bin" sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" fi + export PATH="$HOME/.local/bin:$PATH" command -v chezmoi >/dev/null 2>&1 || { printf 'chezmoi installation failed.\n' >&2; exit 1; } diff --git a/home/.chezmoitemplates/pwsh/env.ps1 b/home/.chezmoitemplates/pwsh/env.ps1 index 39c6805..9ffe63e 100644 --- a/home/.chezmoitemplates/pwsh/env.ps1 +++ b/home/.chezmoitemplates/pwsh/env.ps1 @@ -1,13 +1,22 @@ - -$env:VISUAL = "code" -$env:PAGER = "delta" -$env:PYTHONIOENCODING = "utf-8" - -$PS_USER_FOLDER = "D:\$ENV:USERNAME" -if (Test-Path -Path $PS_USER_FOLDER) { - $ENV:STARSHIP_CACHE = "$PS_USER_FOLDER\Temp\starship" -} -else { - $PS_USER_FOLDER = "$HOME" -} -$ENV:STARSHIP_CONFIG = "$HOME\.config\starship\config.toml" \ No newline at end of file +$preferredEditor = if (Get-Command code -ErrorAction SilentlyContinue) { + "code" +} +elseif (Get-Command micro -ErrorAction SilentlyContinue) { + "micro" +} +else { + "notepad" +} +$env:VISUAL = $preferredEditor +$env:EDITOR = $preferredEditor +$env:PAGER = "delta" +$env:PYTHONIOENCODING = "utf-8" + +$PS_USER_FOLDER = "D:\$ENV:USERNAME" +if (Test-Path -Path $PS_USER_FOLDER) { + $ENV:STARSHIP_CACHE = "$PS_USER_FOLDER\Temp\starship" +} +else { + $PS_USER_FOLDER = "$HOME" +} +$ENV:STARSHIP_CONFIG = "$HOME\.config\starship\config.toml" diff --git a/home/dot_zshenv b/home/dot_zshenv index a87feef..ae5e33e 100644 --- a/home/dot_zshenv +++ b/home/dot_zshenv @@ -1,39 +1,32 @@ -# ~/.zshenv -# Core envionmental variables -# Locations configured here are requred for all other files to be correctly imported +# ~/.zshenv +# Core environmental variables required by all interactive Zsh sessions. -# Set XDG directories export XDG_CONFIG_HOME="${HOME}/.config" export XDG_DATA_HOME="${HOME}/.local/share" -# Set default applications -export EDITOR="vim" +if command -v micro >/dev/null 2>&1; then + export EDITOR="micro" +elif command -v vim >/dev/null 2>&1; then + export EDITOR="vim" +else + export EDITOR="vi" +fi +export VISUAL="$EDITOR" export PAGER="less" -## Respect XDG directories export CARGO_HOME="${XDG_DATA_HOME}/cargo" export DOCKER_CONFIG="${XDG_CONFIG_HOME}/docker" - -# export GIT_CONFIG="${XDG_CONFIG_HOME}/git/.gitconfig" - -export LESSHISTFILE="-" # Disable less history. - +export LESSHISTFILE="-" export PIP_CONFIG_FILE="${XDG_CONFIG_HOME}/pip/pip.conf" export PIP_LOG_FILE="${XDG_DATA_HOME}/pip/log" export PYENV_ROOT="$HOME/.pyenv" - export ZDOTDIR="${XDG_CONFIG_HOME}/zsh" -# local bin -case ":$PATH:" in - *":$HOME/.local/bin:"*) ;; - *) export PATH="$PATH:$HOME/.local/bin" ;; -esac +case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$PATH:$HOME/.local/bin" ;; esac -# Define Chrome executable -if command -v "google-chrome" > /dev/null 2>&1; then +if command -v google-chrome >/dev/null 2>&1; then export CHROME_EXECUTABLE="google-chrome" -elif command -v "google-chrome-stable" > /dev/null 2>&1; then +elif command -v google-chrome-stable >/dev/null 2>&1; then export CHROME_EXECUTABLE="google-chrome-stable" fi @@ -43,5 +36,4 @@ else export STARSHIP_CONFIG="${XDG_CONFIG_HOME}/starship/lean.config.toml" fi -# Encodings, languges and misc settings -export PYTHONIOENCODING='UTF-8'; +export PYTHONIOENCODING="UTF-8" diff --git a/install.ps1 b/install.ps1 index 0f1b329..c13dcf0 100644 --- a/install.ps1 +++ b/install.ps1 @@ -5,14 +5,12 @@ param( [switch]$NonInteractive, - [switch]$CoreOnly, [string]$RepoRoot ) $ErrorActionPreference = "Stop" $NonInteractive = $NonInteractive -or $env:DOTFILES_NONINTERACTIVE -eq "1" -$CoreOnly = $CoreOnly -or $env:DOTFILES_CORE_ONLY -eq "1" if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $PSScriptRoot @@ -24,6 +22,15 @@ if (-not (Test-Path -LiteralPath $managedModulesPath -PathType Leaf)) { } $ManagedModules = @(Get-Content -LiteralPath $managedModulesPath | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') }) +$managedAppsPath = Join-Path $RepoRoot "scripts\windows\managed-apps.csv" +if (-not (Test-Path -LiteralPath $managedAppsPath -PathType Leaf)) { + throw "The managed WinGet application catalog is missing from $RepoRoot." +} +$ManagedApps = @(Import-Csv -LiteralPath $managedAppsPath) +if ($ManagedApps.Count -eq 0) { + throw "The managed WinGet application catalog is empty." +} + function Refresh-Path { $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $pathEntries = foreach ($pathValue in @( @@ -122,92 +129,85 @@ function Install-WithWinget { param( [Parameter(Mandatory)][string]$AppId, [string]$Alias, + [string]$Scope, [switch]$Update ) - $installed = $false - if ($Alias) { - $installed = $null -ne (Get-Command -Name $Alias -ErrorAction SilentlyContinue) + $installed = if ($Alias) { + $null -ne (Get-Command -Name $Alias -ErrorAction SilentlyContinue) } else { & winget list --id $AppId --exact --source winget --accept-source-agreements *> $null - $installed = $LASTEXITCODE -eq 0 + $LASTEXITCODE -eq 0 + } + + $scopeArgs = @() + if (-not [string]::IsNullOrWhiteSpace($Scope)) { + $scopeArgs = @("--scope", $Scope) } if (-not $installed) { Write-Host "Installing $AppId..." -ForegroundColor Cyan - & winget install --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements - if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId." } - } - elseif ($Update) { - $upgradeCandidates = @( - & winget list --upgrade-available --id $AppId --exact --source winget --accept-source-agreements 2>&1 | - ForEach-Object { [string]$_ } - ) - $upgradeAvailable = @($upgradeCandidates | Where-Object { - $_ -match [regex]::Escape($AppId) - }).Count -gt 0 - - if (-not $upgradeAvailable) { - Write-Host "$AppId is already up to date" -ForegroundColor Green - } - else { - Write-Host "Updating $AppId..." -ForegroundColor Yellow - & winget upgrade --id $AppId --exact --source winget --silent --disable-interactivity --accept-source-agreements --accept-package-agreements - if ($LASTEXITCODE -ne 0) { - throw "WinGet could not update $AppId (exit code $LASTEXITCODE)." - } - } + & winget install --id $AppId --exact --source winget @scopeArgs --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "WinGet could not install $AppId in scope '$Scope' (exit code $LASTEXITCODE)." } + return } - else { + + if (-not $Update) { Write-Host "$AppId is already installed" -ForegroundColor Green + return + } + + $upgradeCandidates = @( + & winget list --upgrade-available --id $AppId --exact --source winget --accept-source-agreements 2>&1 | + ForEach-Object { [string]$_ } + ) + $upgradeAvailable = @($upgradeCandidates | Where-Object { $_ -match [regex]::Escape($AppId) }).Count -gt 0 + if (-not $upgradeAvailable) { + Write-Host "$AppId is already up to date" -ForegroundColor Green + return } + + Write-Host "Updating $AppId..." -ForegroundColor Yellow + & winget upgrade --id $AppId --exact --source winget @scopeArgs --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -ne 0) { throw "WinGet could not update $AppId (exit code $LASTEXITCODE)." } } function Install-MustHaveApps { - Write-Host "Installing must-have apps..." -ForegroundColor Cyan - - $corePackages = @( - @{ AppId = "zyedidia.micro"; Alias = "micro" }, - @{ AppId = "lsd-rs.lsd"; Alias = "lsd" }, - @{ AppId = "sharkdp.bat"; Alias = "bat" }, - @{ AppId = "Fastfetch-cli.Fastfetch"; Alias = "fastfetch" }, - @{ AppId = "junegunn.fzf"; Alias = "fzf" }, - @{ AppId = "sharkdp.fd"; Alias = "fd" }, - @{ AppId = "dandavison.delta"; Alias = "delta" }, - @{ AppId = "jqlang.jq"; Alias = "jq" }, - @{ AppId = "BurntSushi.ripgrep.MSVC"; Alias = "rg" }, - @{ AppId = "jdx.mise"; Alias = "mise" } - ) - $workstationPackages = @( - @{ AppId = "7zip.7zip"; Alias = $null }, - @{ AppId = "Microsoft.PowerToys"; Alias = $null }, - @{ AppId = "Microsoft.VisualStudioCode"; Alias = "code" } - ) + Write-Host "Installing baseline apps..." -ForegroundColor Cyan - foreach ($package in $corePackages) { - Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Update:$Update + $coreApps = @($ManagedApps | Where-Object Category -eq "core") + foreach ($app in $coreApps) { + Install-WithWinget -AppId $app.AppId -Alias $app.Alias -Scope $app.Scope -Update:$Update } - if (-not $CoreOnly) { - foreach ($package in $workstationPackages) { - Install-WithWinget -AppId $package.AppId -Alias $package.Alias -Update:$Update + + if (-not $NonInteractive) { + foreach ($app in @($ManagedApps | Where-Object Category -eq "workstation")) { + Install-WithWinget -AppId $app.AppId -Alias $app.Alias -Scope $app.Scope -Update:$Update } } else { - Write-Host "Skipping workstation-only WinGet packages in core-only mode." -ForegroundColor Yellow + Write-Host "Skipping workstation applications in non-interactive mode." -ForegroundColor Yellow } Refresh-Path - foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { - if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { - throw "Core CLI tool '$command' is unavailable after WinGet provisioning." + foreach ($app in $coreApps) { + if ([string]::IsNullOrWhiteSpace($app.Alias)) { continue } + if (-not (Get-Command -Name $app.Alias -ErrorAction SilentlyContinue)) { + throw "Baseline CLI tool '$($app.Alias)' is unavailable after WinGet provisioning." } } - & mise which starship *> $null - if ($LASTEXITCODE -ne 0) { + if ($Update) { & mise use -g starship@latest - if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + if ($LASTEXITCODE -ne 0) { throw "mise could not update starship." } + } + else { + & mise which starship *> $null + if ($LASTEXITCODE -ne 0) { + & mise use -g starship@latest + if ($LASTEXITCODE -ne 0) { throw "mise could not install starship." } + } } $allSigned = $env:DOTFILES_SIGNING_REQUIRED -eq "1" @@ -240,24 +240,16 @@ function Install-MustHaveApps { if ($null -eq $installedModule) { Write-Host "Installing $module module..." -ForegroundColor Cyan - if ($allSigned) { - Save-ManagedModuleForAllSigned -Name $module - } - else { - Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false - } + if ($allSigned) { Save-ManagedModuleForAllSigned -Name $module } + else { Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false } } - elseif ($Update -and -not $CoreOnly) { + elseif ($Update) { Write-Host "Updating $module module..." -ForegroundColor Yellow - if ($allSigned) { - Save-ManagedModuleForAllSigned -Name $module - } + if ($allSigned) { Save-ManagedModuleForAllSigned -Name $module } elseif ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { Update-PSResource -Name $module -Scope CurrentUser -Force } - else { - Update-Module -Name $module -Force - } + else { Update-Module -Name $module -Force } } else { Write-Host "$module module is already installed" -ForegroundColor Green @@ -269,26 +261,16 @@ function Install-MustHaveApps { function Install-OptionalApps { if ($NonInteractive) { - Write-Host "Skipping optional installs in non-interactive mode..." -ForegroundColor Yellow + Write-Host "Skipping optional applications in non-interactive mode." -ForegroundColor Yellow return } - $optionalApps = @( - @{ name = "Google Chrome"; install = { Install-WithWinget -AppId "Google.Chrome" -Update:$Update } }, - @{ name = "KeepassXC"; install = { Install-WithWinget -AppId "KeePassXCTeam.KeePassXC" -Update:$Update } }, - @{ name = "DBeaver"; install = { Install-WithWinget -AppId "dbeaver.dbeaver" -Update:$Update } }, - @{ name = "Postman"; install = { Install-WithWinget -AppId "Postman.Postman" -Update:$Update } }, - @{ name = "Bruno"; install = { Install-WithWinget -AppId "Bruno.Bruno" -Update:$Update } }, - @{ name = "kubectl"; install = { Install-WithWinget -AppId "Kubernetes.kubectl" -Alias "kubectl" -Update:$Update } }, - @{ name = "GIMP"; install = { Install-WithWinget -AppId "GIMP.GIMP" -Update:$Update } }, - @{ name = "Android Studio"; install = { Install-WithWinget -AppId "Google.AndroidStudio" -Update:$Update } }, - @{ name = "Steam"; install = { Install-WithWinget -AppId "Valve.Steam" -Update:$Update } }, - @{ name = "Discord"; install = { Install-WithWinget -AppId "Discord.Discord" -Update:$Update } }, - @{ name = "npiperelay"; install = { Install-WithWinget -AppId "albertony.npiperelay" -Alias "npiperelay" -Update:$Update } } - ) + $optionalApps = @($ManagedApps | Where-Object Category -eq "optional") Write-Host " Optionals" Write-Host "-----------------------------------" -ForegroundColor Cyan - for ($i = 0; $i -lt $optionalApps.Count; $i++) { Write-Host ("{0}. Install {1}" -f ($i + 1), $optionalApps[$i].name) } + for ($i = 0; $i -lt $optionalApps.Count; $i++) { + Write-Host ("{0}. Install {1}" -f ($i + 1), $optionalApps[$i].Name) + } Write-Host "You can use ranges like 1-4 or individual numbers separated by commas" -ForegroundColor Yellow $rawOptions = Read-Host "Select options [e.g. 1-4,8,10]" @@ -298,13 +280,16 @@ function Install-OptionalApps { if ($option -match '^(\d+)-(\d+)$' -and [int]$Matches[1] -le [int]$Matches[2]) { $options += [int]$Matches[1]..[int]$Matches[2] } - elseif ($option -match '^\d+$') { $options += [int]$option } + elseif ($option -match '^\d+$') { + $options += [int]$option + } } + $options = @($options | Where-Object { $_ -gt 0 -and $_ -le $optionalApps.Count } | Select-Object -Unique | Sort-Object) foreach ($index in $options) { - Write-Host "Installing $($optionalApps[$index - 1].name)..." -ForegroundColor Cyan $app = $optionalApps[$index - 1] - & $app.install + Write-Host "Installing $($app.Name)..." -ForegroundColor Cyan + Install-WithWinget -AppId $app.AppId -Alias $app.Alias -Scope $app.Scope -Update:$Update } if ($options.Count -eq 0) { Write-Host "Skipping optional installs..." -ForegroundColor Yellow } Refresh-Path @@ -313,27 +298,32 @@ function Install-OptionalApps { function Download-Fonts { $fonts = Join-Path $RepoRoot "fonts" New-Item -ItemType Directory -Force -Path $fonts | Out-Null - $cascadia = Join-Path $fonts "CascadiaCode" - if (-not (Test-Path "${cascadia}.ttf")) { + + if (-not (Test-Path (Join-Path $fonts "CascadiaCode.ttf"))) { $release = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/cascadia-code/releases/latest" -Headers @{ "User-Agent" = "PowerShell" } - Invoke-WebRequest -Uri $release.assets[0].browser_download_url -OutFile "${cascadia}.zip" - Expand-Archive "${cascadia}.zip" -DestinationPath $cascadia - Remove-Item -Recurse -Force "${cascadia}\ttf\static" -ErrorAction SilentlyContinue - Get-ChildItem -Path "${cascadia}\*.ttf" -Recurse | Move-Item -Destination $fonts - Remove-Item -Recurse -Force "${cascadia}.zip", $cascadia + $asset = @($release.assets | Where-Object name -Match '^CascadiaCode-.*\.zip$' | Select-Object -First 1) + if ($asset.Count -ne 1) { throw "Unable to locate the Cascadia Code ZIP asset in the latest GitHub release." } + + $zip = Join-Path $fonts "CascadiaCode.zip" + $extract = Join-Path $fonts "CascadiaCode" + Invoke-WebRequest -Uri $asset[0].browser_download_url -OutFile $zip + Expand-Archive $zip -DestinationPath $extract -Force + Remove-Item -Recurse -Force (Join-Path $extract "ttf\static") -ErrorAction SilentlyContinue + Get-ChildItem -Path $extract -Filter *.ttf -Recurse -File | Move-Item -Destination $fonts -Force + Remove-Item -Recurse -Force $zip, $extract } + + $nerdRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" -Headers @{ "User-Agent" = "PowerShell" } foreach ($font in @( @{ folder = (Join-Path $fonts "CaskaydiaCoveNerdFont"); filename = "CascadiaCode" }, @{ folder = (Join-Path $fonts "CaskaydiaMonoNerdFont"); filename = "CascadiaMono" } )) { - if (-not (Test-Path "$($font.folder)-Regular.ttf")) { - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" -Headers @{ "User-Agent" = "PowerShell" } - $zip = "$($font.folder).zip" - Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/download/$($release.tag_name)/$($font.filename).zip" -OutFile $zip - Expand-Archive $zip -DestinationPath $font.folder - Get-ChildItem -Path "$($font.folder)\*.ttf" -Recurse | Move-Item -Destination $fonts - Remove-Item -Recurse -Force $zip, $font.folder - } + if (Test-Path "$($font.folder)-Regular.ttf") { continue } + $zip = "$($font.folder).zip" + Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/download/$($nerdRelease.tag_name)/$($font.filename).zip" -OutFile $zip + Expand-Archive $zip -DestinationPath $font.folder -Force + Get-ChildItem -Path $font.folder -Filter *.ttf -Recurse -File | Move-Item -Destination $fonts -Force + Remove-Item -Recurse -Force $zip, $font.folder } } @@ -368,27 +358,33 @@ function Configure-WindowsTerminal { } function Configure-Wsl { - if ($NonInteractive -or $CoreOnly) { - Write-Host "Skipping WSL installation in non-interactive/core-only mode..." -ForegroundColor Yellow + if ($NonInteractive) { + Write-Host "Skipping WSL installation in non-interactive mode." -ForegroundColor Yellow return } - - if (Get-Command wsl -ErrorAction SilentlyContinue) { Write-Host "Installing WSL..." -ForegroundColor Cyan; & wsl --install --no-distribution } + if (Get-Command wsl -ErrorAction SilentlyContinue) { + Write-Host "Installing WSL..." -ForegroundColor Cyan + & wsl --install --no-distribution + } } Refresh-Path Check-RequiredApps -if (-not $CoreOnly) { + +if (-not $NonInteractive) { Download-Fonts Install-UserFonts } + Configure-Git Install-MustHaveApps -if (-not $CoreOnly) { + +if (-not $NonInteractive) { Configure-WindowsTerminal Install-OptionalApps } else { - Write-Host "Skipping optional applications in core-only mode." -ForegroundColor Yellow + Write-Host "Skipping fonts, terminal configuration, and optional applications in non-interactive mode." -ForegroundColor Yellow } + Configure-Wsl diff --git a/install.sh b/install.sh index c1e9282..8879750 100755 --- a/install.sh +++ b/install.sh @@ -1,516 +1,357 @@ -#!/usr/bin/bash +#!/usr/bin/env bash +set -euo pipefail -RED='\033[0;31m' # Red -GREEN='\033[0;32m' # Green -YELLOW='\033[0;33m' # Yellow -BLUE='\033[0;34m' # Blue -CYAN='\033[0;36m' # Cyan +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' LIGHT='\x1b[2m' RESET='\033[0m' - -REPO_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" UPDATE=false -CORE_ONLY=false - -function usage () { - echo "Usage: $0 [--update|-u] [--core-only]" - echo - echo "Options:" - echo " -u, --update Re-run package installers even when commands already exist" - echo " --core-only Install only the runtime prerequisites used by the dotfiles" - echo " -h, --help Show this help message" +NON_INTERACTIVE=false +DISTRO="" +arch="" + +usage() { + cat <<'EOF' +Usage: install.sh [--update|-u] [--non-interactive] + +Options: + -u, --update Refresh packages and tools managed by this installer + --non-interactive Install the baseline without prompts or workstation customization + -h, --help Show this help message +EOF } -function parse_args () { - while [ "$#" -gt 0 ]; do +parse_args() { + while (( $# > 0 )); do case "$1" in - -u|--update) - UPDATE=true - ;; - --core-only) - CORE_ONLY=true - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo -e "${RED}Unknown option: $1${RESET}" - usage - exit 1 - ;; + -u|--update) UPDATE=true ;; + --non-interactive) NON_INTERACTIVE=true ;; + -h|--help) usage; exit 0 ;; + *) printf '%bUnknown option: %s%b\n' "$RED" "$1" "$RESET" >&2; usage >&2; exit 1 ;; esac shift done -} - -function updates_enabled () { - case "${UPDATE}" in - 1|true|TRUE|yes|YES|y|Y) return 0 ;; - *) return 1 ;; - esac -} - -if [[ "${DOTFILES_CORE_ONLY:-0}" == "1" ]]; then - CORE_ONLY=true -fi - -function pre_setup_tasks() { - if [ ! -d "$REPO_ROOT" ]; then - echo -e "${RED}The repository folder '$REPO_ROOT' does not exist; exiting..."; - exit 1; - fi - - detect_arch - - if updates_enabled; then - echo -e "${CYAN}Update mode enabled. Existing packages will be refreshed when possible.${RESET}" + if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]]; then + NON_INTERACTIVE=true fi } -detect_arch() { - - arch="$(uname -m | tr '[:upper:]' '[:lower:]')" - - case "${arch}" in - x86_64) arch="amd64" ;; - arm64) arch="aarch64" ;; - esac - - # `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check - if [ "${arch}" = "amd64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then - arch=i686 - elif [ "${arch}" = "aarch64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then - arch=arm - fi +updates_enabled() { [[ "$UPDATE" == true ]]; } +noninteractive_enabled() { [[ "$NON_INTERACTIVE" == true ]]; } - if [ "${arch}" != "amd64" ] && [ "${arch}" != "aarch64" ]; then - echo -e "${RED}Only amd64 and aarch64 supported"; - exit 1 - fi - - echo -e "${GREEN}Current arch is '${arch}'${RESET}" +read_manifest() { + local path=$1 + [[ -f "$path" ]] || { printf 'Manifest not found: %s\n' "$path" >&2; return 1; } + grep -Ev '^[[:space:]]*(#|$)' "$path" } -function install_with_apt () { - local app=$1 - - if command -v "${app}" >/dev/null 2>&1 && ! updates_enabled; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed${RESET}" - elif dpkg -s "${app}" &> /dev/null && ! updates_enabled; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via APT${RESET}" - elif hash flatpak 2> /dev/null && [[ ! -z $(echo $(flatpak list --columns=ref | grep $app)) ]]; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Flatpak${RESET}" +detect_distro() { + if [[ -f /etc/debian_version ]] && command -v apt-get >/dev/null 2>&1; then + DISTRO="debian" + elif [[ -f /etc/arch-release ]] && command -v pacman >/dev/null 2>&1; then + DISTRO="arch" else - if updates_enabled && { command -v "${app}" >/dev/null 2>&1 || dpkg -s "${app}" &> /dev/null; }; then - echo -e "${CYAN}[Updating]${LIGHT} ${app}...${RESET}" - else - echo -e "${CYAN}[Installing]${LIGHT} Downloading ${app}...${RESET}" - fi - sudo apt install "${app}" --assume-yes + printf '%bUnsupported Linux distribution. Debian and Arch Linux are supported.%b\n' "$RED" "$RESET" >&2 + exit 1 fi } -function check_package_or_run () { - local app=$1 - local installer=$2 - - if command -v "$app" >/dev/null 2>&1 && ! updates_enabled; then - echo -e "${YELLOW}[Skipping]${LIGHT} $app is already installed${RESET}" - else - if command -v "$app" >/dev/null 2>&1; then - echo -e "${CYAN}[Updating]${LIGHT} $app...${RESET}" - else - echo -e "${CYAN}[Installing]${LIGHT} Downloading $app...${RESET}" - fi - "$installer" +detect_arch() { + arch="$(uname -m | tr '[:upper:]' '[:lower:]')" + case "$arch" in x86_64) arch="amd64" ;; arm64) arch="aarch64" ;; esac + if [[ "$arch" == "amd64" && "$(getconf LONG_BIT)" -eq 32 ]]; then + arch="i686" + elif [[ "$arch" == "aarch64" && "$(getconf LONG_BIT)" -eq 32 ]]; then + arch="arm" fi -} - -function github_release_json () { - local repo=$1 - local release - local releases - - if release=$(curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null); then - printf '%s\n' "$release" - return 0 + if [[ "$arch" != "amd64" && "$arch" != "aarch64" ]]; then + printf '%bOnly amd64 and aarch64 are supported.%b\n' "$RED" "$RESET" >&2 + exit 1 fi - - releases=$(curl -fsSL "https://api.github.com/repos/${repo}/releases?per_page=10" 2>/dev/null) || return 1 - jq -e 'map(select((.draft | not) and (.prerelease | not))) | .[0]' <<< "$releases" -} - -function github_release_asset_url () { - local repo=$1 - local asset_regex=$2 - local release - - release=$(github_release_json "$repo") || return 1 - - jq -er --arg asset_regex "$asset_regex" ' - first(.assets[]?.browser_download_url | select(test($asset_regex))) - ' <<< "$release" + printf '%bCurrent arch is %s%b\n' "$GREEN" "$arch" "$RESET" } -function install_github_deb_asset () { - local repo=$1 - local asset_regex=$2 - local asset_url - local deb_file - local install_status - - asset_url=$(github_release_asset_url "$repo" "$asset_regex") || { - echo -e "${RED}Could not find a matching release asset for ${repo}.${RESET}" - return 1 - } - - deb_file="${asset_url##*/}" - - wget -O "$deb_file" "$asset_url" || return 1 - sudo dpkg -i "$deb_file" - install_status=$? - rm -f "$deb_file" - - return "$install_status" +pre_setup_tasks() { + [[ -d "$REPO_ROOT" ]] || { printf '%bRepository folder not found: %s%b\n' "$RED" "$REPO_ROOT" "$RESET" >&2; exit 1; } + mkdir -p "$HOME/.local/bin" + export PATH="$HOME/.local/bin:$PATH" + detect_distro + detect_arch + updates_enabled && printf '%bUpdate mode enabled.%b\n' "$CYAN" "$RESET" } -function debian_release_arch () { - case "$arch" in - aarch64) printf 'arm64\n' ;; - *) printf '%s\n' "$arch" ;; +refresh_package_index() { + [[ "${DOTFILES_PACKAGE_INDEX_READY:-0}" == "1" ]] && return + case "$DISTRO" in + debian) sudo apt-get update ;; + arch) sudo pacman -Syu --noconfirm ;; esac + export DOTFILES_PACKAGE_INDEX_READY=1 } -function install_fastfetch () { - if apt-cache show fastfetch &>/dev/null; then - sudo apt install -y fastfetch +install_debian_manifest() { + local manifest="$REPO_ROOT/scripts/linux/packages-debian.txt" + local -a packages selected=() + mapfile -t packages < <(read_manifest "$manifest") + if updates_enabled; then + selected=("${packages[@]}") else - install_github_deb_asset "fastfetch-cli/fastfetch" "fastfetch-linux-${arch}\\.deb$" + local package + for package in "${packages[@]}"; do + dpkg -s "$package" >/dev/null 2>&1 || selected+=("$package") + done fi -} - -function install_lsd () { - if apt-cache show lsd &>/dev/null; then - sudo apt install -y lsd + if (( ${#selected[@]} > 0 )); then + sudo apt-get install --yes "${selected[@]}" else - local lsd_arch - lsd_arch=$(debian_release_arch) - install_github_deb_asset "lsd-rs/lsd" "lsd_.*_${lsd_arch}_xz\\.deb$" + printf '%bAPT baseline already installed.%b\n' "$YELLOW" "$RESET" fi } -function install_fzf () { - local fzf_dir="$HOME/.config/fzf" - - if [ -d "$fzf_dir/.git" ]; then - git -c safe.directory="$fzf_dir" -C "$fzf_dir" pull --ff-only +install_arch_manifest() { + local manifest="$REPO_ROOT/scripts/linux/packages-arch.txt" + local -a packages selected=() + mapfile -t packages < <(read_manifest "$manifest") + if updates_enabled; then + selected=("${packages[@]}") else - git clone https://github.com/junegunn/fzf.git "$fzf_dir" + local package + for package in "${packages[@]}"; do + pacman -Q "$package" >/dev/null 2>&1 || selected+=("$package") + done + fi + if (( ${#selected[@]} > 0 )); then + sudo pacman -S --needed --noconfirm "${selected[@]}" + else + printf '%bPacman baseline already installed.%b\n' "$YELLOW" "$RESET" fi - - "$fzf_dir/install" --bin } -function install_vivid () { - local vivid_arch - vivid_arch=$(debian_release_arch) - install_github_deb_asset "sharkdp/vivid" "vivid_.*_${vivid_arch}\\.deb$" +check_package_or_run() { + local command_name=$1 installer=$2 + if command -v "$command_name" >/dev/null 2>&1 && ! updates_enabled; then + printf '%b[Skipping]%b %s is already installed%b\n' "$YELLOW" "$LIGHT" "$command_name" "$RESET" + return + fi + "$installer" } -function install_delta () { - local delta_arch - delta_arch=$(debian_release_arch) - install_github_deb_asset "dandavison/delta" "git-delta_.*_${delta_arch}\\.deb$" +github_release_json() { + local repo=$1 release releases + if release="$(curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null)"; then + printf '%s\n' "$release" + return + fi + releases="$(curl -fsSL "https://api.github.com/repos/${repo}/releases?per_page=10")" + jq -e 'map(select((.draft | not) and (.prerelease | not))) | .[0]' <<< "$releases" } -function install_starship () { - curl -sS https://starship.rs/install.sh | sh +github_release_asset_url() { + local repo=$1 asset_regex=$2 release + release="$(github_release_json "$repo")" + jq -er --arg asset_regex "$asset_regex" 'first(.assets[]?.browser_download_url | select(test($asset_regex)))' <<< "$release" } -function install_sheldon () { - local install_args=( - --repo rossmacarthur/sheldon - --to "$HOME/.local/bin" - ) - - if updates_enabled; then - install_args+=(--force) - fi - - curl --proto '=https' -fLsS https://rossmacarthur.github.io/install/crate.sh \ - | bash -s -- "${install_args[@]}" +install_github_deb_asset() { + local repo=$1 asset_regex=$2 asset_url tmp_dir deb_file + asset_url="$(github_release_asset_url "$repo" "$asset_regex")" || { + printf '%bCould not find a matching release asset for %s.%b\n' "$RED" "$repo" "$RESET" >&2 + return 1 + } + tmp_dir="$(mktemp -d)" + deb_file="$tmp_dir/${asset_url##*/}" + if ! curl -fL "$asset_url" -o "$deb_file"; then rm -rf "$tmp_dir"; return 1; fi + if ! sudo dpkg -i "$deb_file"; then rm -rf "$tmp_dir"; return 1; fi + rm -rf "$tmp_dir" } -function install_debian_packages () { - - local debian_apps=( - "git" - "curl" - "wget" - "zsh" - "micro" - "jq" - "tree" - "python3" - "ufw" - "rsync" - "zip" - "unzip" - "less" - "socat" - "bat" - "fd-find" # fd - "binutils" # strings - "ripgrep" - ) +debian_release_arch() { [[ "$arch" == "aarch64" ]] && printf 'arm64\n' || printf '%s\n' "$arch"; } - for app in "${debian_apps[@]}"; do - install_with_apt "$app" - done - - check_package_or_run "fastfetch" "install_fastfetch" - check_package_or_run "lsd" "install_lsd" - check_package_or_run "fzf" "install_fzf" - check_package_or_run "vivid" "install_vivid" - check_package_or_run "delta" "install_delta" - check_package_or_run "starship" "install_starship" - check_package_or_run "sheldon" "install_sheldon" +install_fastfetch() { + if apt-cache show fastfetch >/dev/null 2>&1; then sudo apt-get install --yes fastfetch + else install_github_deb_asset "fastfetch-cli/fastfetch" "fastfetch-linux-${arch}\\.deb$"; fi } - -function install_with_pacman () { - local app=$1 - local pacman_app - local pacman_status - - pacman_app=$(printf '%s' "$app" | tr 'A-Z' 'a-z') - pacman_status=$(pacman -Qk "$pacman_app" 2>/dev/null || true) - - if command -v "${app}" >/dev/null 2>&1 && ! updates_enabled; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed${RESET}" - elif [[ "$pacman_status" == *"total files"* ]] && ! updates_enabled; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Pacman${RESET}" - elif hash flatpak 2> /dev/null && [[ ! -z $(echo $(flatpak list --columns=ref | grep $app)) ]]; then - echo -e "${YELLOW}[Skipping]${LIGHT} ${app} is already installed via Flatpak${RESET}" - else - if updates_enabled && { command -v "${app}" >/dev/null 2>&1 || [[ "$pacman_status" == *"total files"* ]]; }; then - echo -e "${CYAN}[Updating]${LIGHT} ${app}...${RESET}" - else - echo -e "${CYAN}[Installing]${LIGHT} Downloading ${app}...${RESET}" - fi - sudo pacman -S "${app}" --needed --noconfirm - fi +install_lsd() { + if apt-cache show lsd >/dev/null 2>&1; then sudo apt-get install --yes lsd + else local a; a="$(debian_release_arch)"; install_github_deb_asset "lsd-rs/lsd" "lsd_.*_${a}_xz\\.deb$"; fi } - -function install_arch_packages () { - local pacman_apps=( - "git" - "curl" - "wget" - "zsh" - "micro" - "fastfetch" - "tree" - "jq" - "lsd" - "fd" - "fzf" - "vivid" - "git-delta" - "starship" - "sheldon" - "bat" - "python" - "ufw" - "rsync" - "zip" - "unzip" - "less" - "socat" - "binutils" - "ripgrep" - ) - - for app in "${pacman_apps[@]}"; do - install_with_pacman "$app" - done - - # Install yay - if command -v yay >/dev/null 2>&1; then - echo -e "${YELLOW}[Skipping]${LIGHT} yay is already installed${RESET}" - else - sudo pacman -S --needed git base-devel && git clone https://aur.archlinux.org/yay.git ~/.yay && (cd ~/.yay && makepkg -si) && rm -rf ~/.yay - fi +install_fzf() { + local dir="$HOME/.config/fzf" + if [[ -d "$dir/.git" ]]; then git -c safe.directory="$dir" -C "$dir" pull --ff-only + else git clone https://github.com/junegunn/fzf.git "$dir"; fi + "$dir/install" --bin + ln -sfn "$dir/bin/fzf" "$HOME/.local/bin/fzf" +} +install_vivid() { local a; a="$(debian_release_arch)"; install_github_deb_asset "sharkdp/vivid" "vivid_.*_${a}\\.deb$"; } +install_delta() { local a; a="$(debian_release_arch)"; install_github_deb_asset "dandavison/delta" "git-delta_.*_${a}\\.deb$"; } +install_starship() { curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"; } +install_sheldon() { + local -a args=(--repo rossmacarthur/sheldon --to "$HOME/.local/bin") + updates_enabled && args+=(--force) + curl --proto '=https' -fLsS https://rossmacarthur.github.io/install/crate.sh | bash -s -- "${args[@]}" } -function install_must_have_packages() { - echo -e "${CYAN}Installing must have packages...${RESET}" +install_debian_packages() { + install_debian_manifest + check_package_or_run fastfetch install_fastfetch + check_package_or_run lsd install_lsd + check_package_or_run fzf install_fzf + check_package_or_run vivid install_vivid + check_package_or_run delta install_delta + check_package_or_run starship install_starship + check_package_or_run sheldon install_sheldon +} - if [ -f "/etc/debian_version" ]; then - sudo apt update && \ - install_debian_packages - elif [ -f "/etc/arch-release" ]; then - sudo pacman -Syy --noconfirm && \ - install_arch_packages +install_yay() { + if command -v yay >/dev/null 2>&1 && ! updates_enabled; then + printf '%b[Skipping]%b yay is already installed%b\n' "$YELLOW" "$LIGHT" "$RESET" + return fi - + sudo pacman -S --needed --noconfirm git base-devel + local tmp_dir + tmp_dir="$(mktemp -d)" + git clone https://aur.archlinux.org/yay.git "$tmp_dir/yay" + (cd "$tmp_dir/yay" && makepkg -si --noconfirm --needed) + rm -rf "$tmp_dir" } -function setup_sheldon_plugins () { - if command -v sheldon >/dev/null 2>&1; then - sheldon lock - fi +install_arch_packages() { install_arch_manifest; install_yay; } + +install_must_have_packages() { + printf '%bInstalling baseline packages...%b\n' "$CYAN" "$RESET" + refresh_package_index + case "$DISTRO" in debian) install_debian_packages ;; arch) install_arch_packages ;; esac } -function setup_default_shell() { - local target_user="${USER:-$(id -un)}" - local target_shell +setup_sheldon_plugins() { command -v sheldon >/dev/null 2>&1 && sheldon lock; } - current_shell=$(getent passwd "$target_user" | cut -d: -f7) +setup_default_shell() { + local target_user="${USER:-$(id -un)}" current_shell target_shell + current_shell="$(getent passwd "$target_user" | cut -d: -f7)" target_shell="$(command -v zsh)" - - if [ "$current_shell" != "$target_shell" ]; then + if [[ "$current_shell" != "$target_shell" ]]; then chsh -s "$target_shell" "$target_user" - echo -e "${GREEN}Default shell changed to zsh for ${target_user}.${RESET}" + printf '%bDefault shell changed to zsh for %s.%b\n' "$GREEN" "$target_user" "$RESET" else - echo -e "${YELLOW}zsh is already the default shell for ${target_user}. No changes made.${RESET}" + printf '%bzsh is already the default shell for %s.%b\n' "$YELLOW" "$target_user" "$RESET" fi } -function configure_git () { - [ ! -e ~/.gitconfig.local ] && touch ~/.gitconfig.local - echo -e "${GREEN}Git successfully configured!${RESET}" -} +configure_git() { [[ -e "$HOME/.gitconfig.local" ]] || touch "$HOME/.gitconfig.local"; } -function configure_wsl() { - if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]]; then - echo -e "${YELLOW}Skipping WSL system configuration in non-interactive mode.${RESET}" +configure_wsl() { + if noninteractive_enabled; then + printf '%bSkipping WSL system configuration in non-interactive mode.%b\n' "$YELLOW" "$RESET" return fi - - local desired="${REPO_ROOT}/assets/wsl/wsl.conf" - if grep -qi microsoft /proc/version && [ -f "$desired" ]; then - if [ ! -f /etc/wsl.conf ] || ! cmp -s "$desired" /etc/wsl.conf; then + local desired="$REPO_ROOT/assets/wsl/wsl.conf" + if grep -qi microsoft /proc/version 2>/dev/null && [[ -f "$desired" ]]; then + if [[ ! -f /etc/wsl.conf ]] || ! cmp -s "$desired" /etc/wsl.conf; then sudo install -m 0644 "$desired" /etc/wsl.conf - echo -e "${GREEN}wsl.conf configured successfully!${RESET}" - else - echo -e "${YELLOW}wsl.conf already matches the desired configuration.${RESET}" fi fi } -function install_mise_en_place () { - local mise_arch=$arch - case "${mise_arch}" in - aarch64) mise_arch="arm64" ;; - esac +install_with_apt() { + local package=$1 + refresh_package_index + if dpkg -s "$package" >/dev/null 2>&1 && ! updates_enabled; then + printf '%b[Skipping]%b %s is already installed%b\n' "$YELLOW" "$LIGHT" "$package" "$RESET" + else + sudo apt-get install --yes "$package" + fi +} - if apt-cache show mise &>/dev/null; then - sudo apt install -y mise +install_with_pacman() { + local package=$1 + refresh_package_index + if pacman -Q "$package" >/dev/null 2>&1 && ! updates_enabled; then + printf '%b[Skipping]%b %s is already installed%b\n' "$YELLOW" "$LIGHT" "$package" "$RESET" else - sudo apt install -y curl - sudo install -dm 755 /etc/apt/keyrings - curl -fSs https://mise.jdx.dev/gpg-key.pub | sudo tee /etc/apt/keyrings/mise-archive-keyring.pub 1> /dev/null - echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.pub arch=$mise_arch] https://mise.jdx.dev/deb stable main" | sudo tee /etc/apt/sources.list.d/mise.list - sudo apt update -y - sudo apt install -y mise + sudo pacman -S --needed --noconfirm "$package" fi } -function install_docker () { - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh ./get-docker.sh - rm get-docker.sh - sudo usermod -aG docker "${USER:-$(id -un)}" +install_mise_en_place() { + local mise_arch="$arch" + [[ "$mise_arch" == "aarch64" ]] && mise_arch="arm64" + if apt-cache show mise >/dev/null 2>&1; then + sudo apt-get install --yes mise + return + fi + sudo install -dm 755 /etc/apt/keyrings + curl -fSs https://mise.jdx.dev/gpg-key.pub | sudo tee /etc/apt/keyrings/mise-archive-keyring.pub >/dev/null + echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.pub arch=$mise_arch] https://mise.jdx.dev/deb stable main" | sudo tee /etc/apt/sources.list.d/mise.list >/dev/null + sudo apt-get update + sudo apt-get install --yes mise } -function install_dagger () { - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh +install_docker() { + local script + script="$(mktemp)" + curl -fsSL https://get.docker.com -o "$script" + sudo sh "$script" + rm -f "$script" + sudo usermod -aG docker "${USER:-$(id -un)}" } +install_dagger() { curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR="$HOME/.local/bin" sh; } -function install_optional_packages () { - if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]]; then - echo -e "${YELLOW}Skipping optional package selection in non-interactive mode.${RESET}" +install_optional_packages() { + if noninteractive_enabled; then + printf '%bSkipping optional package selection in non-interactive mode.%b\n' "$YELLOW" "$RESET" return fi - local packages=( "mise-en-place|deb:check_package_or_run mise install_mise_en_place|arch:install_with_pacman mise" "docker|deb:check_package_or_run docker install_docker|arch:install_with_pacman docker" "dagger|deb:check_package_or_run dagger install_dagger|arch:install_with_pacman dagger" ) - - echo -e "\n${CYAN}Choose optional packages to install:${RESET}" + printf '\n%bChoose optional packages to install:%b\n' "$CYAN" "$RESET" + local i for i in "${!packages[@]}"; do - IFS='|' read -ra pkg_info <<< "${packages[i]}" - pkg_name="${pkg_info[0]}" - echo -e "${CYAN}$((i+1)). ${pkg_name}${RESET}" + IFS='|' read -r -a pkg_info <<< "${packages[i]}" + printf '%b%d. %s%b\n' "$CYAN" "$((i + 1))" "${pkg_info[0]}" "$RESET" done - echo -e "${CYAN}You can use ranges like 1-4 or individual numbers separated by commas:${RESET}" - read -p "Enter your choices (or press Enter to skip): " user_input - if [ -z "$user_input" ]; then - echo -e "${YELLOW}No optional packages selected for installation.${RESET}" - return - fi - # Parse user input to get selected indices and trim spaces - IFS=',' read -ra selections <<< "$(echo $user_input | tr -d ' ')" - selected_indices=() + read -r -p "Enter choices (e.g. 1-3,5) or press Enter to skip: " user_input + [[ -n "$user_input" ]] || return + local -a selected_indices=() + local sel start end + IFS=',' read -r -a selections <<< "${user_input// /}" for sel in "${selections[@]}"; do - if [[ $sel =~ ^[0-9]+-[0-9]+$ ]]; then - IFS='-' read -ra range <<< "$sel" - start=${range[0]} - end=${range[1]} - for ((i=start; i<=end; i++)); do - if (( i >= 1 && i <= ${#packages[@]} )); then - selected_indices+=("$i") - fi - done - - elif [[ $sel =~ ^[0-9]+$ ]]; then - if (( sel >= 1 && sel <= ${#packages[@]} )); then - selected_indices+=("$sel") - fi + if [[ "$sel" =~ ^[0-9]+-[0-9]+$ ]]; then + IFS='-' read -r start end <<< "$sel" + for ((i=start; i<=end; i++)); do (( i >= 1 && i <= ${#packages[@]} )) && selected_indices+=("$i"); done + elif [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#packages[@]} )); then + selected_indices+=("$sel") fi done - # Remove duplicates - IFS=$'\n' selected_indices=($(sort -u <<<"${selected_indices[*]}")) - unset IFS - # If no valid selections, exit - if [ ${#selected_indices[@]} -eq 0 ]; then - echo -e "${YELLOW}No valid optional packages selected for installation.${RESET}" - return - fi - # Install selected packages + (( ${#selected_indices[@]} > 0 )) || { printf '%bNo valid selection.%b\n' "$YELLOW" "$RESET"; return; } + mapfile -t selected_indices < <(printf '%s\n' "${selected_indices[@]}" | sort -nu) + local index idx deb_func arch_func for index in "${selected_indices[@]}"; do - idx=$((index-1)) - IFS='|' read -ra pkg_info <<< "${packages[idx]}" - pkg_name="${pkg_info[0]}" + idx=$((index - 1)) + IFS='|' read -r -a pkg_info <<< "${packages[idx]}" deb_func="${pkg_info[1]#deb:}" arch_func="${pkg_info[2]#arch:}" - echo -e "${CYAN}[Installing]${LIGHT} ${pkg_name}...${RESET}" - if [ -f "/etc/debian_version" ]; then - $deb_func - elif [ -f "/etc/arch-release" ]; then - $arch_func - fi + case "$DISTRO" in debian) eval "$deb_func" ;; arch) eval "$arch_func" ;; esac done } - parse_args "$@" pre_setup_tasks configure_git -if ! ${CORE_ONLY}; then - configure_wsl - install_must_have_packages - setup_sheldon_plugins - if [[ "${DOTFILES_NONINTERACTIVE:-0}" != "1" ]]; then - setup_default_shell - else - echo -e "${YELLOW}Skipping default-shell change in non-interactive mode.${RESET}" - fi - install_optional_packages +install_must_have_packages +setup_sheldon_plugins + +if noninteractive_enabled; then + printf '%bSkipping shell and WSL customization in non-interactive mode.%b\n' "$YELLOW" "$RESET" else - echo -e "${YELLOW}Skipping workstation package catalog in core-only mode.${RESET}" + setup_default_shell + configure_wsl fi + +install_optional_packages diff --git a/scripts/linux/packages-arch.txt b/scripts/linux/packages-arch.txt new file mode 100644 index 0000000..6c8511c --- /dev/null +++ b/scripts/linux/packages-arch.txt @@ -0,0 +1,25 @@ +# Packages installed through pacman. yay remains managed separately from AUR. +git +curl +zsh +micro +fastfetch +tree +jq +lsd +fd +fzf +vivid +git-delta +starship +sheldon +bat +python +ufw +rsync +zip +unzip +less +socat +binutils +ripgrep diff --git a/scripts/linux/packages-debian.txt b/scripts/linux/packages-debian.txt new file mode 100644 index 0000000..b94e988 --- /dev/null +++ b/scripts/linux/packages-debian.txt @@ -0,0 +1,18 @@ +# Packages installed through APT. Release-fallback tools are handled by install.sh. +git +curl +zsh +micro +jq +tree +python3 +ufw +rsync +zip +unzip +less +socat +bat +fd-find +binutils +ripgrep diff --git a/scripts/linux/required-commands.txt b/scripts/linux/required-commands.txt new file mode 100644 index 0000000..8b36c35 --- /dev/null +++ b/scripts/linux/required-commands.txt @@ -0,0 +1,19 @@ +# Each line is a logical requirement. Alternatives are separated by |. +git +curl +zsh +micro +jq +tree +python3|python +bat|batcat +fd|fdfind +strings +rg +fastfetch +lsd +fzf +vivid +delta +starship +sheldon diff --git a/scripts/windows/deploy-pwsh.ps1 b/scripts/windows/deploy-pwsh.ps1 index 68c7f70..8e0a406 100644 --- a/scripts/windows/deploy-pwsh.ps1 +++ b/scripts/windows/deploy-pwsh.ps1 @@ -24,15 +24,6 @@ foreach ($file in $files) { New-Item -ItemType Directory -Path (Split-Path -Parent $file.Destination) -Force | Out-Null - # Remove a legacy symlink before copying the regular runtime file. - # Copy-Item otherwise follows a broken link and fails instead of replacing it. - if (Test-Path -LiteralPath $file.Destination -PathType Leaf) { - $existing = Get-Item -LiteralPath $file.Destination -Force - if (($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { - Remove-Item -LiteralPath $file.Destination -Force - } - } - Copy-Item -LiteralPath $source -Destination $file.Destination -Force } diff --git a/scripts/windows/managed-apps.csv b/scripts/windows/managed-apps.csv new file mode 100644 index 0000000..ab65f7b --- /dev/null +++ b/scripts/windows/managed-apps.csv @@ -0,0 +1,25 @@ +Category,Name,AppId,Alias,Scope +core,micro,zyedidia.micro,micro,user +core,lsd,lsd-rs.lsd,lsd,user +core,bat,sharkdp.bat,bat,user +core,fastfetch,Fastfetch-cli.Fastfetch,fastfetch,user +core,fzf,junegunn.fzf,fzf,user +core,fd,sharkdp.fd,fd,user +core,delta,dandavison.delta,delta,user +core,jq,jqlang.jq,jq,user +core,ripgrep,BurntSushi.ripgrep.MSVC,rg,user +core,mise,jdx.mise,mise,user +workstation,7-Zip,7zip.7zip,, +workstation,PowerToys,Microsoft.PowerToys,, +workstation,Visual Studio Code,Microsoft.VisualStudioCode,code,user +optional,Google Chrome,Google.Chrome,, +optional,KeePassXC,KeePassXCTeam.KeePassXC,, +optional,DBeaver,dbeaver.dbeaver,, +optional,Postman,Postman.Postman,, +optional,Bruno,Bruno.Bruno,, +optional,kubectl,Kubernetes.kubectl,kubectl, +optional,GIMP,GIMP.GIMP,, +optional,Android Studio,Google.AndroidStudio,, +optional,Steam,Valve.Steam,, +optional,Discord,Discord.Discord,, +optional,npiperelay,albertony.npiperelay,npiperelay, diff --git a/tests/README.md b/tests/README.md index 436c45b..977cd6c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,9 +1,9 @@ # Test suite -The GitHub Actions workflow intentionally contains only orchestration. +GitHub Actions intentionally contains orchestration only. Reusable fixture and assertion logic lives under this directory. -- `static/` validates source structure, shell code, PowerShell syntax, chezmoi templates, and the encoded AllSigned bridge. -- `linux/` owns the Debian and Arch integration fixture and post-bootstrap assertions. -- `windows/` owns Windows fixture setup, normal-policy assertions, AllSigned assertions, and update fixtures. +- `static/` validates source structure, manifests, shell code, PowerShell syntax, chezmoi templates, the encoded AllSigned bridge, and the workflow with actionlint. +- `linux/` runs the real non-interactive Debian and Arch baseline, including package installation, `yay` on Arch, update behavior, and post-bootstrap assertions. +- `windows/` owns WinGet fixture setup, normal-policy assertions, AllSigned assertions, and update fixtures. -Keep reusable validation logic here instead of embedding large scripts in `.github/workflows/validate.yml`. +`DOTFILES_NONINTERACTIVE=1` is the only CI execution mode. It is also a supported real-world mode: baseline dependencies are installed normally while prompts, GUI/workstation customization, shell changes, and WSL setup are skipped. diff --git a/tests/linux/assert-state.sh b/tests/linux/assert-state.sh index b8d8749..c4e0c67 100644 --- a/tests/linux/assert-state.sh +++ b/tests/linux/assert-state.sh @@ -5,12 +5,9 @@ expected_distro="${1:?expected distribution (debian or arch) is required}" expected_commit="${2:-}" repo_root="${GITHUB_WORKSPACE:-$(pwd)}" -fail() { - printf 'ASSERTION FAILED: %s\n' "$1" >&2 - exit 1 -} +fail() { printf 'ASSERTION FAILED: %s\n' "$1" >&2; exit 1; } -printf 'distribution: %s\n' "$(cat /etc/os-release | tr '\n' ' ')" +printf 'distribution: %s\n' "$(tr '\n' ' ' < /etc/os-release)" printf 'uname: %s\n' "$(uname -a)" printf 'user: %s (%s)\n' "$(id -un)" "$(id -u)" printf 'HOME: %s\n' "$HOME" @@ -23,35 +20,35 @@ case "$expected_distro" in *) fail "unknown expected distribution: $expected_distro" ;; esac -command -v apt-get >/dev/null 2>&1 || [[ "$expected_distro" != debian ]] || fail "apt-get is unavailable on Debian" -command -v pacman >/dev/null 2>&1 || [[ "$expected_distro" != arch ]] || fail "pacman is unavailable on Arch" sudo -n true || fail "the CI user does not have passwordless sudo" required_files=( - "$HOME/.gitconfig" - "$HOME/.gitconfig.local" - "$HOME/.fdignore" - "$HOME/.zshenv" - "$HOME/.config/zsh/.zshrc" - "$HOME/.config/zsh/lib/aliases.zsh" - "$HOME/.config/zsh/lib/completions.zsh" - "$HOME/.config/zsh/lib/key-bindings.zsh" - "$HOME/.config/zsh/lib/sheldon.zsh" - "$HOME/.config/starship/config.toml" - "$HOME/.config/starship/lean.config.toml" - "$HOME/.config/sheldon/plugins.toml" - "$HOME/.codex/AGENTS.md" - "$HOME/.codex/skills/mule-munit/SKILL.md" + "$HOME/.gitconfig" "$HOME/.gitconfig.local" "$HOME/.fdignore" "$HOME/.zshenv" + "$HOME/.config/zsh/.zshrc" "$HOME/.config/zsh/lib/aliases.zsh" + "$HOME/.config/zsh/lib/completions.zsh" "$HOME/.config/zsh/lib/key-bindings.zsh" + "$HOME/.config/zsh/lib/sheldon.zsh" "$HOME/.config/starship/config.toml" + "$HOME/.config/starship/lean.config.toml" "$HOME/.config/sheldon/plugins.toml" + "$HOME/.codex/AGENTS.md" "$HOME/.codex/skills/mule-munit/SKILL.md" "$HOME/.codex/skills/mule-munit/agents/openai.yaml" ) -for path in "${required_files[@]}"; do - [[ -f "$path" ]] || fail "expected deployed file is missing: $path" -done +for path in "${required_files[@]}"; do [[ -f "$path" ]] || fail "expected deployed file is missing: $path"; done + +while IFS= read -r requirement; do + [[ -n "$requirement" && "$requirement" != \#* ]] || continue + found=false + IFS='|' read -r -a candidates <<< "$requirement" + for command_name in "${candidates[@]}"; do + if command -v "$command_name" >/dev/null 2>&1; then found=true; break; fi + done + [[ "$found" == true ]] || fail "required baseline command is unavailable: $requirement" +done < "$repo_root/scripts/linux/required-commands.txt" + +if [[ "$expected_distro" == "arch" ]]; then command -v yay >/dev/null 2>&1 || fail "yay is unavailable on Arch"; fi assert_clean() { local status status="$(chezmoi status)" - [[ -z "$status" ]] || fail "chezmoi status is not clean:\n$status" + [[ -z "$status" ]] || fail "chezmoi status is not clean: $status" } chezmoi apply diff --git a/tests/linux/run-ci.sh b/tests/linux/run-ci.sh index 393615a..29e7fc3 100644 --- a/tests/linux/run-ci.sh +++ b/tests/linux/run-ci.sh @@ -1,16 +1,46 @@ #!/usr/bin/env bash set -euo pipefail -distro="${1:?distro required}"; commit="${2:?commit required}"; repo="${GITHUB_WORKSPACE:?}" + +distro="${1:?distro required}" +commit="${2:?commit required}" +repo="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}" + git config --global --add safe.directory "$repo" [[ "$(git -C "$repo" rev-parse HEAD)" == "$commit" ]] -parent="$(mktemp -d)"; remote="$parent/dotfiles.git" -git init --bare "$remote"; git -C "$repo" push "$remote" "$commit:refs/heads/main"; git --git-dir="$remote" symbolic-ref HEAD refs/heads/main -chmod -R a+rX "$repo"; useradd --create-home --shell /bin/bash dotfilesci -printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/dotfilesci; chmod 0440 /etc/sudoers.d/dotfilesci; chown -R dotfilesci:dotfilesci "$parent" -as_ci(){ sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci "$@"; } -as_ci GITHUB_WORKSPACE="$repo" DOTFILES_REPO="file://$remote" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' + +remote_parent="$(mktemp -d)" +remote="$remote_parent/dotfiles.git" +git init --bare "$remote" +git -C "$repo" push "$remote" "$commit:refs/heads/main" +git --git-dir="$remote" symbolic-ref HEAD refs/heads/main +chmod -R a+rX "$repo" + +useradd --create-home --shell /bin/bash dotfilesci +printf 'dotfilesci ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/dotfilesci +chmod 0440 /etc/sudoers.d/dotfilesci +chown -R dotfilesci:dotfilesci "$remote_parent" + +as_ci() { + sudo -u dotfilesci -H env HOME=/home/dotfilesci USER=dotfilesci "$@" +} + +as_ci GITHUB_WORKSPACE="$repo" DOTFILES_REPO="file://$remote" DOTFILES_NONINTERACTIVE=1 bash -lc 'cd /tmp; bash "$GITHUB_WORKSPACE/bootstrap.sh"' + as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$repo" GITHUB_WORKSPACE="$repo" D="$distro" C="$commit" bash -lc 'bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" "$D" "$C"' + marker="ci-update-marker-${GITHUB_RUN_ID:-local}" -as_ci DOTFILES_REPO="file://$remote" M="$marker" bash -lc 'set -euo pipefail; d=$(mktemp -d); git clone "$DOTFILES_REPO" "$d/r"; git -C "$d/r" config user.name ci; git -C "$d/r" config user.email ci@example.invalid; printf "\n%s\n" "$M" >>"$d/r/home/dot_fdignore"; git -C "$d/r" add home/dot_fdignore; git -c commit.gpgsign=false -C "$d/r" commit -m "CI update fixture"; git -C "$d/r" push origin HEAD:refs/heads/main' -as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GITHUB_WORKSPACE="$repo" DOTFILES_NONINTERACTIVE=1 DOTFILES_CORE_ONLY=1 bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --core-only' +as_ci DOTFILES_REPO="file://$remote" M="$marker" bash -lc ' + set -euo pipefail + work="$(mktemp -d)" + git clone "$DOTFILES_REPO" "$work/repo" + git -C "$work/repo" config user.name ci + git -C "$work/repo" config user.email ci@example.invalid + printf "\n%s\n" "$M" >> "$work/repo/home/dot_fdignore" + git -C "$work/repo" add home/dot_fdignore + git -c commit.gpgsign=false -C "$work/repo" commit -m "CI update fixture" + git -C "$work/repo" push origin HEAD:refs/heads/main +' + +as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GITHUB_WORKSPACE="$repo" DOTFILES_NONINTERACTIVE=1 bash -lc 'bash "$GITHUB_WORKSPACE/update.sh" --non-interactive' + as_ci PATH="/home/dotfilesci/.local/bin:$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0="$repo" GITHUB_WORKSPACE="$repo" D="$distro" M="$marker" bash -lc 'grep -Fqx "$M" "$HOME/.fdignore"; bash "$GITHUB_WORKSPACE/tests/linux/assert-state.sh" "$D"' diff --git a/tests/static/validate-manifests.py b/tests/static/validate-manifests.py new file mode 100644 index 0000000..58cffec --- /dev/null +++ b/tests/static/validate-manifests.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import csv +import pathlib + +root = pathlib.Path(__file__).resolve().parents[2] +with (root / "scripts/windows/managed-apps.csv").open(newline="", encoding="utf-8") as handle: + apps = list(csv.DictReader(handle)) + +required_columns = {"Category", "Name", "AppId", "Alias", "Scope"} +if not apps or set(apps[0]) != required_columns: + raise SystemExit("managed-apps.csv has an invalid schema") + +ids = [row["AppId"] for row in apps] +if len(ids) != len(set(ids)): + raise SystemExit("managed-apps.csv contains duplicate AppId values") + +core = [row for row in apps if row["Category"] == "core"] +if not core or any(not row["Alias"] for row in core): + raise SystemExit("every core WinGet app must define an Alias") +if any(row["Scope"] != "user" for row in core): + raise SystemExit("every core WinGet app must use user scope") +if any(row["Category"] not in {"core", "workstation", "optional"} for row in apps): + raise SystemExit("managed-apps.csv contains an unknown category") + +for name in ("packages-debian.txt", "packages-arch.txt", "required-commands.txt"): + path = root / "scripts/linux" / name + lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip() and not line.lstrip().startswith("#")] + if not lines: + raise SystemExit(f"{name} is empty") + if len(lines) != len(set(lines)): + raise SystemExit(f"{name} contains duplicates") + +print("manifest-validation-ok") diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh index 3075ab3..226494b 100644 --- a/tests/static/validate-source.sh +++ b/tests/static/validate-source.sh @@ -1,17 +1,33 @@ #!/usr/bin/env bash set -euo pipefail -cd "${GITHUB_WORKSPACE:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)}" -files=(bootstrap.sh install.sh update.sh tests/linux/assert-state.sh tests/linux/run-ci.sh tests/static/validate-source.sh) -bash -n "${files[@]}" -shellcheck -x -S error "${files[@]}" -chezmoi --source "$PWD" managed >/dev/null -while IFS= read -r -d '' f; do chezmoi --source "$PWD" execute-template < "$f" >/dev/null; done < <(find "$PWD" -type f -name '*.tmpl' -print0) -[[ "$(tr -d '\r\n' < .chezmoiroot)" == home ]] -for p in home scripts/windows/invoke-ps-script.cmd scripts/windows/invoke-ps-script-bridge.ps1 scripts/windows/signing.ps1 scripts/windows/deploy-pwsh.ps1 scripts/windows/managed-modules.txt home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl; do [[ -e "$p" ]] || { echo "missing: $p" >&2; exit 1; }; done -! git ls-files | grep -E '(^|/)symlink_[^/]*$' -! git grep -n -i dotbot -- ':!.github/workflows/validate.yml' -! git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master' -! git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml' -! git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd' -! git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1' + +repo_root="${GITHUB_WORKSPACE:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)}" +cd "$repo_root" + +shell_files=(bootstrap.sh install.sh update.sh tests/linux/assert-state.sh tests/linux/run-ci.sh tests/static/validate-source.sh) +bash -n "${shell_files[@]}" +shellcheck -x -S error "${shell_files[@]}" + +chezmoi --source "$repo_root" managed >/dev/null +while IFS= read -r -d '' template; do + chezmoi --source "$repo_root" execute-template < "$template" >/dev/null +done < <(find "$repo_root" -type f -name '*.tmpl' -print0) + +[[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] +required_paths=( + home scripts/linux/packages-debian.txt scripts/linux/packages-arch.txt scripts/linux/required-commands.txt + scripts/windows/managed-apps.csv scripts/windows/managed-modules.txt scripts/windows/invoke-ps-script.cmd + scripts/windows/invoke-ps-script-bridge.ps1 scripts/windows/signing.ps1 scripts/windows/deploy-pwsh.ps1 + home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl +) +for path in "${required_paths[@]}"; do [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; }; done + +if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only'; then echo 'obsolete core-only mode is still referenced' >&2; exit 1; fi +if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then echo 'chezmoi symlink source state is not allowed' >&2; exit 1; fi +if git grep -n -i dotbot -- ':!.github/workflows/validate.yml'; then echo 'obsolete Dotbot dependency/reference found' >&2; exit 1; fi +if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then echo 'obsolete master bootstrap URL found' >&2; exit 1; fi +if git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml'; then echo 'production execution-policy weakening found' >&2; exit 1; fi +if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then echo 'implementation-specific execution-policy registry probing found' >&2; exit 1; fi +if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then echo 'signed PowerShell source was committed' >&2; exit 1; fi + echo source-validation-ok diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 875ce11..770fa74 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -116,9 +116,8 @@ foreach ($path in $paths) { } } -# The workflow separately proves that unsigned scripts are rejected. The bridge -# sets this variable only on the signed execution path, so the assertion avoids -# autoloading Microsoft.PowerShell.Security inside pwsh under AllSigned. +# The bridge sets this variable only on the signed execution path. The assertion +# avoids autoloading Microsoft.PowerShell.Security inside pwsh under AllSigned. if ($env:DOTFILES_SIGNING_REQUIRED -ne "1") { Fail "assert-allsigned.ps1 was not executed through the AllSigned signing path" } @@ -212,11 +211,17 @@ function Assert-CleanChezMoi { $sourcePath = ((Invoke-Chezmoi @("source-path")) -join "").Trim() Write-Host "chezmoi source-path: $sourcePath" -foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { - if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { - Fail "core CLI tool is unavailable: $command" +$managedAppsPath = Join-Path $RepoRoot "scripts\windows\managed-apps.csv" +$coreApps = @(Import-Csv -LiteralPath $managedAppsPath | Where-Object Category -eq "core") +foreach ($app in $coreApps) { + if ([string]::IsNullOrWhiteSpace($app.Alias)) { continue } + if (-not (Get-Command -Name $app.Alias -ErrorAction SilentlyContinue)) { + Fail "baseline CLI tool is unavailable: $($app.Alias)" } } +if (-not (Get-Command -Name starship -ErrorAction SilentlyContinue)) { + Fail "starship is unavailable" +} Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host Assert-CleanChezMoi diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 index 4590c08..6199ecb 100644 --- a/tests/windows/assert-state.ps1 +++ b/tests/windows/assert-state.ps1 @@ -64,11 +64,17 @@ if ($ExpectedCommit) { if ($LASTEXITCODE -ne 0 -or $sourceCommit -ne $ExpectedCommit) { Fail "source commit $sourceCommit is not expected commit $ExpectedCommit" } } -foreach ($command in @("micro", "lsd", "bat", "fastfetch", "fzf", "fd", "delta", "jq", "rg", "mise")) { - if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { - Fail "core CLI tool is unavailable: $command" +$managedAppsPath = Join-Path $RepoRoot "scripts\windows\managed-apps.csv" +$coreApps = @(Import-Csv -LiteralPath $managedAppsPath | Where-Object Category -eq "core") +foreach ($app in $coreApps) { + if ([string]::IsNullOrWhiteSpace($app.Alias)) { continue } + if (-not (Get-Command -Name $app.Alias -ErrorAction SilentlyContinue)) { + Fail "baseline CLI tool is unavailable: $($app.Alias)" } } +if (-not (Get-Command -Name starship -ErrorAction SilentlyContinue)) { + Fail "starship is unavailable" +} Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host diff --git a/tests/windows/enable-allsigned.ps1 b/tests/windows/enable-allsigned.ps1 index 683e96e..ec161ca 100644 --- a/tests/windows/enable-allsigned.ps1 +++ b/tests/windows/enable-allsigned.ps1 @@ -1,11 +1,34 @@ -$ErrorActionPreference="Stop" -$smoke=Join-Path $env:RUNNER_TEMP "allsigned-bridge-smoke.ps1";'param([string]$Value); if($Value -ne "bridge-ok"){throw "unexpected"}'|Set-Content $smoke -$signer=Join-Path $env:RUNNER_TEMP "allsigned-signer-smoke.ps1";'param([string]$Value); if($Value -ne "signer-ok"){throw "unexpected"}'|Set-Content $signer -$subject="CN=jsilverdev Dotfiles Code Signing";$cert=Get-ChildItem Cert:\CurrentUser\My|?{$_.Subject-eq$subject-and$_.HasPrivateKey-and$_.NotAfter-gt(Get-Date)}|sort NotAfter -Descending|select -First 1 -if(!$cert){$cert=New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256} -$cer=Join-Path $env:RUNNER_TEMP "dotfiles-signing.cer";$thumb=Join-Path $env:RUNNER_TEMP "dotfiles-cert-thumbprint.txt";Set-Content $thumb $cert.Thumbprint -NoNewline;Export-Certificate -Cert $cert -FilePath $cer -Type CERT -Force|Out-Null -foreach($store in @("Cert:\LocalMachine\Root","Cert:\CurrentUser\TrustedPublisher")){if(!(Get-ChildItem $store|? Thumbprint -eq $cert.Thumbprint|select -First 1)){Import-Certificate -FilePath $cer -CertStoreLocation $store -Confirm:$false|Out-Null}} -Set-AuthenticodeSignature $smoke $cert -HashAlgorithm SHA256|Out-Null -if((Get-AuthenticodeSignature $smoke).Status-ne"Valid"){throw "signature invalid"} -Set-ExecutionPolicy -Scope CurrentUser AllSigned -Force -if((Get-ExecutionPolicy)-ne"AllSigned"){throw "AllSigned not effective"} +$ErrorActionPreference = "Stop" + +$smoke = Join-Path $env:RUNNER_TEMP "allsigned-bridge-smoke.ps1" +@' +param([string]$Value) +if ($Value -ne "bridge-ok") { throw "unexpected bridge value: $Value" } +'@ | Set-Content -LiteralPath $smoke + +$subject = "CN=jsilverdev Dotfiles Code Signing" +$certificate = Get-ChildItem Cert:\CurrentUser\My | + Where-Object { $_.Subject -eq $subject -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + +if ($null -eq $certificate) { + $certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(10) -HashAlgorithm SHA256 +} + +$certificatePath = Join-Path $env:RUNNER_TEMP "dotfiles-signing.cer" +$thumbprintPath = Join-Path $env:RUNNER_TEMP "dotfiles-cert-thumbprint.txt" +Set-Content -LiteralPath $thumbprintPath -Value $certificate.Thumbprint -NoNewline +Export-Certificate -Cert $certificate -FilePath $certificatePath -Type CERT -Force | Out-Null + +foreach ($store in @("Cert:\LocalMachine\Root", "Cert:\CurrentUser\TrustedPublisher")) { + if (-not (Get-ChildItem $store | Where-Object Thumbprint -eq $certificate.Thumbprint | Select-Object -First 1)) { + Import-Certificate -FilePath $certificatePath -CertStoreLocation $store -Confirm:$false | Out-Null + } +} + +Set-AuthenticodeSignature -FilePath $smoke -Certificate $certificate -HashAlgorithm SHA256 | Out-Null +if ((Get-AuthenticodeSignature -FilePath $smoke).Status -ne "Valid") { throw "The AllSigned smoke script signature is invalid." } + +Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy AllSigned -Force +if ((Get-ExecutionPolicy) -ne "AllSigned") { throw "AllSigned is not the effective execution policy." } diff --git a/tests/windows/prepare-ci.ps1 b/tests/windows/prepare-ci.ps1 index ad7db87..0772dbf 100644 --- a/tests/windows/prepare-ci.ps1 +++ b/tests/windows/prepare-ci.ps1 @@ -1,10 +1,44 @@ -$ErrorActionPreference="Stop" -cmd /c where winget.exe -if($LASTEXITCODE-ne0){Install-PackageProvider NuGet -Force|Out-Null;Install-Module Microsoft.WinGet.Client -Force -Repository PSGallery|Out-Null;Import-Module Microsoft.WinGet.Client -Force;Repair-WinGetPackageManager -Force -Latest} -cmd /c where winget.exe -if($LASTEXITCODE-ne0){throw "winget unavailable"} -@((Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"),(Join-Path $env:LOCALAPPDATA "Programs\Microsoft.PowerShell"),(Join-Path $env:LOCALAPPDATA "Programs\mise"),(Join-Path $env:LOCALAPPDATA "Programs\chezmoi"))|%{Add-Content $env:GITHUB_PATH $_} -$remote=Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git";git init --bare $remote -$head=(git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim();if($head-ne$env:GITHUB_SHA){throw "checkout mismatch"} -$uri="file:///"+$remote.Replace("\","/");git -C $env:GITHUB_WORKSPACE push $uri "$($env:GITHUB_SHA):refs/heads/main";if($LASTEXITCODE-ne0){throw "push failed"} -git --git-dir=$remote symbolic-ref HEAD refs/heads/main;Add-Content $env:GITHUB_ENV "DOTFILES_REPO=$uri" +$ErrorActionPreference = "Stop" + +function Ensure-WinGet { + cmd /c where winget.exe + if ($LASTEXITCODE -eq 0) { return } + + Install-PackageProvider -Name NuGet -Force | Out-Null + Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null + Import-Module Microsoft.WinGet.Client -Force + Repair-WinGetPackageManager -Force -Latest + + cmd /c where winget.exe + if ($LASTEXITCODE -ne 0) { throw "winget.exe is unavailable after repair." } +} + +function Add-CiUserPaths { + @( + (Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links"), + (Join-Path $env:LOCALAPPDATA "Programs\Microsoft.PowerShell"), + (Join-Path $env:LOCALAPPDATA "Programs\chezmoi") + ) | ForEach-Object { Add-Content -Path $env:GITHUB_PATH -Value $_ } +} + +function New-ExactCommitRemote { + $remote = Join-Path $env:RUNNER_TEMP "dotfiles-remote-$env:GITHUB_RUN_ID.git" + git init --bare $remote + if ($LASTEXITCODE -ne 0) { throw "Unable to create the local bare Git remote." } + + $head = (git -C $env:GITHUB_WORKSPACE rev-parse HEAD).Trim() + if ($head -ne $env:GITHUB_SHA) { throw "Checkout $head does not match GITHUB_SHA $env:GITHUB_SHA." } + + $uri = "file:///" + $remote.Replace("\", "/") + git -C $env:GITHUB_WORKSPACE push $uri "$($env:GITHUB_SHA):refs/heads/main" + if ($LASTEXITCODE -ne 0) { throw "Unable to push the checked-out commit to the local remote." } + + git --git-dir=$remote symbolic-ref HEAD refs/heads/main + if ($LASTEXITCODE -ne 0) { throw "Unable to set the local remote HEAD." } + + Add-Content -Path $env:GITHUB_ENV -Value "DOTFILES_REPO=$uri" +} + +Ensure-WinGet +Add-CiUserPaths +New-ExactCommitRemote diff --git a/update.cmd b/update.cmd index ef7367d..abc0807 100644 --- a/update.cmd +++ b/update.cmd @@ -16,9 +16,8 @@ if not defined REPO_ROOT ( echo Unable to resolve the chezmoi working tree. 1>&2 exit /b 1 ) -if /I "%DOTFILES_CORE_ONLY%"=="1" ( - call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -CoreOnly -NonInteractive -RepoRoot "%REPO_ROOT%" -) else if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( + +if /I "%DOTFILES_NONINTERACTIVE%"=="1" ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -NonInteractive -RepoRoot "%REPO_ROOT%" ) else ( call "%REPO_ROOT%\scripts\windows\invoke-ps-script.cmd" "%REPO_ROOT%\install.ps1" -Update -RepoRoot "%REPO_ROOT%" diff --git a/update.sh b/update.sh old mode 100644 new mode 100755 index d9301f4..6ec14dc --- a/update.sh +++ b/update.sh @@ -9,15 +9,9 @@ resolve_repo_root() { local candidate parent for candidate in "$(chezmoi execute-template '{{ .chezmoi.workingTree }}' 2>/dev/null || true)" "$(chezmoi source-path)"; do [[ -n "$candidate" ]] || continue - if [[ -f "$candidate/install.sh" ]]; then - printf '%s\n' "$candidate" - return 0 - fi + if [[ -f "$candidate/install.sh" ]]; then printf '%s\n' "$candidate"; return; fi parent="$(dirname "$candidate")" - if [[ -f "$parent/install.sh" ]]; then - printf '%s\n' "$parent" - return 0 - fi + if [[ -f "$parent/install.sh" ]]; then printf '%s\n' "$parent"; return; fi done return 1 } From 8d92197ea836428dc7aefbe5c6d5e2e01c223745 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:41:25 -0500 Subject: [PATCH 58/61] Fix baseline validation and managed module updates --- install.ps1 | 60 ++++++++++++------------------ install.sh | 6 ++- tests/static/validate-source.sh | 2 +- tests/windows/assert-allsigned.ps1 | 5 ++- tests/windows/assert-state.ps1 | 5 ++- 5 files changed, 34 insertions(+), 44 deletions(-) diff --git a/install.ps1 b/install.ps1 index c13dcf0..50555f7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -211,45 +211,31 @@ function Install-MustHaveApps { } $allSigned = $env:DOTFILES_SIGNING_REQUIRED -eq "1" - foreach ($module in $ManagedModules) { - $availableModules = @(Get-Module -ListAvailable -Name $module) - if ($allSigned) { - $userModuleRoots = @( - (Join-Path $HOME "Documents\PowerShell\Modules"), - (Join-Path $HOME ".local\share\powershell\Modules") - ) - $installedModule = @($availableModules | Where-Object { - $moduleBase = [IO.Path]::GetFullPath($_.ModuleBase).TrimEnd([IO.Path]::DirectorySeparatorChar) - @($userModuleRoots | Where-Object { - $root = [IO.Path]::GetFullPath($_).TrimEnd([IO.Path]::DirectorySeparatorChar) - $moduleBase.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or - $moduleBase.StartsWith($root + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) - }).Count -gt 0 - } | Select-Object -First 1) - if ($installedModule.Count -eq 0) { $installedModule = $null } - else { $installedModule = $installedModule[0] } - } - else { - $installedModule = $availableModules | Select-Object -First 1 - } - - $installedResource = $null - if (-not $allSigned -and (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue)) { - $installedResource = Get-InstalledPSResource -Name $module -ErrorAction SilentlyContinue | Select-Object -First 1 - } + $userModuleRoots = @( + (Join-Path $HOME "Documents\PowerShell\Modules"), + (Join-Path $HOME ".local\share\powershell\Modules") + ) - if ($null -eq $installedModule) { - Write-Host "Installing $module module..." -ForegroundColor Cyan - if ($allSigned) { Save-ManagedModuleForAllSigned -Name $module } - else { Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false } - } - elseif ($Update) { - Write-Host "Updating $module module..." -ForegroundColor Yellow - if ($allSigned) { Save-ManagedModuleForAllSigned -Name $module } - elseif ($null -ne $installedResource -and (Get-Command Update-PSResource -ErrorAction SilentlyContinue)) { - Update-PSResource -Name $module -Scope CurrentUser -Force + foreach ($module in $ManagedModules) { + $installedModule = @(Get-Module -ListAvailable -Name $module | Where-Object { + $moduleBase = [IO.Path]::GetFullPath($_.ModuleBase).TrimEnd([IO.Path]::DirectorySeparatorChar) + @($userModuleRoots | Where-Object { + $root = [IO.Path]::GetFullPath($_).TrimEnd([IO.Path]::DirectorySeparatorChar) + $moduleBase.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or + $moduleBase.StartsWith($root + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) + }).Count -gt 0 + } | Select-Object -First 1) + + if ($installedModule.Count -eq 0 -or $Update) { + $verb = if ($installedModule.Count -eq 0) { "Installing" } else { "Updating" } + Write-Host "$verb $module module..." -ForegroundColor Cyan + + if ($allSigned) { + Save-ManagedModuleForAllSigned -Name $module + } + else { + Install-Module -Name $module -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -AcceptLicense -Confirm:$false } - else { Update-Module -Name $module -Force } } else { Write-Host "$module module is already installed" -ForegroundColor Green diff --git a/install.sh b/install.sh index 8879750..2d752f6 100755 --- a/install.sh +++ b/install.sh @@ -81,7 +81,9 @@ pre_setup_tasks() { export PATH="$HOME/.local/bin:$PATH" detect_distro detect_arch - updates_enabled && printf '%bUpdate mode enabled.%b\n' "$CYAN" "$RESET" + if updates_enabled; then + printf '%bUpdate mode enabled.%b\n' "$CYAN" "$RESET" + fi } refresh_package_index() { @@ -173,7 +175,7 @@ debian_release_arch() { [[ "$arch" == "aarch64" ]] && printf 'arm64\n' || printf install_fastfetch() { if apt-cache show fastfetch >/dev/null 2>&1; then sudo apt-get install --yes fastfetch - else install_github_deb_asset "fastfetch-cli/fastfetch" "fastfetch-linux-${arch}\\.deb$"; fi + else install_github_deb_asset "fastfetch-cli/fastfetch" "fastfetch-linux-${arch}-polyfilled\\.deb$"; fi } install_lsd() { if apt-cache show lsd >/dev/null 2>&1; then sudo apt-get install --yes lsd diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh index 226494b..96667fa 100644 --- a/tests/static/validate-source.sh +++ b/tests/static/validate-source.sh @@ -22,7 +22,7 @@ required_paths=( ) for path in "${required_paths[@]}"; do [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; }; done -if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only'; then echo 'obsolete core-only mode is still referenced' >&2; exit 1; fi +if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only' -- ':!tests/static/validate-source.sh'; then echo 'obsolete core-only mode is still referenced' >&2; exit 1; fi if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then echo 'chezmoi symlink source state is not allowed' >&2; exit 1; fi if git grep -n -i dotbot -- ':!.github/workflows/validate.yml'; then echo 'obsolete Dotbot dependency/reference found' >&2; exit 1; fi if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then echo 'obsolete master bootstrap URL found' >&2; exit 1; fi diff --git a/tests/windows/assert-allsigned.ps1 b/tests/windows/assert-allsigned.ps1 index 770fa74..7811e75 100644 --- a/tests/windows/assert-allsigned.ps1 +++ b/tests/windows/assert-allsigned.ps1 @@ -219,8 +219,9 @@ foreach ($app in $coreApps) { Fail "baseline CLI tool is unavailable: $($app.Alias)" } } -if (-not (Get-Command -Name starship -ErrorAction SilentlyContinue)) { - Fail "starship is unavailable" +& mise which starship *> $null +if ($LASTEXITCODE -ne 0) { + Fail "starship is not managed by mise" } Invoke-Chezmoi @("apply") | Out-Host Invoke-Chezmoi @("apply") | Out-Host diff --git a/tests/windows/assert-state.ps1 b/tests/windows/assert-state.ps1 index 6199ecb..e348df2 100644 --- a/tests/windows/assert-state.ps1 +++ b/tests/windows/assert-state.ps1 @@ -72,8 +72,9 @@ foreach ($app in $coreApps) { Fail "baseline CLI tool is unavailable: $($app.Alias)" } } -if (-not (Get-Command -Name starship -ErrorAction SilentlyContinue)) { - Fail "starship is unavailable" +& mise which starship *> $null +if ($LASTEXITCODE -ne 0) { + Fail "starship is not managed by mise" } Invoke-Chezmoi @("apply") | Out-Host From dfb9f703a59b0cd88b1dbe2b745d7cc9d4c210fd Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:45:27 -0500 Subject: [PATCH 59/61] Split static source validation phases --- .github/workflows/validate.yml | 10 ++- tests/static/validate-source.sh | 104 +++++++++++++++++++++++++------- 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 598e1ca..007f7d7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -41,8 +41,14 @@ jobs: - name: Validate workflow run: actionlint .github/workflows/validate.yml - - name: Validate source state - run: bash tests/static/validate-source.sh + - name: Validate shell source + run: bash tests/static/validate-source.sh shell + + - name: Validate chezmoi source + run: bash tests/static/validate-source.sh chezmoi + + - name: Validate source architecture + run: bash tests/static/validate-source.sh architecture - name: Validate manifests run: python3 tests/static/validate-manifests.py diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh index 96667fa..daf05c8 100644 --- a/tests/static/validate-source.sh +++ b/tests/static/validate-source.sh @@ -4,30 +4,88 @@ set -euo pipefail repo_root="${GITHUB_WORKSPACE:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)}" cd "$repo_root" -shell_files=(bootstrap.sh install.sh update.sh tests/linux/assert-state.sh tests/linux/run-ci.sh tests/static/validate-source.sh) -bash -n "${shell_files[@]}" -shellcheck -x -S error "${shell_files[@]}" +validate_shell() { + local shell_files=( + bootstrap.sh + install.sh + update.sh + tests/linux/assert-state.sh + tests/linux/run-ci.sh + tests/static/validate-source.sh + ) + bash -n "${shell_files[@]}" + shellcheck -x -S error "${shell_files[@]}" +} -chezmoi --source "$repo_root" managed >/dev/null -while IFS= read -r -d '' template; do - chezmoi --source "$repo_root" execute-template < "$template" >/dev/null -done < <(find "$repo_root" -type f -name '*.tmpl' -print0) +validate_chezmoi() { + chezmoi --source "$repo_root" managed >/dev/null + while IFS= read -r -d '' template; do + chezmoi --source "$repo_root" execute-template < "$template" >/dev/null + done < <(find "$repo_root" -type f -name '*.tmpl' -print0) +} -[[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] -required_paths=( - home scripts/linux/packages-debian.txt scripts/linux/packages-arch.txt scripts/linux/required-commands.txt - scripts/windows/managed-apps.csv scripts/windows/managed-modules.txt scripts/windows/invoke-ps-script.cmd - scripts/windows/invoke-ps-script-bridge.ps1 scripts/windows/signing.ps1 scripts/windows/deploy-pwsh.ps1 - home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl -) -for path in "${required_paths[@]}"; do [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; }; done +validate_architecture() { + [[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] -if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only' -- ':!tests/static/validate-source.sh'; then echo 'obsolete core-only mode is still referenced' >&2; exit 1; fi -if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then echo 'chezmoi symlink source state is not allowed' >&2; exit 1; fi -if git grep -n -i dotbot -- ':!.github/workflows/validate.yml'; then echo 'obsolete Dotbot dependency/reference found' >&2; exit 1; fi -if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then echo 'obsolete master bootstrap URL found' >&2; exit 1; fi -if git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml'; then echo 'production execution-policy weakening found' >&2; exit 1; fi -if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then echo 'implementation-specific execution-policy registry probing found' >&2; exit 1; fi -if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then echo 'signed PowerShell source was committed' >&2; exit 1; fi + local required_paths=( + home + scripts/linux/packages-debian.txt + scripts/linux/packages-arch.txt + scripts/linux/required-commands.txt + scripts/windows/managed-apps.csv + scripts/windows/managed-modules.txt + scripts/windows/invoke-ps-script.cmd + scripts/windows/invoke-ps-script-bridge.ps1 + scripts/windows/signing.ps1 + scripts/windows/deploy-pwsh.ps1 + home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl + ) + local path + for path in "${required_paths[@]}"; do + [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; } + done -echo source-validation-ok + if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only' -- ':!tests/static/validate-source.sh'; then + echo 'obsolete core-only mode is still referenced' >&2 + exit 1 + fi + if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then + echo 'chezmoi symlink source state is not allowed' >&2 + exit 1 + fi + if git grep -n -i dotbot -- ':!.github/workflows/validate.yml'; then + echo 'obsolete Dotbot dependency/reference found' >&2 + exit 1 + fi + if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then + echo 'obsolete master bootstrap URL found' >&2 + exit 1 + fi + if git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml'; then + echo 'production execution-policy weakening found' >&2 + exit 1 + fi + if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then + echo 'implementation-specific execution-policy registry probing found' >&2 + exit 1 + fi + if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then + echo 'signed PowerShell source was committed' >&2 + exit 1 + fi +} + +case "${1:-all}" in + shell) validate_shell ;; + chezmoi) validate_chezmoi ;; + architecture) validate_architecture ;; + all) + validate_shell + validate_chezmoi + validate_architecture + ;; + *) + printf 'usage: %s [shell|chezmoi|architecture|all]\n' "$0" >&2 + exit 2 + ;; +esac From 008ecd6bf52e32683f65b93a25a011ac4ca59229 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:47:02 -0500 Subject: [PATCH 60/61] Make static architecture checks explicit --- .github/workflows/validate.yml | 13 +++++++++++-- tests/static/validate-source.sh | 27 ++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 007f7d7..0a485a8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -47,8 +47,17 @@ jobs: - name: Validate chezmoi source run: bash tests/static/validate-source.sh chezmoi - - name: Validate source architecture - run: bash tests/static/validate-source.sh architecture + - name: Validate required source paths + run: bash tests/static/validate-source.sh paths + + - name: Validate legacy constraints + run: bash tests/static/validate-source.sh legacy + + - name: Validate execution-policy constraints + run: bash tests/static/validate-source.sh policy + + - name: Validate unsigned repository source + run: bash tests/static/validate-source.sh signatures - name: Validate manifests run: python3 tests/static/validate-manifests.py diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh index daf05c8..07c8202 100644 --- a/tests/static/validate-source.sh +++ b/tests/static/validate-source.sh @@ -24,8 +24,11 @@ validate_chezmoi() { done < <(find "$repo_root" -type f -name '*.tmpl' -print0) } -validate_architecture() { - [[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] +validate_paths() { + [[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] || { + echo '.chezmoiroot must contain home' >&2 + exit 1 + } local required_paths=( home @@ -44,7 +47,9 @@ validate_architecture() { for path in "${required_paths[@]}"; do [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; } done +} +validate_legacy() { if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only' -- ':!tests/static/validate-source.sh'; then echo 'obsolete core-only mode is still referenced' >&2 exit 1 @@ -61,6 +66,9 @@ validate_architecture() { echo 'obsolete master bootstrap URL found' >&2 exit 1 fi +} + +validate_policy() { if git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml'; then echo 'production execution-policy weakening found' >&2 exit 1 @@ -69,6 +77,9 @@ validate_architecture() { echo 'implementation-specific execution-policy registry probing found' >&2 exit 1 fi +} + +validate_signatures() { if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then echo 'signed PowerShell source was committed' >&2 exit 1 @@ -78,14 +89,20 @@ validate_architecture() { case "${1:-all}" in shell) validate_shell ;; chezmoi) validate_chezmoi ;; - architecture) validate_architecture ;; + paths) validate_paths ;; + legacy) validate_legacy ;; + policy) validate_policy ;; + signatures) validate_signatures ;; all) validate_shell validate_chezmoi - validate_architecture + validate_paths + validate_legacy + validate_policy + validate_signatures ;; *) - printf 'usage: %s [shell|chezmoi|architecture|all]\n' "$0" >&2 + printf 'usage: %s [shell|chezmoi|paths|legacy|policy|signatures|all]\n' "$0" >&2 exit 2 ;; esac From 887805b53533863548861d71bd8191f7281742f6 Mon Sep 17 00:00:00 2001 From: Julio Rios <31230801+jsilverdev@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:48:05 -0500 Subject: [PATCH 61/61] Make architecture validation deterministic --- .github/workflows/validate.yml | 13 +-- tests/static/validate-architecture.py | 118 ++++++++++++++++++++++++++ tests/static/validate-source.sh | 72 +--------------- 3 files changed, 121 insertions(+), 82 deletions(-) create mode 100644 tests/static/validate-architecture.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0a485a8..8ae22ea 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -47,17 +47,8 @@ jobs: - name: Validate chezmoi source run: bash tests/static/validate-source.sh chezmoi - - name: Validate required source paths - run: bash tests/static/validate-source.sh paths - - - name: Validate legacy constraints - run: bash tests/static/validate-source.sh legacy - - - name: Validate execution-policy constraints - run: bash tests/static/validate-source.sh policy - - - name: Validate unsigned repository source - run: bash tests/static/validate-source.sh signatures + - name: Validate source architecture + run: python3 tests/static/validate-architecture.py - name: Validate manifests run: python3 tests/static/validate-manifests.py diff --git a/tests/static/validate-architecture.py b/tests/static/validate-architecture.py new file mode 100644 index 0000000..82d9269 --- /dev/null +++ b/tests/static/validate-architecture.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import pathlib +import re +import subprocess + +ROOT = pathlib.Path(__file__).resolve().parents[2] +SELF = pathlib.Path(__file__).resolve().relative_to(ROOT).as_posix() + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def tracked_files() -> list[str]: + result = subprocess.run( + ["git", "-C", str(ROOT), "ls-files", "-z"], + check=True, + capture_output=True, + ) + return [item.decode() for item in result.stdout.split(b"\0") if item] + + +def read_text(path: str) -> str: + try: + return (ROOT / path).read_text(encoding="utf-8") + except UnicodeDecodeError: + return "" + + +if (ROOT / ".chezmoiroot").read_text(encoding="utf-8").strip() != "home": + fail(".chezmoiroot must contain 'home'") + +required_paths = ( + "home", + "scripts/linux/packages-debian.txt", + "scripts/linux/packages-arch.txt", + "scripts/linux/required-commands.txt", + "scripts/windows/managed-apps.csv", + "scripts/windows/managed-modules.txt", + "scripts/windows/invoke-ps-script.cmd", + "scripts/windows/invoke-ps-script-bridge.ps1", + "scripts/windows/signing.ps1", + "scripts/windows/deploy-pwsh.ps1", + "home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl", +) +for relative in required_paths: + if not (ROOT / relative).exists(): + fail(f"required path is missing: {relative}") + +paths = tracked_files() +for path in paths: + if pathlib.PurePosixPath(path).name.startswith("symlink_"): + fail(f"chezmoi symlink source state is not allowed: {path}") + +checks = ( + ( + re.compile(r"DOTFILES_CORE_ONLY|CoreOnly|--core-only"), + {SELF}, + "obsolete core-only mode is still referenced", + ), + ( + re.compile(r"dotbot", re.IGNORECASE), + {".github/workflows/validate.yml", SELF}, + "obsolete Dotbot dependency/reference found", + ), + ( + re.compile( + r"https://github\.com/jsilverdev/dotfiles\.git.*master|" + r"raw\.githubusercontent\.com/jsilverdev/dotfiles/master" + ), + {SELF}, + "obsolete master bootstrap URL found", + ), + ( + re.compile( + r"Set-ExecutionPolicy.*(?:Bypass|Unrestricted)|" + r"-ExecutionPolicy\s+(?:Bypass|Unrestricted)", + re.IGNORECASE, + ), + {".github/workflows/validate.yml", SELF}, + "production execution-policy weakening found", + ), +) + +for pattern, exclusions, message in checks: + for path in paths: + if path in exclusions: + continue + text = read_text(path) + match = pattern.search(text) + if match: + line = text.count("\n", 0, match.start()) + 1 + fail(f"{message}: {path}:{line}") + +registry_pattern = re.compile( + r"PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds", + re.IGNORECASE, +) +for path in paths: + if pathlib.PurePosixPath(path).suffix.lower() not in {".ps1", ".psm1", ".cmd"}: + continue + text = read_text(path) + match = registry_pattern.search(text) + if match: + line = text.count("\n", 0, match.start()) + 1 + fail(f"implementation-specific execution-policy registry probing found: {path}:{line}") + +signature_marker = "# SIG # Begin signature block" +for path in paths: + if pathlib.PurePosixPath(path).suffix.lower() not in {".ps1", ".psm1"}: + continue + text = read_text(path) + if any(line.startswith(signature_marker) for line in text.splitlines()): + fail(f"signed PowerShell source was committed: {path}") + +print("architecture-validation-ok") diff --git a/tests/static/validate-source.sh b/tests/static/validate-source.sh index 07c8202..5426ef6 100644 --- a/tests/static/validate-source.sh +++ b/tests/static/validate-source.sh @@ -24,85 +24,15 @@ validate_chezmoi() { done < <(find "$repo_root" -type f -name '*.tmpl' -print0) } -validate_paths() { - [[ "$(tr -d '\r\n' < .chezmoiroot)" == "home" ]] || { - echo '.chezmoiroot must contain home' >&2 - exit 1 - } - - local required_paths=( - home - scripts/linux/packages-debian.txt - scripts/linux/packages-arch.txt - scripts/linux/required-commands.txt - scripts/windows/managed-apps.csv - scripts/windows/managed-modules.txt - scripts/windows/invoke-ps-script.cmd - scripts/windows/invoke-ps-script-bridge.ps1 - scripts/windows/signing.ps1 - scripts/windows/deploy-pwsh.ps1 - home/.chezmoiscripts/run_after_90-deploy-pwsh.cmd.tmpl - ) - local path - for path in "${required_paths[@]}"; do - [[ -e "$path" ]] || { printf 'required path is missing: %s\n' "$path" >&2; exit 1; } - done -} - -validate_legacy() { - if git grep -n -E 'DOTFILES_CORE_ONLY|CoreOnly|--core-only' -- ':!tests/static/validate-source.sh'; then - echo 'obsolete core-only mode is still referenced' >&2 - exit 1 - fi - if git ls-files | grep -E '(^|/)symlink_[^/]*$'; then - echo 'chezmoi symlink source state is not allowed' >&2 - exit 1 - fi - if git grep -n -i dotbot -- ':!.github/workflows/validate.yml'; then - echo 'obsolete Dotbot dependency/reference found' >&2 - exit 1 - fi - if git grep -n -E 'https://github\.com/jsilverdev/dotfiles\.git.*master|raw\.githubusercontent\.com/jsilverdev/dotfiles/master'; then - echo 'obsolete master bootstrap URL found' >&2 - exit 1 - fi -} - -validate_policy() { - if git grep -n -E 'Set-ExecutionPolicy.*(Bypass|Unrestricted)|-ExecutionPolicy[[:space:]]+(Bypass|Unrestricted)' -- ':!.github/workflows/validate.yml'; then - echo 'production execution-policy weakening found' >&2 - exit 1 - fi - if git grep -n -E 'PowerShellCore\\ShellIds|Software\\Microsoft\\PowerShell\\1\\ShellIds' -- '*.ps1' '*.psm1' '*.cmd'; then - echo 'implementation-specific execution-policy registry probing found' >&2 - exit 1 - fi -} - -validate_signatures() { - if git grep -n '^# SIG # Begin signature block' -- '*.ps1' '*.psm1'; then - echo 'signed PowerShell source was committed' >&2 - exit 1 - fi -} - case "${1:-all}" in shell) validate_shell ;; chezmoi) validate_chezmoi ;; - paths) validate_paths ;; - legacy) validate_legacy ;; - policy) validate_policy ;; - signatures) validate_signatures ;; all) validate_shell validate_chezmoi - validate_paths - validate_legacy - validate_policy - validate_signatures ;; *) - printf 'usage: %s [shell|chezmoi|paths|legacy|policy|signatures|all]\n' "$0" >&2 + printf 'usage: %s [shell|chezmoi|all]\n' "$0" >&2 exit 2 ;; esac