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
62 changes: 50 additions & 12 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
name: Pages

# Publishes the landing page (site/, as it is) at / and the docs (docs-site/, built with
# Starlight from uptide-dev/uptide's docs/ folder) at /docs. The docs live in the other
# repository, so the site is rebuilt on every push to main here, once a day to pick up docs
# changes there, and by hand. See README.md.
# Starlight) at /docs, both from the latest uptide release: the `latest` dist-tag of `uptide`
# on npm, checked out as tag v<version> of uptide-dev/uptide (docs-site/scripts/release.mjs).
# A missing tag fails the build; it never falls back to main. The site is rebuilt on every push
# to main here, once a day so a new release reaches it within a day, and by hand. See README.md.
#
# A manual run with `ref` set builds a preview from that ref and uploads it as an artifact. It
# is never deployed: the deploy job runs only when the build reports it was built from the
# release tag, and only when no `ref` was given.
on:
push:
branches: [main]
schedule:
- cron: '23 5 * * *'
workflow_dispatch:
inputs:
ref:
description: 'Preview only: build from this uptide-dev/uptide ref (branch, tag or commit) instead of the latest release. Never deployed.'
required: false
default: ''
type: string

concurrency:
group: pages
group: pages-${{ inputs.ref || 'release' }}
cancel-in-progress: false

permissions: {}

jobs:
deploy:
build:
runs-on: ubuntu-latest
permissions:
# checkout reads the repository; deploy-pages publishes with the OIDC identity.
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
outputs:
release: ${{ steps.assemble.outputs.release }}
version: ${{ steps.assemble.outputs.version }}
ref: ${{ steps.assemble.outputs.ref }}
env:
# The page's own address, for its canonical and social-preview URLs. The one value to
# change when the page moves to a domain.
Expand All @@ -41,14 +50,17 @@ jobs:
node-version-file: docs-site/.node-version
cache: npm
cache-dependency-path: docs-site/package-lock.json
- name: Build the docs from uptide-dev/uptide@main
- name: Build the docs from the latest uptide release (or the preview ref)
working-directory: docs-site
env:
ASTRO_TELEMETRY_DISABLED: '1'
# Empty on push and schedule; set only by a manual run's `ref` input.
UPTIDE_REF: ${{ inputs.ref }}
run: |
npm ci
npm run build
- name: Put the landing page and the docs together
id: assemble
run: node scripts/assemble-site.mjs _site
- name: Write the site URL into the page
run: |
Expand All @@ -59,8 +71,34 @@ jobs:
fi
- name: No broken internal links
run: node scripts/check-links.mjs _site
- name: Say which uptide release the site reflects
env:
VERSION: ${{ steps.assemble.outputs.version }}
REF: ${{ steps.assemble.outputs.ref }}
RELEASE: ${{ steps.assemble.outputs.release }}
run: |
echo "### uptide ${VERSION} (${REF})" >> "$GITHUB_STEP_SUMMARY"
if [ "$RELEASE" = true ]; then
echo "Built from the latest release on npm; deployed by the next job." >> "$GITHUB_STEP_SUMMARY"
else
echo "Preview built from ${REF}: uploaded as an artifact, not deployed." >> "$GITHUB_STEP_SUMMARY"
fi
- uses: actions/upload-pages-artifact@v3
with:
path: _site

deploy:
needs: build
# Only a build from the npm release's tag, and never a manual run with a `ref`.
if: needs.build.outputs.release == 'true' && (github.event_name != 'workflow_dispatch' || inputs.ref == '')
runs-on: ubuntu-latest
permissions:
# deploy-pages publishes the uploaded artifact with the OIDC identity.
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Two parts, published together:
uptide-dev/uptide's `docs/` folder, checked out at build time and never copied here:
[docs-site/README.md](docs-site/README.md).

Both reflect the latest published release of uptide: the `latest` dist-tag on npm, checked out
as tag `v<version>`. Every built page names it in `<meta name="uptide-version">` and
`<meta name="uptide-ref">`.

## See it locally

```sh
Expand All @@ -32,8 +36,14 @@ python3 -m http.server 8000 --directory _site # http://localhost:8000 and /doc
## Publishing

`.github/workflows/pages.yml` builds the docs, puts them under `/docs` next to `site/`, checks
every internal link and deploys to GitHub Pages: on every push to `main`, daily (to pick up
docs changes in uptide-dev/uptide), and by hand. Its `SITE_URL` is the one value to change if
every internal link and deploys to GitHub Pages: on every push to `main`, daily (so a new
uptide release reaches the site within a day), and by hand. A missing release tag fails the
build rather than falling back to `main`.

**Previews:** Actions → Pages → Run workflow, with `ref` set to an uptide-dev/uptide branch,
tag or commit, builds the site from that ref and uploads it as the run's `github-pages`
artifact. It is never deployed: the deploy job runs only when the build reports it was built
from the release tag and no `ref` was given. Its `SITE_URL` is the one value to change if
the page moves to a domain; the docs take their canonical and `og:image` URLs from it too.

One-time setup:
Expand All @@ -52,7 +62,7 @@ One-time setup:
the URLs from `SITE_URL`, nothing loaded from another origin, the deploy's triggers and
permissions, exact docs dependency versions, no docs committed, and the docs transforms
(titles, sidebar, links).
- **The site build**: the docs build against uptide-dev/uptide@main, the landing page's
- **The site build**: the docs build against the latest uptide release, the landing page's
version and packs are rendered from that checkout and its terminal blocks checked against
their public sources (`scripts/landing-data.mjs`), and `scripts/check-links.mjs` finds no
broken internal link or anchor in landing + docs.
Expand Down
23 changes: 20 additions & 3 deletions docs-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@

The Uptide documentation at `/docs`, built with [Starlight](https://starlight.astro.build)
from [uptide-dev/uptide](https://github.com/uptide-dev/uptide)'s `docs/` folder. The docs
are never copied into this repository: every build checks out `main` of uptide-dev/uptide
are never copied into this repository: every build checks out the latest uptide release
(anonymous HTTPS) and generates the content collection from it.

## Which release

`scripts/release.mjs` asks npm for the `latest` dist-tag of `uptide` and checks out tag
`v<version>` of uptide-dev/uptide. If that tag does not exist the build fails, naming the
version and the tag; it never falls back to `main`. The build log prints what it resolved:

```
uptide source: uptide 0.6.1: npm latest, tag v0.6.1 (7df8235)
```

Every built page, landing and 404 included, carries it in its head:
`<meta name="uptide-version">`, `<meta name="uptide-ref">` and `<meta name="uptide-commit">`.
Links into the repository point at the tag; "Edit page" opens the file on `main`, since a tag
cannot be edited.

```sh
npm ci
npm run build # fetch the docs, then astro build into dist/
Expand All @@ -19,8 +34,10 @@ node scripts/check-links.mjs # no broken internal link or #
python3 -m http.server 8000 --directory _site # http://localhost:8000/docs/
```

`UPTIDE_CHECKOUT=/path/to/uptide` builds from a local checkout instead of cloning (offline
work, or previewing docs changes before they land); `UPTIDE_REF` picks another branch.
For previews, `UPTIDE_REF=<branch, tag or commit>` builds from another ref, and
`UPTIDE_CHECKOUT=/path/to/uptide` from a local checkout (offline work, or docs changes before
they land). A site built either way is marked not deployable, and the Pages workflow never
deploys it. `UPTIDE_NPM_REGISTRY` points the release lookup at another registry.

## How the content is made

Expand Down
6 changes: 6 additions & 0 deletions docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// The docs site, served at /docs next to the landing page (site/). Content comes from
// uptide-dev/uptide's docs/ folder at build time: see scripts/fetch-docs.mjs.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import { unified } from '@astrojs/markdown-remark';
import remarkUptideLinks from './src/remark-uptide-links.mjs';
import { sidebar } from './src/sidebar.mjs';
import { metaFields } from './scripts/release.mjs';

// The site's address, as in .github/workflows/pages.yml; canonical and og:image URLs use it.
const SITE_URL = process.env.SITE_URL || 'https://uptide-dev.github.io/';
const base = '/docs';
const siteDir = fileURLToPath(new URL('.', import.meta.url));
const ogImage = new URL('og.png', SITE_URL).href;
// The uptide release (or preview ref) the docs were built from, written by scripts/fetch-docs.mjs.
const source = JSON.parse(readFileSync(new URL('./src/generated/source.json', import.meta.url), 'utf8'));
const releaseMeta = metaFields(source).map((attrs) => ({ tag: 'meta', attrs }));

export default defineConfig({
site: SITE_URL,
Expand All @@ -38,6 +43,7 @@ export default defineConfig({
{ tag: 'meta', attrs: { property: 'og:image:height', content: '640' } },
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
{ tag: 'meta', attrs: { name: 'twitter:image', content: ogImage } },
...releaseMeta,
],
// Code blocks are a dark terminal in both themes, like the landing page's output blocks.
expressiveCode: {
Expand Down
68 changes: 49 additions & 19 deletions docs-site/scripts/fetch-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,27 @@
// node scripts/fetch-docs.mjs
//
// The docs are never committed here: the checkout (.cache/uptide) and the generated collection
// (src/content/docs) are both gitignored and rebuilt on every build. Environment:
//
// UPTIDE_REPO git URL to clone (default: https://github.com/uptide-dev/uptide.git)
// UPTIDE_REF branch to check out (default: main)
// UPTIDE_CHECKOUT an existing local checkout to read instead of cloning (offline work)
// (src/content/docs) are both gitignored and rebuilt on every build. What is checked out is
// decided by scripts/release.mjs: the tag of the latest uptide release on npm, or, for previews
// that are never deployed, UPTIDE_REF or UPTIDE_CHECKOUT. The resolved version, ref and commit
// are printed and recorded in src/generated/source.json, which the site's meta tags and the
// landing page read.
//
// For each Markdown page under docs/ (docs/screenshots/ is image evidence, not pages):
// - README.md becomes the index page;
// - the page's front matter title and description are kept; a page without them takes its
// title from its first `# ` heading and its description from its first paragraph;
// - the first `# ` heading is dropped, since Starlight renders the title as the page's h1;
// - editUrl points at the file on GitHub.
// - editUrl points at the file on main on GitHub (a release tag cannot be edited).
// Links are rewritten when the page is rendered, by src/remark-uptide-links.mjs, from the map
// this script writes to src/generated/source.json.
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { deployable, describe, resolveSource } from './release.mjs';

const site = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repoUrl = process.env.UPTIDE_REPO || 'https://github.com/uptide-dev/uptide.git';
const ref = process.env.UPTIDE_REF || 'main';
const github = 'https://github.com/uptide-dev/uptide';

const git = (args, cwd) =>
Expand All @@ -36,12 +35,15 @@ const git = (args, cwd) =>
stdio: ['ignore', 'pipe', 'inherit'],
}).trim();

function checkout() {
if (process.env.UPTIDE_CHECKOUT) return resolve(process.env.UPTIDE_CHECKOUT);
/** A shallow checkout of exactly `source.fetchRef` (a tag, branch or commit), nothing else. */
function checkout(source) {
if (source.kind === 'local') return resolve(source.path);
const dir = join(site, '.cache', 'uptide');
rmSync(dir, { recursive: true, force: true });
mkdirSync(dirname(dir), { recursive: true });
git(['-c', 'credential.helper=', 'clone', '--quiet', '--depth', '1', '--branch', ref, repoUrl, dir]);
mkdirSync(dir, { recursive: true });
git(['init', '--quiet'], dir);
git(['-c', 'credential.helper=', 'fetch', '--quiet', '--depth', '1', source.repoUrl, source.fetchRef], dir);
git(['checkout', '--quiet', '--detach', 'FETCH_HEAD'], dir);
return dir;
}

Expand Down Expand Up @@ -125,9 +127,17 @@ const hasKey = (data, key) => new RegExp(`^${key}\\s*:`, 'm').test(data);
export const slugOf = (docPath) =>
docPath.replace(/(^|\/)README\.md$/i, '$1').replace(/\.md$/i, '').replace(/\/$/, '');

function main() {
const repo = checkout();
const sha = git(['rev-parse', 'HEAD'], repo);
async function main() {
const source = await resolveSource();
const repo = checkout(source);
source.sha = git(['rev-parse', 'HEAD'], repo);
const version = JSON.parse(readFileSync(join(repo, 'packages/cli/package.json'), 'utf8')).version;
if (source.kind === 'release' && version !== source.version)
throw new Error(`tag ${source.ref} has packages/cli/package.json version ${version}, but npm's latest is ${source.version}`);
source.version = version;
// Links into the repository point at what was built; a local checkout has no ref to link to.
const linkRef = source.kind === 'local' ? 'main' : source.ref;
console.log(`uptide source: ${describe(source)}`);
const docsDir = join(repo, 'docs');
if (!existsSync(join(docsDir, 'README.md'))) throw new Error(`${docsDir}/README.md not found`);

Expand All @@ -150,7 +160,9 @@ function main() {
}
if (!hasKey(data, 'description') && read.description)
extra.push(`description: ${JSON.stringify(read.description)}`);
if (!hasKey(data, 'editUrl')) extra.push(`editUrl: ${JSON.stringify(`${github}/edit/${ref}/docs/${file}`)}`);
// A tag cannot be edited: "Edit page" opens the file on main.
if (!hasKey(data, 'editUrl'))
extra.push(`editUrl: ${JSON.stringify(`${github}/edit/main/docs/${file}`)}`);
const front = [data, ...extra].filter(Boolean).join('\n');
const target = join(out, slug === '' ? 'index.md' : `${slug}.md`);
mkdirSync(dirname(target), { recursive: true });
Expand All @@ -162,9 +174,27 @@ function main() {
mkdirSync(generated, { recursive: true });
writeFileSync(
join(generated, 'source.json'),
`${JSON.stringify({ repo: github, ref, sha, checkout: relative(site, repo) || '.', pages }, null, 2)}\n`,
`${JSON.stringify(
{
kind: source.kind,
deployable: deployable(source),
version: source.version,
ref: source.ref,
sha: source.sha,
repo: github,
linkRef,
checkout: relative(site, repo) || '.',
pages,
},
null,
2,
)}\n`,
);
console.log(`docs: ${files.length} pages from ${github}@${sha.slice(0, 7)} (${ref})`);
console.log(`docs: ${files.length} pages from ${github}@${source.sha.slice(0, 7)} (${source.ref})`);
}

if (process.argv[1] === fileURLToPath(import.meta.url)) main();
if (process.argv[1] === fileURLToPath(import.meta.url))
main().catch((error) => {
console.error(`fetch-docs: ${error.message}`);
process.exit(1);
});
Loading
Loading