Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Pages

on:
push:
branches: [main]
paths:
- 'site/**'
- '.github/workflows/pages.yml'
pull_request:
branches: [main]
paths:
- 'site/**'
- '.github/workflows/pages.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Build and check local links
run: python3 site/build.py

deploy:
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: validate
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Build site
run: python3 site/build.py
- name: Configure Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload site
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: site/_build
- name: Deploy Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: Release

# Tag-driven release. To cut a release:
#
# git tag v0.1.0
# git push origin v0.1.0
# git tag v0.2.0
# git push origin v0.2.0
#
# Builds win-x64 and win-arm64 AOT executables, packages them with the README and
# LICENSE into zips, computes SHA-256 checksums, and publishes a GitHub Release with
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ bin/
obj/
publish/
dist/
/site/_build/
/packaging/release-assets/
/packaging/winget-manifests/
*.nupkg
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20arm64-0078D4?style=flat&logo=windows&logoColor=white)](https://www.microsoft.com/windows)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=flat)](LICENSE)

[Website](https://wingetscout.com/) · [Documentation](https://wingetscout.com/docs.html) · [Releases](https://github.com/harder/wingetscout/releases)

## Install

**Recommended: download the portable ZIP** from [GitHub Releases](https://github.com/harder/wingetscout/releases). It includes `wingetscout.exe` and the companion files needed for the full WinGet COM experience. No .NET runtime or installer is required.
Expand All @@ -14,8 +16,6 @@ A WinGet community package is being prepared as `Harder.WinGetScout`. Use the re

After that package is accepted, `winget install -e --id Harder.WinGetScout` will install the ZIP into WinGet's managed portable directory and add `wingetscout` to your user `PATH`. Open a **new terminal** to use the command from any folder. The WinGet manifest already requests this behavior; it does not require a separate installer or manual PATH editing. WinGet verifies the downloaded ZIP hash, but it does not sign the executable. See [signing and distribution](code-signing.md).

> **Release availability:** The Scout filenames below will appear with the first Scout release. If they are not on the Releases page yet, Scout has not been published; see [Build from source](#build-from-source) to try the current code.

1. Choose the ZIP for your Windows PC:

| PC | Download |
Expand Down
2 changes: 1 addition & 1 deletion WinGetScout.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<AssemblyName>wingetscout</AssemblyName>
<Product>Scout for WinGet</Product>
<Title>Scout for WinGet</Title>
<Version>0.1.3</Version>
<Version>0.2.0</Version>
<Description>Scout for WinGet is a terminal app for finding, installing, and updating Windows packages.</Description>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
Expand Down
2 changes: 1 addition & 1 deletion packaging/WINGET-SUBMISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The installer manifest has `InstallerType: zip`, `NestedInstallerType: portable`
2. Download that artifact from the successful release workflow run. The files are under `manifests/h/Harder/WinGetScout/<version>/`. To regenerate directly from the published ZIPs and validate in one command on Windows, run:

```powershell
pwsh ./packaging/prepare-winget-submission.ps1 -Version 0.1.4
pwsh ./packaging/prepare-winget-submission.ps1 -Version 0.2.0
```

The helper needs `gh`, `winget`, and a GitHub login with access to the public release. It downloads the exact published assets and uses [new-winget-manifest.ps1](new-winget-manifest.ps1) to check the archive layout and compute SHA-256 values. The release workflow also verifies its published ZIPs are byte-for-byte identical to its build artifacts. Before submitting, inspect the generated URLs and hashes against the GitHub Release.
Expand Down
6 changes: 3 additions & 3 deletions packaging/build-msix.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

.EXAMPLE
# CI: pack only (sign separately with Azure Artifact Signing, see code-signing.md):
pwsh ./packaging/build-msix.ps1 -Arch x64 -Version 0.1.3.0
pwsh ./packaging/build-msix.ps1 -Arch x64 -Version 0.2.0.0
#>
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')] [string]$Arch = 'arm64',
[string]$Version = '0.1.3.0',
[string]$Version = '0.2.0.0',
[string]$Publisher = 'CN=wingetscout (Dev)',
[string]$CertPath,
[string]$CertPassword = 'spike',
Expand All @@ -33,7 +33,7 @@ param(

$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
if ($Version -notmatch '^\d+\.\d+\.\d+\.\d+$') { throw "Version must be 4-part (e.g. 0.1.3.0); got '$Version'." }
if ($Version -notmatch '^\d+\.\d+\.\d+\.\d+$') { throw "Version must be 4-part (e.g. 0.2.0.0); got '$Version'." }
if (-not $OutDir) { $OutDir = Join-Path $repo 'dist' }
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null

Expand Down
1 change: 1 addition & 0 deletions site/CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wingetscout.com
50 changes: 50 additions & 0 deletions site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# WinGet Scout website

The website lives in `site/` and is deployed by [Pages](../.github/workflows/pages.yml)
when site files reach `main`. It uses plain HTML, CSS, JavaScript, and Python's standard
library. [SkillView](https://github.com/harder/gh-skillview) uses the same build/deploy
shape, but this site has its own Windows-inspired design.

Run `python site/build.py` from the repository root to copy published files to
`site/_build/` and check local links. The Pages workflow runs the same check on PRs.
The real application captures in `site/media/` were recorded with
[tuirec](https://github.com/tui-cs/tuirec) in `--mock` mode. They show demo data and do not
change installed packages.

## Set up wingetscout.com

The workflow can deploy after the site is merged. GitHub Pages needs **GitHub Actions**
as its publishing source and `wingetscout.com` saved as its custom domain in
**harder/wingetscout → Settings → Pages**. The included `CNAME` is a copy of the intended
domain for the built output; GitHub ignores that file for custom-workflow publishing,
so the Pages setting is required.

At the domain's DNS provider, create these records (or an equivalent apex ALIAS/ANAME):

| Type | Name | Value |
| --- | --- | --- |
| A | `@` | `185.199.108.153` |
| A | `@` | `185.199.109.153` |
| A | `@` | `185.199.110.153` |
| A | `@` | `185.199.111.153` |
| CNAME | `www` | `harder.github.io` |

Remove conflicting apex A/AAAA/ALIAS/ANAME records and conflicting `www` records.
An optional IPv6 setup uses GitHub's published AAAA addresses; see the
[GitHub DNS guidance](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site).
The `www` record lets GitHub redirect `www.wingetscout.com` to the apex domain.
Avoid wildcard DNS records for this domain.

Verify the domain under the GitHub account's **Settings → Pages → Verified domains**;
GitHub supplies a unique TXT record for this step. After DNS and the certificate are
ready, enable **Enforce HTTPS** in the repository's Pages settings. DNS propagation
and certificate issuance can take time. Confirm with:

```powershell
Resolve-DnsName wingetscout.com -Type A
Resolve-DnsName www.wingetscout.com -Type CNAME
Invoke-WebRequest https://wingetscout.com/ -UseBasicParsing
```

See [GitHub's custom-domain instructions](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site)
for current DNS records and setup details.
22 changes: 22 additions & 0 deletions site/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const sectionLinks = [...document.querySelectorAll('.docs-sidebar a[href^="#"]')];
if (sectionLinks.length && 'IntersectionObserver' in window) {
const visibility = new Map();
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => visibility.set(entry.target.id, entry.isIntersecting ? entry.intersectionRatio : 0));
const visible = [...visibility].filter(([, ratio]) => ratio > 0).sort((a, b) => b[1] - a[1])[0];
if (!visible) return;
sectionLinks.forEach(link => link.classList.toggle('active', link.hash === `#${visible[0]}`));
}, { rootMargin: '-10% 0px -55% 0px', threshold: [0, 0.25, 0.5] });
document.querySelectorAll('.docs-content section[id]').forEach(section => observer.observe(section));
}

const tourImage = document.querySelector('[data-tour-image]');
const tourToggle = document.querySelector('[data-tour-toggle]');
if (tourImage && tourToggle) {
tourToggle.addEventListener('click', () => {
const playing = tourToggle.getAttribute('aria-pressed') !== 'true';
tourToggle.setAttribute('aria-pressed', String(playing));
tourToggle.textContent = playing ? 'Pause tour' : 'Play tour';
tourImage.src = playing ? tourImage.dataset.animatedSrc : tourImage.dataset.staticSrc;
});
}
59 changes: 59 additions & 0 deletions site/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Build and validate the dependency-free Scout documentation site."""

from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urlsplit
import shutil

SOURCE = Path(__file__).resolve().parent
OUTPUT = SOURCE / "_build"
FILES = (
"index.html", "docs.html", "styles.css", "app.js", "favicon.svg",
"CNAME", "robots.txt", "sitemap.xml", "media/scout-discover.png",
"media/scout-installed.png", "media/scout-tour.gif",
)


class References(HTMLParser):
def __init__(self):
super().__init__()
self.values = []

def handle_starttag(self, tag, attrs):
for name, value in attrs:
if name in {"href", "src", "data-static-src", "data-animated-src"} and value:
self.values.append(value)


if not OUTPUT.resolve().is_relative_to(SOURCE.resolve()) or OUTPUT == SOURCE:
raise SystemExit("Build output must stay inside the site directory")
if OUTPUT.exists():
shutil.rmtree(OUTPUT)
OUTPUT.mkdir()
for name in FILES:
source = SOURCE / name
if not source.is_file():
raise SystemExit(f"Missing site file: {source}")
destination = OUTPUT / name
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
(OUTPUT / ".nojekyll").touch()

for page in ("index.html", "docs.html"):
parser = References()
parser.feed((OUTPUT / page).read_text(encoding="utf-8"))
for reference in parser.values:
parsed = urlsplit(reference)
if parsed.scheme or parsed.netloc or not parsed.path:
continue
local = unquote(parsed.path)
if local in {".", "./"}:
local = "index.html"
target = (OUTPUT / local).resolve()
if not target.is_relative_to(OUTPUT.resolve()) or not target.is_file():
raise SystemExit(f"Broken local link in {page}: {reference}")

if (OUTPUT / "CNAME").read_text(encoding="utf-8").strip() != "wingetscout.com":
raise SystemExit("CNAME must contain wingetscout.com")

print(f"Built and validated {len(FILES)} site files in {OUTPUT}")
Loading
Loading