From ab0f90656f322b5744a599c6f2fdf188e5a19982 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 4 Oct 2026 17:48:08 -0700 Subject: [PATCH] chore(release): default to alpha releases --- .goreleaser.yaml | 6 ++++ CONTRIBUTING.md | 4 +++ Justfile | 6 ++-- .../v1alpha2/application.schema.json | 6 ++-- internal/app/constraints.go | 2 +- internal/app/types.go | 2 +- internal/buildinfo/calver.go | 17 ++++++--- internal/buildinfo/calver_test.go | 7 ++++ internal/onebox/runner_policy_test.go | 14 ++++++++ internal/proxy/proxy.go | 6 ++-- internal/proxy/proxy_test.go | 14 ++++++++ scripts/release.sh | 9 ++--- scripts/release_workflow_test.go | 26 +++++++------- scripts/require-newest-release.sh | 10 +++--- scripts/require_newest_release_test.go | 7 +++- scripts/validate-release-tag.sh | 4 +-- scripts/validate_release_tag_test.go | 13 ++++--- scripts/verify_release_dist_test.go | 2 +- .../v1alpha2/application.schema.json | 6 ++-- .../docs/guides/environment-variables.mdx | 2 +- .../docs/reference/fields/environments.mdx | 2 +- site/src/content/docs/reference/policies.mdx | 22 ++++++------ site/src/content/docs/start/install.mdx | 35 +++++++++---------- site/src/content/docs/status/capabilities.mdx | 2 +- 24 files changed, 141 insertions(+), 83 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index dac25684..718957be 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,6 +2,10 @@ version: 2 project_name: onebox +release: + prerelease: true + make_latest: false + builds: - id: ob main: ./cmd/ob @@ -80,6 +84,7 @@ notarize: homebrew_casks: - name: onebox + skip_upload: false ids: - ob binaries: @@ -101,6 +106,7 @@ homebrew_casks: scoops: - name: onebox + skip_upload: false ids: - ob url_template: "https://github.com/labstack/onebox/releases/download/{{ .Tag }}/{{ .ArtifactName }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99c6a5ad..ab783c1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,6 +124,10 @@ deployment tool is judged by what it does when something goes wrong. just release ``` +Onebox is in alpha. `just release` creates the next `vYYYY.M.REVISION-alpha` +tag, starting at revision zero each UTC month. GitHub marks it as a prerelease; +Homebrew and Scoop publish the same alpha version. + This requires a clean, checked, up-to-date `main`. It atomically publishes a metadata-only fast-forward release commit plus its tag to `origin`, so the release identity needs permission to fast-forward `main`. A branch policy that diff --git a/Justfile b/Justfile index 1f611aab..9bd0f4ab 100644 --- a/Justfile +++ b/Justfile @@ -13,8 +13,8 @@ build: set -euo pipefail ob_build_dir="${ONEBOX_BIN_DIR:-bin}" ob_build_version="${ONEBOX_VERSION:-}" - if [ -n "$ob_build_version" ] && [[ ! "$ob_build_version" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})$ ]]; then - echo "ONEBOX_VERSION must match vYYYY.M.REVISION" >&2; exit 1 + if [ -n "$ob_build_version" ] && [[ ! "$ob_build_version" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})(-alpha)?$ ]]; then + echo "ONEBOX_VERSION must match vYYYY.M.REVISION with an optional -alpha suffix" >&2; exit 1 fi if [ -z "$ob_build_version" ]; then # --long keeps the commit suffix even on a tagged commit, so a checkout build @@ -314,7 +314,7 @@ social-card: mv "$staged" site/public/social-card.png echo "rendered site/public/social-card.png" -# Create and publish the next vYYYY.M.REVISION tag from releasable main. +# Create and publish the next vYYYY.M.REVISION-alpha tag from releasable main. release: bash scripts/release.sh diff --git a/api/application/v1alpha2/application.schema.json b/api/application/v1alpha2/application.schema.json index 3d1b945e..b3bd3f37 100644 --- a/api/application/v1alpha2/application.schema.json +++ b/api/application/v1alpha2/application.schema.json @@ -588,11 +588,11 @@ "type": "object" }, "minOneboxVersion": { - "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.8.0.", + "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.10.0-alpha.", "examples": [ - "v2026.8.0" + "v2026.10.0-alpha" ], - "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})$", + "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})(-alpha)?$", "type": "string" }, "minPlanSchema": { diff --git a/internal/app/constraints.go b/internal/app/constraints.go index 3760d893..072fa72e 100644 --- a/internal/app/constraints.go +++ b/internal/app/constraints.go @@ -118,7 +118,7 @@ var ( "a lower-case ASCII or IDNA A-label hostname, optionally prefixed by the complete wildcard label *."} gCalVer = grammar{"version", buildinfo.ReleaseVersionPattern, - "a CalVer release such as v2026.8.0"} + "a CalVer release such as v2026.10.0-alpha"} gHTTPURL = grammar{"url", regexp.MustCompile(`^https?://`), "an http or https URL"} diff --git a/internal/app/types.go b/internal/app/types.go index 5b9835f6..a7c99988 100644 --- a/internal/app/types.go +++ b/internal/app/types.go @@ -102,7 +102,7 @@ type Jump struct { type Policy struct { RequireApproval bool `json:"require_approval" description:"Require a plan-bound local confirmation before mutating this environment." default:"true"` AllowAgentProposals bool `json:"allow_agent_proposals" description:"Declared permission for agent-authored proposals. The current CLI does not distinguish agent identity; execution remains approval-gated." default:"true"` - MinOneboxVersion string `json:"min_onebox_version,omitempty" description:"Oldest released Onebox runner allowed to operate this environment." example:"v2026.8.0"` + MinOneboxVersion string `json:"min_onebox_version,omitempty" description:"Oldest released Onebox runner allowed to operate this environment." example:"v2026.10.0-alpha"` MinPlanSchema string `json:"min_plan_schema,omitempty" description:"Oldest executable plan schema accepted by this environment." example:"onebox.run/executable-deploy-plan/v1alpha2"` // Migrations groups what this environment demands of a release that carries // migration risk. Grouped rather than four flat keys each repeating the diff --git a/internal/buildinfo/calver.go b/internal/buildinfo/calver.go index 3579563a..1308a5c9 100644 --- a/internal/buildinfo/calver.go +++ b/internal/buildinfo/calver.go @@ -7,16 +7,17 @@ import ( ) // ReleaseVersionPattern is the one grammar for a release identity. It is -// exported so the project loader validates `min_version` against the +// exported so the project loader validates `minOneboxVersion` against the // same expression this package parses: a loader that accepted a version no tag // can carry would fail closed on a release that is perfectly valid. -var ReleaseVersionPattern = regexp.MustCompile(`^v([1-9][0-9]{3})\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})$`) +var ReleaseVersionPattern = regexp.MustCompile(`^v([1-9][0-9]{3})\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})(-alpha)?$`) -// ReleaseVersion is a canonical Onebox vYYYY.M.REVISION release identity. +// ReleaseVersion is a canonical Onebox CalVer identity, optionally in alpha. type ReleaseVersion struct { year uint64 month uint64 revision uint64 + alpha bool } // ParseReleaseVersion parses the exact version form used by Onebox tags, @@ -24,7 +25,7 @@ type ReleaseVersion struct { func ParseReleaseVersion(value string) (ReleaseVersion, error) { matches := ReleaseVersionPattern.FindStringSubmatch(value) if matches == nil { - return ReleaseVersion{}, fmt.Errorf("%q must match vYYYY.M.REVISION with a four-digit year, an unpadded month from 1 through 12, and an unpadded non-negative revision of at most nineteen digits", value) + return ReleaseVersion{}, fmt.Errorf("%q must match vYYYY.M.REVISION with an optional -alpha suffix, a four-digit year, an unpadded month from 1 through 12, and an unpadded non-negative revision of at most nineteen digits", value) } year, err := strconv.ParseUint(matches[1], 10, 64) if err != nil { @@ -38,7 +39,7 @@ func ParseReleaseVersion(value string) (ReleaseVersion, error) { if err != nil { return ReleaseVersion{}, fmt.Errorf("parse release revision: %w", err) } - return ReleaseVersion{year: year, month: month, revision: revision}, nil + return ReleaseVersion{year: year, month: month, revision: revision, alpha: matches[4] != ""}, nil } // CompareReleaseVersions returns -1, 0, or 1 when actual is older than, equal @@ -56,5 +57,11 @@ func CompareReleaseVersions(actual, minimum ReleaseVersion) int { return 1 } } + if actual.alpha != minimum.alpha { + if actual.alpha { + return -1 + } + return 1 + } return 0 } diff --git a/internal/buildinfo/calver_test.go b/internal/buildinfo/calver_test.go index ec52b9d5..c0811e0f 100644 --- a/internal/buildinfo/calver_test.go +++ b/internal/buildinfo/calver_test.go @@ -6,6 +6,7 @@ func TestParseReleaseVersion(t *testing.T) { valid := []string{ "v2010.1.0", "v2026.8.42", + "v2026.10.0-alpha", "v9999.12.9999999999999999999", } for _, value := range valid { @@ -26,6 +27,8 @@ func TestParseReleaseVersion(t *testing.T) { "v2026.8.00", "v2026.8.01", "v2026.8.0-rc1", + "v2026.8.0-alpha.1", + "v2026.8.0-alpha-0-gabcdef", "v2026.8.18446744073709551616", "dev", } @@ -53,6 +56,10 @@ func TestCompareReleaseVersions(t *testing.T) { want int }{ {actual: "v2026.8.4", minimum: "v2026.8.3", want: 1}, + {actual: "v2026.10.0-alpha", minimum: "v2026.10.0-alpha", want: 0}, + {actual: "v2026.10.0-alpha", minimum: "v2026.10.0", want: -1}, + {actual: "v2026.10.0", minimum: "v2026.10.0-alpha", want: 1}, + {actual: "v2026.10.1-alpha", minimum: "v2026.10.0", want: 1}, {actual: "v2026.8.4", minimum: "v2026.8.4", want: 0}, {actual: "v2026.8.4", minimum: "v2026.8.5", want: -1}, {actual: "v2026.7.20", minimum: "v2026.8.0", want: -1}, diff --git a/internal/onebox/runner_policy_test.go b/internal/onebox/runner_policy_test.go index b9d19037..ca08338a 100644 --- a/internal/onebox/runner_policy_test.go +++ b/internal/onebox/runner_policy_test.go @@ -41,6 +41,20 @@ func TestEnforceRunnerPolicy(t *testing.T) { } }) + t.Run("alpha runner", func(t *testing.T) { + alpha := runner + alpha.Version = "v2026.10.0-alpha" + minimum := policy + minimum.MinOneboxVersion = alpha.Version + if err := enforceRunnerPolicy(minimum, alpha, "onebox.run/executable-deploy-plan/v1alpha2"); err != nil { + t.Fatal(err) + } + minimum.MinOneboxVersion = "v2026.10.0" + if err := enforceRunnerPolicy(minimum, alpha, "onebox.run/executable-deploy-plan/v1alpha2"); err == nil { + t.Fatal("alpha runner must not satisfy the same stable version") + } + }) + t.Run("invalid minimum", func(t *testing.T) { invalid := policy invalid.MinOneboxVersion = "2026.8.3" diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index f557e435..3010a784 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -24,13 +24,13 @@ import ( "hash" "os" "path/filepath" - "regexp" "sort" "strings" "time" "github.com/compose-spec/compose-go/v2/dotenv" "github.com/labstack/onebox/internal/app" + "github.com/labstack/onebox/internal/buildinfo" "github.com/pelletier/go-toml/v2" "gopkg.in/yaml.v3" ) @@ -46,13 +46,11 @@ const ( DiscoveryContainerName = "onebox-discovery" ) -var releaseVersion = regexp.MustCompile(`^v[0-9]{4}\.[0-9]{1,2}\.[0-9]+$`) - // DiscoveryImage pins the controller to the runner release. Development // builds use the explicitly unstable edge tag so they cannot masquerade as a // released controller. func DiscoveryImage(version string) string { - if releaseVersion.MatchString(version) { + if buildinfo.ReleaseVersionPattern.MatchString(version) { return DiscoveryImageRepository + ":" + version } return DiscoveryImageRepository + ":edge" diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index a12d6179..03f05b54 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -24,6 +24,20 @@ import ( ) const testSocketlessStatic = "ping: {}\nproviders:\n file:\n directory: /etc/traefik/dynamic\n" + +func TestDiscoveryImageReleaseIdentity(t *testing.T) { + for _, version := range []string{"v2026.10.0-alpha", "v2026.9.15"} { + if got := DiscoveryImage(version); got != DiscoveryImageRepository+":"+version { + t.Errorf("DiscoveryImage(%q) = %q", version, got) + } + } + for _, version := range []string{"dev", "v2026.10.0-alpha-0-gabcdef", "v2026.10.0-alpha-dirty"} { + if got := DiscoveryImage(version); got != DiscoveryImageRepository+":edge" { + t.Errorf("DiscoveryImage(%q) = %q", version, got) + } + } +} + const testSocketlessStaticWithResolver = testSocketlessStatic + "certificatesResolvers:\n " + app.ManagedCertificateResolver + ":\n acme:\n storage: /letsencrypt/acme.json\n httpChallenge:\n entryPoint: web\n" func writeCfg(t *testing.T, files map[string]string) string { diff --git a/scripts/release.sh b/scripts/release.sh index 21a0090d..a468fa8b 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -83,7 +83,7 @@ increment_decimal() { require_previous_release_terminal() { local repository=$1 previous="" candidate previous_commit run_status while IFS= read -r candidate; do - if [[ "$candidate" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})$ ]]; then + if [[ "$candidate" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})(-alpha)?$ ]]; then previous=$candidate break fi @@ -134,7 +134,7 @@ release_tags=$(git tag --list "v${release_period}.*" --sort=-v:refname) release_period_pattern=${release_period//./\\.} release_last="" while IFS= read -r release_candidate; do - if [[ "$release_candidate" =~ ^v${release_period_pattern}\.(0|[1-9][0-9]{0,18})$ ]]; then + if [[ "$release_candidate" =~ ^v${release_period_pattern}\.(0|[1-9][0-9]{0,18})(-alpha)?$ ]]; then release_last=$release_candidate break fi @@ -146,13 +146,14 @@ else # 64-bit and the grammar admits revisions wider than that, which would wrap to # a negative number and be tagged. Exhaustion is a twenty-digit result, which # is the only value the contract cannot express. - release_number=$(increment_decimal "${release_last##*.}") + release_base=${release_last%-alpha} + release_number=$(increment_decimal "${release_base##*.}") if [ "${#release_number}" -gt 19 ]; then echo "revision space for ${release_period} is exhausted at ${release_last}." >&2 exit 1 fi fi -release_tag="v${release_period}.${release_number}" +release_tag="v${release_period}.${release_number}-alpha" release_commit=$(printf 'chore(release): %s\n' "$release_tag" | git commit-tree "${release_head}^{tree}" -p "$release_head") echo "tagging $release_tag" diff --git a/scripts/release_workflow_test.go b/scripts/release_workflow_test.go index 7fac3b8d..42f7e23f 100644 --- a/scripts/release_workflow_test.go +++ b/scripts/release_workflow_test.go @@ -28,7 +28,7 @@ func TestReleaseRevision(t *testing.T) { t.Run("first release in UTC month", func(t *testing.T) { repo := newTestRepository(t) month := utcPeriod() - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" output, err := runRelease(t, repo, normalJustShim, nil) skipIfUTCMonthChanged(t, month) @@ -41,12 +41,12 @@ func TestReleaseRevision(t *testing.T) { t.Run("revision compares integers across nine to ten", func(t *testing.T) { repo := newTestRepository(t) month := utcPeriod() - for _, tag := range []string{"v" + month + ".8", "v" + month + ".9", "v" + month + ".08", "v" + month + ".09", "v" + month + ".010", "v" + month + ".invalid"} { + for _, tag := range []string{"v" + month + ".8", "v" + month + ".9-alpha", "v" + month + ".08", "v" + month + ".09", "v" + month + ".010", "v" + month + ".invalid"} { runGit(t, repo.work, "tag", tag, repo.head) } runGit(t, repo.work, "push", "origin", "--tags") - tag := "v" + month + ".10" + tag := "v" + month + ".10-alpha" output, err := runRelease(t, repo, normalJustShim, nil) skipIfUTCMonthChanged(t, month) if err != nil { @@ -67,7 +67,7 @@ func TestReleaseRevision(t *testing.T) { runGit(t, repo.work, "tag", "v"+previousMonth+".41", repo.head) runGit(t, repo.work, "push", "origin", "--tags") - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" output, err := runRelease(t, repo, normalJustShim, nil) skipIfUTCMonthChanged(t, month) if err != nil { @@ -125,7 +125,7 @@ func TestReleaseRejectsMainAdvanceBeforeAtomicPublication(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) month := utcPeriod() - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" hook := `#!/bin/sh set -eu git -C "$RACER_REPO" fetch origin main @@ -151,7 +151,7 @@ func TestNoOpBranchRefMissesMainAdvanceAfterAdvertisement(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) month := utcPeriod() - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" hook := `#!/bin/sh set -eu git -C "$RACER_REPO" fetch origin main @@ -183,7 +183,7 @@ func TestReleaseLosesCompetingTagRaceWithoutReplacingWinner(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) month := utcPeriod() - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" hook := `#!/bin/sh set -eu git -C "$RACER_REPO" commit --allow-empty -m competing-tag @@ -205,7 +205,7 @@ func TestReleaseFailsClosedWhenBranchPolicyRejectsMainUpdate(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) month := utcPeriod() - tag := "v" + month + ".0" + tag := "v" + month + ".0-alpha" hook := `#!/bin/sh set -eu while read -r old_object new_object ref_name; do @@ -445,7 +445,7 @@ func TestReleaseIncrementsARevisionWiderThanMachineArithmetic(t *testing.T) { repo := newTestRepository(t) month := utcPeriod() // One past the signed 64-bit maximum, so plain arithmetic would wrap. - runGit(t, repo.work, "tag", "v"+month+".9223372036854775808", repo.head) + runGit(t, repo.work, "tag", "v"+month+".9223372036854775808-alpha", repo.head) runGit(t, repo.work, "push", "origin", "--tags") output, err := runRelease(t, repo, normalJustShim, nil) @@ -453,7 +453,7 @@ func TestReleaseIncrementsARevisionWiderThanMachineArithmetic(t *testing.T) { if err != nil { t.Fatalf("release failed: %v\n%s", err, output) } - assertPublishedRelease(t, repo, "v"+month+".9223372036854775809") + assertPublishedRelease(t, repo, "v"+month+".9223372036854775809-alpha") } // Bash arithmetic is signed 64-bit, so incrementing a revision the grammar still @@ -462,7 +462,7 @@ func TestReleaseRefusesWhenTheRevisionSpaceIsExhausted(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) month := utcPeriod() - exhausted := "v" + month + ".9999999999999999999" + exhausted := "v" + month + ".9999999999999999999-alpha" runGit(t, repo.work, "tag", exhausted, repo.head) runGit(t, repo.work, "push", "origin", "--tags") @@ -504,7 +504,7 @@ func TestReleaseWaitsForThePreviousReleaseRunToFinish(t *testing.T) { t.Run(test.name, func(t *testing.T) { repo := newTestRepository(t) month := utcPeriod() - runGit(t, repo.work, "tag", "v"+month+".0", repo.head) + runGit(t, repo.work, "tag", "v"+month+".0-alpha", repo.head) runGit(t, repo.work, "push", "origin", "--tags") stub := "#!/usr/bin/env bash\nprintf '%s\\n' '" + test.status + "'\n" @@ -517,7 +517,7 @@ func TestReleaseWaitsForThePreviousReleaseRunToFinish(t *testing.T) { case test.wantErr == "" && err != nil: t.Fatalf("release failed: %v\n%s", err, output) case test.wantErr == "": - assertPublishedRelease(t, repo, "v"+month+".1") + assertPublishedRelease(t, repo, "v"+month+".1-alpha") case err == nil: t.Fatalf("release created a tag while the previous run was %s:\n%s", test.status, output) case !strings.Contains(output, test.wantErr): diff --git a/scripts/require-newest-release.sh b/scripts/require-newest-release.sh index 60f0e60f..03b2bd4f 100755 --- a/scripts/require-newest-release.sh +++ b/scripts/require-newest-release.sh @@ -37,10 +37,12 @@ if [ "$published" = "$release_tag" ]; then exit 1 fi -# sort -V orders the release grammar correctly: fields are numeric and unpadded, -# which is exactly the case a lexicographic sort gets wrong. -newest=$(printf '%s\n%s\n' "$published" "$release_tag" | LC_ALL=C sort -V | tail -n 1) -if [ "$newest" != "$release_tag" ]; then +# Compare calendar fields numerically. An alpha precedes the stable version +# with the same calendar fields, which sort -V alone does not guarantee. +published_base=${published%-alpha} +release_base=${release_tag%-alpha} +newest=$(printf '%s\n%s\n' "$published_base" "$release_base" | LC_ALL=C sort -V | tail -n 1) +if [ "$newest" != "$release_base" ] || { [ "$published_base" = "$release_base" ] && [ "$release_tag" != "$release_base" ]; }; then echo "release tag ${release_tag} is behind the published ${published}; publishing it would downgrade Homebrew and Scoop." >&2 exit 1 fi diff --git a/scripts/require_newest_release_test.go b/scripts/require_newest_release_test.go index 4e40e405..e39c1b08 100644 --- a/scripts/require_newest_release_test.go +++ b/scripts/require_newest_release_test.go @@ -24,7 +24,12 @@ func TestRequireNewestRelease(t *testing.T) { apiFails bool wantErr string }{ - {name: "first release ever", published: "", tag: "v2026.8.0"}, + {name: "first release ever", published: "", tag: "v2026.10.0-alpha"}, + {name: "new alpha revision", published: "v2026.10.9-alpha", tag: "v2026.10.10-alpha"}, + {name: "alpha after earlier stable revision", published: "v2026.10.0", tag: "v2026.10.1-alpha"}, + {name: "stable after same alpha", published: "v2026.10.0-alpha", tag: "v2026.10.0"}, + {name: "alpha behind same stable", published: "v2026.10.0", tag: "v2026.10.0-alpha", wantErr: "is behind the published"}, + {name: "alpha already published", published: "v2026.10.0-alpha", tag: "v2026.10.0-alpha", wantErr: "is already published"}, {name: "newer revision", published: "v2026.8.0", tag: "v2026.8.1"}, {name: "revision past nine", published: "v2026.8.9", tag: "v2026.8.10"}, {name: "month past nine", published: "v2026.9.0", tag: "v2026.11.0"}, diff --git a/scripts/validate-release-tag.sh b/scripts/validate-release-tag.sh index 366773d3..46dcb271 100755 --- a/scripts/validate-release-tag.sh +++ b/scripts/validate-release-tag.sh @@ -7,8 +7,8 @@ main_ref=${2:-origin/main} # The revision is capped at nineteen digits because that is the widest value the # runner's own parser can hold: every 19-digit number fits in a uint64, and a tag # it cannot parse is a release with no usable provenance. -if [[ ! "$release_tag" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})$ ]]; then - echo "release tag must match vYYYY.M.REVISION." >&2 +if [[ ! "$release_tag" =~ ^v[1-9][0-9]{3}\.([1-9]|1[0-2])\.(0|[1-9][0-9]{0,18})(-alpha)?$ ]]; then + echo "release tag must match vYYYY.M.REVISION with an optional -alpha suffix." >&2 exit 1 fi diff --git a/scripts/validate_release_tag_test.go b/scripts/validate_release_tag_test.go index 8ecab4bd..8125068c 100644 --- a/scripts/validate_release_tag_test.go +++ b/scripts/validate_release_tag_test.go @@ -17,13 +17,13 @@ var validateReleaseTagScript string func TestValidateReleaseTagAcceptsTagOnMain(t *testing.T) { requireReleaseTools(t) repo := newTestRepository(t) - runGit(t, repo.work, "tag", "v2026.8.0", repo.head) + runGit(t, repo.work, "tag", "v2026.8.0-alpha", repo.head) - output, err := runTagValidator(t, repo.work, "v2026.8.0", "origin/main") + output, err := runTagValidator(t, repo.work, "v2026.8.0-alpha", "origin/main") if err != nil { t.Fatalf("validator rejected a release on main: %v\n%s", err, output) } - if !strings.Contains(output, "validated v2026.8.0") { + if !strings.Contains(output, "validated v2026.8.0-alpha") { t.Fatalf("validator did not report the accepted tag:\n%s", output) } } @@ -49,9 +49,9 @@ func TestValidateReleaseTagRejectsCommitOffMain(t *testing.T) { repo := newTestRepository(t) runGit(t, repo.work, "checkout", "-b", "release-candidate") runGit(t, repo.work, "commit", "--allow-empty", "-m", "off-main release") - runGit(t, repo.work, "tag", "v2026.8.0") + runGit(t, repo.work, "tag", "v2026.8.0-alpha") - output, err := runTagValidator(t, repo.work, "v2026.8.0", "origin/main") + output, err := runTagValidator(t, repo.work, "v2026.8.0-alpha", "origin/main") if err == nil { t.Fatalf("validator accepted a release commit off main:\n%s", output) } @@ -87,6 +87,9 @@ func TestTagValidatorAndParserAgreeOnTheGrammar(t *testing.T) { }{ {tag: "v2026.1.0", valid: true}, {tag: "v2026.10.0", valid: true}, + {tag: "v2026.10.0-alpha", valid: true}, + {tag: "v2026.10.0-alpha.1"}, + {tag: "v2026.10.0-alpha-0-gabcdef"}, {tag: "v2026.12.99", valid: true}, {tag: "v2026.8.10", valid: true}, {tag: "v2026.8.9999999999999999999", valid: true}, diff --git a/scripts/verify_release_dist_test.go b/scripts/verify_release_dist_test.go index b410b7af..4169da9e 100644 --- a/scripts/verify_release_dist_test.go +++ b/scripts/verify_release_dist_test.go @@ -18,7 +18,7 @@ func TestReleaseDistVerifierResolvesGeneratedCaskVersion(t *testing.T) { requireArtifactVerifierTools(t) t.Run("accepts GoReleaser interpolation", func(t *testing.T) { - dist, binDir := releaseDistFixture(t, "2026.8.1", "v2026.8.1") + dist, binDir := releaseDistFixture(t, "2026.10.0-alpha", "v2026.10.0-alpha") output, err := runArtifactVerifier(t, dist, binDir) if err != nil { t.Fatalf("verifier rejected a generated Cask: %v\n%s", err, output) diff --git a/site/public/schemas/application/v1alpha2/application.schema.json b/site/public/schemas/application/v1alpha2/application.schema.json index 3d1b945e..b3bd3f37 100644 --- a/site/public/schemas/application/v1alpha2/application.schema.json +++ b/site/public/schemas/application/v1alpha2/application.schema.json @@ -588,11 +588,11 @@ "type": "object" }, "minOneboxVersion": { - "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.8.0.", + "description": "Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.10.0-alpha.", "examples": [ - "v2026.8.0" + "v2026.10.0-alpha" ], - "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})$", + "pattern": "^v([1-9][0-9]{3})\\.([1-9]|1[0-2])\\.(0|[1-9][0-9]{0,18})(-alpha)?$", "type": "string" }, "minPlanSchema": { diff --git a/site/src/content/docs/guides/environment-variables.mdx b/site/src/content/docs/guides/environment-variables.mdx index 9f29fdbf..143705cc 100644 --- a/site/src/content/docs/guides/environment-variables.mdx +++ b/site/src/content/docs/guides/environment-variables.mdx @@ -67,7 +67,7 @@ printf '{"schema_version":"onebox.run/job-result/v1alpha1","changed":true}' > "$ | `ONEBOX_LOCAL` | `1` or `true` runs against the local Docker daemon instead of connecting over SSH | | `ONEBOX_BIN_DIR` | Where `just build` writes the binary, and where the other recipes look for it (default `bin`) | | `ONEBOX_INSTALL_DIR` | Where `just install` places the binary (default `~/.local/bin`) | -| `ONEBOX_VERSION` | Version string `just build` stamps into the binary; must match `vYYYY.M.REVISION` | +| `ONEBOX_VERSION` | Version string `just build` stamps into the binary; must match `vYYYY.M.REVISION` with an optional `-alpha` suffix | | `ONEBOX_RELEASE_REPOSITORY` | Repository `scripts/release.sh` queries to confirm the previous release run finished, when the git remote does not name it | `ONEBOX_LOCAL` is the only one of these Onebox itself reads; the rest are build diff --git a/site/src/content/docs/reference/fields/environments.mdx b/site/src/content/docs/reference/fields/environments.mdx index e6b8fe18..03b4bef8 100644 --- a/site/src/content/docs/reference/fields/environments.mdx +++ b/site/src/content/docs/reference/fields/environments.mdx @@ -47,7 +47,7 @@ cannot drift from what `ob validate` accepts. | `.policy.migrations.backupMaxAge` | string | `24h` | Maximum age of a backup report accepted for a migration. Expects a duration such as 30s, 5m, 1h30m or 14d. | | `.policy.migrations.requireBackup` | boolean | `false` | Require a plan-bound backup report before a release with migration risk. | | `.policy.migrations.requireRestoreTest` | boolean | `false` | Require the backup report to state that a restore test succeeded. | -| `.policy.minOneboxVersion` | string | — | Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.8.0. | +| `.policy.minOneboxVersion` | string | — | Oldest released Onebox runner allowed to operate this environment. Expects a CalVer release such as v2026.10.0-alpha. | | `.policy.minPlanSchema` | string | — | Oldest executable plan schema accepted by this environment. Expects a plan schema identity such as onebox.run/executable-deploy-plan/v1alpha2. | | `.policy.requireApproval` | boolean | `true` | Require a plan-bound local confirmation before mutating this environment. | | `.server` | object | — | SSH server, written as user@host or as an object with host, user, and port. Also accepts user@host. | diff --git a/site/src/content/docs/reference/policies.mdx b/site/src/content/docs/reference/policies.mdx index 0c31ad03..fd5a4c1d 100644 --- a/site/src/content/docs/reference/policies.mdx +++ b/site/src/content/docs/reference/policies.mdx @@ -12,9 +12,11 @@ read_when: ## Release identity -Onebox releases use `vYYYY.M.REVISION`, for example `v2026.8.0` for the first -release in August 2026. The year has four digits, months are unpadded, and each -UTC calendar month starts at revision zero. +Onebox releases use `vYYYY.M.REVISION-alpha`, for example `v2026.10.0-alpha` for the +first alpha release in October 2026. The year has four digits, months are +unpadded, and each UTC calendar month starts at revision zero. Every release +increments the revision. +An alpha precedes the stable version with the same year, month, and revision. Checkout builds use Git-derived provenance and remain **visibly distinct** from a release. `ob version` reports which you have. @@ -25,8 +27,8 @@ release. `ob version` reports which you have. environments: production: policy: - min_onebox_version: v2026.8.0 - min_plan_schema: onebox.run/executable-deploy-plan/v1alpha2 + minOneboxVersion: v2026.10.0-alpha + minPlanSchema: onebox.run/executable-deploy-plan/v1alpha2 ``` `ob doctor` reports whether the runner selected by `PATH` is compatible. @@ -166,13 +168,11 @@ mentions it somewhere. The other five are maintained by hand and could drift without failing the build. ::: -## Pre-release evolution +## Alpha releases -Onebox is not released. The current contract is intentionally strict and may -replace earlier development shapes without aliases, migrations, or hidden -fallbacks. Unknown fields and unsupported schema identities are rejected. -Compatibility guarantees begin with the first public release, not with local -development snapshots. +Onebox is in alpha. The contract may change as the product develops. Unknown +fields and unsupported schema identities are rejected. Use the documentation +and schema that match your installed release. The JSON Schema published by `ob schema` is generated from the same declarations the loader enforces and is checked against the conformance corpus, so what your diff --git a/site/src/content/docs/start/install.mdx b/site/src/content/docs/start/install.mdx index 4b60cfa5..1c19b5ff 100644 --- a/site/src/content/docs/start/install.mdx +++ b/site/src/content/docs/start/install.mdx @@ -11,20 +11,17 @@ read_when: import { Steps, Tabs, TabItem } from '@astrojs/starlight/components'; -Choose the package manager for your machine, download a released binary, or -build from source. Then verify which `ob` your shell will run. +Onebox is in alpha. Choose the package manager for your machine, download an +alpha binary, or build from source. Then verify which `ob` your shell will run. ## Choose an install method -:::note[Releases begin at the first tag] Every download below, including the assets Homebrew and Scoop fetch, comes from -a published `vYYYY.M.REVISION` release. If -[the releases page](https://github.com/labstack/onebox/releases) is empty, or -lists nothing for your platform, build from source instead. -::: +a published alpha on [the releases page](https://github.com/labstack/onebox/releases). +Homebrew and Scoop install the current alpha release. Set `version` to the release without its leading `v`. The asset name uses that -value, while the download URL uses the full `vYYYY.M.REVISION` tag. +value, while the download URL uses the full `vYYYY.M.REVISION-alpha` tag. @@ -40,7 +37,7 @@ value, while the download URL uses the full `vYYYY.M.REVISION` tag. ```sh - version=2026.8.0 + version=2026.10.0-alpha arch=arm64 # use amd64 on Intel Macs asset="onebox_${version}_darwin_${arch}.tar.gz" curl -fLO "https://github.com/labstack/onebox/releases/download/v${version}/${asset}" @@ -55,7 +52,7 @@ value, while the download URL uses the full `vYYYY.M.REVISION` tag. ```sh - version=2026.8.0 + version=2026.10.0-alpha arch=amd64 # use arm64 on 64-bit ARM asset="onebox_${version}_linux_${arch}.tar.gz" curl -fLO "https://github.com/labstack/onebox/releases/download/v${version}/${asset}" @@ -70,7 +67,7 @@ value, while the download URL uses the full `vYYYY.M.REVISION` tag. ```powershell - $Version = "2026.8.0" + $Version = "2026.10.0-alpha" $Arch = "amd64" # use arm64 on Windows on ARM $Asset = "onebox_${Version}_windows_${Arch}.zip" $Base = "https://github.com/labstack/onebox/releases/download/v${Version}" @@ -111,10 +108,10 @@ the package entry as above, then install the local file: ```sh # Debian or Ubuntu (choose amd64 or arm64) -sudo apt install ./onebox_2026.8.0_linux_amd64.deb +sudo apt install ./onebox_2026.10.0-alpha_linux_amd64.deb # Fedora or RHEL (choose amd64 or arm64) -sudo dnf install ./onebox_2026.8.0_linux_amd64.rpm +sudo dnf install ./onebox_2026.10.0-alpha_linux_amd64.rpm ``` These are downloadable package files, not hosted APT or RPM repositories. @@ -123,9 +120,9 @@ WinGet is not published yet. Homebrew and Scoop metadata live in the dedicated Onebox source repository. :::caution[`go install` is not a release channel] -The application tag `v2026.8.0` is intentionally CalVer-shaped, but the Go module +The application tag `v2026.10.0-alpha` is intentionally CalVer-shaped, but the Go module path does not declare a `/v2026` module major. Use a precompiled release asset or -build from source; `go install github.com/labstack/onebox/cmd/ob@v2026.8.0` is not +build from source; `go install github.com/labstack/onebox/cmd/ob@v2026.10.0-alpha` is not a supported installation contract. ::: @@ -152,8 +149,8 @@ build, test, formatting and check targets. ob version ``` - Releases use `vYYYY.M.REVISION`, for example `v2026.8.0` for the first release - in August 2026. The year is four digits, months are unpadded, and each UTC + Releases use `vYYYY.M.REVISION-alpha`, for example `v2026.10.0-alpha` for the + first release in October 2026. The year is four digits, months are unpadded, and each UTC calendar month starts at revision zero. Checkout builds use Git-derived provenance and stay visibly distinct from a release. @@ -164,7 +161,7 @@ build, test, formatting and check targets. ``` `ob doctor` reports whether the runner selected by `PATH` satisfies the - environment's `min_onebox_version` and `min_plan_schema`, and names every + environment's `minOneboxVersion` and `minPlanSchema`, and names every workload and service holding durable data that has no backup. Run it from a directory that has an `ob.yml`. Outside a project it reports @@ -177,7 +174,7 @@ build, test, formatting and check targets. Both commands support `--output json`. :::note[Why a checkout build can be refused] -When an environment sets `min_onebox_version`, commit-derived and dirty +When an environment sets `minOneboxVersion`, commit-derived and dirty checkout builds **fail closed** — they are not released runners. That is deliberate: an environment that pins a minimum is asking for a runner whose identity can be checked, and a dirty working tree has none. diff --git a/site/src/content/docs/status/capabilities.mdx b/site/src/content/docs/status/capabilities.mdx index 2532b8d0..34bc08f3 100644 --- a/site/src/content/docs/status/capabilities.mdx +++ b/site/src/content/docs/status/capabilities.mdx @@ -60,7 +60,7 @@ This page is the reconciliation. Three states: ## Distribution, published by tag Configured and verified in CI on every pull request, and published by pushing a -`vYYYY.M.REVISION` tag. What is installable at any moment is what +`vYYYY.M.REVISION-alpha` tag. What is installable at any moment is what [the releases page](https://github.com/labstack/onebox/releases) lists; the install commands work from the first published tag onward.