mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-13 13:48:28 +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"
|
||||
Reference in New Issue
Block a user