diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/agent_detection_manifest_check.py | 75 | ||||
| -rw-r--r-- | scripts/changelog.py | 10 | ||||
| -rw-r--r-- | scripts/docs/preview.integration.test.ts | 90 | ||||
| -rw-r--r-- | scripts/docs/preview.mjs | 75 | ||||
| -rw-r--r-- | scripts/docs/snapshot.mjs | 116 | ||||
| -rw-r--r-- | scripts/docs/versions.integration.test.ts | 129 | ||||
| -rw-r--r-- | scripts/docs/versions.mjs | 283 | ||||
| -rw-r--r-- | scripts/docs/versions.test.ts | 33 | ||||
| -rw-r--r-- | scripts/preview.py | 7 | ||||
| -rwxr-xr-x | scripts/release_perf_smoke.sh | 2 | ||||
| -rw-r--r-- | scripts/test_agent_detection_manifest_check.py | 66 | ||||
| -rw-r--r-- | scripts/test_package_windows_conpty.py | 2 | ||||
| -rw-r--r-- | scripts/test_preview.py | 55 | ||||
| -rw-r--r-- | scripts/test_unix_installer.py | 2 | ||||
| -rw-r--r-- | scripts/windows_arm64_installer_test.ps1 | 2 | ||||
| -rw-r--r-- | scripts/windows_install_conpty_package_test.ps1 | 18 |
16 files changed, 857 insertions, 108 deletions
diff --git a/scripts/agent_detection_manifest_check.py b/scripts/agent_detection_manifest_check.py index f180318b..f0dc0005 100644 --- a/scripts/agent_detection_manifest_check.py +++ b/scripts/agent_detection_manifest_check.py @@ -13,7 +13,7 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] DEFAULT_BUNDLED_DIR = PROJECT_ROOT / "src" / "detect" / "manifests" -DEFAULT_WEBSITE_DIR = PROJECT_ROOT / "website" / "agent-detection" +DEFAULT_PUBLISHED_DIR = PROJECT_ROOT / "distribution" / "agent-detection" ENGINE_SOURCE = PROJECT_ROOT / "src" / "detect" / "manifest_update.rs" MANIFEST_KEYS = {"id", "version", "min_engine_version", "updated_at", "aliases", "rules"} @@ -54,9 +54,9 @@ MAX_TOTAL_MATCHERS = 1024 MAX_MATCHER_CHARS = 512 # Keep engine-2 clients on the OSC-capable manifest until an engine-3 release -# can consume top_non_empty_lines. Remove this entry when the website publishes -# the bundled Grok manifest. -STAGED_WEBSITE_MANIFESTS = { +# can consume top_non_empty_lines. Remove this entry when the distribution +# publishes the bundled Grok manifest. +STAGED_PUBLISHED_MANIFESTS = { "grok": ( "2026.07.16.2", "2026.07.16.1", @@ -64,16 +64,31 @@ STAGED_WEBSITE_MANIFESTS = { ), } +# Muse is bundled on master but has not shipped in a stable binary yet. Stable +# clients cannot identify it, so publish it with the release that first ships +# the corresponding Agent variant and remove this exact-content exception. +UNPUBLISHED_BUNDLED_MANIFESTS = { + "muse": ( + "2026.08.26.1", + "b69c4d87fa9c19e3e6453b706fbe39c98a8b33ffbaa48e8cd5ae6751e9615074", + ), +} + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--bundled-dir", type=Path, default=DEFAULT_BUNDLED_DIR) - parser.add_argument("--website-dir", type=Path, default=DEFAULT_WEBSITE_DIR) + parser.add_argument("--published-dir", type=Path, default=DEFAULT_PUBLISHED_DIR) parser.add_argument("--engine-version", type=int) parser.add_argument( - "--require-website", + "--require-published", + action="store_true", + help="fail if published agent-detection assets or catalog are missing", + ) + parser.add_argument( + "--require-all-published", action="store_true", - help="fail if website agent-detection assets or catalog are missing", + help="fail if any bundled manifest is intentionally held for a future stable release", ) return parser.parse_args() @@ -286,11 +301,13 @@ def load_manifest_dir(path: Path, engine_version: int) -> dict[str, tuple[Path, def validate_catalog( - website_dir: Path, + published_dir: Path, bundled: dict[str, tuple[Path, dict]], engine_version: int, + *, + allow_unpublished: bool = False, ) -> None: - catalog_path = website_dir / "index.toml" + catalog_path = published_dir / "index.toml" catalog = load_toml(catalog_path) if set(catalog) != {"schema_version", "agents"}: raise CheckError(f"{catalog_path}: expected only schema_version and agents") @@ -314,7 +331,7 @@ def validate_catalog( raise CheckError(f"{catalog_path}: unsafe path for {agent_id}: {rel_path}") if agent_id not in bundled: raise CheckError(f"{catalog_path}: unknown agent {agent_id}; binary cannot identify it") - manifest_path = website_dir / rel_path + manifest_path = published_dir / rel_path manifest = validate_manifest(manifest_path, engine_version) if manifest["id"] != agent_id: raise CheckError(f"{manifest_path}: id {manifest['id']} does not match catalog {agent_id}") @@ -322,17 +339,17 @@ def validate_catalog( bundled_path, bundled_manifest = bundled[agent_id] cmp = compare_versions(manifest["version"], bundled_manifest["version"], manifest_path) - staged_manifest = STAGED_WEBSITE_MANIFESTS.get(agent_id) - website_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() + staged_manifest = STAGED_PUBLISHED_MANIFESTS.get(agent_id) + published_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest() stages_new_engine_manifest = ( staged_manifest - == (bundled_manifest["version"], manifest["version"], website_digest) + == (bundled_manifest["version"], manifest["version"], published_digest) and bundled_manifest["min_engine_version"] == engine_version and manifest["min_engine_version"] < bundled_manifest["min_engine_version"] ) if cmp < 0 and not stages_new_engine_manifest: raise CheckError( - f"{manifest_path}: website version {manifest['version']} is lower than bundled " + f"{manifest_path}: published version {manifest['version']} is lower than bundled " f"{bundled_manifest['version']} in {bundled_path}" ) if cmp == 0 and manifest_path.read_text(encoding="utf-8") != bundled_path.read_text(encoding="utf-8"): @@ -341,13 +358,22 @@ def validate_catalog( ) missing = sorted(set(bundled) - set(seen)) - if missing: - raise CheckError(f"{catalog_path}: missing bundled agent(s): {', '.join(missing)}") + unexpected_missing = [] + for agent_id in missing: + bundled_path, bundled_manifest = bundled[agent_id] + staged = UNPUBLISHED_BUNDLED_MANIFESTS.get(agent_id) if allow_unpublished else None + digest = hashlib.sha256(bundled_path.read_bytes()).hexdigest() + if staged != (bundled_manifest["version"], digest): + unexpected_missing.append(agent_id) + if unexpected_missing: + raise CheckError( + f"{catalog_path}: missing bundled agent(s): {', '.join(unexpected_missing)}" + ) catalog_paths = set(seen.values()) | {"index.toml"} - extra = sorted(path.name for path in website_dir.glob("*.toml") if path.name not in catalog_paths) + extra = sorted(path.name for path in published_dir.glob("*.toml") if path.name not in catalog_paths) if extra: - raise CheckError(f"{website_dir}: TOML file(s) not listed in catalog: {', '.join(extra)}") + raise CheckError(f"{published_dir}: TOML file(s) not listed in catalog: {', '.join(extra)}") def main() -> int: @@ -355,10 +381,15 @@ def main() -> int: try: engine_version = read_engine_version(args.engine_version) bundled = load_manifest_dir(args.bundled_dir, engine_version) - if args.require_website or args.website_dir.exists(): - if not args.website_dir.is_dir(): - raise CheckError(f"{args.website_dir}: website manifest directory is missing") - validate_catalog(args.website_dir, bundled, engine_version) + if args.require_published or args.require_all_published or args.published_dir.exists(): + if not args.published_dir.is_dir(): + raise CheckError(f"{args.published_dir}: published manifest directory is missing") + validate_catalog( + args.published_dir, + bundled, + engine_version, + allow_unpublished=not args.require_all_published, + ) except CheckError as exc: print(f"error: {exc}", file=sys.stderr) return 1 diff --git a/scripts/changelog.py b/scripts/changelog.py index 9a96ea4e..9fcae9cd 100644 --- a/scripts/changelog.py +++ b/scripts/changelog.py @@ -16,7 +16,7 @@ DEFAULT_LIVE_MANIFEST_URL = "https://herdr.dev/latest.json" SECTION_RE = re.compile(r"^##\s+(?:\[(?P<bracketed>[^\]]+)\]|(?P<plain>.+?))\s*$", re.MULTILINE) VERSION_WITH_DATE_RE = re.compile(r"^(?P<version>.+?)\s+-\s+\d{4}-\d{2}-\d{2}$") DEFAULT_RELEASE_REPO = "herdrdev/herdr" -DEFAULT_LATEST_JSON_PATH = Path("website/latest.json") +DEFAULT_LATEST_JSON_PATH = Path("distribution/latest.json") DEFAULT_PRODUCT_ANNOUNCEMENT_PATH = Path("docs/next/product-announcement.json") PROTOCOL_SOURCE_PATH = Path("src/protocol/wire.rs") CORE_ASSET_TARGETS = ( @@ -619,11 +619,11 @@ def verify_asset_urls_resolve(assets: dict[str, str], label: str) -> None: def ensure_manifest_is_outdated(current_manifest: dict[str, Any], version: str) -> None: current_version = current_manifest.get("version") if not isinstance(current_version, str): - raise ChangelogError("website/latest.json is missing a string version") + raise ChangelogError("distribution/latest.json is missing a string version") if parse_version(current_version) >= parse_version(version): raise ChangelogError( - f"website/latest.json is already at v{normalize_version(current_version)}; expected something older than v{normalize_version(version)}" + f"distribution/latest.json is already at v{normalize_version(current_version)}; expected something older than v{normalize_version(version)}" ) @@ -696,7 +696,7 @@ def cmd_sync_latest_json(args: argparse.Namespace) -> int: print("next:") print(f" git diff -- {manifest_path}") print(f" git add {manifest_path}") - print(f" git commit -m \"docs: update website manifest for v{version}\"") + print(f" git commit -m \"docs: publish release distribution for v{version}\"") print(" git push") return 0 @@ -763,7 +763,7 @@ def build_parser() -> argparse.ArgumentParser: sync_latest_json = subparsers.add_parser( "sync-latest-json", - help="Update website/latest.json from a published GitHub release", + help="Update distribution/latest.json from a published GitHub release", ) sync_latest_json.add_argument("--version", required=True) sync_latest_json.add_argument("--repo", default=DEFAULT_RELEASE_REPO) diff --git a/scripts/docs/preview.integration.test.ts b/scripts/docs/preview.integration.test.ts new file mode 100644 index 00000000..74e54957 --- /dev/null +++ b/scripts/docs/preview.integration.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { execFileSync } from 'node:child_process'; +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; + +const script = resolve(import.meta.dir, 'preview.mjs'); +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +describe('preview documentation snapshots', () => { + test('snapshots and validates the selected commit exactly', async () => { + const root = await fixture(); + const commit = git(root, ['rev-parse', 'HEAD']).trim(); + await write(root, 'docs/preview/website/stale.mdx', 'stale\n'); + await writeManifest(root, commit); + + runScript(root, ['snapshot', commit]); + runScript(root, ['check']); + + expect(await read(root, 'docs/preview/website/src/content/docs/index.mdx')).toBe( + 'selected preview\n', + ); + await expect(read(root, 'docs/preview/website/stale.mdx')).rejects.toThrow(); + }); + + test('rejects snapshot drift', async () => { + const root = await fixture(); + const commit = git(root, ['rev-parse', 'HEAD']).trim(); + await writeManifest(root, commit); + runScript(root, ['snapshot', commit]); + await write(root, 'docs/preview/website/src/content/docs/index.mdx', 'changed\n'); + + expect(() => runScript(root, ['check'])).toThrow(); + }); +}); + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'herdr-preview-docs-')); + temporaryDirectories.push(root); + await write(root, 'docs/next/website/src/content/docs/index.mdx', 'selected preview\n'); + await write(root, 'docs/next/website/src/data/config-reference.json', '{"preview":true}\n'); + git(root, ['init', '-q']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'Test']); + git(root, ['add', '.']); + git(root, ['commit', '-qm', 'preview fixture']); + return root; +} + +async function writeManifest(root: string, commit: string) { + await write( + root, + 'distribution/preview.json', + `${JSON.stringify({ + schema_version: 1, + channel: 'preview', + build_id: 'test-build', + commit, + builds: { 'test-build': { commit, tag: 'preview-test-build' } }, + })}\n`, + ); +} + +async function write(root: string, path: string, content: string) { + const destination = resolve(root, path); + await mkdir(resolve(destination, '..'), { recursive: true }); + await writeFile(destination, content, 'utf8'); +} + +async function read(root: string, path: string) { + return readFile(resolve(root, path), 'utf8'); +} + +function git(root: string, args: string[]) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: 'pipe' }); +} + +function runScript(root: string, args: string[]) { + execFileSync('node', [script, ...args], { + cwd: root, + env: { ...process.env, HERDR_DOCS_REPO_ROOT: root }, + stdio: 'pipe', + }); +} diff --git a/scripts/docs/preview.mjs b/scripts/docs/preview.mjs new file mode 100644 index 00000000..9ef85728 --- /dev/null +++ b/scripts/docs/preview.mjs @@ -0,0 +1,75 @@ +import { readFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + compareGitTree, + createGit, + extractGitTree, + gitPathExists, + resolveCommit, +} from './snapshot.mjs'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = process.env.HERDR_DOCS_REPO_ROOT + ? resolve(process.env.HERDR_DOCS_REPO_ROOT) + : resolve(scriptDir, '../..'); +const git = createGit(repoRoot); +const sourceRoot = 'docs/next/website'; +const snapshotRoot = resolve(repoRoot, 'docs/preview/website'); +const manifestPath = resolve(repoRoot, 'distribution/preview.json'); +const fullShaPattern = /^[0-9a-f]{40}$/; + +export async function snapshotPreview(ref) { + const commit = resolveCommit(git, ref); + if (!gitPathExists(git, commit, `${sourceRoot}/src/content/docs`)) { + throw new Error(`${commit} does not contain staged website documentation`); + } + if (!gitPathExists(git, commit, `${sourceRoot}/src/data/config-reference.json`)) { + throw new Error(`${commit} does not contain the staged config reference`); + } + await extractGitTree(git, commit, sourceRoot, snapshotRoot); + process.stdout.write(`snapshotted preview documentation from ${commit}\n`); +} + +export async function checkPreview() { + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); + if (manifest.schema_version !== 1 || manifest.channel !== 'preview') { + throw new Error(`${manifestPath} has an unsupported schema`); + } + if (typeof manifest.commit !== 'string' || !fullShaPattern.test(manifest.commit)) { + throw new Error(`${manifestPath} must contain a full preview commit SHA`); + } + const build = manifest.builds?.[manifest.build_id]; + if (!build || build.commit !== manifest.commit) { + throw new Error(`${manifestPath} active build does not match its preview commit`); + } + const expectedTag = `preview-${manifest.build_id}`; + if (build.tag !== expectedTag) { + throw new Error(`${manifestPath} active build tag does not match ${expectedTag}`); + } + if (!gitPathExists(git, manifest.commit, `${sourceRoot}/src/content/docs`)) { + throw new Error(`${manifest.commit} does not contain staged website documentation`); + } + if (!gitPathExists(git, manifest.commit, `${sourceRoot}/src/data/config-reference.json`)) { + throw new Error(`${manifest.commit} does not contain the staged config reference`); + } + await compareGitTree(git, manifest.commit, sourceRoot, snapshotRoot); + process.stdout.write(`validated preview documentation snapshot ${manifest.commit}\n`); +} + +async function main() { + const [command, value] = process.argv.slice(2); + if (command === 'snapshot' && value) { + await snapshotPreview(value); + return; + } + if (command === 'check' && !value) { + await checkPreview(); + return; + } + throw new Error('usage: node scripts/docs/preview.mjs check | snapshot <commit>'); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} diff --git a/scripts/docs/snapshot.mjs b/scripts/docs/snapshot.mjs new file mode 100644 index 00000000..e9eaf92c --- /dev/null +++ b/scripts/docs/snapshot.mjs @@ -0,0 +1,116 @@ +import { execFileSync } from 'node:child_process'; +import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { dirname, relative, resolve } from 'node:path'; + +export function createGit(repoRoot) { + return function git(args, options = {}) { + return execFileSync('git', args, { + cwd: repoRoot, + encoding: options.binary ? undefined : 'utf8', + maxBuffer: 64 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }); + }; +} + +export function resolveCommit(git, ref) { + return git(['rev-parse', `${ref}^{commit}`]).trim(); +} + +export function gitPathExists(git, ref, path) { + try { + git(['cat-file', '-e', `${ref}:${path}`]); + return true; + } catch { + return false; + } +} + +export function gitTreesEqual(git, ref, left, right) { + try { + git(['diff', '--quiet', `${ref}:${left}`, `${ref}:${right}`]); + return true; + } catch { + return false; + } +} + +export function listGitEntries(git, ref, root) { + const output = git(['ls-tree', '-r', '-z', ref, '--', root]); + return output + .split('\0') + .filter(Boolean) + .map((line) => { + const separator = line.indexOf('\t'); + const [mode, type, object] = line.slice(0, separator).split(' '); + return { mode, type, object, path: line.slice(separator + 1) }; + }); +} + +export async function extractGitTree(git, ref, sourceRoot, destinationRoot) { + const entries = listGitEntries(git, ref, sourceRoot); + if (entries.length === 0) throw new Error(`${ref}:${sourceRoot} contains no files`); + + await rm(destinationRoot, { recursive: true, force: true }); + for (const entry of entries) { + assertOrdinaryFile(entry, ref); + const relativePath = relative(sourceRoot, entry.path); + assertRelativePath(relativePath, entry.path); + const destination = resolve(destinationRoot, relativePath); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, git(['cat-file', 'blob', entry.object], { binary: true })); + } +} + +export async function compareGitTree(git, ref, sourceRoot, snapshotRoot) { + const expectedEntries = listGitEntries(git, ref, sourceRoot); + if (expectedEntries.length === 0) throw new Error(`${ref}:${sourceRoot} contains no files`); + + const expected = new Map(); + for (const entry of expectedEntries) { + assertOrdinaryFile(entry, ref); + const relativePath = relative(sourceRoot, entry.path); + assertRelativePath(relativePath, entry.path); + expected.set(relativePath, entry); + } + const actualPaths = await listDocumentationPaths(snapshotRoot); + const expectedPaths = [...expected.keys()].sort(); + if (JSON.stringify(actualPaths) !== JSON.stringify(expectedPaths)) { + throw new Error(`${snapshotRoot} file list differs from ${ref}:${sourceRoot}`); + } + + for (const relativePath of expectedPaths) { + const entry = expected.get(relativePath); + const snapshotPath = resolve(snapshotRoot, relativePath); + const expectedContent = git(['cat-file', 'blob', entry.object], { binary: true }); + if (!(await readFile(snapshotPath)).equals(expectedContent)) { + throw new Error(`${relativePath} differs from ${ref}:${entry.path}`); + } + } +} + +export async function listDocumentationPaths(root) { + const paths = []; + async function walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = resolve(directory, entry.name); + if (entry.isDirectory()) await walk(path); + else if (entry.isFile()) paths.push(relative(root, path)); + else throw new Error(`${path} is not an ordinary documentation file`); + } + } + await walk(root); + return paths.sort(); +} + +function assertOrdinaryFile(entry, ref) { + if (entry.type !== 'blob' || entry.mode !== '100644') { + throw new Error(`${ref}:${entry.path} is not an ordinary documentation file`); + } +} + +function assertRelativePath(relativePath, originalPath) { + if (!relativePath || relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) { + throw new Error(`unsafe snapshot path ${originalPath}`); + } +} diff --git a/scripts/docs/versions.integration.test.ts b/scripts/docs/versions.integration.test.ts new file mode 100644 index 00000000..a52be1be --- /dev/null +++ b/scripts/docs/versions.integration.test.ts @@ -0,0 +1,129 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { execFileSync } from 'node:child_process'; +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; + +const script = resolve(import.meta.dir, 'versions.mjs'); +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +describe('documentation release publishing', () => { + test('publishes immutable tagged snapshots and validates maintained corrections', async () => { + const root = await mkdtemp(join(tmpdir(), 'herdr-docs-')); + temporaryDirectories.push(root); + await write(root, 'distribution/latest.json', '{"version":"0.9.0"}\n'); + await write(root, 'README.md', 'stable readme\n'); + await write(root, 'README.zh-CN.md', 'stable readme zh-cn\n'); + const nextDocs = '---\ntitle: Documentation\n---\n\nnext docs\n'; + await write(root, 'docs/next/website/src/content/docs/index.mdx', nextDocs); + await write(root, 'docs/next/website/src/data/config-reference.json', '{"next":true}\n'); + await write(root, 'docs/next/README.md', 'next readme\n'); + await write(root, 'docs/next/README.zh-CN.md', 'next readme zh-cn\n'); + + git(root, ['init', '-q']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'Test']); + git(root, ['add', '.']); + git(root, ['commit', '-qm', 'release fixture']); + git(root, ['tag', 'v0.9.0']); + git(root, ['tag', 'v1.0.0']); + + runScript(root, ['publish', 'v1.0.0']); + + expect(await read(root, 'README.md')).toBe('next readme\n'); + expect(await read(root, 'README.zh-CN.md')).toBe('next readme zh-cn\n'); + expect(await read(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx')).toBe( + nextDocs, + ); + + const manifest = JSON.parse(await read(root, 'docs/versions/manifest.json')); + expect(manifest.current).toBe('1.0.0'); + expect(manifest.stable_source).toBe('snapshot'); + expect(manifest.versions[0]).toMatchObject({ + version: '1.0.0', + tag: 'v1.0.0', + commit: expect.stringMatching(/^[0-9a-f]{40}$/), + source: 'docs/next/website/src/content/docs', + }); + + await write(root, 'distribution/latest.json', '{"version":"1.0.0"}\n'); + runScript(root, ['check']); + + const correctedDocs = '---\ntitle: Documentation\n---\n\ncorrected docs\n'; + await write(root, 'docs/versions/1.0.0/website/src/content/docs/index.mdx', correctedDocs); + await write( + root, + 'docs/versions/1.0.0/website/src/data/config-reference.json', + '{"corrected":true}\n', + ); + runScript(root, ['check']); + + await write(root, 'README.md', 'post-release correction\n'); + runScript(root, ['publish', 'v1.0.0']); + expect(await read(root, 'README.md')).toBe('post-release correction\n'); + + runScript(root, ['publish', 'v0.9.0']); + const archivedManifest = JSON.parse(await read(root, 'docs/versions/manifest.json')); + expect(archivedManifest.current).toBe('1.0.0'); + expect(archivedManifest.versions.map(({ version }) => version)).toEqual(['1.0.0', '0.9.0']); + expect(await read(root, 'README.md')).toBe('post-release correction\n'); + runScript(root, ['check']); + + const correctedArchivedDocs = '---\ntitle: Documentation\n---\n\ncorrected archived docs\n'; + await write( + root, + 'docs/versions/0.9.0/website/src/content/docs/index.mdx', + correctedArchivedDocs, + ); + runScript(root, ['publish', 'v0.9.0']); + expect(await read(root, 'docs/versions/0.9.0/website/src/content/docs/index.mdx')).toBe( + correctedArchivedDocs, + ); + + const archivedDocsRoot = resolve(root, 'docs/versions/0.9.0/website/src/content/docs'); + await rm(archivedDocsRoot, { recursive: true, force: true }); + expect(() => runScript(root, ['check'])).toThrow(); + await write( + root, + 'docs/versions/0.9.0/website/src/content/docs/index.mdx', + correctedArchivedDocs, + ); + const linkedPage = resolve(archivedDocsRoot, 'linked.mdx'); + await symlink('index.mdx', linkedPage); + expect(() => runScript(root, ['check'])).toThrow(); + await rm(linkedPage); + runScript(root, ['check']); + + delete archivedManifest.versions[0].commit; + await write(root, 'docs/versions/manifest.json', `${JSON.stringify(archivedManifest)}\n`); + expect(() => runScript(root, ['check'])).toThrow(); + }, 30_000); +}); + +async function write(root: string, path: string, content: string) { + const destination = resolve(root, path); + await mkdir(resolve(destination, '..'), { recursive: true }); + await writeFile(destination, content, 'utf8'); +} + +async function read(root: string, path: string) { + return readFile(resolve(root, path), 'utf8'); +} + +function git(root: string, args: string[]) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: 'pipe' }); +} + +function runScript(root: string, args: string[]) { + execFileSync('node', [script, ...args], { + cwd: root, + env: { ...process.env, HERDR_DOCS_REPO_ROOT: root }, + stdio: 'pipe', + }); +} diff --git a/scripts/docs/versions.mjs b/scripts/docs/versions.mjs new file mode 100644 index 00000000..163d3ae6 --- /dev/null +++ b/scripts/docs/versions.mjs @@ -0,0 +1,283 @@ +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + createGit, + extractGitTree, + gitPathExists, + gitTreesEqual, + listDocumentationPaths, + resolveCommit, +} from './snapshot.mjs'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = process.env.HERDR_DOCS_REPO_ROOT + ? resolve(process.env.HERDR_DOCS_REPO_ROOT) + : resolve(scriptDir, '../..'); +const versionsDir = resolve(repoRoot, 'docs/versions'); +const manifestPath = resolve(versionsDir, 'manifest.json'); +const latestManifestPath = resolve(repoRoot, 'distribution/latest.json'); +const git = createGit(repoRoot); + +const VERSION_PATTERN = /^v?(\d+\.\d+\.\d+)$/; + +export function normalizeVersion(value) { + const match = VERSION_PATTERN.exec(value); + if (!match) throw new Error(`version must look like 0.7.5 or v0.7.5, got ${value}`); + return match[1]; +} + +export function sortVersionsNewestFirst(versions) { + return [...versions].sort((left, right) => compareVersions(right.version, left.version)); +} + +function compareVersions(left, right) { + const a = left.split('.').map(Number); + const b = right.split('.').map(Number); + for (let index = 0; index < 3; index += 1) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; +} + +async function readManifest() { + try { + return JSON.parse(await readFile(manifestPath, 'utf8')); + } catch (error) { + if (error.code === 'ENOENT') { + return { schema_version: 1, stable_source: 'legacy', current: null, versions: [] }; + } + throw error; + } +} + +async function writeManifest(manifest) { + manifest.versions = sortVersionsNewestFirst(manifest.versions).map( + ({ version, tag, commit, source }) => ({ + version, + tag, + ...(commit ? { commit } : {}), + source, + }), + ); + await mkdir(dirname(manifestPath), { recursive: true }); + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); +} + +function releaseMetadata(tag) { + const version = normalizeVersion(tag); + const normalizedTag = `v${version}`; + return { version, tag: normalizedTag, commit: resolveCommit(git, normalizedTag) }; +} + +async function snapshotTag(tag, sourceRoot) { + const metadata = { ...releaseMetadata(tag), source: sourceRoot }; + const destination = resolve(versionsDir, metadata.version); + await rm(destination, { recursive: true, force: true }); + await extractGitTree(git, tag, sourceRoot, resolve(destination, 'website/src/content/docs')); + + const referenceSource = sourceRoot.includes('docs/next/') + ? 'docs/next/website/src/data/config-reference.json' + : 'website/src/data/config-reference.json'; + if (gitPathExists(git, tag, referenceSource)) { + const referenceDestination = resolve(destination, 'website/src/data/config-reference.json'); + await mkdir(dirname(referenceDestination), { recursive: true }); + await writeFile(referenceDestination, git(['show', `${tag}:${referenceSource}`], { binary: true })); + } + + return metadata; +} + +export async function backfillVersions() { + const tags = git(['tag', '--list', 'v*', '--sort=v:refname']).split('\n').filter(Boolean); + const manifest = await readManifest(); + const entries = new Map(manifest.versions.map((entry) => [entry.version, entry])); + + for (const tag of tags) { + const version = normalizeVersion(tag); + if (entries.has(version)) continue; + const nextRoot = 'docs/next/website/src/content/docs'; + const legacyRoot = 'website/src/content/docs'; + const hasNext = gitPathExists(git, tag, nextRoot); + const hasLegacy = gitPathExists(git, tag, legacyRoot); + if (!hasNext && !hasLegacy) continue; + const sourceRoot = + hasNext && (!hasLegacy || !gitTreesEqual(git, tag, legacyRoot, nextRoot)) + ? nextRoot + : legacyRoot; + const metadata = await snapshotTag(tag, sourceRoot); + entries.set(metadata.version, metadata); + process.stdout.write(`snapshotted ${tag}\n`); + } + + const current = JSON.parse(await readFile(latestManifestPath, 'utf8')).version; + await writeManifest({ + schema_version: 1, + stable_source: manifest.stable_source ?? 'legacy', + current: normalizeVersion(current), + versions: [...entries.values()], + }); +} + +export async function checkVersions() { + const manifest = await readManifest(); + if ( + manifest.schema_version !== 1 || + typeof manifest.current !== 'string' || + !['legacy', 'snapshot'].includes(manifest.stable_source ?? 'legacy') + ) { + throw new Error(`${manifestPath} has an unsupported schema`); + } + + const latest = JSON.parse(await readFile(latestManifestPath, 'utf8')).version; + if (manifest.current !== normalizeVersion(latest)) { + throw new Error(`docs current version ${manifest.current} does not match distribution latest ${latest}`); + } + + const expectedOrder = sortVersionsNewestFirst(manifest.versions).map(({ version }) => version); + if (JSON.stringify(manifest.versions.map(({ version }) => version)) !== JSON.stringify(expectedOrder)) { + throw new Error('documentation versions are not sorted newest first'); + } + + const seen = new Set(); + for (const entry of manifest.versions) { + if (seen.has(entry.version)) throw new Error(`duplicate docs version ${entry.version}`); + seen.add(entry.version); + if (normalizeVersion(entry.tag) !== entry.version) { + throw new Error(`docs version ${entry.version} has mismatched tag ${entry.tag}`); + } + const requiresCommit = + entry.source === 'docs/next/website/src/content/docs' || + ((manifest.stable_source ?? 'legacy') === 'snapshot' && entry.version === manifest.current); + if (requiresCommit && !entry.commit) { + throw new Error(`docs version ${entry.version} is missing commit provenance`); + } + if (entry.commit) { + if (!/^[0-9a-f]{40}$/.test(entry.commit)) { + throw new Error(`docs version ${entry.version} has invalid commit ${entry.commit}`); + } + const taggedCommit = resolveCommit(git, entry.tag); + if (taggedCommit !== entry.commit) { + throw new Error( + `docs version ${entry.version} tag ${entry.tag} moved from ${entry.commit} to ${taggedCommit}`, + ); + } + } + if (!['website/src/content/docs', 'docs/next/website/src/content/docs'].includes(entry.source)) { + throw new Error(`docs version ${entry.version} has unsupported source ${entry.source}`); + } + + const versionRoot = resolve(versionsDir, entry.version, 'website'); + const documentationPaths = await listDocumentationPaths( + resolve(versionRoot, 'src/content/docs'), + ); + if (!documentationPaths.includes('index.md') && !documentationPaths.includes('index.mdx')) { + throw new Error(`docs version ${entry.version} has no root index`); + } + + try { + JSON.parse(await readFile(resolve(versionRoot, 'src/data/config-reference.json'), 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } + + if (!seen.has(manifest.current)) { + throw new Error(`current docs version ${manifest.current} has no snapshot`); + } + process.stdout.write(`validated ${manifest.versions.length} documentation versions\n`); +} + +export async function publishVersion(tag) { + const release = releaseMetadata(tag); + if (!gitPathExists(git, tag, 'docs/next/website/src/content/docs')) { + throw new Error(`${tag} does not contain staged website documentation`); + } + + const manifest = await readManifest(); + const existing = manifest.versions.find((entry) => entry.version === release.version); + if (manifest.current === release.version) { + if ((manifest.stable_source ?? 'legacy') === 'legacy') { + process.stdout.write(`documentation ${release.tag} is already published with the legacy stable source\n`); + return; + } + if (!existing?.commit || existing.commit !== release.commit) { + throw new Error(`published documentation ${release.version} has mismatched commit provenance`); + } + process.stdout.write(`documentation ${release.tag} is already published\n`); + return; + } + if (manifest.current && compareVersions(release.version, manifest.current) < 0) { + if (existing) { + if (existing.tag !== release.tag) { + throw new Error(`version ${release.version} is already associated with ${existing.tag}`); + } + if (existing.commit && existing.commit !== release.commit) { + throw new Error(`version ${release.version} has mismatched commit provenance`); + } + process.stdout.write(`documentation ${release.tag} is already archived\n`); + return; + } + const archived = await snapshotTag(tag, 'docs/next/website/src/content/docs'); + const archivedEntries = new Map(manifest.versions.map((entry) => [entry.version, entry])); + archivedEntries.set(archived.version, archived); + await writeManifest({ + ...manifest, + versions: [...archivedEntries.values()], + }); + process.stdout.write( + `archived documentation snapshot ${release.tag} without changing current ${manifest.current}\n`, + ); + return; + } + if (existing) { + throw new Error(`version ${release.version} already has published documentation`); + } + + const metadata = await snapshotTag(tag, 'docs/next/website/src/content/docs'); + + for (const readme of ['README.md', 'README.zh-CN.md']) { + const nextReadme = `docs/next/${readme}`; + if (gitPathExists(git, tag, nextReadme)) { + await writeFile(resolve(repoRoot, readme), git(['show', `${tag}:${nextReadme}`], { binary: true })); + } + } + + const entries = new Map(manifest.versions.map((entry) => [entry.version, entry])); + entries.set(metadata.version, metadata); + await writeManifest({ + schema_version: 1, + stable_source: 'snapshot', + current: metadata.version, + versions: [...entries.values()], + }); + process.stdout.write(`published documentation snapshot ${metadata.tag}\n`); +} + +async function main() { + const [command, value] = process.argv.slice(2); + if (command === 'backfill' && !value) { + await backfillVersions(); + return; + } + if (command === 'publish' && value) { + await publishVersion(value); + return; + } + if (command === 'check' && !value) { + await checkVersions(); + return; + } + if (command === 'current' && !value) { + const manifest = await readManifest(); + process.stdout.write(`${manifest.current ?? ''}\n`); + return; + } + throw new Error( + 'usage: node scripts/docs/versions.mjs backfill | check | current | publish <tag>', + ); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} diff --git a/scripts/docs/versions.test.ts b/scripts/docs/versions.test.ts new file mode 100644 index 00000000..c40ee854 --- /dev/null +++ b/scripts/docs/versions.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from 'bun:test'; +import { normalizeVersion, sortVersionsNewestFirst } from './versions.mjs'; + +describe('normalizeVersion', () => { + test.each([ + ['0.7.5', '0.7.5'], + ['v0.7.5', '0.7.5'], + ])('normalizes %s', (input, expected) => { + expect(normalizeVersion(input)).toBe(expected); + }); + + test('rejects non-release refs', () => { + expect(() => normalizeVersion('preview-123')).toThrow(); + }); +}); + +describe('sortVersionsNewestFirst', () => { + test('sorts semantic versions numerically', () => { + const versions = [ + { version: '0.6.10' }, + { version: '0.7.2' }, + { version: '0.7.10' }, + { version: '0.5.12' }, + ]; + + expect(sortVersionsNewestFirst(versions).map(({ version }) => version)).toEqual([ + '0.7.10', + '0.7.2', + '0.6.10', + '0.5.12', + ]); + }); +}); diff --git a/scripts/preview.py b/scripts/preview.py index fa55d623..16c52b6e 100644 --- a/scripts/preview.py +++ b/scripts/preview.py @@ -20,6 +20,7 @@ EXPECTED_ASSET_NAMES = { "windows-x86_64": "herdr-windows-x86_64.zip", } HIDDEN_SUBJECTS = ( + "docs: publish release distribution", "docs: update website manifest", "docs: update preview manifest", "chore: approve contributor", @@ -294,7 +295,7 @@ def main() -> int: sub = parser.add_subparsers(required=True) notes = sub.add_parser("notes") - notes.add_argument("--manifest", default="website/preview.json") + notes.add_argument("--manifest", default="distribution/preview.json") notes.add_argument("--previous") notes.add_argument("--commit", required=True) notes.add_argument("--build-id", required=True) @@ -304,7 +305,7 @@ def main() -> int: notes.set_defaults(func=cmd_notes) manifest = sub.add_parser("manifest") - manifest.add_argument("--output", default="website/preview.json") + manifest.add_argument("--output", default="distribution/preview.json") manifest.add_argument("--repo", default="herdrdev/herdr") manifest.add_argument("--tag", required=True) manifest.add_argument("--build-id", required=True) @@ -318,7 +319,7 @@ def main() -> int: manifest.set_defaults(func=cmd_manifest) current = sub.add_parser("current-commit") - current.add_argument("--manifest", default="website/preview.json") + current.add_argument("--manifest", default="distribution/preview.json") current.set_defaults(func=cmd_current_commit) select = sub.add_parser("select-commit") diff --git a/scripts/release_perf_smoke.sh b/scripts/release_perf_smoke.sh index 7ce1f736..2b7972e2 100755 --- a/scripts/release_perf_smoke.sh +++ b/scripts/release_perf_smoke.sh @@ -37,7 +37,7 @@ trap 'exit 143' TERM mkdir -p "$root/results" if [[ -z "$baseline" ]]; then - baseline_version=$(jq -er '.version' "$repo_root/website/latest.json") + baseline_version=$(jq -er '.version' "$repo_root/distribution/latest.json") baseline="$root/herdr-baseline" curl -fL --retry 3 \ "https://github.com/herdrdev/herdr/releases/download/v${baseline_version}/herdr-${platform}-${arch}" \ diff --git a/scripts/test_agent_detection_manifest_check.py b/scripts/test_agent_detection_manifest_check.py index a9ec38d1..6c4cd726 100644 --- a/scripts/test_agent_detection_manifest_check.py +++ b/scripts/test_agent_detection_manifest_check.py @@ -29,21 +29,33 @@ path = "{path}" def staged_grok_dirs(root: Path) -> tuple[Path, Path]: bundled = root / "bundled" - website = root / "website" + published = root / "published" bundled.mkdir() - website.mkdir() + published.mkdir() (bundled / "grok.toml").write_bytes( (check.DEFAULT_BUNDLED_DIR / "grok.toml").read_bytes() ) - (website / "grok.toml").write_bytes( - (check.DEFAULT_WEBSITE_DIR / "grok.toml").read_bytes() + (published / "grok.toml").write_bytes( + (check.DEFAULT_PUBLISHED_DIR / "grok.toml").read_bytes() ) - (website / "index.toml").write_text(catalog("grok", "grok.toml")) - return bundled, website + (published / "index.toml").write_text(catalog("grok", "grok.toml")) + return bundled, published + + +def unpublished_muse_dirs(root: Path) -> tuple[Path, Path]: + bundled = root / "bundled" + published = root / "published" + bundled.mkdir() + published.mkdir() + (bundled / "muse.toml").write_bytes( + (check.DEFAULT_BUNDLED_DIR / "muse.toml").read_bytes() + ) + (published / "index.toml").write_text("schema_version = 1\nagents = []\n") + return bundled, published class AgentDetectionManifestCheckTests(unittest.TestCase): - def test_validates_bundled_and_matching_website_catalog(self): + def test_validates_bundled_and_matching_published_catalog(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) bundled = root / "bundled" @@ -58,7 +70,7 @@ class AgentDetectionManifestCheckTests(unittest.TestCase): bundled_manifests = check.load_manifest_dir(bundled, engine_version=1) check.validate_catalog(website, bundled_manifests, engine_version=1) - def test_rejects_website_version_lower_than_bundled(self): + def test_rejects_published_version_lower_than_bundled(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) bundled = root / "bundled" @@ -73,14 +85,14 @@ class AgentDetectionManifestCheckTests(unittest.TestCase): with self.assertRaisesRegex(check.CheckError, "lower than bundled"): check.validate_catalog(website, bundled_manifests, engine_version=1) - def test_allows_explicitly_staged_website_manifest(self): + def test_allows_explicitly_staged_published_manifest(self): with tempfile.TemporaryDirectory() as tmp: bundled, website = staged_grok_dirs(Path(tmp)) bundled_manifests = check.load_manifest_dir(bundled, engine_version=3) check.validate_catalog(website, bundled_manifests, engine_version=3) - def test_rejects_mutated_staged_website_manifest(self): + def test_rejects_mutated_staged_published_manifest(self): with tempfile.TemporaryDirectory() as tmp: bundled, website = staged_grok_dirs(Path(tmp)) with (website / "grok.toml").open("a") as manifest_file: @@ -90,7 +102,7 @@ class AgentDetectionManifestCheckTests(unittest.TestCase): with self.assertRaisesRegex(check.CheckError, "lower than bundled"): check.validate_catalog(website, bundled_manifests, engine_version=3) - def test_rejects_unlisted_website_manifest_lag_for_new_engine(self): + def test_rejects_unlisted_published_manifest_lag_for_new_engine(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) bundled = root / "bundled" @@ -123,6 +135,38 @@ class AgentDetectionManifestCheckTests(unittest.TestCase): with self.assertRaisesRegex(check.CheckError, "same version"): check.validate_catalog(website, bundled_manifests, engine_version=1) + def test_allows_exact_unpublished_bundled_manifest(self): + with tempfile.TemporaryDirectory() as tmp: + bundled, published = unpublished_muse_dirs(Path(tmp)) + bundled_manifests = check.load_manifest_dir(bundled, engine_version=3) + check.validate_catalog( + published, + bundled_manifests, + engine_version=3, + allow_unpublished=True, + ) + + def test_release_gate_rejects_exact_unpublished_bundled_manifest(self): + with tempfile.TemporaryDirectory() as tmp: + bundled, published = unpublished_muse_dirs(Path(tmp)) + bundled_manifests = check.load_manifest_dir(bundled, engine_version=3) + with self.assertRaisesRegex(check.CheckError, "missing bundled agent"): + check.validate_catalog(published, bundled_manifests, engine_version=3) + + def test_rejects_mutated_unpublished_bundled_manifest(self): + with tempfile.TemporaryDirectory() as tmp: + bundled, published = unpublished_muse_dirs(Path(tmp)) + with (bundled / "muse.toml").open("a") as manifest_file: + manifest_file.write("\n# unexpected mutation\n") + bundled_manifests = check.load_manifest_dir(bundled, engine_version=3) + with self.assertRaisesRegex(check.CheckError, "missing bundled agent"): + check.validate_catalog( + published, + bundled_manifests, + engine_version=3, + allow_unpublished=True, + ) + def test_rejects_unknown_catalog_agent(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) diff --git a/scripts/test_package_windows_conpty.py b/scripts/test_package_windows_conpty.py index b8d73a6c..0736208b 100644 --- a/scripts/test_package_windows_conpty.py +++ b/scripts/test_package_windows_conpty.py @@ -29,7 +29,7 @@ class WindowsConptyPackageTests(unittest.TestCase): loader = ( package.PROJECT_ROOT / "vendor/portable-pty/src/win/psuedocon.rs" ).read_text(encoding="utf-8") - installer = (package.PROJECT_ROOT / "website/install.ps1").read_text( + installer = (package.PROJECT_ROOT / "distribution/install.ps1").read_text( encoding="utf-8" ) for item in metadata["bundles"]["x86_64"]["files"]: diff --git a/scripts/test_preview.py b/scripts/test_preview.py index 137a13f0..0f586437 100644 --- a/scripts/test_preview.py +++ b/scripts/test_preview.py @@ -83,6 +83,7 @@ class PreviewNotesTests(unittest.TestCase): def test_hidden_subjects_include_preview_manifest_commits(self): self.assertTrue(preview.hidden_subject("docs: update preview manifest")) self.assertTrue(preview.hidden_subject("docs: update website manifest")) + self.assertTrue(preview.hidden_subject("docs: publish release distribution")) self.assertFalse(preview.hidden_subject("release: v0.7.0")) self.assertFalse(preview.hidden_subject("fix: repair preview manifest")) @@ -161,60 +162,6 @@ class PreviewNotesTests(unittest.TestCase): finally: os.chdir(original_cwd) - def test_preview_docs_rewrite_links_to_preview_namespace(self): - source = """--- -title: Install Herdr ---- - -import ConfigReference from '../../components/ConfigReference.astro'; -import LocaleWidget from '../../../components/LocaleWidget.astro'; - -[Install](/docs/install/) -file: ../../../public/assets/logo.svg -""" - output = subprocess.check_output( - ["node", "website/scripts/prepare-docs.mjs", "--rewrite-preview-doc-fixture"], - input=source, - text=True, - ) - self.assertIn("[Install](/docs/preview/install/)", output) - self.assertIn("file: ../../../../public/assets/logo.svg", output) - self.assertIn("from '../../../components/ConfigReference.astro'", output) - self.assertIn("from '../../../../components/LocaleWidget.astro'", output) - self.assertIn("Preview build `2026-07-29-44b3adb12552`", output) - self.assertIn( - "blob/44b3adb125524ea9a55739eee3776f922f2115ad/docs/next/website/src/content/docs/", - output, - ) - - def test_version_docs_rewrite_links_and_source_paths(self): - source = """--- -title: Install Herdr ---- - -import ConfigReference from '../../components/ConfigReference.astro'; - -[Install](/docs/install/) -[Skill](https://github.com/herdrdev/herdr/blob/master/SKILL.md) -file: ../../../public/assets/logo.svg -""" - output = subprocess.check_output( - [ - "node", - "website/scripts/prepare-docs.mjs", - "--rewrite-version-doc-fixture", - "0.7.4", - ], - input=source, - text=True, - ) - self.assertIn("[Install](/docs/0.7.4/install/)", output) - self.assertIn("file: ../../../../../public/assets/logo.svg", output) - self.assertIn("from '../../../../components/ConfigReference.astro'", output) - self.assertIn("blob/master/docs/versions/0.7.4/website/src/content/docs/index.mdx", output) - self.assertIn("blob/v0.7.4/SKILL.md", output) - - class ConventionalCommitTests(unittest.TestCase): def test_valid_subjects_allow_scopes_and_bang(self): self.assertTrue(conventional_commits.valid_subject("fix(update): handle preview")) diff --git a/scripts/test_unix_installer.py b/scripts/test_unix_installer.py index 82de55c8..ca7d3b19 100644 --- a/scripts/test_unix_installer.py +++ b/scripts/test_unix_installer.py @@ -11,7 +11,7 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -INSTALLER = REPO_ROOT / "website" / "install.sh" +INSTALLER = REPO_ROOT / "distribution" / "install.sh" REQUIRED_COMMANDS = ("awk", "cat", "chmod", "cp", "mkdir", "mktemp", "mv", "rm") diff --git a/scripts/windows_arm64_installer_test.ps1 b/scripts/windows_arm64_installer_test.ps1 index 7f5d7174..6d40dec0 100644 --- a/scripts/windows_arm64_installer_test.ps1 +++ b/scripts/windows_arm64_installer_test.ps1 @@ -17,7 +17,7 @@ $env:HERDR_INSTALL_DIR = Join-Path $root "bin" $env:HERDR_CHANNEL = "preview" Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $root | Out-Null -$installer = (Resolve-Path (Join-Path $PSScriptRoot "..\website\install.ps1")).Path +$installer = (Resolve-Path (Join-Path $PSScriptRoot "..\distribution\install.ps1")).Path $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" diff --git a/scripts/windows_install_conpty_package_test.ps1 b/scripts/windows_install_conpty_package_test.ps1 index 264fafe2..055de63f 100644 --- a/scripts/windows_install_conpty_package_test.ps1 +++ b/scripts/windows_install_conpty_package_test.ps1 @@ -6,8 +6,8 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$installerPath = (Resolve-Path -LiteralPath "$PSScriptRoot\..\website\install.ps1").Path -$bootstrapPath = (Resolve-Path -LiteralPath "$PSScriptRoot\..\website\install.cmd").Path +$installerPath = (Resolve-Path -LiteralPath "$PSScriptRoot\..\distribution\install.ps1").Path +$bootstrapPath = (Resolve-Path -LiteralPath "$PSScriptRoot\..\distribution\install.cmd").Path $bootstrapContent = Get-Content -LiteralPath $bootstrapPath -Raw foreach ($forbiddenCommand in @("Invoke-RestMethod", "Invoke-WebRequest", "Invoke-Expression", "iex")) { if ($bootstrapContent -match "(?i)\b$forbiddenCommand\b") { @@ -263,7 +263,7 @@ try { $badManifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath $previewManifestPath -Encoding utf8 $downloadFailed = $false try { - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel preview ` -ManifestUrl $previewManifestUrl ` -InstallDir $installDir ` @@ -389,7 +389,7 @@ try { } } - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel preview ` -ManifestUrl $previewManifestUrl ` -InstallDir $installDir ` @@ -402,7 +402,7 @@ try { $junctionTarget = Join-Path $root "junction-target" Move-Item -LiteralPath $x64HostDir -Destination $junctionTarget New-Item -ItemType Junction -Path $x64HostDir -Target $junctionTarget | Out-Null - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel preview ` -ManifestUrl $previewManifestUrl ` -InstallDir $installDir ` @@ -414,7 +414,7 @@ try { $rejected = $false try { - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel preview ` -ManifestUrl $previewManifestUrl ` -InstallDir $installDir ` @@ -430,7 +430,7 @@ try { } $stableManifest | Out-File -LiteralPath $stableManifestPath -Encoding utf8 - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel stable ` -ManifestUrl $stableManifestUrl ` -InstallDir $installDir @@ -465,7 +465,7 @@ exit /b 1 $preserveBin = Join-Path $root "preserve-bin" $env:HERDR_HOME = $preserveHome $env:Path = "$fakeBin;$oldProcessPath" - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -ManifestUrl "http://127.0.0.1:$port/candidate.json" ` -InstallDir $preserveBin ` -ExpectedBuildId "installer-test" @@ -476,7 +476,7 @@ exit /b 1 throw "installer did not preserve the existing preview channel" } - & "$PSScriptRoot\..\website\install.ps1" ` + & "$PSScriptRoot\..\distribution\install.ps1" ` -Channel stable ` -ManifestUrl $stableManifestUrl ` -InstallDir $preserveBin |
