Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ version: 2

project_name: onebox

release:
prerelease: true
make_latest: false

builds:
- id: ob
main: ./cmd/ob
Expand Down Expand Up @@ -80,6 +84,7 @@ notarize:

homebrew_casks:
- name: onebox
skip_upload: false
ids:
- ob
binaries:
Expand All @@ -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 }}"
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions api/application/v1alpha2/application.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 12 additions & 5 deletions internal/buildinfo/calver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@ 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,
// binary provenance, and minimum-runner policy.
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 {
Expand All @@ -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
Expand All @@ -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
}
7 changes: 7 additions & 0 deletions internal/buildinfo/calver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
}
Expand Down Expand Up @@ -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},
Expand Down
14 changes: 14 additions & 0 deletions internal/onebox/runner_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions internal/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
26 changes: 13 additions & 13 deletions scripts/release_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -445,15 +445,15 @@ 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)
skipIfUTCMonthChanged(t, month)
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
Expand All @@ -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")

Expand Down Expand Up @@ -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"
Expand All @@ -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):
Expand Down
Loading
Loading