mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Restore v6 development version semantics
This commit is contained in:
parent
7e0f84a40f
commit
283f2e25ae
20 changed files with 323 additions and 29 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
6.0.0-rc.1
|
||||
6.0.0-dev
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
`pulse/v6-release`.
|
||||
5. The active control-plane target is still `v6-rc-cut`, not
|
||||
`v6-ga-promotion`.
|
||||
6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.1`, so the
|
||||
6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-dev`, so the
|
||||
working line is still prerelease and there is not yet a governed local stable
|
||||
`6.0.0` candidate.
|
||||
7. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
The blocker is no longer missing governance text. The remaining problem is that
|
||||
the control plane still holds v6 on the pre-GA prerelease line, the working
|
||||
version is still prerelease (`6.0.0-rc.1`), and there is still no exercised
|
||||
version is still prerelease (`6.0.0-dev`), and there is still no exercised
|
||||
`Release Dry Run` record proving the eventual stable `6.0.0`
|
||||
candidate is ready for GA-style promotion. Until that rehearsal exists, stable
|
||||
users would still be the first real cohort for the final promotion path.
|
||||
|
|
|
|||
|
|
@ -183,6 +183,13 @@ management, and fleet control surfaces.
|
|||
11. Keep shared `internal/api/router.go` workload-chart downsampling presentation-only: when that router caps mixed-cadence workload history into equal-time buckets for operator-facing cards, lifecycle-adjacent setup and fleet surfaces must not reuse the shaped chart samples as heartbeat, enrollment, or last-seen authority.
|
||||
That same presentation-only boundary must preserve canonical millisecond timestamps when it serializes chart points, so lifecycle-adjacent first-host and fleet surfaces do not misread rounded chart samples as duplicate or restarted heartbeat evidence.
|
||||
The same rule now applies to storage summary interaction. Shared sticky-card or row-hover focus behavior on infrastructure, workloads, and storage may reuse the canonical chart transport, but lifecycle-adjacent install, enrollment, and fleet surfaces must not treat highlighted summary series or sticky-shell state as agent freshness or setup progress.
|
||||
12. Keep lifecycle installer fallback pinned to published release lineage only.
|
||||
When `internal/api/unified_agent.go` has to proxy `/install.sh` or
|
||||
`/install.ps1` from GitHub, the shared lifecycle path may only treat stable
|
||||
tags and explicit RC prerelease tags as release assets. Working-line dev
|
||||
prereleases and build-metadata versions must fail closed so first-host
|
||||
install, repair, and fleet continuity do not depend on unpublished or
|
||||
branch-local installer URLs.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
|
|||
|
|
@ -341,6 +341,14 @@ Own canonical runtime payload shapes between backend and frontend.
|
|||
route API-backed first systems to `/settings/infrastructure/platforms`
|
||||
instead of implying that a host install command is required before those
|
||||
platforms can report into Pulse.
|
||||
24. Keep shared install-script fallback transport pinned to published release
|
||||
lineage. `internal/api/unified_agent.go` and
|
||||
`internal/api/contract_test.go` must only map stable tags or explicit RC
|
||||
prerelease tags without build metadata to GitHub install-script release
|
||||
assets; dev prereleases such as `v6.0.0-dev`, git-described
|
||||
`+git...` builds, and other unpublished prerelease identifiers must fail
|
||||
closed on that API boundary instead of generating fake release URLs from
|
||||
a local runtime version string.
|
||||
23. Keep local trial-start transport explicit on the shared commercial API
|
||||
boundary: `/api/license/trial/start` must preserve the hosted-signup
|
||||
redirect contract during the allowed retry burst, then return the actual
|
||||
|
|
@ -1453,6 +1461,13 @@ contract: when `/install.sh` or `/install.ps1` cannot be served locally, the
|
|||
backend must proxy the install script asset from the exact GitHub release that
|
||||
matches `serverVersion` and must fail closed for dev or unreleased builds
|
||||
rather than serving branch-tip installer logic.
|
||||
That same transport rule is now explicit about prerelease classes too: only
|
||||
stable tags and explicit RC prerelease tags without build metadata qualify as
|
||||
published install-script release assets. Working-line dev prereleases such as
|
||||
`v6.0.0-dev`, git-described builds with `+git...` metadata, and other
|
||||
non-published prerelease identifiers must fail closed on that shared
|
||||
`internal/api/unified_agent.go` boundary instead of generating fake GitHub
|
||||
release URLs from a local runtime version string.
|
||||
The `/api/updates/plan` contract must also fail closed without becoming a
|
||||
transport error on supported non-auto-update deployments: `manual`,
|
||||
`development`, and `source` runtimes must return an explicit manual update
|
||||
|
|
|
|||
|
|
@ -124,6 +124,14 @@ fallbacks, so shipped release builds report the exact promoted version even
|
|||
when the install path has no `.git` metadata or a stale `VERSION` file nearby.
|
||||
Runtime bootstrap must seed that build version before the server starts rather
|
||||
than leaving version detection to deployment-local filesystem guesses.
|
||||
That same version boundary now also owns the working-line development base:
|
||||
the checked-in `VERSION` file is the canonical intended semver base for
|
||||
current v6 development, and source/dev runtime detection must append git build
|
||||
metadata to that base instead of inheriting accidental prerelease tag lineage
|
||||
from `git describe`. Non-published prerelease bases such as `6.0.0-dev`
|
||||
therefore remain prerelease for branch policy, release-control blocked
|
||||
records, and future release promotion planning, but they must not be treated
|
||||
as shipped RC lineage or as published release-asset versions.
|
||||
That release-build metadata path is now explicit too: `scripts/release_ldflags.sh`
|
||||
is the canonical owner for server and agent build ldflags, and release artifact
|
||||
assembly must route through it instead of hand-writing overlapping `main.Version`,
|
||||
|
|
|
|||
|
|
@ -212,6 +212,13 @@ querying, and the operator-facing storage health presentation layer.
|
|||
snapshot-backed platforms, provider-backed fixtures, unified inventory,
|
||||
and recovery/storage context stay aligned instead of drifting through
|
||||
recovery-local fixture assembly or partial mock helper APIs.
|
||||
12. Keep adjacent shared install-script fallback semantics honest on the
|
||||
`internal/api/` boundary. When storage- or recovery-adjacent routes reuse
|
||||
shared public endpoint or installer helpers, dev prerelease runtime
|
||||
versions such as `v6.0.0-dev` and build-metadata versions must not be
|
||||
treated as published GitHub release assets; only stable or explicit RC
|
||||
tags may back the shared installer fallback that those adjacent surfaces
|
||||
inherit.
|
||||
12. Keep storage summary chart identity and sticky-shell behavior on the
|
||||
shared storage path. Pool rows, disk rows, storage summary cards, and
|
||||
storage detail charts must all address history through the canonical
|
||||
|
|
|
|||
|
|
@ -5211,6 +5211,14 @@ func TestContract_InstallScriptReleaseAssetURLRejectsUnreleasedBuild(t *testing.
|
|||
}
|
||||
}
|
||||
|
||||
func TestContract_InstallScriptReleaseAssetURLRejectsDevPrereleaseBuild(t *testing.T) {
|
||||
router := &Router{serverVersion: "v6.0.0-dev"}
|
||||
|
||||
if _, err := router.installScriptReleaseAssetURL("install.sh"); err == nil {
|
||||
t.Fatalf("expected dev prerelease build to reject release asset lookup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_ProxmoxInstallCommandIncludesInsecureForPlainHTTP(t *testing.T) {
|
||||
got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
|
||||
BaseURL: "http://pulse.example.com:7655/",
|
||||
|
|
|
|||
|
|
@ -468,8 +468,8 @@ func (r *Router) installScriptReleaseAssetURL(scriptName string) (string, error)
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("server version %q is not a published release version", rawVersion)
|
||||
}
|
||||
if version.Build != "" {
|
||||
return "", fmt.Errorf("server version %q includes build metadata and cannot map to a release asset", rawVersion)
|
||||
if !version.IsPublishedReleaseAssetVersion() {
|
||||
return "", fmt.Errorf("server version %q is not a published release asset version", rawVersion)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
|
|
|
|||
|
|
@ -192,6 +192,23 @@ func TestDownloadUnifiedInstallScript_ProxyFallbackRejectsDevelopmentBuild(t *te
|
|||
}
|
||||
}
|
||||
|
||||
func TestDownloadUnifiedInstallScript_ProxyFallbackRejectsDevPrereleaseBuild(t *testing.T) {
|
||||
router, _ := setupUnifiedAgentRouter(t)
|
||||
router.serverVersion = "v6.0.0-dev"
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.handleDownloadUnifiedInstallScript(w, req)
|
||||
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("expected 503, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "Install script unavailable for current server build") {
|
||||
t.Fatalf("unexpected response body: %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadUnifiedInstallScript_ProxyFallbackPreservesHEAD(t *testing.T) {
|
||||
router, _ := setupUnifiedAgentRouter(t)
|
||||
router.serverVersion = "v6.0.0-rc.1"
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import (
|
|||
|
||||
// Pre-compiled regexes for performance (avoid recompilation on each call)
|
||||
var (
|
||||
semverRe = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$`)
|
||||
rcNumRe = regexp.MustCompile(`rc\.?(\d+)`)
|
||||
semverRe = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$`)
|
||||
rcNumRe = regexp.MustCompile(`rc\.?(\d+)`)
|
||||
gitHashRe = regexp.MustCompile(`^([0-9a-fA-F]{7,})(-dirty)?$`)
|
||||
)
|
||||
|
||||
// BuildVersion is the version string injected at build time via ldflags.
|
||||
|
|
@ -34,6 +35,23 @@ type Version struct {
|
|||
Build string
|
||||
}
|
||||
|
||||
func (v *Version) IsRCPrerelease() bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return rcNumRe.MatchString(strings.ToLower(strings.TrimSpace(v.Prerelease)))
|
||||
}
|
||||
|
||||
func (v *Version) IsPublishedReleaseAssetVersion() bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
if v.Build != "" {
|
||||
return false
|
||||
}
|
||||
return v.Prerelease == "" || v.IsRCPrerelease()
|
||||
}
|
||||
|
||||
// VersionInfo contains detailed version information
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
|
|
@ -132,6 +150,7 @@ func (v *Version) IsPrerelease() bool {
|
|||
// GetCurrentVersion gets the current running version
|
||||
func GetCurrentVersion() (*VersionInfo, error) {
|
||||
allowDockerUpdates := dockerUpdatesAllowed()
|
||||
versionFileRaw, hasVersionFile := readCurrentVersionFile()
|
||||
|
||||
buildInfo := func(raw string, build string, isDev bool) *VersionInfo {
|
||||
normalized := normalizeVersionString(raw)
|
||||
|
|
@ -160,23 +179,12 @@ func GetCurrentVersion() (*VersionInfo, error) {
|
|||
}
|
||||
|
||||
if gitVersion, err := getGitVersion(); err == nil && gitVersion != "" {
|
||||
return buildInfo(gitVersion, "development", true), nil
|
||||
return buildInfo(normalizeDevelopmentVersion(gitVersion, versionFileRaw), "development", true), nil
|
||||
}
|
||||
|
||||
// Try to read from VERSION file first (release builds)
|
||||
versionPaths := []string{
|
||||
"VERSION",
|
||||
"/opt/pulse/VERSION",
|
||||
filepath.Join(filepath.Dir(os.Args[0]), "VERSION"),
|
||||
}
|
||||
|
||||
for _, path := range versionPaths {
|
||||
versionBytes, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
if raw := strings.TrimSpace(string(versionBytes)); raw != "" {
|
||||
return buildInfo(raw, "release", false), nil
|
||||
}
|
||||
}
|
||||
if hasVersionFile {
|
||||
return buildInfo(versionFileRaw, "release", false), nil
|
||||
}
|
||||
|
||||
// Fall back to git (development builds)
|
||||
|
|
@ -211,9 +219,49 @@ func normalizeVersionString(version string) string {
|
|||
return fmt.Sprintf("0.0.0-%s", sanitizePrereleaseIdentifier(version))
|
||||
}
|
||||
|
||||
func normalizeDevelopmentVersion(gitVersion string, versionBase string) string {
|
||||
normalizedGit := normalizeVersionString(gitVersion)
|
||||
normalizedBase := normalizeVersionString(versionBase)
|
||||
if versionBase == "" {
|
||||
return normalizedGit
|
||||
}
|
||||
if _, err := ParseVersion(normalizedBase); err != nil {
|
||||
return normalizedGit
|
||||
}
|
||||
|
||||
if buildMetadata, ok := gitBuildMetadata(gitVersion); ok {
|
||||
if strings.Contains(normalizedBase, "+") {
|
||||
return normalizedBase + "." + buildMetadata
|
||||
}
|
||||
return normalizedBase + "+" + buildMetadata
|
||||
}
|
||||
|
||||
return normalizedBase
|
||||
}
|
||||
|
||||
func gitBuildMetadata(version string) (string, bool) {
|
||||
if normalized, ok := normalizeGitDescribeVersion(version); ok {
|
||||
if plus := strings.Index(normalized, "+"); plus >= 0 && plus < len(normalized)-1 {
|
||||
return normalized[plus+1:], true
|
||||
}
|
||||
}
|
||||
|
||||
matches := gitHashRe.FindStringSubmatch(strings.TrimSpace(version))
|
||||
if matches == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
build := "git.0.g" + strings.ToLower(matches[1])
|
||||
if matches[2] != "" {
|
||||
build += ".dirty"
|
||||
}
|
||||
return build, true
|
||||
}
|
||||
|
||||
var gitDescribeRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+(?:-[0-9A-Za-z\.-]+)?)-(\d+)-g([0-9a-fA-F]+)(-dirty)?$`)
|
||||
|
||||
func normalizeGitDescribeVersion(version string) (string, bool) {
|
||||
version = strings.TrimSpace(strings.TrimPrefix(version, "v"))
|
||||
matches := gitDescribeRegex.FindStringSubmatch(version)
|
||||
if matches == nil {
|
||||
return "", false
|
||||
|
|
@ -270,13 +318,35 @@ func sanitizePrereleaseIdentifier(raw string) string {
|
|||
}
|
||||
|
||||
func detectChannelFromVersion(version string) string {
|
||||
versionLower := strings.ToLower(version)
|
||||
if strings.Contains(versionLower, "rc") {
|
||||
return "rc"
|
||||
trimmed := strings.TrimSpace(version)
|
||||
if parsed, err := ParseVersion(trimmed); err == nil {
|
||||
if parsed.IsRCPrerelease() {
|
||||
return "rc"
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
|
||||
func readCurrentVersionFile() (string, bool) {
|
||||
versionPaths := []string{
|
||||
"VERSION",
|
||||
"/opt/pulse/VERSION",
|
||||
filepath.Join(filepath.Dir(os.Args[0]), "VERSION"),
|
||||
}
|
||||
|
||||
for _, path := range versionPaths {
|
||||
versionBytes, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
if raw := strings.TrimSpace(string(versionBytes)); raw != "" {
|
||||
return raw, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// getGitVersion gets version information from git
|
||||
func getGitVersion() (string, error) {
|
||||
// Get the latest tag
|
||||
|
|
|
|||
|
|
@ -76,6 +76,57 @@ func TestGetCurrentVersion_UsesVersionFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentVersion_UsesVersionFileAsDevelopmentBase(t *testing.T) {
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Chdir(oldwd)
|
||||
}()
|
||||
|
||||
oldBuildVersion := BuildVersion
|
||||
BuildVersion = ""
|
||||
defer func() {
|
||||
BuildVersion = oldBuildVersion
|
||||
}()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
gitPath := filepath.Join(tmpDir, "git")
|
||||
gitScript := "#!/bin/sh\nprintf '%s\\n' 'v6.0.0-rc.1-45-gABCDEF'\n"
|
||||
if err := os.WriteFile(gitPath, []byte(gitScript), 0755); err != nil {
|
||||
t.Fatalf("write fake git: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "VERSION"), []byte("6.0.0-dev"), 0644); err != nil {
|
||||
t.Fatalf("write VERSION: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("PATH", tmpDir)
|
||||
t.Setenv("PULSE_MOCK_MODE", "")
|
||||
t.Setenv("PULSE_ALLOW_DOCKER_UPDATES", "")
|
||||
|
||||
info, err := GetCurrentVersion()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCurrentVersion error: %v", err)
|
||||
}
|
||||
if info.Version != "6.0.0-dev+git.45.gabcdef" {
|
||||
t.Fatalf("Version = %q, want 6.0.0-dev+git.45.gabcdef", info.Version)
|
||||
}
|
||||
if info.Build != "development" {
|
||||
t.Fatalf("Build = %q, want development", info.Build)
|
||||
}
|
||||
if !info.IsDevelopment {
|
||||
t.Fatalf("IsDevelopment = false, want true")
|
||||
}
|
||||
if info.Channel != "stable" {
|
||||
t.Fatalf("Channel = %q, want stable", info.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeploymentType_Mock(t *testing.T) {
|
||||
t.Setenv("PULSE_MOCK_MODE", "true")
|
||||
if got := GetDeploymentType(); got != "mock" {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ func TestNormalizeVersionString(t *testing.T) {
|
|||
input: "4.24.0-rc.3-45-gabc123-dirty",
|
||||
expected: "4.24.0-rc.3+git.45.gabc123.dirty",
|
||||
},
|
||||
{
|
||||
name: "development base rebases accidental prerelease lineage",
|
||||
input: "6.0.0-dev",
|
||||
expected: "6.0.0-dev",
|
||||
},
|
||||
{
|
||||
name: "branch name falls back to prerelease",
|
||||
input: "issue-551",
|
||||
|
|
@ -142,11 +147,84 @@ func TestDetectChannelFromVersion(t *testing.T) {
|
|||
t.Fatalf("detectChannelFromVersion rc = %s, expected rc", got)
|
||||
}
|
||||
|
||||
if got := detectChannelFromVersion("6.0.0-dev+git.45.gabcdef"); got != "stable" {
|
||||
t.Fatalf("detectChannelFromVersion dev = %s, expected stable", got)
|
||||
}
|
||||
|
||||
if got := detectChannelFromVersion("0.0.0-feature-x"); got != "stable" {
|
||||
t.Fatalf("detectChannelFromVersion stable = %s, expected stable", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDevelopmentVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
gitVersion string
|
||||
versionBase string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "git describe inherits current version base not accidental tag",
|
||||
gitVersion: "6.0.0-rc.1-45-gABCDEF",
|
||||
versionBase: "6.0.0-dev",
|
||||
expected: "6.0.0-dev+git.45.gabcdef",
|
||||
},
|
||||
{
|
||||
name: "hash only git version still carries build metadata",
|
||||
gitVersion: "ABCDEF1-dirty",
|
||||
versionBase: "6.0.0-dev",
|
||||
expected: "6.0.0-dev+git.0.gabcdef1.dirty",
|
||||
},
|
||||
{
|
||||
name: "missing base falls back to normalized git version",
|
||||
gitVersion: "6.0.0-rc.1-45-gABCDEF",
|
||||
versionBase: "",
|
||||
expected: "6.0.0-rc.1+git.45.gabcdef",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := normalizeDevelopmentVersion(tc.gitVersion, tc.versionBase); got != tc.expected {
|
||||
t.Fatalf("normalizeDevelopmentVersion(%q, %q) = %q, expected %q", tc.gitVersion, tc.versionBase, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionIsPublishedReleaseAssetVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
expected bool
|
||||
}{
|
||||
{name: "stable release", version: "6.0.0", expected: true},
|
||||
{name: "rc prerelease", version: "6.0.0-rc.1", expected: true},
|
||||
{name: "dev prerelease", version: "6.0.0-dev", expected: false},
|
||||
{name: "build metadata", version: "6.0.0+git.1.gabcdef", expected: false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
parsed, err := ParseVersion(tc.version)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseVersion(%q) error: %v", tc.version, err)
|
||||
}
|
||||
if got := parsed.IsPublishedReleaseAssetVersion(); got != tc.expected {
|
||||
t.Fatalf("IsPublishedReleaseAssetVersion(%q) = %v, expected %v", tc.version, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ PROFILE_PATH_FIELDS = {
|
|||
"registry_schema",
|
||||
"subsystem_contract_template",
|
||||
}
|
||||
PRERELEASE_VERSION_PATTERN = re.compile(r"-(?:rc|alpha|beta)\.[0-9]+$")
|
||||
PRERELEASE_VERSION_PATTERN = re.compile(r"-(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)(?:\+[0-9A-Za-z.-]+)?$")
|
||||
COMPLETION_RULE_BLOCKING_LEVELS = {
|
||||
"repo_ready": ("repo-ready",),
|
||||
"rc_ready": ("repo-ready", "rc-ready"),
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ class ControlPlaneAuditTest(unittest.TestCase):
|
|||
|
||||
def test_release_branch_for_version_uses_profile_branch_policy(self) -> None:
|
||||
self.assertEqual(release_branch_for_version("6.0.0-rc.1", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
self.assertEqual(release_branch_for_version("6.0.0-dev", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
self.assertEqual(release_branch_for_version("6.0.0", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
|
||||
def test_audit_flags_stale_active_target(self) -> None:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from typing import Callable
|
|||
from repo_file_io import REPO_ROOT, git_env
|
||||
|
||||
|
||||
SEMVER_PRERELEASE_RE = re.compile(r"-(?:rc|alpha|beta)\.\d+$")
|
||||
SEMVER_PRERELEASE_RE = re.compile(r"-(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)(?:\+[0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
def normalize_tag(value: str) -> str:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,22 @@ import resolve_release_promotion as resolver
|
|||
|
||||
|
||||
class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
def test_dev_prerelease_uses_prerelease_path(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
version="6.0.0-dev",
|
||||
promoted_from_tag_input="",
|
||||
rollback_version_input="5.1.14",
|
||||
ga_date_input="",
|
||||
v5_eos_date_input="",
|
||||
hotfix_exception=False,
|
||||
hotfix_reason_input="",
|
||||
release_notes_input="",
|
||||
tag_exists_fn=lambda tag: tag == "v5.1.14",
|
||||
)
|
||||
self.assertEqual(metadata["rollback_tag"], "v5.1.14")
|
||||
self.assertEqual(metadata["promoted_from_tag"], "")
|
||||
self.assertEqual(metadata["soak_hours"], "")
|
||||
|
||||
def test_prerelease_requires_explicit_stable_rollback(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
version="6.0.0-rc.2",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ GA_DATE_RE = re.compile(
|
|||
V5_EOS_RE = re.compile(
|
||||
r"existing v5 users until `(?P<v5_eos_date>\[v5-eos-date\]|\d{4}-\d{2}-\d{2})`\."
|
||||
)
|
||||
PRERELEASE_RE = re.compile(r"^(?P<stable>\d+\.\d+\.\d+)-(?:rc|alpha|beta)\.\d+$")
|
||||
PRERELEASE_RE = re.compile(r"^(?P<stable>\d+\.\d+\.\d+)-(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)$")
|
||||
TAG_LITERAL_RE = re.compile(r"`(v\d+\.\d+\.\d+-rc\.\d+)`")
|
||||
ACCIDENTAL_RC_TAG_DECISION_ID = "accidental-prerelease-tags-do-not-count-as-shipped-rcs"
|
||||
RELEASE_DRY_RUN_WORKFLOW = ".github/workflows/release-dry-run.yml"
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
def test_blocked_record_tracks_current_target_and_candidate_version(self) -> None:
|
||||
blocked = read("docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md")
|
||||
self.assertIn("origin/pulse/v6-release", blocked)
|
||||
self.assertIn("VERSION=6.0.0-rc.1", blocked)
|
||||
self.assertIn("VERSION=6.0.0-dev", blocked)
|
||||
self.assertIn("artifact-owned candidate stable tag", blocked)
|
||||
self.assertIn("artifact-owned promotion channel", blocked)
|
||||
self.assertIn("artifact-owned promoted prerelease tag", blocked)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from typing import Callable
|
|||
from repo_file_io import REPO_ROOT, git_env
|
||||
|
||||
|
||||
SEMVER_PRERELEASE_RE = re.compile(r"-(?:rc|alpha|beta)\.\d+$")
|
||||
SEMVER_PRERELEASE_RE = re.compile(r"-(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)(?:\+[0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
def normalize_tag(value: str) -> str:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,22 @@ import resolve_release_promotion as resolver
|
|||
|
||||
|
||||
class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
def test_dev_prerelease_uses_prerelease_path(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
version="6.0.0-dev",
|
||||
promoted_from_tag_input="",
|
||||
rollback_version_input="5.1.14",
|
||||
ga_date_input="",
|
||||
v5_eos_date_input="",
|
||||
hotfix_exception=False,
|
||||
hotfix_reason_input="",
|
||||
release_notes_input="",
|
||||
tag_exists_fn=lambda tag: tag == "v5.1.14",
|
||||
)
|
||||
self.assertEqual(metadata["rollback_tag"], "v5.1.14")
|
||||
self.assertEqual(metadata["promoted_from_tag"], "")
|
||||
self.assertEqual(metadata["soak_hours"], "")
|
||||
|
||||
def test_prerelease_requires_explicit_stable_rollback(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
version="6.0.0-rc.2",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue