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
6 changes: 6 additions & 0 deletions .github/workflows/seo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,11 @@ jobs:
--scheme http
--scheme https
--exclude '^https://docs\.flyto2\.com'
--exclude '^https://github\.com/flytohub/flyto-docs/blob/main/'
--exclude '^https://github\.com/flytohub/flyto-flow'
--exclude '^https://github\.com/flytohub/flyto-warroom'
--accept 200,204,206,301,302,307,308,429
'./.external-links.txt'

- name: Check that link exclusions have not outlived their reason
run: npm run check:unpublished
26 changes: 26 additions & 0 deletions config/unpublished-repos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"comment": [
"Repositories the documentation already describes and links to, that are not",
"public yet. Their links 404 for a reader today, which is why the external",
"link check excludes them.",
"",
"The exclusion is meant to expire. check-unpublished-links.mjs fails once a",
"repository here answers publicly, because at that moment the exclusion is",
"hiding a link that works and the docs should be checked against it again.",
"",
"Removing a repository from this list means removing its --exclude from the",
"lychee step in .github/workflows/seo.yml at the same time."
],
"repositories": [
{
"owner": "flytohub",
"name": "flyto-flow",
"reason": "Flow CE source-available publication is prepared but paused"
},
{
"owner": "flytohub",
"name": "flyto-warroom",
"reason": "Warroom CE publication is prepared but paused"
}
]
}
81 changes: 67 additions & 14 deletions docs/documentation-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
"version": 1,
"repository": "flyto-docs",
"status": "active",
"audiences": ["Flyto2 users", "operators", "integrators", "documentation contributors"],
"audiences": [
"Flyto2 users",
"operators",
"integrators",
"documentation contributors"
],
"documentation": {
"index": "docs/README.md",
"architecture": "ARCHITECTURE.md",
Expand All @@ -15,38 +20,86 @@
"feature_reference": "docs/README.md",
"cloud_architecture": "cloud/mission-stations.md",
"source_reference": "reference/docs-code.md",
"module_roots": [".vitepress", "scripts"]
"module_roots": [
".vitepress",
"scripts"
]
},
"owners": [
{
"area": "vitepress-runtime",
"paths": [".vitepress/**"],
"docs": ["ARCHITECTURE.md", "reference/docs-code.md"]
"paths": [
".vitepress/**"
],
"docs": [
"ARCHITECTURE.md",
"reference/docs-code.md"
]
},
{
"area": "documentation-generators",
"paths": ["scripts/**"],
"docs": ["scripts/README.md", "reference/docs-code.md"]
"paths": [
"scripts/**",
"config/**"
],
"docs": [
"scripts/README.md",
"reference/docs-code.md"
]
},
{
"area": "package-and-build",
"paths": ["package.json", "package-lock.json", "lighthouserc.cjs"],
"docs": ["README.md", "docs/README.md"]
"paths": [
"package.json",
"package-lock.json",
"lighthouserc.cjs"
],
"docs": [
"README.md",
"docs/README.md"
]
},
{
"area": "automation",
"paths": [".github/**"],
"docs": ["engineering/ci-standard.md", "docs/README.md"]
"paths": [
".github/**"
],
"docs": [
"engineering/ci-standard.md",
"docs/README.md"
]
},
{
"area": "seo-contract",
"paths": [".seo/**", "public/*.txt", "public/*.xml", "public/*.json", "public/**/*.txt", "public/robots.txt", "public/CNAME"],
"docs": ["scripts/README.md", "STATE.md"]
"paths": [
".seo/**",
"public/*.txt",
"public/*.xml",
"public/*.json",
"public/**/*.txt",
"public/robots.txt",
"public/CNAME"
],
"docs": [
"scripts/README.md",
"STATE.md"
]
},
{
"area": "repository-policy",
"paths": [".flyto-rules.yaml", ".flyto/coding.yaml", ".gitleaks.toml", ".gitignore", ".env.example", "docs/documentation-manifest.json"],
"docs": ["AGENTS.md", "SECURITY.md", "docs/README.md"]
"paths": [
".flyto-rules.yaml",
".flyto/coding.yaml",
".gitleaks.toml",
".gitignore",
".env.example",
"docs/documentation-manifest.json"
],
"docs": [
"AGENTS.md",
"SECURITY.md",
"docs/README.md"
]
}
]
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
"links:external": "node scripts/list-external-links.mjs",
"seo:discovery": "node scripts/generate-discovery.mjs",
"build": "npm run seo:discovery && vitepress build",
"verify": "npm run test && npm run lint && npm run build && npm run check:links && npm run audit:seo && npm run seo:score && npm run seo:manage",
"verify": "npm run test && npm run lint && npm run build && npm run check:links && npm run check:unpublished && npm run audit:seo && npm run seo:score && npm run seo:manage",
"docs:dev": "vitepress dev",
"docs:build": "npm run build",
"docs:preview": "vitepress preview"
"docs:preview": "vitepress preview",
"check:unpublished": "node scripts/check-unpublished-links.mjs"
},
"devDependencies": {
"vitepress": "^1.6.4"
Expand Down
11 changes: 9 additions & 2 deletions reference/docs-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Source map for every maintained function and configuration method in the Flyto2 Docs generators, audits, SEO gates, and VitePress configuration.

Inventory: **14 files**, **3915 lines**, and **156 declarations**.
Inventory: **15 files**, **4029 lines**, and **158 declarations**.

## `.vitepress/config.mts`

Expand Down Expand Up @@ -95,6 +95,12 @@ Inventory: **14 files**, **3915 lines**, and **156 declarations**.
| function | `checkUrl(rawUrl, fromFile)` | Implements `checkUrl` in this documentation surface. | [`scripts/check-public-links.mjs:92`](https://github.com/flytohub/flyto-docs/blob/main/scripts/check-public-links.mjs#L92) |
| function | `checkHtmlFile(filePath)` | Implements `checkHtmlFile` in this documentation surface. | [`scripts/check-public-links.mjs:108`](https://github.com/flytohub/flyto-docs/blob/main/scripts/check-public-links.mjs#L108) |

## `scripts/check-unpublished-links.mjs`

| Kind | Signature | Responsibility | Source |
|---|---|---|---|
| function | `isPublic(owner, name)` | Implements `isPublic` in this documentation surface. | [`scripts/check-unpublished-links.mjs:34`](https://github.com/flytohub/flyto-docs/blob/main/scripts/check-unpublished-links.mjs#L34) |

## `scripts/generate-code-reference.py`

| Kind | Signature | Responsibility | Source |
Expand All @@ -105,7 +111,8 @@ Inventory: **14 files**, **3915 lines**, and **156 declarations**.
| function | `def javascript_declarations(path: Path)` | Implements `javascript_declarations`. | [`scripts/generate-code-reference.py:58`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L58) |
| function | `def escape(value: str)` | Implements `escape`. | [`scripts/generate-code-reference.py:79`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L79) |
| function | `def render()` | Implements `render`. | [`scripts/generate-code-reference.py:83`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L83) |
| function | `def main()` | Implements `main`. | [`scripts/generate-code-reference.py:126`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L126) |
| function | `def unresolved_source_links(content: str)` | Self-links that name a file or line this working tree does not have. | [`scripts/generate-code-reference.py:126`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L126) |
| function | `def main()` | Implements `main`. | [`scripts/generate-code-reference.py:151`](https://github.com/flytohub/flyto-docs/blob/main/scripts/generate-code-reference.py#L151) |

## `scripts/generate-discovery.mjs`

Expand Down
13 changes: 13 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ Important checks:
inventories, Core provenance, declaration totals, code-reference drift,
source ownership, Flyto2 naming, and the 16 approved public mailboxes.
- `check-public-links.mjs` resolves every built internal link and anchor.
- `check-unpublished-links.mjs` retires the external-link exclusions. The
docs describe Flow CE and Warroom CE before those repositories are public,
so their links 404 today and the lychee step in `seo.yml` excludes them.
This reads `config/unpublished-repos.json` and fails once a listed
repository answers publicly, which is the moment the exclusion starts
hiding a link that works. Removing an entry there means removing its
matching `--exclude` in the same change.
- The lychee step also excludes this repository's own
`blob/main/...` source links. A file added by a pull request does not
exist on `main` until it merges, so those links 404 for the run that
introduces them and no external check can judge them. They are checked
instead by `generate-code-reference.py --check`, against the working
tree that produced them, which is the only place the answer is knowable.
- `seo-score.mjs` and `seo-manage.mjs` create page-level and portfolio-level
reports from built evidence rather than subjective claims.

Expand Down
84 changes: 84 additions & 0 deletions scripts/check-unpublished-links.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Expire the external-link exclusions once the repositories behind them ship.
*
* The docs describe Flow CE and Warroom CE ahead of their publication, so those
* repository links 404 for a reader today and the lychee step excludes them. An
* exclusion that outlives its reason is worse than the 404 it hid: the day the
* repositories go public, nothing checks those links again and nobody
* remembers the flag is there.
*
* So this fails in exactly that case. While a repository is still private it
* says so and passes; the moment it answers publicly it fails and asks for the
* exclusion to be removed.
*/
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const configPath = path.join(root, 'config', 'unpublished-repos.json');
const workflowPath = path.join(root, '.github', 'workflows', 'seo.yml');

const config = JSON.parse(readFileSync(configPath, 'utf8'));
const repositories = config.repositories ?? [];
const workflow = readFileSync(workflowPath, 'utf8');

const failures = [];
const notes = [];

// GitHub's own shape for an owner or repository name. Interpolating anything
// else would let an edit to the config file choose where this script sends a
// request -- a path traversal, or a whole other host smuggled through a name.
const SLUG = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;

async function isPublic(owner, name) {
if (!SLUG.test(owner) || !SLUG.test(name)) {
throw new Error(`not a repository slug: ${JSON.stringify(`${owner}/${name}`)}`);
}
const url = new URL(`https://api.github.com/repos/${owner}/${name}`);
if (url.origin !== 'https://api.github.com' || !url.pathname.startsWith('/repos/')) {
throw new Error(`refusing to request ${url.origin}${url.pathname}`);
}
const response = await fetch(url, {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'flyto-docs-link-audit' },
redirect: 'error',
});
if (response.status === 404) return false;
if (response.status === 200) return true;
// Rate limiting or an outage must not turn into a false "now public", and
// must not silently pass either.
throw new Error(`GitHub answered ${response.status} for ${owner}/${name}`);
}

for (const entry of repositories) {
const { owner, name } = entry;
const slug = `${owner}/${name}`;
if (!workflow.includes(name)) {
failures.push(`${slug} is listed as unpublished but no lychee --exclude mentions it`);
continue;
}
let published;
try {
published = await isPublic(owner, name);
} catch (error) {
failures.push(`could not determine visibility of ${slug}: ${error.message}`);
continue;
}
if (published) {
failures.push(
`${slug} is public now. Remove its --exclude from .github/workflows/seo.yml and its entry ` +
`from config/unpublished-repos.json so the docs links to it are checked again.`,
);
} else {
notes.push(`${slug} is still private (${entry.reason ?? 'no reason recorded'})`);
}
}

if (failures.length > 0) {
console.error('unpublished link exclusions need attention:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}

console.log(`unpublished link exclusions still apply: ${repositories.length} repository(ies)`);
for (const note of notes) console.log(` ${note}`);
30 changes: 30 additions & 0 deletions scripts/generate-code-reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,31 @@ def render() -> str:
)


def unresolved_source_links(content: str) -> list[str]:
"""Self-links that name a file or line this working tree does not have.

These point at `blob/main`, so a file added by a pull request 404s until it
merges and the external link check cannot judge them. It is skipped for
that reason -- which leaves this as the only place they are checked at all,
against the tree that produced them.
"""
prefix = "https://github.com/flytohub/flyto-docs/blob/main/"
failures = []
for match in re.finditer(re.escape(prefix) + r"([^)#\s]+)#L(\d+)", content):
relative, line = match.group(1), int(match.group(2))
path = ROOT / relative
if not path.is_file():
failures.append(f"source link names a missing file: {relative}")
continue
try:
total = len(path.read_text(encoding="utf-8").splitlines())
except (OSError, UnicodeDecodeError):
continue
if line > total:
failures.append(f"source link names line {line} of {relative}, which has {total}")
return failures


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true")
Expand All @@ -132,6 +157,11 @@ def main() -> int:
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != content:
print(f"generated reference drift: {OUTPUT.relative_to(ROOT)}", file=sys.stderr)
return 1
broken = unresolved_source_links(OUTPUT.read_text(encoding="utf-8"))
if broken:
for failure in broken:
print(failure, file=sys.stderr)
return 1
print(f"docs code reference checked: {OUTPUT.relative_to(ROOT)}")
return 0
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
Expand Down
Loading