From dcda38e0f188e4bb65fbeaba120d7d1ab94a5bad Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Sun, 10 May 2026 21:52:35 +0530 Subject: [PATCH 1/2] feat(install): add GHE/air-gap env vars to install.ps1 (Windows parity) Mirror install.sh: APM_INSTALL_DIR, GITHUB_URL, APM_REPO, VERSION; derive GH API root from GITHUB_URL; skip releases/latest when VERSION is set. --- CHANGELOG.md | 1 + .../docs/getting-started/installation.md | 37 +- install.ps1 | 323 ++++++++++++------ .../.apm/skills/apm-usage/installation.md | 13 + 4 files changed, 260 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de7844e0..7fdbde533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`install.ps1` (Windows)** now mirrors **`install.sh`**: `APM_INSTALL_DIR`, `GITHUB_URL`, `APM_REPO`, and `VERSION` environment variables; pinning `VERSION` skips the releases/latest API; `GITHUB_URL` selects GitHub.com (`api.github.com`) vs GHES (`{host}/api/v3`) for latest discovery. (#668) - Virtual subdirectory and raw-file packages now resolve from self-hosted Git services (Gitea, Gogs) via raw URL with API v1/v3 fallback. (#587) - `shared/apm.md` gh-aw shared workflow exposes a `target:` import input (default `all`) so consumer workflows can ship slim, single-harness bundles instead of always packing every layout. (#1184) - **GitLab host support:** `gitlab.com` and self-managed instances (via `GITLAB_HOST` / `APM_GITLAB_HOSTS`) use GitLab REST **v4** for `marketplace.json` and install-time raw file reads; nested GitLab group paths are disambiguated in dependency references with object-form `git:` + `path:` where shorthand is ambiguous. GitHub, GHES, Azure DevOps, and registry-proxy behavior remain unchanged. (#1149) diff --git a/docs/src/content/docs/getting-started/installation.md b/docs/src/content/docs/getting-started/installation.md index 125e8cccf..b44c9af19 100644 --- a/docs/src/content/docs/getting-started/installation.md +++ b/docs/src/content/docs/getting-started/installation.md @@ -29,7 +29,7 @@ The installer automatically detects your platform (macOS/Linux/Windows, Intel/AR ### Installer options -The Unix installer supports environment variables for custom environments: +**macOS / Linux (`install.sh`):** ```bash # Install a specific version @@ -42,15 +42,36 @@ curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh ``` +**Windows (`install.ps1` in PowerShell):** + +```powershell +# Pin a version (skips GitHub API — required for many air-gapped / GHES setups) +$env:VERSION = "v1.2.3"; irm https://aka.ms/apm-windows | iex + +# Or pass a positional parameter when running a saved script: +# .\install.ps1 v1.2.3 + +# Custom directory for apm.cmd (default: %LOCALAPPDATA%\Programs\apm\bin) +$env:APM_INSTALL_DIR = "$env:LOCALAPPDATA\Programs\apm\bin"; irm https://aka.ms/apm-windows | iex + +# Fork, enterprise host, or internal mirror +$env:GITHUB_URL = "https://github.corp.com" +$env:APM_REPO = "my-org/apm" +$env:VERSION = "v1.2.3" +irm https://aka.ms/apm-windows | iex +``` + | Variable | Default | Description | |----------|---------|-------------| -| `APM_INSTALL_DIR` | `/usr/local/bin` | Directory for the `apm` symlink | -| `APM_LIB_DIR` | `$(dirname APM_INSTALL_DIR)/lib/apm` | Directory for the full binary bundle | -| `GITHUB_URL` | `https://github.com` | Base URL for downloads (mirrors, GHE) | -| `APM_REPO` | `microsoft/apm` | GitHub repository | -| `VERSION` | *(latest)* | Pin a specific release (skips GitHub API) | - -> **Note:** When using `GITHUB_URL` for a GitHub Enterprise or air-gapped mirror, set `VERSION` as well. The GitHub API call for latest-release discovery still targets `api.github.com`; `VERSION` bypasses it entirely. +| `APM_INSTALL_DIR` | `/usr/local/bin` (Unix) / `%LOCALAPPDATA%\Programs\apm\bin` (Windows) | Directory for the `apm` symlink / `apm.cmd` shim | +| `APM_LIB_DIR` | `$(dirname APM_INSTALL_DIR)/lib/apm` | *(Unix only)* Directory for the full binary bundle | +| `GITHUB_URL` | `https://github.com` | Base URL for downloads (mirrors, GHES) | +| `APM_REPO` | `microsoft/apm` | GitHub repository (`owner/name`) | +| `VERSION` | *(latest)* | Pin a specific release tag (skips the **releases/latest** HTTP API) | + +> **Note — Unix (`install.sh`):** Latest-release discovery still calls `https://api.github.com/repos/.../releases/latest` unless `VERSION` is set. For GHES or mirrors with no access to `api.github.com`, pin `VERSION` so the script never hits that endpoint. +> +> **Note — Windows (`install.ps1`):** The **releases/latest** URL is derived from `GITHUB_URL`: `https://api.github.com` for GitHub.com, or `{GITHUB_URL}/api/v3` for GitHub Enterprise Server. Air-gapped runners should still set `VERSION` so the installer does not need the API at all. ## Package managers diff --git a/install.ps1 b/install.ps1 index a4386bb0f..9fb67451e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,18 +1,88 @@ +# APM CLI Installer Script (Windows / PowerShell) +# +# Usage: +# irm https://aka.ms/apm-windows | iex +# +# Pin a version (skips GitHub HTTP API — use for air-gapped / GHE): +# $env:VERSION = 'v1.2.3'; irm https://aka.ms/apm-windows | iex +# .\install.ps1 v1.2.3 +# +# Custom install location (directory that will contain apm.cmd): +# $env:APM_INSTALL_DIR = "$env:LOCALAPPDATA\Programs\apm\bin"; irm ... | iex +# +# Fork or private mirror: +# $env:APM_REPO = 'my-org/apm'; irm ... | iex +# +# GitHub Enterprise Server / mirror (set VERSION to avoid unreachable api.github.com): +# $env:GITHUB_URL = 'https://github.corp.com' +# $env:VERSION = 'v1.2.3' +# irm https://.../install.ps1 | iex +# +# Private repositories: set GITHUB_APM_PAT or GITHUB_TOKEN + param( + [Parameter(Position = 0)] + [string]$Version = $null, [string]$Repo = "microsoft/apm" ) $ErrorActionPreference = "Stop" -$installRoot = Join-Path $env:LOCALAPPDATA "Programs\apm" -$binDir = Join-Path $installRoot "bin" -$releasesDir = Join-Path $installRoot "releases" +# --------------------------------------------------------------------------- +# Configuration (overridable via environment variables — parity with install.sh) +# --------------------------------------------------------------------------- + +$githubUrl = if ($env:GITHUB_URL) { + $env:GITHUB_URL.Trim().Trim('"').TrimEnd('/') +} else { + "https://github.com" +} +$apmRepo = if ($env:APM_REPO) { $env:APM_REPO.Trim() } else { $Repo } + +$pinnedVersion = $null +if ($env:VERSION) { + $pinnedVersion = $env:VERSION.Trim().TrimStart('@') +} elseif ($Version) { + $pinnedVersion = $Version.Trim().TrimStart('@') +} elseif ($args.Count -gt 0) { + $a0 = [string]$args[0] + $pinnedVersion = $a0.Trim().TrimStart('@') +} + +$defaultInstallRoot = Join-Path $env:LOCALAPPDATA "Programs\apm" +$defaultBinDir = Join-Path $defaultInstallRoot "bin" + +if ($env:APM_INSTALL_DIR) { + $binDir = $env:APM_INSTALL_DIR.Trim().TrimEnd('\', '/') + $parent = Split-Path $binDir -Parent + if ($parent) { + $installRoot = $parent + } else { + # Single-segment path: keep bundles next to the shim directory + $installRoot = $binDir + } + $releasesDir = Join-Path $installRoot "releases" +} else { + $installRoot = $defaultInstallRoot + $binDir = $defaultBinDir + $releasesDir = Join-Path $installRoot "releases" +} + $assetName = "apm-windows-x86_64.zip" # --------------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------------- +function Get-GitHubApiRoot { + param([string]$Url) + $u = $Url.Trim().TrimEnd('/') + if ($u -match '(?i)^https?://github\.com$') { + return "https://api.github.com" + } + return "$u/api/v3" +} + function Write-Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan @@ -37,42 +107,35 @@ function Get-AuthHeader { if ($env:GITHUB_APM_PAT) { return @{ Authorization = "token $($env:GITHUB_APM_PAT)" } } - if ($env:GITHUB_TOKEN) { return @{ Authorization = "token $($env:GITHUB_TOKEN)" } } - return @{} } function Invoke-GitHubJson { param( - [string]$Url, + [string]$Uri, [hashtable]$Headers ) - if ($Headers.Count -gt 0) { - return Invoke-RestMethod -Uri $Url -Headers $Headers + return Invoke-RestMethod -Uri $Uri -Headers $Headers } - - return Invoke-RestMethod -Uri $Url + return Invoke-RestMethod -Uri $Uri } function Add-ToUserPath { param([string]$PathEntry) - $currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User") $userEntries = @() if ($currentUserPath) { $userEntries = $currentUserPath.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) } - if ($userEntries -notcontains $PathEntry) { $newUserPath = if ($currentUserPath) { "$PathEntry;$currentUserPath" } else { $PathEntry } [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") Write-Info "Added $PathEntry to your user PATH." } - if (($env:Path -split ";") -notcontains $PathEntry) { $env:Path = "$PathEntry;$env:Path" } @@ -93,7 +156,6 @@ function Test-PythonRequirement { } } } catch { - # Ignore; try next candidate } } } @@ -106,9 +168,7 @@ function Install-ViaPip { Write-ErrorText "Python 3.9+ is not available — cannot fall back to pip." return $false } - Write-Info "Attempting installation via pip ($pythonCmd)..." - $pipCmd = $null foreach ($candidate in @("pip3", "pip")) { if (Get-Command $candidate -ErrorAction SilentlyContinue) { @@ -119,9 +179,7 @@ function Install-ViaPip { if (-not $pipCmd) { $pipCmd = "$pythonCmd -m pip" } - try { - $pipArgs = "install --user apm-cli" if ($pipCmd -like "* -m pip") { $output = & $pythonCmd -m pip install --user apm-cli 2>&1 $pipExitCode = $LASTEXITCODE @@ -139,8 +197,6 @@ function Install-ViaPip { Write-ErrorText "pip install failed: $_" return $false } - - # Verify apm is available after pip install $apmExe = Get-Command apm -ErrorAction SilentlyContinue if ($apmExe) { $ver = & apm --version 2>$null @@ -154,14 +210,18 @@ function Install-ViaPip { } function Write-ManualInstallHelp { + param( + [string]$GithubUrl, + [string]$ApmRepo + ) Write-Host "" Write-Info "Manual installation options:" Write-Host " 1. pip (recommended): pip install --user apm-cli" Write-Host " 2. From source:" - Write-Host " git clone https://github.com/$Repo.git" + Write-Host " git clone $GithubUrl/${ApmRepo}.git" Write-Host " cd apm && uv sync && uv run pip install -e ." Write-Host "" - Write-Host "Need help? Create an issue at: https://github.com/$Repo/issues" + Write-Host "Need help? Create an issue at: $GithubUrl/$ApmRepo/issues" } # --------------------------------------------------------------------------- @@ -175,56 +235,63 @@ Write-Host " The NPM for AI-Native Development " -Foregr Write-Host "===========================================================" -ForegroundColor Blue Write-Host "" +$apiRoot = Get-GitHubApiRoot -Url $githubUrl +$headers = @{} + # --------------------------------------------------------------------------- -# Stage 1 — Fetch release info (unauthenticated first, then authenticated) +# Stage 1 — Release metadata (skip GitHub API when VERSION is pinned) # --------------------------------------------------------------------------- -Write-Info "Fetching latest release information..." - $release = $null -$headers = @{} +$asset = $null +$tagName = $null + +if ($pinnedVersion) { + $tagName = $pinnedVersion + Write-Success "Version: $tagName (pinned — skipping releases/latest API)" + Write-Info "Download base: $githubUrl/$apmRepo/releases/download/$tagName/" +} else { + Write-Info "Fetching latest release information..." + $latestUri = "$apiRoot/repos/$apmRepo/releases/latest" + try { + $release = Invoke-RestMethod -Uri $latestUri + } catch { + } -# Try unauthenticated first -try { - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -} catch { - # Swallow — will try authenticated below -} + if (-not $release -or -not $release.tag_name) { + Write-Info "Unauthenticated request failed or returned no data. Retrying with authentication..." + $headers = Get-AuthHeader + if ($headers.Count -eq 0) { + Write-ErrorText "Repository may be private but no authentication token found." + Write-Host "Set GITHUB_APM_PAT or GITHUB_TOKEN and retry." + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo + exit 1 + } + try { + $release = Invoke-GitHubJson -Uri $latestUri -Headers $headers + } catch { + Write-ErrorText "Failed to fetch release information: $_" + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo + exit 1 + } + } -if (-not $release -or -not $release.tag_name) { - Write-Info "Unauthenticated request failed or returned no data. Retrying with authentication..." - $headers = Get-AuthHeader - if ($headers.Count -eq 0) { - Write-ErrorText "Repository may be private but no authentication token found." - Write-Host "Set GITHUB_APM_PAT or GITHUB_TOKEN and retry." - Write-ManualInstallHelp + if (-not $release.tag_name) { + Write-ErrorText "Could not determine the latest release tag." + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } - try { - $release = Invoke-GitHubJson -Url "https://api.github.com/repos/$Repo/releases/latest" -Headers $headers - } catch { - Write-ErrorText "Failed to fetch release information: $_" - Write-ManualInstallHelp + + $tagName = $release.tag_name + $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { + Write-ErrorText "Release $tagName does not contain $assetName." + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } + Write-Success "Latest version: $tagName" } -if (-not $release.tag_name) { - Write-ErrorText "Could not determine the latest release tag." - Write-ManualInstallHelp - exit 1 -} - -$asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 -if (-not $asset) { - Write-ErrorText "Release $($release.tag_name) does not contain $assetName." - Write-ManualInstallHelp - exit 1 -} - -$tagName = $release.tag_name -Write-Success "Latest version: $tagName" - $releaseDir = Join-Path $releasesDir $tagName $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("apm-install-" + [System.Guid]::NewGuid().ToString("N")) $zipPath = Join-Path $tempDir $assetName @@ -235,54 +302,73 @@ New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null try { # ------------------------------------------------------------------ - # Stage 2 — Download binary (3-stage fallback chain) + # Stage 2 — Download binary # ------------------------------------------------------------------ - Write-Info "Downloading $assetName from $tagName..." + Write-Info "Downloading $assetName ($tagName)..." $downloadOk = $false + $directUrl = "$githubUrl/$apmRepo/releases/download/$tagName/$assetName" - # 2a. Direct browser_download_url without auth - try { - Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing - $downloadOk = $true - Write-Success "Download successful" - } catch { - Write-WarningText "Unauthenticated download failed, retrying with authentication..." - } + if ($pinnedVersion) { + try { + Invoke-WebRequest -Uri $directUrl -OutFile $zipPath -UseBasicParsing + $downloadOk = $true + Write-Success "Download successful" + } catch { + Write-WarningText "Unauthenticated download failed, retrying with authentication..." + } + if (-not $downloadOk) { + if ($headers.Count -eq 0) { $headers = Get-AuthHeader } + if ($headers.Count -gt 0) { + try { + Invoke-WebRequest -Uri $directUrl -Headers $headers -OutFile $zipPath -UseBasicParsing + $downloadOk = $true + Write-Success "Download successful with authentication" + } catch { + } + } + } + } else { + try { + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing + $downloadOk = $true + Write-Success "Download successful" + } catch { + Write-WarningText "Unauthenticated download failed, retrying with authentication..." + } - # 2b. API asset URL with Accept: application/octet-stream (authenticated) - if (-not $downloadOk) { - if ($headers.Count -eq 0) { $headers = Get-AuthHeader } - if ($headers.Count -gt 0 -and $asset.url) { - try { - $apiHeaders = $headers.Clone() - $apiHeaders["Accept"] = "application/octet-stream" - Invoke-WebRequest -Uri $asset.url -Headers $apiHeaders -OutFile $zipPath -UseBasicParsing - $downloadOk = $true - Write-Success "Download successful via GitHub API" - } catch { - Write-WarningText "API download failed, trying direct URL with auth..." + if (-not $downloadOk) { + if ($headers.Count -eq 0) { $headers = Get-AuthHeader } + if ($headers.Count -gt 0 -and $asset.url) { + try { + $apiHeaders = @{} + $headers + $apiHeaders["Accept"] = "application/octet-stream" + Invoke-WebRequest -Uri $asset.url -Headers $apiHeaders -OutFile $zipPath -UseBasicParsing + $downloadOk = $true + Write-Success "Download successful via GitHub API" + } catch { + Write-WarningText "API download failed, trying direct URL with auth..." + } } } - } - # 2c. Direct browser_download_url with auth header - if (-not $downloadOk) { - if ($headers.Count -eq 0) { $headers = Get-AuthHeader } - if ($headers.Count -gt 0) { - try { - Invoke-WebRequest -Uri $asset.browser_download_url -Headers $headers -OutFile $zipPath -UseBasicParsing - $downloadOk = $true - Write-Success "Download successful with authentication" - } catch { - # Will fall through to pip fallback + if (-not $downloadOk) { + if ($headers.Count -eq 0) { $headers = Get-AuthHeader } + if ($headers.Count -gt 0) { + try { + Invoke-WebRequest -Uri $asset.browser_download_url -Headers $headers -OutFile $zipPath -UseBasicParsing + $downloadOk = $true + Write-Success "Download successful with authentication" + } catch { + } } } } if (-not $downloadOk) { Write-ErrorText "All download attempts failed." + Write-Host "Direct URL was: $directUrl" Write-Host "This might mean:" Write-Host " - Network connectivity issues" Write-Host " - Invalid GitHub token or insufficient permissions" @@ -291,21 +377,48 @@ try { Write-Info "Attempting automatic fallback to pip..." if (Install-ViaPip) { exit 0 } - Write-ManualInstallHelp + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } # ------------------------------------------------------------------ - # Verify checksum (if .sha256 asset is available) + # Verify checksum (if .sha256 file is available) # ------------------------------------------------------------------ $sha256AssetName = "$assetName.sha256" - $sha256Asset = $release.assets | Where-Object { $_.name -eq $sha256AssetName } | Select-Object -First 1 - if ($sha256Asset) { + $sha256Url = "$githubUrl/$apmRepo/releases/download/$tagName/$sha256AssetName" + + $sha256Source = $null + if (-not $pinnedVersion) { + $shaObj = $release.assets | Where-Object { $_.name -eq $sha256AssetName } | Select-Object -First 1 + if ($shaObj) { $sha256Source = $shaObj } + } + + if ($sha256Source -or $pinnedVersion) { Write-Info "Verifying download checksum..." $sha256Path = Join-Path $tempDir $sha256AssetName + $fetched = $false try { - Invoke-WebRequest -Uri $sha256Asset.browser_download_url -OutFile $sha256Path -UseBasicParsing + if ($sha256Source) { + Invoke-WebRequest -Uri $sha256Source.browser_download_url -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } else { + try { + Invoke-WebRequest -Uri $sha256Url -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } catch { + if ($headers.Count -eq 0) { $headers = Get-AuthHeader } + if ($headers.Count -gt 0) { + Invoke-WebRequest -Uri $sha256Url -Headers $headers -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } + } + } + } catch { + Write-WarningText "Could not download checksum file (non-fatal): $_" + } + + if ($fetched -and (Test-Path $sha256Path)) { $expectedHash = (Get-Content $sha256Path -Raw).Trim().Split(" ")[0] $actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower() if ($actualHash -ne $expectedHash) { @@ -314,12 +427,10 @@ try { Write-Host " Actual: $actualHash" Write-Info "Attempting automatic fallback to pip..." if (Install-ViaPip) { exit 0 } - Write-ManualInstallHelp + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } Write-Success "Checksum verified" - } catch { - Write-WarningText "Could not verify checksum (non-fatal): $_" } } @@ -336,12 +447,12 @@ try { Write-ErrorText "Extracted package is missing apm.exe." Write-Info "Attempting automatic fallback to pip..." if (Install-ViaPip) { exit 0 } - Write-ManualInstallHelp + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } # ------------------------------------------------------------------ - # Stage 3 — Binary test before installation + # Binary test # ------------------------------------------------------------------ Write-Info "Testing binary..." @@ -354,7 +465,7 @@ try { Write-Host "" Write-Info "Attempting automatic fallback to pip..." if (Install-ViaPip) { exit 0 } - Write-ManualInstallHelp + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo exit 1 } @@ -383,10 +494,10 @@ try { Write-Host " cd my-app && apm install # Install dependencies" Write-Host " apm run # Run your first prompt" Write-Host "" - Write-Host "Documentation: https://github.com/$Repo" + Write-Host "Documentation: $githubUrl/$apmRepo" Write-Info "Run 'apm --version' in a new terminal to verify the installation." } finally { if (Test-Path $tempDir) { Remove-Item -Recurse -Force $tempDir } -} \ No newline at end of file +} diff --git a/packages/apm-guide/.apm/skills/apm-usage/installation.md b/packages/apm-guide/.apm/skills/apm-usage/installation.md index 831b5c697..a5740070b 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/installation.md +++ b/packages/apm-guide/.apm/skills/apm-usage/installation.md @@ -50,6 +50,19 @@ curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh ``` +## Installer options (Windows PowerShell) + +Uses the same variables as `install.sh` where applicable (`GITHUB_URL`, `APM_REPO`, `VERSION`, `APM_INSTALL_DIR`). Pin a version to skip the releases/latest API (recommended offline). + +```powershell +$env:VERSION = "v1.2.3"; irm https://aka.ms/apm-windows | iex + +$env:GITHUB_URL = "https://github.corp.com" +$env:APM_REPO = "my-org/apm" +$env:VERSION = "v1.2.3" +irm https://aka.ms/apm-windows | iex +``` + ## Troubleshooting - **macOS/Linux "command not found":** ensure your install directory (default `/usr/local/bin`) is in `$PATH`. From 5e5793a722a0d5122bda00e9c0e44016884fddaf Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Tue, 12 May 2026 10:16:45 +0530 Subject: [PATCH 2/2] fix(install): ASCII-only installer text; auth checksum fetch; harden verify --- .../docs/getting-started/installation.md | 6 +- install.ps1 | 58 +++++++++++++------ .../.apm/skills/apm-usage/installation.md | 2 +- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/docs/src/content/docs/getting-started/installation.md b/docs/src/content/docs/getting-started/installation.md index b44c9af19..bb9fb4f40 100644 --- a/docs/src/content/docs/getting-started/installation.md +++ b/docs/src/content/docs/getting-started/installation.md @@ -45,7 +45,7 @@ GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh **Windows (`install.ps1` in PowerShell):** ```powershell -# Pin a version (skips GitHub API — required for many air-gapped / GHES setups) +# Pin a version (skips GitHub API - required for many air-gapped / GHES setups) $env:VERSION = "v1.2.3"; irm https://aka.ms/apm-windows | iex # Or pass a positional parameter when running a saved script: @@ -69,9 +69,9 @@ irm https://aka.ms/apm-windows | iex | `APM_REPO` | `microsoft/apm` | GitHub repository (`owner/name`) | | `VERSION` | *(latest)* | Pin a specific release tag (skips the **releases/latest** HTTP API) | -> **Note — Unix (`install.sh`):** Latest-release discovery still calls `https://api.github.com/repos/.../releases/latest` unless `VERSION` is set. For GHES or mirrors with no access to `api.github.com`, pin `VERSION` so the script never hits that endpoint. +> **Note - Unix (`install.sh`):** Latest-release discovery still calls `https://api.github.com/repos/.../releases/latest` unless `VERSION` is set. For GHES or mirrors with no access to `api.github.com`, pin `VERSION` so the script never hits that endpoint. > -> **Note — Windows (`install.ps1`):** The **releases/latest** URL is derived from `GITHUB_URL`: `https://api.github.com` for GitHub.com, or `{GITHUB_URL}/api/v3` for GitHub Enterprise Server. Air-gapped runners should still set `VERSION` so the installer does not need the API at all. +> **Note - Windows (`install.ps1`):** The **releases/latest** URL is derived from `GITHUB_URL`: `https://api.github.com` for GitHub.com, or `{GITHUB_URL}/api/v3` for GitHub Enterprise Server. Air-gapped runners should still set `VERSION` so the installer does not need the API at all. ## Package managers diff --git a/install.ps1 b/install.ps1 index 9fb67451e..2c4208d52 100644 --- a/install.ps1 +++ b/install.ps1 @@ -3,7 +3,7 @@ # Usage: # irm https://aka.ms/apm-windows | iex # -# Pin a version (skips GitHub HTTP API — use for air-gapped / GHE): +# Pin a version (skips GitHub HTTP API - use for air-gapped / GHE): # $env:VERSION = 'v1.2.3'; irm https://aka.ms/apm-windows | iex # .\install.ps1 v1.2.3 # @@ -29,7 +29,7 @@ param( $ErrorActionPreference = "Stop" # --------------------------------------------------------------------------- -# Configuration (overridable via environment variables — parity with install.sh) +# Configuration (overridable via environment variables - parity with install.sh) # --------------------------------------------------------------------------- $githubUrl = if ($env:GITHUB_URL) { @@ -165,7 +165,7 @@ function Test-PythonRequirement { function Install-ViaPip { $pythonCmd = Test-PythonRequirement if (-not $pythonCmd) { - Write-ErrorText "Python 3.9+ is not available — cannot fall back to pip." + Write-ErrorText "Python 3.9+ is not available - cannot fall back to pip." return $false } Write-Info "Attempting installation via pip ($pythonCmd)..." @@ -239,7 +239,7 @@ $apiRoot = Get-GitHubApiRoot -Url $githubUrl $headers = @{} # --------------------------------------------------------------------------- -# Stage 1 — Release metadata (skip GitHub API when VERSION is pinned) +# Stage 1 - Release metadata (skip GitHub API when VERSION is pinned) # --------------------------------------------------------------------------- $release = $null @@ -248,7 +248,7 @@ $tagName = $null if ($pinnedVersion) { $tagName = $pinnedVersion - Write-Success "Version: $tagName (pinned — skipping releases/latest API)" + Write-Success "Version: $tagName (pinned - skipping releases/latest API)" Write-Info "Download base: $githubUrl/$apmRepo/releases/download/$tagName/" } else { Write-Info "Fetching latest release information..." @@ -302,7 +302,7 @@ New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null try { # ------------------------------------------------------------------ - # Stage 2 — Download binary + # Stage 2 - Download binary # ------------------------------------------------------------------ Write-Info "Downloading $assetName ($tagName)..." @@ -400,8 +400,24 @@ try { $fetched = $false try { if ($sha256Source) { - Invoke-WebRequest -Uri $sha256Source.browser_download_url -OutFile $sha256Path -UseBasicParsing - $fetched = $true + try { + Invoke-WebRequest -Uri $sha256Source.browser_download_url -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } catch { + Write-WarningText "Unauthenticated checksum download failed, retrying with authentication..." + if ($headers.Count -eq 0) { $headers = Get-AuthHeader } + if ($headers.Count -eq 0) { throw } + try { + Invoke-WebRequest -Uri $sha256Source.browser_download_url -Headers $headers -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } catch { + if (-not $sha256Source.url) { throw } + $apiHeaders = @{} + $headers + $apiHeaders["Accept"] = "application/octet-stream" + Invoke-WebRequest -Uri $sha256Source.url -Headers $apiHeaders -OutFile $sha256Path -UseBasicParsing + $fetched = $true + } + } } else { try { Invoke-WebRequest -Uri $sha256Url -OutFile $sha256Path -UseBasicParsing @@ -419,18 +435,22 @@ try { } if ($fetched -and (Test-Path $sha256Path)) { - $expectedHash = (Get-Content $sha256Path -Raw).Trim().Split(" ")[0] - $actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower() - if ($actualHash -ne $expectedHash) { - Write-ErrorText "Checksum verification FAILED." - Write-Host " Expected: $expectedHash" - Write-Host " Actual: $actualHash" - Write-Info "Attempting automatic fallback to pip..." - if (Install-ViaPip) { exit 0 } - Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo - exit 1 + try { + $expectedHash = (Get-Content $sha256Path -Raw).Trim().Split(" ")[0] + $actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower() + if ($actualHash -ne $expectedHash) { + Write-ErrorText "Checksum verification FAILED." + Write-Host " Expected: $expectedHash" + Write-Host " Actual: $actualHash" + Write-Info "Attempting automatic fallback to pip..." + if (Install-ViaPip) { exit 0 } + Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo + exit 1 + } + Write-Success "Checksum verified" + } catch { + Write-WarningText "Could not verify checksum (non-fatal): $_" } - Write-Success "Checksum verified" } } diff --git a/packages/apm-guide/.apm/skills/apm-usage/installation.md b/packages/apm-guide/.apm/skills/apm-usage/installation.md index a5740070b..45ded3aeb 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/installation.md +++ b/packages/apm-guide/.apm/skills/apm-usage/installation.md @@ -46,7 +46,7 @@ curl -sSL https://aka.ms/apm-unix | sh -s -- @v1.2.3 # Custom install dir curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh -# Air-gapped / GHE mirror — VERSION is required (skips GitHub API) +# Air-gapped / GHE mirror - VERSION is required (skips GitHub API) GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh ```