mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-11 04:38:18 +00:00
Stage npm releases from a release workflow, add CI, bump reallysimpledocs to 1.0.14 (#215)
* Stage npm releases from a release workflow, add CI checks, bump reallysimpledocs to 1.0.14 * Announce releases on Discord only after the site deploys --------- Co-authored-by: grugbraindev <178141691+grugbraindev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
site/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# Package: build, check every export exists and the JS parses, list the tarball.
|
||||
- run: npm run check
|
||||
|
||||
# Docs site: build it and check llms.txt links to the .md exports.
|
||||
- run: npm run check:site
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Deploy site on release
|
||||
|
||||
# Cloudflare Workers Builds deploys the site from the `release` branch.
|
||||
# Publishing a GitHub release moves `release` to the released tag.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag to deploy (for example 1.1.0)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
move-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Point release branch at tag
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
run: |
|
||||
SHA=$(git rev-parse "refs/tags/$TAG^{commit}")
|
||||
git push --force origin "$SHA:refs/heads/release"
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Announce release on Discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to announce
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
announce:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Send Discord announcement
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
RELEASE_TAG_INPUT: ${{ inputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
test -n "$DISCORD_WEBHOOK_URL"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
test -n "$RELEASE_TAG_INPUT"
|
||||
curl --fail-with-body \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$REPO/releases/tags/$RELEASE_TAG_INPUT" \
|
||||
> release.json
|
||||
else
|
||||
jq '.release' "$GITHUB_EVENT_PATH" > release.json
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.prerelease' release.json)" = "true" ]; then
|
||||
echo "Skipping prerelease."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg repo "$REPO" \
|
||||
--arg name "$(jq -r '.name // ""' release.json)" \
|
||||
--arg tag "$(jq -r '.tag_name' release.json)" \
|
||||
--arg url "$(jq -r '.html_url' release.json)" \
|
||||
--arg body "$(jq -r '.body // ""' release.json)" \
|
||||
'($body | if length > 1500 then .[:1500] + "...\n\n[Read more](" + $url + ")" else . end) as $description |
|
||||
{
|
||||
username: "Basecoat Releases",
|
||||
allowed_mentions: { parse: [] },
|
||||
embeds: [{
|
||||
title: ($repo + " " + $tag),
|
||||
url: $url,
|
||||
description: ((if $name != "" then "**" + $name + "**\n\n" else "" end) + $description),
|
||||
color: 5793266
|
||||
}]
|
||||
}' > payload.json
|
||||
|
||||
curl --fail-with-body \
|
||||
--retry 3 \
|
||||
--retry-all-errors \
|
||||
--retry-delay 2 \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d @payload.json \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
@@ -0,0 +1,277 @@
|
||||
name: Release
|
||||
|
||||
# Publishing a GitHub release (not a prerelease):
|
||||
# 1. publish: checks the tag is on main and matches package.json, runs the checks,
|
||||
# then stages the version on npm with `npm stage publish`, using trusted publishing
|
||||
# (OIDC, no token). A maintainer then approves it on npmjs.com (Staged Packages tab)
|
||||
# or with `npm stage approve <stage-id>`, with 2FA. Until then the version is not
|
||||
# installable.
|
||||
# 2. deploy-site: waits until the version is live on npm (polls every 30 seconds for
|
||||
# up to about 5h50m), then points the `release` branch at the tag. Cloudflare
|
||||
# Workers Builds deploys basecoatui.com from `release`, so the docs never describe
|
||||
# a version npm does not serve. If you approve later than that, re-run the failed
|
||||
# jobs once approved (deploy-site, then announce).
|
||||
# 3. announce: posts the release to Discord (DISCORD_WEBHOOK_URL secret), only after
|
||||
# deploy-site succeeded: the version is on npm and `release` has moved.
|
||||
#
|
||||
# Running this workflow manually never publishes to npm. Pick a mode:
|
||||
# deploy move `release` to the tag (redeploy or roll back the site),
|
||||
# only if that version is already on npm. No Discord post.
|
||||
# announce post the tag's GitHub release to Discord again. No deploy.
|
||||
# deploy and announce both, announcing only if the deploy succeeded.
|
||||
#
|
||||
# Prereleases are ignored by every job.
|
||||
#
|
||||
# npm trusted publisher settings (allow `npm stage publish` only): repository
|
||||
# hunvreus/basecoat, workflow release.yml, no environment. Renaming this file breaks
|
||||
# publishing.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag (for example 1.1.0). Never publishes to npm.
|
||||
required: true
|
||||
type: string
|
||||
mode:
|
||||
description: "deploy: move the site to this tag (version must be on npm). announce: post the release to Discord again. deploy and announce: both."
|
||||
required: true
|
||||
type: choice
|
||||
default: deploy
|
||||
options:
|
||||
- deploy
|
||||
- announce
|
||||
- deploy and announce
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release' && github.event.release.prerelease == false
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: refs/tags/${{ github.event.release.tag_name }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check the tag is on main
|
||||
run: |
|
||||
git fetch --no-tags origin main
|
||||
if ! git merge-base --is-ancestor HEAD FETCH_HEAD; then
|
||||
echo "::error::Tag $TAG is not on main. Release from a commit on main."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
# No dependency cache in a publishing job (cache poisoning).
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Ensure npm supports staged publishing (>= 11.15.0)
|
||||
run: |
|
||||
if ! node -e 'const [a,b]=process.argv[1].split(".").map(Number);process.exit(a>11||(a===11&&b>=15)?0:1)' "$(npm --version)"; then
|
||||
npm install -g npm@^11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Check package.json version matches the tag
|
||||
id: version
|
||||
run: |
|
||||
NAME=$(node -p 'require("./package.json").name')
|
||||
VERSION=$(node -p 'require("./package.json").version')
|
||||
if [ "$VERSION" != "${TAG#v}" ]; then
|
||||
echo "::error::package.json version is $VERSION but the release tag is $TAG. Bump package.json and tag that commit."
|
||||
exit 1
|
||||
fi
|
||||
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check whether this version is already on npm
|
||||
id: npm
|
||||
env:
|
||||
NAME: ${{ steps.version.outputs.name }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/$NAME/$VERSION")
|
||||
case "$STATUS" in
|
||||
200)
|
||||
echo "::notice::$NAME@$VERSION is already on npm. Skipping publish."
|
||||
echo "published=true" >> "$GITHUB_OUTPUT" ;;
|
||||
404)
|
||||
echo "published=false" >> "$GITHUB_OUTPUT" ;;
|
||||
*)
|
||||
echo "::error::Unexpected HTTP $STATUS from the npm registry."
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.npm.outputs.published == 'false'
|
||||
run: npm ci
|
||||
|
||||
# Builds dist/ and templates/, checks every export exists, and lists the tarball.
|
||||
# `npm stage publish` builds again through the prepack script.
|
||||
- name: Run checks
|
||||
if: steps.npm.outputs.published == 'false'
|
||||
run: npm run check
|
||||
|
||||
# Short-lived OIDC tokens cannot run `npm stage list`, so an already-staged version
|
||||
# is detected from the registry's conflict error instead: staged and published
|
||||
# versions share one version index, so re-staging the same version is refused.
|
||||
- name: Stage on npm
|
||||
if: steps.npm.outputs.published == 'false'
|
||||
run: |
|
||||
set +e
|
||||
npm stage publish --provenance 2>&1 | tee "$RUNNER_TEMP/stage.log"
|
||||
STATUS=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "::notice::Staged. Approve it on npmjs.com (Staged Packages tab) or with npm stage approve, using 2FA. The site deploys once it is live."
|
||||
elif grep -Eqi 'E409|EPUBLISHCONFLICT|previously published|already (been )?(staged|published|exists)|cannot (publish|stage) over' "$RUNNER_TEMP/stage.log"; then
|
||||
echo "::notice::This version is already staged or published on npm. Skipping."
|
||||
else
|
||||
exit "$STATUS"
|
||||
fi
|
||||
|
||||
deploy-site:
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.publish.result == 'success' || needs.publish.result == 'skipped') &&
|
||||
((github.event_name == 'release' && github.event.release.prerelease == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.mode != 'announce'))
|
||||
# GitHub-hosted jobs stop at 6 hours. The wait below gives up before that.
|
||||
timeout-minutes: 355
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check the tag is on main
|
||||
id: tag
|
||||
run: |
|
||||
SHA=$(git rev-parse --verify "refs/tags/$TAG^{commit}")
|
||||
if ! git merge-base --is-ancestor "$SHA" origin/main; then
|
||||
echo "::error::Tag $TAG is not on main. Only tags on main can go live."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "sha=$SHA"
|
||||
echo "name=$(git show "$SHA:package.json" | jq -r .name)"
|
||||
echo "version=$(git show "$SHA:package.json" | jq -r .version)"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Wait until the version is live on npm
|
||||
env:
|
||||
NAME: ${{ steps.tag.outputs.name }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
# Manual runs do not wait: the version must already be on npm.
|
||||
MAX_WAIT: ${{ github.event_name == 'release' && '21000' || '0' }}
|
||||
run: |
|
||||
while :; do
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/$NAME/$VERSION" || echo 000)
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "$NAME@$VERSION is live on npm."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$SECONDS" -ge "$MAX_WAIT" ]; then
|
||||
if [ "$MAX_WAIT" = "0" ]; then
|
||||
echo "::error::$NAME@$VERSION is not on npm (HTTP $STATUS). A manual run only deploys versions that are already published."
|
||||
else
|
||||
echo "::error::$NAME@$VERSION is still not live on npm after $((MAX_WAIT / 60)) minutes. Approve the staged package on npmjs.com (Staged Packages tab), then re-run this deploy-site job (Re-run failed jobs). If you rejected it, nothing needs to happen: the site was not deployed."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for $NAME@$VERSION on npm (HTTP $STATUS). Approve the staged package on npmjs.com. Checking again in 30 seconds."
|
||||
sleep 30
|
||||
done
|
||||
|
||||
- name: Point release branch at tag
|
||||
env:
|
||||
SHA: ${{ steps.tag.outputs.sha }}
|
||||
run: git push --force origin "$SHA:refs/heads/release"
|
||||
|
||||
# Same message as the old discord-release.yml.
|
||||
announce:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-site
|
||||
if: >-
|
||||
always() &&
|
||||
((needs.deploy-site.result == 'success' &&
|
||||
((github.event_name == 'release' && github.event.release.prerelease == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'deploy and announce'))) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'announce'))
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Send Discord announcement
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
RELEASE_TAG_INPUT: ${{ inputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
test -n "$DISCORD_WEBHOOK_URL"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
test -n "$RELEASE_TAG_INPUT"
|
||||
curl --fail-with-body \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$REPO/releases/tags/$RELEASE_TAG_INPUT" \
|
||||
> release.json
|
||||
else
|
||||
jq '.release' "$GITHUB_EVENT_PATH" > release.json
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.prerelease' release.json)" = "true" ]; then
|
||||
echo "Skipping prerelease."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg repo "$REPO" \
|
||||
--arg name "$(jq -r '.name // ""' release.json)" \
|
||||
--arg tag "$(jq -r '.tag_name' release.json)" \
|
||||
--arg url "$(jq -r '.html_url' release.json)" \
|
||||
--arg body "$(jq -r '.body // ""' release.json)" \
|
||||
'($body | if length > 1500 then .[:1500] + "...\n\n[Read more](" + $url + ")" else . end) as $description |
|
||||
{
|
||||
username: "Basecoat Releases",
|
||||
allowed_mentions: { parse: [] },
|
||||
embeds: [{
|
||||
title: ($repo + " " + $tag),
|
||||
url: $url,
|
||||
description: ((if $name != "" then "**" + $name + "**\n\n" else "" end) + $description),
|
||||
color: 5793266
|
||||
}]
|
||||
}' > payload.json
|
||||
|
||||
curl --fail-with-body \
|
||||
--retry 3 \
|
||||
--retry-all-errors \
|
||||
--retry-delay 2 \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d @payload.json \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
+26
-12
@@ -2,13 +2,14 @@
|
||||
|
||||
- Support development by sponsoring the author: https://github.com/sponsors/hunvreus
|
||||
- Submit pull requests (PRs) against the `main` branch.
|
||||
- For branches, we have:
|
||||
- `main` is our production and default branch. This is what we deploy to https://basecoatui.com and publish to https://www.npmjs.com/package/basecoat-css.
|
||||
- Branches:
|
||||
- `main` is the default branch. All work lands here, and https://main-basecoat.hunvreus.workers.dev previews it.
|
||||
- `release` is what https://basecoatui.com deploys. Only the release workflow moves it (see below).
|
||||
- New features are worked in `feature/name-of-the-feature` branches.
|
||||
- Issues are addressed in `issue/123-main-issue` branches.
|
||||
- When ready, open a PR against `main`.
|
||||
- Keep changes focused: one feature or fix per PR.
|
||||
- Test locally before submitting.
|
||||
- Test locally before submitting: `npm run check` (package) and `npm run check:site` (docs site). CI runs both on every PR.
|
||||
- Follow existing code style.
|
||||
|
||||
## Cloudflare Workers deploys
|
||||
@@ -24,19 +25,32 @@ Workers Builds settings:
|
||||
- Build command: `npm run docs:build`
|
||||
- Deploy command: `npx wrangler deploy`
|
||||
- Non-production branch deploy command: `npx wrangler versions upload`
|
||||
- Production branch: `main`
|
||||
- Production branch: `release`
|
||||
- Enable builds for non-production branches for branch preview URLs.
|
||||
|
||||
Thanks for helping!
|
||||
|
||||
## Releases and the docs site
|
||||
## Releasing
|
||||
|
||||
The site at basecoatui.com deploys from the `release` branch, not `main`, so the docs always match the version on npm. Nobody commits to `release` directly.
|
||||
|
||||
To release:
|
||||
To cut a release:
|
||||
|
||||
1. Bump the version in `package.json`, commit, tag (for example `1.1.0`) and push.
|
||||
2. Run `npm publish`.
|
||||
3. Publish a GitHub release for the tag, for example `gh release create 1.1.0 --generate-notes`.
|
||||
1. Bump the version on `main`: `npm version 1.1.0 --no-git-tag-version`, then commit (for example "Prepare 1.1.0 release") and push.
|
||||
2. Tag that commit with the same version, with no `v` prefix, and push: `git tag 1.1.0 && git push origin 1.1.0`.
|
||||
3. Publish a GitHub release for the tag: `gh release create 1.1.0 --generate-notes --latest --verify-tag`.
|
||||
4. Approve the staged version on npmjs.com, in the package's Staged Packages tab, or with `npm stage approve <stage-id>`. Both ask for 2FA.
|
||||
|
||||
Publishing the release runs `.github/workflows/deploy-site.yml`, which points `release` at the tag. Cloudflare then builds and deploys the site. To redeploy or roll back, run that workflow manually with the tag you want live.
|
||||
Publishing the release runs `.github/workflows/release.yml`:
|
||||
|
||||
- **publish** checks that the tag is on `main` and matches the version in `package.json`, runs `npm run check`, and stages the version on npm with `npm stage publish`, using trusted publishing. A staged version is not installable until a maintainer approves it. If the version is already staged or published, this step is skipped.
|
||||
- **deploy-site** waits until the version is live on npm, then points `release` at the tag, and Cloudflare builds and deploys the site. It checks every 30 seconds for up to about 5h50m. If you approve later than that, the job fails with a message: after approving, use Re-run failed jobs on that run, which re-runs deploy-site and then announce. If staging fails or you reject the version, the site is not deployed.
|
||||
- **announce** posts the release notes to Discord, only after deploy-site succeeded. By then the version is installable from npm and `release` has moved, so Cloudflare is building the new site (that takes a few minutes).
|
||||
|
||||
Do not run `npm publish` by hand. Versions reach npm only through this workflow and your approval. The workflow needs no npm token and adds a provenance statement linking each version to the commit it was built from. Prereleases are not staged, deployed or announced.
|
||||
|
||||
You can also run the Release workflow manually from the Actions tab, with a tag and a mode. A manual run never publishes to npm.
|
||||
|
||||
- `deploy` (default) redeploys or rolls back the site: it moves `release` to the tag, only if that version is already on npm. It doesn't post to Discord.
|
||||
- `announce` posts the tag's GitHub release to Discord again, for example after a failed post. It doesn't deploy.
|
||||
- `deploy and announce` does both, and announces only if the deploy succeeded.
|
||||
|
||||
Thanks for helping!
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.js",
|
||||
"prepack": "node ./scripts/build.js",
|
||||
"check": "npm run build && node ./scripts/check.js && npm pack --dry-run --ignore-scripts",
|
||||
"check:site": "npm run docs:build && node ./scripts/check.js site",
|
||||
"docs:build": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run build",
|
||||
"docs:dev": "npm --prefix site run dev",
|
||||
"docs:preview": "npm run docs:build && npm --prefix site run preview",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Cheap sanity checks. Run after a build.
|
||||
// node scripts/check.js package: every export target exists, JS parses
|
||||
// node scripts/check.js site docs site: site/dist built, llms.txt links to .md exports
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const errors = [];
|
||||
|
||||
function checkPackage() {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
|
||||
for (const [key, target] of Object.entries(pkg.exports)) {
|
||||
const file = path.join(root, target);
|
||||
if (target.includes('*')) {
|
||||
const dir = path.dirname(file.slice(0, file.indexOf('*')) + 'x');
|
||||
if (!fs.existsSync(dir) || fs.readdirSync(dir).length === 0) {
|
||||
errors.push(`export "${key}": no files in ${path.relative(root, dir)}`);
|
||||
}
|
||||
} else if (!fs.existsSync(file) || fs.statSync(file).size === 0) {
|
||||
errors.push(`export "${key}": ${target} is missing or empty`);
|
||||
}
|
||||
}
|
||||
|
||||
const jsDir = path.join(root, 'dist', 'js');
|
||||
for (const name of fs.existsSync(jsDir) ? fs.readdirSync(jsDir) : []) {
|
||||
if (!name.endsWith('.js')) continue;
|
||||
try {
|
||||
execFileSync(process.execPath, ['--check', path.join(jsDir, name)], { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
const message = error.stderr.toString().split('\n').find((line) => /Error/.test(line));
|
||||
errors.push(`dist/js/${name} does not parse: ${message || 'syntax error'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkSite() {
|
||||
const dist = path.join(root, 'site', 'dist');
|
||||
for (const file of ['index.html', 'llms.txt', 'llms-full.txt']) {
|
||||
if (!fs.existsSync(path.join(dist, file))) errors.push(`site/dist/${file} is missing`);
|
||||
}
|
||||
const llms = path.join(dist, 'llms.txt');
|
||||
if (!fs.existsSync(llms)) return;
|
||||
const links = [...fs.readFileSync(llms, 'utf8').matchAll(/^- \[[^\]]*\]\(([^)]+)\)/gm)].map((m) => m[1]);
|
||||
if (links.length === 0) errors.push('site/dist/llms.txt has no links');
|
||||
for (const link of links) {
|
||||
if (!link.endsWith('.md')) errors.push(`site/dist/llms.txt links to ${link}, not a .md export`);
|
||||
}
|
||||
}
|
||||
|
||||
const target = process.argv[2] || 'package';
|
||||
if (target === 'package') checkPackage();
|
||||
else if (target === 'site') checkSite();
|
||||
else {
|
||||
console.error(`Unknown target "${target}". Use "package" or "site".`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`${target} check failed:\n- ${errors.join('\n- ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`${target} check passed.`);
|
||||
Generated
+5
-203
@@ -10,7 +10,7 @@
|
||||
"htmx.org": "^2.0.10",
|
||||
"lucide-static": "^1.17.0",
|
||||
"nunjucks": "^3.2.4",
|
||||
"reallysimpledocs": "^1.0.12"
|
||||
"reallysimpledocs": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^6.4.6",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"..": {
|
||||
"name": "basecoat-css",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.17",
|
||||
@@ -812,7 +812,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -835,7 +834,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -858,7 +856,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -875,7 +872,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -892,7 +888,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -909,7 +904,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -926,7 +920,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -943,7 +936,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -960,7 +952,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -977,7 +968,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -994,7 +984,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1011,7 +1000,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1028,7 +1016,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1051,7 +1038,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1074,7 +1060,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1097,7 +1082,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1120,7 +1104,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1143,7 +1126,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1166,7 +1148,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1189,7 +1170,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1212,7 +1192,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1232,7 +1211,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1252,7 +1230,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1272,7 +1249,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2439,20 +2415,6 @@
|
||||
"resolved": "..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/blake3-wasm": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
|
||||
@@ -2466,20 +2428,6 @@
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
@@ -2530,32 +2478,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
|
||||
@@ -3160,20 +3082,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/flattie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
|
||||
@@ -3239,20 +3147,6 @@
|
||||
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -3575,20 +3469,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-decimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
||||
@@ -3614,31 +3494,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hexadecimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
||||
@@ -3682,17 +3537,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
@@ -5502,38 +5346,10 @@
|
||||
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/reallysimpledocs": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/reallysimpledocs/-/reallysimpledocs-1.0.12.tgz",
|
||||
"integrity": "sha512-nZIvrHdDcr2TImpT3TlhjLb3420xikeyXUqOuuvfosuEPgbfTYvnfg9MpgLvOciQLCjsuSGdpQzNrdW5MTVurQ==",
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/reallysimpledocs/-/reallysimpledocs-1.0.14.tgz",
|
||||
"integrity": "sha512-TRiCxZZ3j6+AacD3HzeYr1+6soUdJN3ZkNtrdYON2OtiqlLc5qb0GHV1c4k+49l9z4/20FQ0qGH2r0YXoWwOQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/markdown-remark": "^7.2.0",
|
||||
@@ -6231,20 +6047,6 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"htmx.org": "^2.0.10",
|
||||
"lucide-static": "^1.17.0",
|
||||
"nunjucks": "^3.2.4",
|
||||
"reallysimpledocs": "^1.0.12"
|
||||
"reallysimpledocs": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^6.4.6",
|
||||
|
||||
Reference in New Issue
Block a user