mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
fix(deploy): pin docs links to release refs
This commit is contained in:
parent
893d003c0a
commit
0b98a0d3e0
9 changed files with 242 additions and 22 deletions
6
.github/workflows/helm-pages.yml
vendored
6
.github/workflows/helm-pages.yml
vendored
|
|
@ -120,8 +120,10 @@ jobs:
|
|||
- name: Update Chart.yaml version
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
sed -i "s/^version: .*/version: $VERSION/" deploy/helm/pulse/Chart.yaml
|
||||
sed -i "s/^appVersion: .*/appVersion: \"$VERSION\"/" deploy/helm/pulse/Chart.yaml
|
||||
python3 scripts/sync_chart_release_metadata.py \
|
||||
--chart deploy/helm/pulse/Chart.yaml \
|
||||
--version "$VERSION" \
|
||||
--repo "${{ github.repository }}"
|
||||
|
||||
# Commit if Chart.yaml changed
|
||||
if ! git diff --quiet deploy/helm/pulse/Chart.yaml; then
|
||||
|
|
|
|||
7
.github/workflows/publish-helm-chart.yml
vendored
7
.github/workflows/publish-helm-chart.yml
vendored
|
|
@ -92,6 +92,13 @@ jobs:
|
|||
|
||||
echo "[OK] ${RELEASE_TAG} validated against release line ${REQUIRED_BRANCH}"
|
||||
|
||||
- name: Align chart metadata links
|
||||
run: |
|
||||
python3 scripts/sync_chart_release_metadata.py \
|
||||
--chart deploy/helm/pulse/Chart.yaml \
|
||||
--version "${{ steps.versions.outputs.chart_version }}" \
|
||||
--repo "${{ github.repository }}"
|
||||
|
||||
- name: Helm lint (strict)
|
||||
run: helm lint deploy/helm/pulse --strict
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Helm chart for deploying the Pulse hub and optional Docker monitori
|
|||
type: application
|
||||
version: 5.1.7
|
||||
appVersion: "5.1.7"
|
||||
icon: https://raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg
|
||||
icon: https://raw.githubusercontent.com/rcourtman/Pulse/v5.1.7/docs/images/pulse-logo.svg
|
||||
keywords:
|
||||
- monitoring
|
||||
- proxmox
|
||||
|
|
@ -32,7 +32,7 @@ annotations:
|
|||
description: Smoke tests with kind cluster deployment
|
||||
artifacthub.io/links: |
|
||||
- name: Documentation
|
||||
url: https://github.com/rcourtman/Pulse/blob/main/docs/KUBERNETES.md
|
||||
url: https://github.com/rcourtman/Pulse/blob/v5.1.7/docs/KUBERNETES.md
|
||||
- name: Support
|
||||
url: https://github.com/rcourtman/Pulse/discussions
|
||||
artifacthub.io/maintainers: |
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ server-side update execution surfaces.
|
|||
4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts`
|
||||
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `Makefile`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
|
||||
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, artifact publication lineage enforcement, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
|
||||
7. Preserve release-matched installer and Helm operator documentation links through `scripts/install.sh`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-helm-chart.yml`, and the chart metadata itself so deployment guidance and packaged chart metadata do not drift back to branch-tip `main` docs when a release line or promoted tag already exists.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
|
|||
103
install.sh
103
install.sh
|
|
@ -88,22 +88,8 @@ resolve_install_script_download_url() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
local releases_json=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
releases_json=$(timeout 15 curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null || true)
|
||||
else
|
||||
releases_json=$(curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
local rc_release=""
|
||||
if [[ -n "$releases_json" ]]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
rc_release=$(echo "$releases_json" | jq -r '[.[] | select(.draft == false)][0].tag_name' 2>/dev/null || true)
|
||||
else
|
||||
rc_release=$(echo "$releases_json" | grep -v '"draft": true' | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
rc_release=$(resolve_latest_release_tag_for_channel rc 2>/dev/null || true)
|
||||
if [[ -z "$rc_release" || "$rc_release" == "null" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -521,8 +507,93 @@ repo_web_url() {
|
|||
printf 'https://github.com/%s\n' "$GITHUB_REPO"
|
||||
}
|
||||
|
||||
resolve_latest_release_tag_for_channel() {
|
||||
local channel="${1:-stable}"
|
||||
|
||||
if [[ "$channel" != "rc" ]]; then
|
||||
local stable_release=""
|
||||
stable_release=$(get_latest_release_from_redirect 2>/dev/null || true)
|
||||
if [[ -n "$stable_release" ]]; then
|
||||
printf '%s\n' "$stable_release"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local latest_json=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
latest_json=$(timeout 15 curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases/latest" 2>/dev/null || true)
|
||||
else
|
||||
latest_json=$(curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases/latest" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
local latest_release=""
|
||||
if [[ -n "$latest_json" ]]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
latest_release=$(echo "$latest_json" | jq -r '.tag_name' 2>/dev/null || true)
|
||||
else
|
||||
latest_release=$(echo "$latest_json" | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$latest_release" && "$latest_release" != "null" ]]; then
|
||||
printf '%s\n' "$latest_release"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
local releases_json=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
releases_json=$(timeout 15 curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null || true)
|
||||
else
|
||||
releases_json=$(curl -fsSL --connect-timeout 10 --max-time 30 "https://api.github.com/repos/$GITHUB_REPO/releases" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
local rc_release=""
|
||||
if [[ -n "$releases_json" ]]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
rc_release=$(echo "$releases_json" | jq -r '[.[] | select(.draft == false)][0].tag_name' 2>/dev/null || true)
|
||||
else
|
||||
rc_release=$(echo "$releases_json" | grep -v '"draft": true' | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$rc_release" || "$rc_release" == "null" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$rc_release"
|
||||
}
|
||||
|
||||
repo_release_docs_ref() {
|
||||
if [[ -n "${FORCE_VERSION:-}" ]]; then
|
||||
printf '%s\n' "$FORCE_VERSION"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${LATEST_RELEASE:-}" ]]; then
|
||||
printf '%s\n' "$LATEST_RELEASE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local channel="${FORCE_CHANNEL:-${UPDATE_CHANNEL:-stable}}"
|
||||
resolve_latest_release_tag_for_channel "$channel"
|
||||
}
|
||||
|
||||
repo_docs_url_for_path() {
|
||||
local docs_path="$1"
|
||||
local ref=""
|
||||
ref=$(repo_release_docs_ref 2>/dev/null || true)
|
||||
if [[ -n "$ref" ]]; then
|
||||
printf '%s/blob/%s/%s\n' "$(repo_web_url)" "$ref" "$docs_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s/releases/latest\n' "$(repo_web_url)"
|
||||
}
|
||||
|
||||
repo_docker_docs_url() {
|
||||
printf '%s/blob/main/docs/DOCKER.md\n' "$(repo_web_url)"
|
||||
repo_docs_url_for_path "docs/DOCKER.md"
|
||||
}
|
||||
|
||||
repo_docker_image_ref() {
|
||||
|
|
|
|||
|
|
@ -1507,7 +1507,11 @@ func TestPulseAutoUpdateResolveInstallScriptURLUsesConfiguredRepo(t *testing.T)
|
|||
func TestRepoDockerDocsURLUsesConfiguredRepo(t *testing.T) {
|
||||
script := `
|
||||
GITHUB_REPO="example/pulse-fork"
|
||||
LATEST_RELEASE="v9.9.9"
|
||||
` + extractRootInstallShellFunction(t, "repo_web_url") + `
|
||||
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
|
||||
repo_docker_docs_url
|
||||
`
|
||||
|
|
@ -1516,11 +1520,34 @@ func TestRepoDockerDocsURLUsesConfiguredRepo(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("bash: %v\n%s", err, out)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/blob/main/docs/DOCKER.md" {
|
||||
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/blob/v9.9.9/docs/DOCKER.md" {
|
||||
t.Fatalf("repo_docker_docs_url = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoDockerDocsURLFallsBackToReleaseLandingPageWhenVersionUnknown(t *testing.T) {
|
||||
script := `
|
||||
GITHUB_REPO="example/pulse-fork"
|
||||
get_latest_release_from_redirect() { return 1; }
|
||||
curl() { return 1; }
|
||||
timeout() { return 1; }
|
||||
` + extractRootInstallShellFunction(t, "repo_web_url") + `
|
||||
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
|
||||
repo_docker_docs_url
|
||||
`
|
||||
|
||||
out, err := exec.Command("bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bash: %v\n%s", err, out)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/releases/latest" {
|
||||
t.Fatalf("repo_docker_docs_url fallback = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoDockerImageRefUsesConfiguredImageRepo(t *testing.T) {
|
||||
script := `
|
||||
DOCKER_IMAGE_REPO="example/pulse-enterprise"
|
||||
|
|
@ -1540,10 +1567,14 @@ func TestRepoDockerImageRefUsesConfiguredImageRepo(t *testing.T) {
|
|||
func TestCheckDockerEnvironmentUsesConfiguredImageAndDocs(t *testing.T) {
|
||||
script := `
|
||||
GITHUB_REPO="example/pulse-fork"
|
||||
LATEST_RELEASE="v9.9.9"
|
||||
DOCKER_IMAGE_REPO="example/pulse-enterprise"
|
||||
print_error() { printf 'ERR:%s\n' "$1"; }
|
||||
grep() { return 1; }
|
||||
` + extractRootInstallShellFunction(t, "repo_web_url") + `
|
||||
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_docker_image_ref") + `
|
||||
` + extractRootInstallShellFunction(t, "check_docker_environment") + `
|
||||
|
|
@ -1559,7 +1590,7 @@ func TestCheckDockerEnvironmentUsesConfiguredImageAndDocs(t *testing.T) {
|
|||
if !strings.Contains(got, "docker run -d -p 7655:7655 example/pulse-enterprise:latest") {
|
||||
t.Fatalf("docker guidance missing configured image repo:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "https://github.com/example/pulse-fork/blob/main/docs/DOCKER.md") {
|
||||
if !strings.Contains(got, "https://github.com/example/pulse-fork/blob/v9.9.9/docs/DOCKER.md") {
|
||||
t.Fatalf("docker guidance missing configured docs url:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
demo = read(".github/workflows/update-demo-server.yml")
|
||||
helm = read(".github/workflows/publish-helm-chart.yml")
|
||||
helm_pages = read(".github/workflows/helm-pages.yml")
|
||||
chart = read("deploy/helm/pulse/Chart.yaml")
|
||||
chart_sync = read("scripts/sync_chart_release_metadata.py")
|
||||
runbook = read("docs/releases/V6_PRERELEASE_RUNBOOK.md")
|
||||
self.assertIn("control_plane.py --branch-for-version", publish)
|
||||
self.assertIn("control_plane.py --branch-for-version", promote)
|
||||
|
|
@ -223,6 +225,14 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn("does not descend from any matching prerelease tag", publish)
|
||||
self.assertIn("does not descend from any matching prerelease tag", promote)
|
||||
self.assertIn("Refusing cross-line Helm pages release", helm_pages)
|
||||
self.assertIn("sync_chart_release_metadata.py", helm)
|
||||
self.assertIn("sync_chart_release_metadata.py", helm_pages)
|
||||
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm)
|
||||
self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm_pages)
|
||||
self.assertNotIn("blob/main/docs/KUBERNETES.md", chart)
|
||||
self.assertNotIn("raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg", chart)
|
||||
self.assertIn("blob/{tag}/docs/KUBERNETES.md", chart_sync)
|
||||
self.assertIn("raw.githubusercontent.com/{repo}/{tag}/docs/images/pulse-logo.svg", chart_sync)
|
||||
self.assertIn("both stable and prerelease releases dispatch", runbook)
|
||||
self.assertIn("Release `6.0.0` from `pulse/v6-release`", runbook)
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(runbook))
|
||||
|
|
|
|||
50
scripts/sync_chart_release_metadata.py
Normal file
50
scripts/sync_chart_release_metadata.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Keep Helm chart release metadata aligned with the packaged version."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def replace_one(text: str, pattern: str, replacement: str) -> str:
|
||||
updated, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE)
|
||||
if count != 1:
|
||||
raise ValueError(f"expected exactly one match for pattern: {pattern}")
|
||||
return updated
|
||||
|
||||
|
||||
def sync_chart_metadata(text: str, version: str, repo: str) -> str:
|
||||
tag = version if version.startswith("v") else f"v{version}"
|
||||
icon_url = f"https://raw.githubusercontent.com/{repo}/{tag}/docs/images/pulse-logo.svg"
|
||||
docs_url = f"https://github.com/{repo}/blob/{tag}/docs/KUBERNETES.md"
|
||||
|
||||
text = replace_one(text, r"^version: .*$", f"version: {version}")
|
||||
text = replace_one(text, r'^appVersion: ".*"$', f'appVersion: "{version}"')
|
||||
text = replace_one(text, r"^icon: .*$", f"icon: {icon_url}")
|
||||
text = replace_one(
|
||||
text,
|
||||
r"(^\s+- name: Documentation\n\s+url: ).*$",
|
||||
rf"\1{docs_url}",
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--chart", required=True, help="Path to Chart.yaml")
|
||||
parser.add_argument("--version", required=True, help="Chart/app version")
|
||||
parser.add_argument("--repo", default="rcourtman/Pulse", help="GitHub owner/repo")
|
||||
args = parser.parse_args()
|
||||
|
||||
chart_path = Path(args.chart)
|
||||
original = chart_path.read_text(encoding="utf-8")
|
||||
updated = sync_chart_metadata(original, args.version, args.repo)
|
||||
if updated != original:
|
||||
chart_path.write_text(updated, encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
48
scripts/tests/test_sync_chart_release_metadata.py
Normal file
48
scripts/tests/test_sync_chart_release_metadata.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tests for scripts/sync_chart_release_metadata.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "sync_chart_release_metadata.py"
|
||||
SPEC = importlib.util.spec_from_file_location("sync_chart_release_metadata", MODULE_PATH)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class SyncChartReleaseMetadataTest(unittest.TestCase):
|
||||
def test_sync_chart_metadata_updates_versioned_links(self) -> None:
|
||||
original = """apiVersion: v2
|
||||
name: pulse
|
||||
version: 5.1.7
|
||||
appVersion: "5.1.7"
|
||||
icon: https://raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg
|
||||
annotations:
|
||||
artifacthub.io/links: |
|
||||
- name: Documentation
|
||||
url: https://github.com/rcourtman/Pulse/blob/main/docs/KUBERNETES.md
|
||||
- name: Support
|
||||
url: https://github.com/rcourtman/Pulse/discussions
|
||||
"""
|
||||
|
||||
updated = MODULE.sync_chart_metadata(original, "6.0.0-rc.1", "example/pulse")
|
||||
|
||||
self.assertIn("version: 6.0.0-rc.1", updated)
|
||||
self.assertIn('appVersion: "6.0.0-rc.1"', updated)
|
||||
self.assertIn(
|
||||
"icon: https://raw.githubusercontent.com/example/pulse/v6.0.0-rc.1/docs/images/pulse-logo.svg",
|
||||
updated,
|
||||
)
|
||||
self.assertIn(
|
||||
"url: https://github.com/example/pulse/blob/v6.0.0-rc.1/docs/KUBERNETES.md",
|
||||
updated,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue