diff --git a/Makefile b/Makefile index fec9cd59..e3d0d02e 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,10 @@ doctor: ## Check that your Node/npm versions match .nvmrc and the packageManager src-tag-and-push: ## Tag and push the web client source code to the repository $(ROOT)/VITE_VERSION.sh $(VERSION) && git push --tags; git push + $(ROOT)/publish-gh-release.sh $(VERSION) + +gen-release-notes: ## Generate web-client release notes draft from git log NEEDS VERSION + $(ROOT)/gen-release-notes.sh $(VERSION) upload-assets: ## Upload hashed JS/CSS assets with long cache duration export AWS_MAX_ATTEMPTS=10 AWS_RETRY_MODE=standard && \ diff --git a/README.md b/README.md index eb7c1189..01d854ed 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,63 @@ This project is designed to be deployed as a secure static site using Amazon Clo We use [HashiCorp Terraform](https://www.terraform.io/) to deploy this website. +### Releasing + +The recommended workflow is to release a new tagged version to the test site first, +sanity-check it, then promote that same build to production. + +**1. Tag and release to test** (requires `VERSION` in `vX.Y.Z` form): + +```bash +make release-live-update-to-testsliderule VERSION=v4.5.3 +``` + +This step does all the version-stamping work: + +1. **Generates and commits the release notes** for the version from the git commit + subjects since the previous tag, then creates and pushes the annotated `vX.Y.Z` + tag (`src-tag-and-push` → `VITE_VERSION.sh` → `gen-release-notes.sh`). +2. **Mirrors the notes to a GitHub Release** (`publish-gh-release.sh`). This step is + non-fatal — if the `gh` CLI is missing or unauthenticated it warns and is skipped, + so it never blocks a deploy. +3. **Builds and deploys** the client to **testsliderule.org** — the tag is injected + as `VITE_APP_VERSION`, assets are uploaded to S3, and CloudFront is invalidated. + +Sanity-check the result at . + +**2. Promote the same tagged build to production:** + +```bash +make live-update-slideruleearth +``` + +This rebuilds the current checkout and deploys it to **slideruleearth.io**. It does +**not** create a new tag — the build reads the existing tag via `git describe --tags`, +so the release notes and the GitHub Release are created exactly once, during step 1. + +> A one-shot `make release-live-update-to-slideruleearth VERSION=v4.5.3` also exists +> (it tags and deploys straight to production), but the test-first workflow above is +> recommended. + +### Release notes + +Web-client release notes are maintained in this repo as one Markdown file per +version under `web-client/src/assets/content/release-notes/` (e.g. `v4.5.3.md`). +They are bundled into the client at build time and shown in the **Web Client +Releases** tab on the landing page (the **SlideRule Releases** tab continues to +show the platform/server notes from the docs site). + +The release flow auto-generates a draft from the commits since the previous tag. +To curate the notes before releasing, generate the draft first, edit it, then run +the release (step 1 above) — an existing file is preserved (use `--force` to +regenerate): + +```bash +make gen-release-notes VERSION=v4.5.3 # writes .../release-notes/v4.5.3.md +# edit the generated file... +make release-live-update-to-testsliderule VERSION=v4.5.3 +``` + ## License This project is licensed under the following University of Washington Open Source License - see the [LICENSE](LICENSE) file for details. diff --git a/VITE_VERSION.sh b/VITE_VERSION.sh index 5b88c420..f0abdd7a 100755 --- a/VITE_VERSION.sh +++ b/VITE_VERSION.sh @@ -16,6 +16,21 @@ if git tag -l | grep -w $VERSION; then exit 1 fi +# +# Generate (if missing) and commit the web-client release notes for this version +# BEFORE tagging, so the tag — and the build, which reads the tag for +# VITE_APP_VERSION — includes the notes file. A pre-existing file (e.g. one a +# developer generated via `make gen-release-notes` and hand-edited) is preserved. +# +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(git rev-parse --show-toplevel)" +NOTES_FILE="$ROOT_DIR/web-client/src/assets/content/release-notes/$VERSION.md" +"$SCRIPT_DIR/gen-release-notes.sh" "$VERSION" +git add "$NOTES_FILE" +if ! git diff --cached --quiet -- "$NOTES_FILE"; then + git commit -m "Add release notes for $VERSION" +fi + # # Create tag and acrhive # diff --git a/gen-release-notes.sh b/gen-release-notes.sh new file mode 100755 index 00000000..116983e0 --- /dev/null +++ b/gen-release-notes.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Generate a web-client release-notes markdown file for a given version, +# derived from the git commit subjects since the previous tag. +# +# Usage: +# gen-release-notes.sh [--force] +# +# Writes web-client/src/assets/content/release-notes/.md +# These files are bundled into the web client at build time (see LandingView.vue) +# and mirrored to a GitHub Release at tag time (see publish-gh-release.sh). +# +# Idempotent: an existing file is left untouched unless --force is passed. This +# lets a developer pre-generate the draft (`make gen-release-notes VERSION=...`), +# hand-edit it, and have the tag flow preserve their edits. +# +set -euo pipefail + +VERSION="${1:-}" +FORCE="${2:-}" + +if [[ "$VERSION" != "v"*"."*"."* ]]; then + echo "Usage: gen-release-notes.sh [--force]" >&2 + echo "Invalid version number: '$VERSION'" >&2 + exit 1 +fi + +ROOT_DIR="$(git rev-parse --show-toplevel)" +NOTES_DIR="$ROOT_DIR/web-client/src/assets/content/release-notes" +NOTES_FILE="$NOTES_DIR/$VERSION.md" + +if [[ -f "$NOTES_FILE" && "$FORCE" != "--force" ]]; then + echo "Release notes already exist: $NOTES_FILE (use --force to regenerate)" + exit 0 +fi + +mkdir -p "$NOTES_DIR" + +# Previous tag — computed before the new tag is created. Falls back to the full +# history when no tags exist yet (first release). +PREV_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)" +if [[ -n "$PREV_TAG" ]]; then + RANGE="$PREV_TAG..HEAD" +else + RANGE="HEAD" +fi + +BODY="$(git log "$RANGE" --no-merges --pretty='- %s (%h)')" +if [[ -z "$BODY" ]]; then + BODY="- No notable changes." +fi + +DATE="$(date +%Y-%m-%d)" + +{ + echo "# $VERSION — $DATE" + echo + echo "$BODY" +} > "$NOTES_FILE" + +echo "Wrote $NOTES_FILE" diff --git a/publish-gh-release.sh b/publish-gh-release.sh new file mode 100755 index 00000000..cd2e1064 --- /dev/null +++ b/publish-gh-release.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# +# Mirror a web-client release-notes file to a GitHub Release. +# +# Usage: +# publish-gh-release.sh +# +# The committed markdown file remains the source of truth and is bundled into the +# client; this just publishes the same notes to github.com so the Releases page +# is populated. Run AFTER the tag has been pushed (see `src-tag-and-push`). +# +# This is intentionally non-fatal: a missing/unauthenticated `gh`, or a GitHub +# API hiccup, must never break a deploy. It warns and exits 0 in those cases. +# +set -uo pipefail + +VERSION="${1:-}" + +if [[ "$VERSION" != "v"*"."*"."* ]]; then + echo "Usage: publish-gh-release.sh " >&2 + echo "Invalid version number: '$VERSION'" >&2 + exit 1 +fi + +ROOT_DIR="$(git rev-parse --show-toplevel)" +NOTES_FILE="$ROOT_DIR/web-client/src/assets/content/release-notes/$VERSION.md" + +if [[ ! -f "$NOTES_FILE" ]]; then + echo "WARNING: no release-notes file at $NOTES_FILE — skipping GitHub Release." >&2 + exit 0 +fi + +if ! command -v gh >/dev/null 2>&1; then + echo "WARNING: 'gh' CLI not found — skipping GitHub Release for $VERSION." >&2 + exit 0 +fi + +if ! gh auth status >/dev/null 2>&1; then + echo "WARNING: 'gh' not authenticated — skipping GitHub Release for $VERSION." >&2 + exit 0 +fi + +# Update in place if the release already exists, otherwise create it. +if gh release view "$VERSION" >/dev/null 2>&1; then + echo "Updating existing GitHub Release $VERSION..." + gh release edit "$VERSION" --title "$VERSION" --notes-file "$NOTES_FILE" \ + || echo "WARNING: failed to update GitHub Release $VERSION (continuing)." >&2 +else + echo "Creating GitHub Release $VERSION..." + gh release create "$VERSION" --title "$VERSION" --notes-file "$NOTES_FILE" \ + || echo "WARNING: failed to create GitHub Release $VERSION (continuing)." >&2 +fi + +exit 0 diff --git a/web-client/src/assets/content/release-notes/v4.4.4.md b/web-client/src/assets/content/release-notes/v4.4.4.md new file mode 100644 index 00000000..f2c1d7a8 --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.4.4.md @@ -0,0 +1,3 @@ +# v4.4.4 — 2026-05-13 + +- Decouple plot and map point caps (#1070) (bd91a1b9) diff --git a/web-client/src/assets/content/release-notes/v4.4.5.md b/web-client/src/assets/content/release-notes/v4.4.5.md new file mode 100644 index 00000000..3318edfb --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.4.5.md @@ -0,0 +1,7 @@ +# v4.4.5 — 2026-05-20 + +- updated articles location to new docs path; added easter egg globe view (8c3c47c6) +- polished global view (51a96c7c) +- added atl18 skin to globe (81c68f43) +- updated some bathy types that were fixed on the server side (ef4df7fd) +- initial commit for globe (df5a4012) diff --git a/web-client/src/assets/content/release-notes/v4.4.6.md b/web-client/src/assets/content/release-notes/v4.4.6.md new file mode 100644 index 00000000..c2d01032 --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.4.6.md @@ -0,0 +1,4 @@ +# v4.4.6 — 2026-05-20 + +- updated terraform lock file when deployment executed on linux machine (a04545b6) +- added docs.slideruleearth.io as a trusted image src (c1f660ce) diff --git a/web-client/src/assets/content/release-notes/v4.5.0.md b/web-client/src/assets/content/release-notes/v4.5.0.md new file mode 100644 index 00000000..767c752e --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.5.0.md @@ -0,0 +1,3 @@ +# v4.5.0 — 2026-05-28 + +- Scope advanced parameters to the selected endpoint (#1074) (3ef7669b) diff --git a/web-client/src/assets/content/release-notes/v4.5.1.md b/web-client/src/assets/content/release-notes/v4.5.1.md new file mode 100644 index 00000000..ab200a31 --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.5.1.md @@ -0,0 +1,4 @@ +# v4.5.1 — 2026-06-08 + +- landing page release both web client and server release notes (7a16e4e2) +- updated landing page to pull release notes instead of articles (fc23ef4f) diff --git a/web-client/src/assets/content/release-notes/v4.5.2.md b/web-client/src/assets/content/release-notes/v4.5.2.md new file mode 100644 index 00000000..ecbe67a3 --- /dev/null +++ b/web-client/src/assets/content/release-notes/v4.5.2.md @@ -0,0 +1,4 @@ +# v4.5.2 — 2026-06-09 + +- Fix TS2742 typecheck failure in deck3DConfigStore (#1079) (b5249683) +- Add "Zoom plot to map extent" button to Time Series tab (#1077) (c997a0e6) diff --git a/web-client/src/utils/docLinks.ts b/web-client/src/utils/docLinks.ts index ac57b1c6..e8375ae8 100644 --- a/web-client/src/utils/docLinks.ts +++ b/web-client/src/utils/docLinks.ts @@ -41,5 +41,9 @@ export const DOCS = { releaseNotes: { index: `${DOCS_BASE}/developer_guide/release_notes/release_notes.html`, base: `${DOCS_BASE}/developer_guide/release_notes/` + }, + webClient: { + repo: 'https://github.com/SlideRuleEarth/sliderule-web-client', + tags: 'https://github.com/SlideRuleEarth/sliderule-web-client/releases/tag/' } } as const diff --git a/web-client/src/views/LandingView.vue b/web-client/src/views/LandingView.vue index d2c276b1..e76e8445 100644 --- a/web-client/src/views/LandingView.vue +++ b/web-client/src/views/LandingView.vue @@ -15,7 +15,7 @@ function stripFrontmatter(md: string): string { const aboutHtml = DOMPurify.sanitize(marked(stripFrontmatter(aboutRaw)) as string) const contactHtml = DOMPurify.sanitize(marked(stripFrontmatter(contactRaw)) as string) -const tabOptions = ['About', 'Contact', 'Release Notes'] +const tabOptions = ['About', 'Contact', 'SlideRule Releases', 'Web Client Releases'] const selectedTab = ref('About') const panelHtml = computed(() => { @@ -30,6 +30,11 @@ const panelHtml = computed(() => { }) // --- Release Notes --- +// +// Two release-note sources share the list/detail UI below: +// - 'remote' = the SlideRule platform notes scraped from the docs site +// - 'local' = this repo's web-client notes, bundled at build time (see the +// import.meta.glob of src/assets/content/release-notes/*.md) const RELEASE_NOTES_INDEX_URL = DOCS.releaseNotes.index const RELEASE_NOTES_BASE_URL = DOCS.releaseNotes.base @@ -37,8 +42,9 @@ const RELEASE_NOTES_BASE_URL = DOCS.releaseNotes.base interface ReleaseNote { title: string date: string - url: string + url: string // external link: docs page (remote) or GitHub release tag (local) snippet?: string + html?: string // precomputed detail HTML (local notes only) } const releaseNotes = ref([]) @@ -47,6 +53,82 @@ const releaseHtml = ref('') const releaseLoading = ref(false) const releaseError = ref('') +const releaseMode = computed<'remote' | 'local' | null>(() => { + if (selectedTab.value === 'SlideRule Releases') return 'remote' + if (selectedTab.value === 'Web Client Releases') return 'local' + return null +}) + +// --- Local web-client release notes (bundled markdown) --- + +const localNoteModules = import.meta.glob('@/assets/content/release-notes/*.md', { + query: '?raw', + import: 'default', + eager: true +}) as Record + +function versionKey(v: string): number[] { + return v + .replace(/^v/, '') + .split('.') + .map((n) => parseInt(n, 10) || 0) +} + +// Sort newest-first by semantic version (v4.5.10 after v4.5.2). +function compareVersionDesc(a: string, b: string): number { + const av = versionKey(a) + const bv = versionKey(b) + for (let i = 0; i < Math.max(av.length, bv.length); i++) { + const diff = (bv[i] ?? 0) - (av[i] ?? 0) + if (diff !== 0) return diff + } + return 0 +} + +const localReleaseNotes = computed(() => { + const notes = Object.entries(localNoteModules).map(([path, raw]) => { + const version = path.split('/').pop()?.replace(/\.md$/, '') ?? '' + const body = stripFrontmatter(raw) + // First heading line, e.g. "# v4.5.2 — 2026-06-09" + const header = body.match(/^#\s+(\S+)\s+[—-]\s+(\d{4}-\d{2}-\d{2})/m) + const title = header?.[1] ?? version + const date = header?.[2] ?? '' + // First bullet line used as a one-line snippet in the list view. + const bullet = body.match(/^\s*[-*]\s+(.+)$/m) + const snippet = bullet?.[1] ?? '' + const html = DOMPurify.sanitize(marked(body) as string) + return { title, date, url: DOCS.webClient.tags + version, snippet, html } + }) + return notes.sort((a, b) => compareVersionDesc(a.title, b.title) || b.date.localeCompare(a.date)) +}) + +const currentNotes = computed(() => { + if (releaseMode.value === 'local') return localReleaseNotes.value + if (releaseMode.value === 'remote') return releaseNotes.value + return [] +}) + +const externalLink = computed(() => { + if (!selectedRelease.value) return '' + return releaseMode.value === 'local' + ? selectedRelease.value.url + : RELEASE_NOTES_BASE_URL + selectedRelease.value.url +}) + +const externalLinkLabel = computed(() => + releaseMode.value === 'local' ? 'View on GitHub ↗' : 'View on docs site ↗' +) + +function openRelease(note: ReleaseNote) { + if (releaseMode.value === 'local') { + // Local notes are already rendered — no network fetch needed. + selectedRelease.value = note + releaseHtml.value = note.html ?? '' + } else { + void fetchRelease(note) + } +} + async function fetchReleaseNotesIndex() { releaseLoading.value = true releaseError.value = '' @@ -155,8 +237,12 @@ onMounted(() => { }) watch(selectedTab, (tab) => { - if (tab === 'Release Notes') { - selectedRelease.value = null + // Reset the detail view whenever the tab changes; only the remote source needs + // an on-switch fetch (local notes are bundled and rendered eagerly). + selectedRelease.value = null + releaseHtml.value = '' + releaseError.value = '' + if (tab === 'SlideRule Releases') { void fetchReleaseNotesIndex() } }) @@ -175,7 +261,7 @@ watch(selectedTab, (tab) => { -
+
@@ -184,7 +270,7 @@ watch(selectedTab, (tab) => {
- +
Loading...
{{ releaseError }}
@@ -193,15 +279,15 @@ watch(selectedTab, (tab) => { -
    -
  • +
      +
    • {{ r.date }}
      {{ r.title }} @@ -209,6 +295,7 @@ watch(selectedTab, (tab) => {
    +
    No release notes available.