From 4e40ccf92ac0bfbc73602942336036c8b37c0ecc Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 19 Mar 2026 00:40:26 +0100 Subject: [PATCH 1/2] fix: address AI code quality findings across install scripts and utilities - install.ps1: add error handling for GitHub API calls and downloads, verify extracted exe exists before move, fix PATH instruction quoting - install.sh: add jq support for JSON parsing with grep/sed fallback, validate version tag format, add explicit curl/wget check in download, improve /dev/tty readability check with fallback message - tests/test-utils.ts: consume stdout/stderr concurrently via Promise.all to prevent potential deadlock on large output - HeadSEO.astro: extract toBreadcrumbName() helper with special-case handling for acronyms (API, CLI, PT-BR), simplify markdownUrl ternary --- docs/src/components/HeadSEO.astro | 53 ++++++++++++++++++++++++++++--- install.ps1 | 24 +++++++++++--- install.sh | 48 ++++++++++++++++++++++++---- tests/test-utils.ts | 10 ++++-- 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/docs/src/components/HeadSEO.astro b/docs/src/components/HeadSEO.astro index c08419fd..ceca8520 100644 --- a/docs/src/components/HeadSEO.astro +++ b/docs/src/components/HeadSEO.astro @@ -30,6 +30,45 @@ const ptBrPath = isPtBr ? pathname : `/pt-br${pathname}`; const enUrl = `${siteUrl}${enPath}`; const ptBrUrl = `${siteUrl}${ptBrPath}`; +/** + * Convert a URL path segment into a human-readable breadcrumb label. + * Handles common special cases (e.g., language codes, acronyms), + * then falls back to a reasonable title-casing strategy. + */ +function toBreadcrumbName(segment: string): string { + const lower = segment.toLowerCase(); + + // Special-case known slugs that should not be naively title-cased. + const specialCases: Record = { + "pt-br": "PT-BR", + "api": "API", + "cli": "CLI", + "http": "HTTP", + "https": "HTTPS", + }; + if (specialCases[lower]) { + return specialCases[lower]; + } + + // Replace dashes with spaces for readability. + const withSpaces = segment.replaceAll("-", " "); + + // Title-case words, but keep short/all-caps tokens as-is (e.g., "API"). + return withSpaces + .split(" ") + .filter((part) => part.length > 0) + .map((part) => { + const isAllCaps = /^[A-Z0-9]+$/.test(part); + if (isAllCaps || part.length <= 3) { + return part.toUpperCase(); + } + const first = part.charAt(0).toUpperCase(); + const rest = part.slice(1).toLowerCase(); + return first + rest; + }) + .join(" "); +} + // ── BreadcrumbList ──────────────────────────────────────────────── // Build breadcrumbs from the URL path segments. const segments = pathname @@ -51,9 +90,7 @@ if (hasRoute && segments.length > 0) { const name = i === segments.length - 1 ? title - : segments[i] - .replaceAll("-", " ") - .replaceAll(/\b\w/g, (c: string) => c.toUpperCase()); + : toBreadcrumbName(segments[i]); breadcrumbItems.push({ "@type": "ListItem", position: i + 2, @@ -75,8 +112,14 @@ const breadcrumbLd = // ── Markdown alternate link for LLM agents ────────────────────── // Points to the raw .mdx source on GitHub so AI crawlers can fetch markdown. const entryId = hasRoute ? route.entry.id : null; -const markdownUrl = entryId - ? `https://raw.githubusercontent.com/archgate/cli/main/docs/src/content/docs/${entryId}${entryId.endsWith(".mdx") || entryId.endsWith(".md") ? "" : ".mdx"}` +const markdownPath = + entryId && (entryId.endsWith(".mdx") || entryId.endsWith(".md")) + ? entryId + : entryId + ? `${entryId}.mdx` + : null; +const markdownUrl = markdownPath + ? `https://raw.githubusercontent.com/archgate/cli/main/docs/src/content/docs/${markdownPath}` : null; // ── TechArticle (all doc pages except splash/home) ──────────────── diff --git a/install.ps1 b/install.ps1 index 699485a1..af7d5617 100644 --- a/install.ps1 +++ b/install.ps1 @@ -20,8 +20,13 @@ function Get-LatestVersion { if ($env:ARCHGATE_VERSION) { return $env:ARCHGATE_VERSION } - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" - return $release.tag_name + try { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -ErrorAction Stop + return $release.tag_name + } catch { + Write-Error "Error: failed to query GitHub for latest archgate version. Details: $($_.Exception.Message)" + return $null + } } $Version = Get-LatestVersion @@ -39,7 +44,7 @@ Write-Host "Installing archgate $Version ($Artifact)..." $TmpDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "archgate-install-$(Get-Random)") try { $ZipPath = Join-Path $TmpDir "archgate.zip" - Invoke-WebRequest -Uri $Url -OutFile $ZipPath -UseBasicParsing + Invoke-WebRequest -Uri $Url -OutFile $ZipPath -UseBasicParsing -ErrorAction Stop Expand-Archive -Path $ZipPath -DestinationPath $TmpDir -Force @@ -47,7 +52,16 @@ try { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } - Move-Item -Path (Join-Path $TmpDir "archgate.exe") -Destination (Join-Path $InstallDir "archgate.exe") -Force + $ExtractedExe = Join-Path $TmpDir "archgate.exe" + if (-not (Test-Path $ExtractedExe)) { + Write-Error "Error: 'archgate.exe' was not found in the extracted archive at '$ExtractedExe'. The downloaded package may be corrupt or incompatible." + exit 1 + } + + Move-Item -Path $ExtractedExe -Destination (Join-Path $InstallDir "archgate.exe") -Force +} catch { + Write-Error "Error: failed to download archgate from $Url. Please verify the version '$Version' exists and check your network connection. $($_.Exception.Message)" + exit 1 } finally { Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue } @@ -69,7 +83,7 @@ if ($CurrentPath -notlike "*$InstallDir*") { Write-Host "" Write-Host "Skipped. To add manually, run:" Write-Host "" - Write-Host " [Environment]::SetEnvironmentVariable('Path', '$InstallDir;' + [Environment]::GetEnvironmentVariable('Path', 'User'), 'User')" + Write-Host " [Environment]::SetEnvironmentVariable('Path', `"$InstallDir;`" + [Environment]::GetEnvironmentVariable('Path', 'User'), 'User')" } else { [Environment]::SetEnvironmentVariable("Path", "$InstallDir;$CurrentPath", "User") $env:Path = "$InstallDir;$env:Path" diff --git a/install.sh b/install.sh index 08b5305b..d9993da1 100644 --- a/install.sh +++ b/install.sh @@ -53,19 +53,46 @@ resolve_version() { return fi + api_url="https://api.github.com/repos/${REPO}/releases/latest" + if command -v curl >/dev/null 2>&1; then - VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"//;s/".*//')" + response="$(curl -fsSL "$api_url" || true)" elif command -v wget >/dev/null 2>&1; then - VERSION="$(wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"//;s/".*//')" + response="$(wget -qO- "$api_url" 2>/dev/null || true)" else echo "Error: curl or wget is required." >&2 exit 1 fi + # Basic sanity check that we got a JSON-like response + case "$response" in + \{*) + ;; + *) + echo "Error: unexpected response from GitHub releases API." >&2 + echo "Response (truncated): $(printf '%s' "$response" | head -c 200)" >&2 + exit 1 + ;; + esac + + if command -v jq >/dev/null 2>&1; then + VERSION="$(printf '%s' "$response" | jq -r '.tag_name // empty')" + else + VERSION="$(printf '%s' "$response" | grep "tag_name" | sed 's/.*"tag_name": *"//;s/".*//')" + fi + if [ -z "$VERSION" ]; then - echo "Error: could not determine latest version." >&2 + echo "Error: could not determine latest version (empty tag_name)." >&2 exit 1 fi + + # Validate that VERSION looks reasonable (non-empty and not an obvious error) + case "$VERSION" in + *[!A-Za-z0-9._-]*) + echo "Error: invalid version tag received: '$VERSION'" >&2 + exit 1 + ;; + esac } # --- Download and install --- @@ -80,8 +107,11 @@ download_and_install() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o "$tmpdir/archgate.tar.gz" - else + elif command -v wget >/dev/null 2>&1; then wget -qO "$tmpdir/archgate.tar.gz" "$url" + else + echo "Error: neither 'curl' nor 'wget' is installed. Please install one of them to download archgate." >&2 + exit 1 fi tar -xzf "$tmpdir/archgate.tar.gz" -C "$tmpdir" @@ -207,13 +237,17 @@ setup_path() { echo "" # Prompt requires /dev/tty — available even when stdin is piped (curl | sh) - if [ ! -e /dev/tty ]; then - echo "No TTY available. To add archgate to your PATH manually, add the lines above to your shell profile." + if [ ! -r /dev/tty ]; then + echo "No readable TTY available. To add archgate to your PATH manually, add the lines above to your shell profile." return fi printf "Update these files now? [Y/n] " - read -r answer { stdout: "pipe", stderr: "pipe", }); - const stdout = await new Response(proc.stdout).text(); - const exitCode = await proc.exited; + const stdoutPromise = new Response(proc.stdout).text(); + const stderrPromise = new Response(proc.stderr).text(); + const [stdout, stderr, exitCode] = await Promise.all([ + stdoutPromise, + stderrPromise, + proc.exited, + ]); if (exitCode !== 0) { - const stderr = await new Response(proc.stderr).text(); throw new Error( `git ${args.join(" ")} failed (exit ${exitCode}): ${stderr.trim()}` ); From 2c926432881a9f8d7f93732d84aae0f9cdcad2c3 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 19 Mar 2026 00:47:34 +0100 Subject: [PATCH 2/2] test: add install script smoke tests to CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename windows-smoke-test.yml → smoke-test.yml and add: - install.ps1 smoke test on Windows (downloads real release, verifies binary) - install.sh smoke test on Linux (downloads real release, verifies binary) Both use ARCHGATE_VERSION env var to pin the version, avoiding GitHub API rate limits. Gracefully skip if no releases exist yet. --- .github/workflows/smoke-test.yml | 147 ++++++++++++++++++++ .github/workflows/windows-smoke-test.yml | 164 ----------------------- 2 files changed, 147 insertions(+), 164 deletions(-) create mode 100644 .github/workflows/smoke-test.yml delete mode 100644 .github/workflows/windows-smoke-test.yml diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 00000000..9000bc0a --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,147 @@ +name: Smoke Test + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + windows: + name: Windows Smoke Test + runs-on: windows-latest + timeout-minutes: 15 + if: github.event.pull_request.draft == false + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: moonrepo/setup-toolchain@v0 + with: + auto-install: true + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Typecheck + run: bun run typecheck + + - name: Format check + run: bun run format:check + + - name: Tests + run: bun test --timeout 60000 + + - name: Build Windows binary + run: bun build src/cli.ts --compile --bytecode --outfile dist/archgate-smoke-test + + - name: Smoke test binary + shell: pwsh + run: | + $binary = "dist/archgate-smoke-test.exe" + if (-not (Test-Path $binary)) { + Write-Error "Binary not found at $binary" + exit 1 + } + $output = & $binary --version 2>&1 + Write-Host "archgate version: $output" + if ($LASTEXITCODE -ne 0) { + Write-Error "Binary exited with code $LASTEXITCODE" + exit 1 + } + + - name: Smoke test install.ps1 + shell: pwsh + env: + ARCHGATE_VERSION: ${{ vars.LATEST_RELEASE_TAG || '' }} + run: | + # Resolve version: use ARCHGATE_VERSION if set, otherwise query GitHub + if (-not $env:ARCHGATE_VERSION) { + $release = gh release view --json tagName --jq .tagName 2>$null + if ($LASTEXITCODE -ne 0 -or -not $release) { + Write-Host "::warning::No releases found, skipping install.ps1 smoke test" + exit 0 + } + $env:ARCHGATE_VERSION = $release + } + Write-Host "Testing install.ps1 with version $env:ARCHGATE_VERSION" + + $installDir = Join-Path $env:TEMP "archgate-smoke-install-$(Get-Random)" + $env:ARCHGATE_INSTALL_DIR = $installDir + + # Run installer non-interactively (Read-Host returns empty → defaults to adding PATH) + & "$env:GITHUB_WORKSPACE\install.ps1" + if ($LASTEXITCODE -ne 0) { + Write-Error "install.ps1 exited with code $LASTEXITCODE" + exit 1 + } + + # Verify binary was installed + $exe = Join-Path $installDir "archgate.exe" + if (-not (Test-Path $exe)) { + Write-Error "archgate.exe not found at $exe after install" + exit 1 + } + + $output = & $exe --version 2>&1 + Write-Host "Installed archgate version: $output" + if ($LASTEXITCODE -ne 0) { + Write-Error "Installed binary exited with code $LASTEXITCODE" + exit 1 + } + + # Cleanup + Remove-Item -Path $installDir -Recurse -Force -ErrorAction SilentlyContinue + + linux: + name: Linux Install Script Smoke Test + runs-on: ubuntu-latest + timeout-minutes: 10 + if: github.event.pull_request.draft == false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Smoke test install.sh + env: + ARCHGATE_VERSION: ${{ vars.LATEST_RELEASE_TAG || '' }} + run: | + # Resolve version: use ARCHGATE_VERSION if set, otherwise query GitHub + if [ -z "$ARCHGATE_VERSION" ]; then + ARCHGATE_VERSION="$(gh release view --json tagName --jq .tagName 2>/dev/null || true)" + if [ -z "$ARCHGATE_VERSION" ]; then + echo "::warning::No releases found, skipping install.sh smoke test" + exit 0 + fi + export ARCHGATE_VERSION + fi + echo "Testing install.sh with version $ARCHGATE_VERSION" + + export ARCHGATE_INSTALL_DIR="$(mktemp -d)" + + # Run installer (no TTY in CI → skips PATH prompt automatically) + sh install.sh + + # Verify binary was installed + if [ ! -f "$ARCHGATE_INSTALL_DIR/archgate" ]; then + echo "::error::archgate binary not found at $ARCHGATE_INSTALL_DIR/archgate after install" + exit 1 + fi + + output="$("$ARCHGATE_INSTALL_DIR/archgate" --version 2>&1)" + echo "Installed archgate version: $output" + + # Cleanup + rm -rf "$ARCHGATE_INSTALL_DIR" diff --git a/.github/workflows/windows-smoke-test.yml b/.github/workflows/windows-smoke-test.yml deleted file mode 100644 index 9ee41499..00000000 --- a/.github/workflows/windows-smoke-test.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: Smoke Tests - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - smoke-test: - name: Smoke Test (${{ matrix.os }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 15 - if: github.event.pull_request.draft == false - strategy: - fail-fast: false - matrix: - include: - - os: Windows - runner: windows-latest - binary_name: archgate.exe - - os: Linux - runner: ubuntu-latest - binary_name: archgate - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: moonrepo/setup-toolchain@v0 - with: - auto-install: true - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Lint - if: matrix.os == 'Windows' - run: bun run lint - - - name: Typecheck - if: matrix.os == 'Windows' - run: bun run typecheck - - - name: Format check - if: matrix.os == 'Windows' - run: bun run format:check - - - name: Tests - if: matrix.os == 'Windows' - run: bun test --timeout 60000 - - - name: Build binary - run: bun build src/cli.ts --compile --bytecode --outfile dist/archgate-smoke-test - - - name: Smoke test binary (Windows) - if: matrix.os == 'Windows' - shell: pwsh - run: | - $binary = "dist/archgate-smoke-test.exe" - if (-not (Test-Path $binary)) { - Write-Error "Binary not found at $binary" - exit 1 - } - $output = & $binary --version 2>&1 - Write-Host "archgate version: $output" - if ($LASTEXITCODE -ne 0) { - Write-Error "Binary exited with code $LASTEXITCODE" - exit 1 - } - - - name: Smoke test binary (Linux) - if: matrix.os == 'Linux' - run: | - binary="dist/archgate-smoke-test" - if [ ! -f "$binary" ]; then - echo "::error::Binary not found at $binary" - exit 1 - fi - chmod +x "$binary" - output=$("$binary" --version 2>&1) - echo "archgate version: $output" - - - name: Build old-version binary for upgrade test (Windows) - if: matrix.os == 'Windows' - shell: pwsh - run: | - $pkg = Get-Content package.json -Raw | ConvertFrom-Json - $originalVersion = $pkg.version - $pkg.version = "0.0.1" - $pkg | ConvertTo-Json -Depth 10 | Set-Content package.json - bun build src/cli.ts --compile --bytecode --outfile dist/archgate-upgrade-test - $pkg.version = $originalVersion - $pkg | ConvertTo-Json -Depth 10 | Set-Content package.json - - - name: Build old-version binary for upgrade test (Linux) - if: matrix.os == 'Linux' - run: | - original_version=$(jq -r .version package.json) - jq '.version = "0.0.1"' package.json > package.json.tmp && mv package.json.tmp package.json - bun build src/cli.ts --compile --bytecode --outfile dist/archgate-upgrade-test - jq --arg v "$original_version" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json - - - name: Smoke test upgrade (Windows) - if: matrix.os == 'Windows' - shell: pwsh - run: | - $binDir = "$HOME\.archgate\bin" - New-Item -ItemType Directory -Path $binDir -Force | Out-Null - Copy-Item "dist/archgate-upgrade-test.exe" "$binDir\archgate.exe" -Force - - $before = & "$binDir\archgate.exe" --version 2>&1 - Write-Host "Before upgrade: $before" - if ($before -ne "0.0.1") { - Write-Error "Expected version 0.0.1, got $before" - exit 1 - } - - & "$binDir\archgate.exe" upgrade 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error "Upgrade failed with exit code $LASTEXITCODE" - exit 1 - } - - $after = & "$binDir\archgate.exe" --version 2>&1 - Write-Host "After upgrade: $after" - if ($after -eq "0.0.1") { - Write-Error "Binary was not replaced — still reports 0.0.1" - exit 1 - } - Write-Host "Upgrade smoke test passed: $before -> $after" - - - name: Smoke test upgrade (Linux) - if: matrix.os == 'Linux' - run: | - bin_dir="$HOME/.archgate/bin" - mkdir -p "$bin_dir" - cp dist/archgate-upgrade-test "$bin_dir/archgate" - chmod +x "$bin_dir/archgate" - - before=$("$bin_dir/archgate" --version 2>&1) - echo "Before upgrade: $before" - if [ "$before" != "0.0.1" ]; then - echo "::error::Expected version 0.0.1, got $before" - exit 1 - fi - - "$bin_dir/archgate" upgrade 2>&1 - - after=$("$bin_dir/archgate" --version 2>&1) - echo "After upgrade: $after" - if [ "$after" = "0.0.1" ]; then - echo "::error::Binary was not replaced — still reports 0.0.1" - exit 1 - fi - echo "Upgrade smoke test passed: $before -> $after"