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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ jobs:
- name: Changelog fragments (docs/changes)
run: cargo xtask changelog --check
# The pages of the documentation site: every link goes to a page, a
# heading or a file that exists, and SUMMARY.md lists every guide. A
# change to the crates can break them too (a link to a renamed file).
# heading or a file that exists, and SUMMARY.md and docs/README.md
# list every guide. A change to the crates can break them too (a link
# to a renamed file).
- name: Documentation links (docs/SUMMARY.md)
run: cargo xtask docs --check
- name: cargo test -p xtask
Expand Down
5 changes: 4 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,10 @@ them. The staging rewrites a link to another page into that page of the
site, and a link into the source tree (`../crates/…`) into the file on
GitHub, on `main`. It fails on a link to a file or heading that does not
exist, and on a Markdown file directly in `docs/` that SUMMARY.md does not
list: a new guide goes into SUMMARY.md, under its part. `cargo xtask docs
list: a new guide goes into SUMMARY.md, under its part, and into
[docs/README.md](docs/README.md), the index GitHub shows for the folder. That
index is no page of the site, but its links are checked as a page's, and the
staging fails when it does not link a page SUMMARY.md lists. `cargo xtask docs
--check` runs the staging without mdBook; CI's `fmt` job runs it for a change
to the pages or to the Rust code, whose files the pages link.

Expand Down
51 changes: 51 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Forge documentation

The guides to running Forge and publishing to it. They are also a site, with
navigation and search: **<https://forgepkg.github.io/forge/>**.

The [README](../README.md) says what Forge is and starts a first instance in
its [quickstart](../README.md#quickstart);
[DEVELOPMENT.md](../DEVELOPMENT.md) is the guide to working on Forge itself.

## Running Forge

- **[Production deployment](deployment.md)**: required settings, TLS and the
reverse proxy, secret encryption, health probes, backup and restore, and
upgrades.
- **[Retention size budgets](retention.md)**: keep a repository under a
logical size: the budget, how size is counted, and what goes first.
- **[Importing from Nexus](nexus-import.md)**: copy a Nexus hosted repository
(NuGet, Docker) into Forge, with its publish dates.

## Formats

- **NuGet**: the README's [quickstart](../README.md#quickstart) points a
`dotnet` client at a feed, and the CI guides below publish to one.
- **[Docker / OCI registry](docker.md)**: image repositories (hosted, proxy,
group), naming, `docker login`, retention and garbage collection.
- **[npm registries](npm.md)**: npm packages for npm, pnpm, Yarn and Bun:
`.npmrc`, logins, publishing, dist-tags, proxies of `registry.npmjs.org`
and audits.
- **[Raw file repositories](raw.md)**: files under arbitrary paths: uploads
with curl, ranges and checksum sidecars, listings, and proxies of release
downloads.
- **[Format plugins](format-plugins.md)**: further package formats, built
into the server: design, operation, and writing one.

## CI and publishing

- **[Trusted publishing](trusted-publishing.md)**: short-lived workload
identity for GitHub Actions, GitLab CI and other OIDC issuers, and the
exchange of a job's token for an API key.
- **[GitLab CI with workload identity](gitlab-ci.md)**: step by step, every
job of a GitLab project restores from Forge, and only the release job
pushes. No secret in GitLab.
- **[CI with service accounts](ci-service-accounts.md)**: for pipelines that
cannot use workload identity, a read token and a release token in GitLab's
CI/CD variables.

## Releases

- **[Changelog](../CHANGELOG.md)**: release notes, upgrade notes and known
limitations. A change adds its entry as a fragment under
[changes/](changes/README.md).
3 changes: 3 additions & 0 deletions docs/changes/2.1.1/628.docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The `docs/` folder on GitHub has an index, [docs/README.md](docs/README.md):
the guides by part, with what each covers. `cargo xtask docs` checks its
links and fails when it leaves out a page of the site.
77 changes: 76 additions & 1 deletion xtask/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
//! * a link to a file or to a heading that does not exist is an error, and so
//! is a Markdown file directly under `docs/` that SUMMARY.md does not list.
//!
//! [`DOCS_INDEX`], the index GitHub shows for `docs/`, is no page of the site,
//! which has a start page and a sidebar of its own. Its links are checked as a
//! page's, and it must link every page, so that no guide is missing from it.
//!
//! `cargo xtask docs --check` stops after the staging, without mdBook.

use crate::changelog::repo_url;
Expand All @@ -25,6 +29,10 @@ use std::path::Path;
const DOCS: &str = "docs";
/// The table of contents: the site's pages, in their order.
const SUMMARY: &str = "docs/SUMMARY.md";
/// The index of the guides GitHub shows under the files of `docs/`. On the
/// site it would be `readme.md`, which mdBook turns into `index.md`, the
/// README's name there.
const DOCS_INDEX: &str = "docs/README.md";
/// mdBook's configuration, copied next to the staged pages.
const BOOK_TOML: &str = "docs/book.toml";
/// Where the pages are staged (`src/`, the book's `src`) and the site is
Expand Down Expand Up @@ -169,7 +177,7 @@ fn stage(tree: &impl Tree, repo: &str) -> Result<Vec<(String, String)>, String>
target.to_string()
});
for path in tree.markdown_in(DOCS)? {
if path != SUMMARY && !pages.contains_key(&path) {
if path != SUMMARY && path != DOCS_INDEX && !pages.contains_key(&path) {
errors.push(format!("{path}: no page of the site; list it in {SUMMARY}"));
}
}
Expand All @@ -194,6 +202,27 @@ fn stage(tree: &impl Tree, repo: &str) -> Result<Vec<(String, String)>, String>
site.rewrite(SUMMARY, &summary, &mut errors),
));

// The index of docs/ is not staged: only its links and its list count.
if tree.entry(DOCS_INDEX) == Some(Entry::File) {
let index = tree.read(DOCS_INDEX)?;
site.rewrite(DOCS_INDEX, &index, &mut errors);
let mut linked = BTreeSet::new();
map_links(&index, &mut |target| {
linked.extend(resolve(
DOCS_INDEX,
target.split('#').next().unwrap_or(target),
));
target.to_string()
});
for path in &order {
if !linked.contains(path) {
errors.push(format!(
"{DOCS_INDEX}: links no {path}, which {SUMMARY} lists"
));
}
}
}

if errors.is_empty() {
Ok(files)
} else {
Expand Down Expand Up @@ -651,6 +680,52 @@ mod tests {
}
}

#[test]
fn the_docs_index_is_no_page_but_links_every_page() {
let summary = "[Home](../README.md)\n\n- [Guide](guide.md)\n- [Other](other.md)\n";
let pages = [
("docs/SUMMARY.md", summary),
("README.md", "# Home\n## Quickstart\n"),
("docs/guide.md", "# Guide\n"),
("docs/other.md", "# Other\n"),
];
let tree = repo(
&[
&pages[..],
&[(
"docs/README.md",
"# Guides\n\n[Home](../README.md#quickstart), [the guide](guide.md), \
[the other](other.md#other).\n",
)],
]
.concat(),
);
let files = stage(&tree, REPO).expect("stages");
let names: Vec<_> = files.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(names, ["index.md", "guide.md", "other.md", "SUMMARY.md"]);

let tree = repo(
&[
&pages[..],
&[(
"docs/README.md",
"# Guides\n\n[the guide](guide.md#set-up), [gone](gone.md).\n",
)],
]
.concat(),
);
let err = stage(&tree, REPO).expect_err("incomplete index");
for problem in [
"docs/README.md: guide.md#set-up: docs/guide.md has no heading #set-up",
"docs/README.md: gone.md links to docs/gone.md, which does not exist",
"docs/README.md: links no README.md, which docs/SUMMARY.md lists",
"docs/README.md: links no docs/other.md, which docs/SUMMARY.md lists",
] {
assert!(err.contains(problem), "{problem} missing from:\n{err}");
}
assert!(!err.contains("no page of the site"), "{err}");
}

#[test]
fn resolves_relative_paths() {
assert_eq!(
Expand Down
Loading