Compare commits

...

1 Commits

Author SHA1 Message Date
Jamison Lahman
5e34450051 chore(devtools): introduce dev utility script, ods 2025-12-03 08:37:12 -08:00
13 changed files with 493 additions and 0 deletions

39
.github/workflows/release-devtools.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Release Devtools
on:
push:
tags:
- "ods/v*.*.*"
jobs:
pypi:
runs-on: ubuntu-latest
environment:
name: release-devtools
permissions:
id-token: write
strategy:
matrix:
os-arch:
- {goos: "linux", goarch: "amd64"}
- {goos: "linux", goarch: "arm64"}
- {goos: "windows", goarch: "amd64"}
- {goos: "windows", goarch: "arm64"}
- {goos: "darwin", goarch: "amd64"}
- {goos: "darwin", goarch: "arm64"}
- {goos: "", goarch: ""}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # ratchet:actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # ratchet:astral-sh/setup-uv@v7
with:
enable-cache: false
- run: |
GOOS="${{ matrix.os-arch.goos }}" \
GOARCH="${{ matrix.os-arch.goarch }}" \
uv build --wheel
working-directory: tools/ods
- run: uv publish
working-directory: tools/ods

View File

@@ -250,6 +250,7 @@ numpy==1.26.4
# pandas-stubs
# shapely
# voyageai
onyx-devtools==0.0.6
openai==2.6.1
# via
# litellm

View File

@@ -180,6 +180,7 @@ dev = [
"ipykernel==6.29.5",
"release-tag==0.4.3",
"zizmor==1.18.0",
"onyx-devtools==0.0.6",
]
# Enterprise Edition features

3
tools/ods/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
ods
__pycache__
*.dist-info/

4
tools/ods/README.md Normal file
View File

@@ -0,0 +1,4 @@
# Onyx Developer Script
`ods` is [onyx.app](https://github.com/onyx-dot-app/onyx)'s devtools utility script.

View File

@@ -0,0 +1,235 @@
package cmd
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// CherryPickOptions holds options for the cherry-pick command
type CherryPickOptions struct {
Releases []string
}
// NewCherryPickCommand creates a new cherry-pick command
func NewCherryPickCommand() *cobra.Command {
opts := &CherryPickOptions{}
cmd := &cobra.Command{
Use: "cherry-pick <commit-sha>",
Short: "Cherry-pick a commit to a release branch",
Long: `Cherry-pick a commit to a release branch and create a PR.
This command will:
1. Find the nearest stable version tag (v*.*.* if --release not specified)
2. Fetch the corresponding release branch (release/vMAJOR.MINOR)
3. Create a hotfix branch with the cherry-picked commit
4. Push and create a PR using the GitHub CLI
5. Switch back to the original branch
The --release flag can be specified multiple times to cherry-pick to multiple release branches.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runCherryPick(cmd, args, opts)
},
}
cmd.Flags().StringSliceVar(&opts.Releases, "release", []string{}, "Release version(s) to cherry-pick to (e.g., 1.0, v1.1). 'v' prefix is optional. Can be specified multiple times.")
return cmd
}
func runCherryPick(cmd *cobra.Command, args []string, opts *CherryPickOptions) {
commitSHA := args[0]
log.Debugf("Cherry-picking commit: %s", commitSHA)
// Save the current branch to switch back later
originalBranch, err := getCurrentBranch()
if err != nil {
log.Fatalf("Failed to get current branch: %v", err)
}
log.Debugf("Original branch: %s", originalBranch)
// Get the short SHA for branch naming
shortSHA := commitSHA
if len(shortSHA) > 8 {
shortSHA = shortSHA[:8]
}
// Determine which releases to target
var releases []string
if len(opts.Releases) > 0 {
// Normalize versions to ensure they have 'v' prefix
for _, rel := range opts.Releases {
releases = append(releases, normalizeVersion(rel))
}
log.Infof("Using specified release versions: %v", releases)
} else {
// Find the nearest stable tag
version, err := findNearestStableTag(commitSHA)
if err != nil {
log.Fatalf("Failed to find nearest stable tag: %v", err)
}
releases = []string{version}
log.Infof("Auto-detected release version: %s", version)
}
// Get commit message for PR title
commitMsg, err := getCommitMessage(commitSHA)
if err != nil {
log.Warnf("Failed to get commit message, using default title: %v", err)
commitMsg = fmt.Sprintf("Hotfix: cherry-pick %s", shortSHA)
}
// Process each release
prURLs := []string{}
for _, release := range releases {
log.Infof("\n--- Processing release %s ---", release)
prURL, err := cherryPickToRelease(commitSHA, shortSHA, release, commitMsg, originalBranch)
if err != nil {
// Switch back to original branch before exiting on error
runGitCommand("checkout", originalBranch)
log.Fatalf("Failed to cherry-pick to release %s: %v", release, err)
}
prURLs = append(prURLs, prURL)
}
// Switch back to the original branch
log.Infof("\nSwitching back to original branch: %s", originalBranch)
if err := runGitCommand("checkout", originalBranch); err != nil {
log.Warnf("Failed to switch back to original branch: %v", err)
}
// Print all PR URLs
log.Info("\n=== Summary ===")
for i, prURL := range prURLs {
log.Infof("PR %d: %s", i+1, prURL)
}
}
// cherryPickToRelease cherry-picks a commit to a specific release branch
func cherryPickToRelease(commitSHA, shortSHA, version, commitMsg, originalBranch string) (string, error) {
releaseBranch := fmt.Sprintf("release/%s", version)
hotfixBranch := fmt.Sprintf("hotfix/%s-%s", shortSHA, version)
// Fetch the release branch
log.Infof("Fetching release branch: %s", releaseBranch)
if err := runGitCommand("fetch", "origin", releaseBranch); err != nil {
return "", fmt.Errorf("failed to fetch release branch %s: %w", releaseBranch, err)
}
// Create the hotfix branch from the release branch
log.Infof("Creating hotfix branch: %s", hotfixBranch)
if err := runGitCommand("checkout", "-b", hotfixBranch, fmt.Sprintf("origin/%s", releaseBranch)); err != nil {
return "", fmt.Errorf("failed to create hotfix branch: %w", err)
}
// Cherry-pick the commit
log.Infof("Cherry-picking commit: %s", commitSHA)
if err := runGitCommand("cherry-pick", commitSHA); err != nil {
return "", fmt.Errorf("failed to cherry-pick commit: %w", err)
}
// Push the hotfix branch
log.Infof("Pushing hotfix branch: %s", hotfixBranch)
if err := runGitCommand("push", "-u", "origin", hotfixBranch); err != nil {
return "", fmt.Errorf("failed to push hotfix branch: %w", err)
}
// Create PR using GitHub CLI
log.Info("Creating PR...")
prURL, err := createPR(hotfixBranch, releaseBranch, commitMsg, commitSHA)
if err != nil {
return "", fmt.Errorf("failed to create PR: %w", err)
}
log.Infof("PR created successfully: %s", prURL)
return prURL, nil
}
// getCurrentBranch returns the name of the current git branch
func getCurrentBranch() (string, error) {
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse failed: %w", err)
}
return strings.TrimSpace(string(output)), nil
}
// normalizeVersion ensures the version has a 'v' prefix
func normalizeVersion(version string) string {
if !strings.HasPrefix(version, "v") {
return "v" + version
}
return version
}
// findNearestStableTag finds the nearest tag matching v*.*.* pattern and returns major.minor
func findNearestStableTag(commitSHA string) (string, error) {
// Get tags that are ancestors of the commit, sorted by version
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0", "--match", "v*.*.*", commitSHA)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git describe failed: %w", err)
}
tag := strings.TrimSpace(string(output))
log.Debugf("Found tag: %s", tag)
// Extract major.minor with v prefix from tag (e.g., v1.2.3 -> v1.2)
re := regexp.MustCompile(`^(v\d+\.\d+)\.\d+`)
matches := re.FindStringSubmatch(tag)
if len(matches) < 2 {
return "", fmt.Errorf("tag %s does not match expected format v*.*.* ", tag)
}
return matches[1], nil
}
// runGitCommand executes a git command and returns any error
func runGitCommand(args ...string) error {
log.Debugf("Running: git %s", strings.Join(args, " "))
cmd := exec.Command("git", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// getCommitMessage gets the first line of a commit message
func getCommitMessage(commitSHA string) (string, error) {
cmd := exec.Command("git", "log", "-1", "--format=%s", commitSHA)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// createPR creates a pull request using the GitHub CLI
func createPR(headBranch, baseBranch, title, commitSHA string) (string, error) {
body := fmt.Sprintf("Cherry-pick of commit %s to %s branch.", commitSHA, baseBranch)
cmd := exec.Command("gh", "pr", "create",
"--base", baseBranch,
"--head", headBranch,
"--title", title,
"--body", body,
)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("%w: %s", err, string(exitErr.Stderr))
}
return "", err
}
prURL := strings.TrimSpace(string(output))
return prURL, nil
}

48
tools/ods/cmd/root.go Normal file
View File

@@ -0,0 +1,48 @@
package cmd
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
Version string
Commit string
)
// RootOptions holds options for the root command
type RootOptions struct {
Debug bool
}
// NewRootCommand creates the root command
func NewRootCommand() *cobra.Command {
opts := &RootOptions{}
cmd := &cobra.Command{
Use: "ods ",
Short: "Developer utilities for working on onyx.app",
Run: rootCmd,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if opts.Debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
},
Version: fmt.Sprintf("%s\ncommit %s", Version, Commit),
}
cmd.PersistentFlags().BoolVar(&opts.Debug, "debug", false, "run in debug mode")
// Add subcommands
cmd.AddCommand(NewCherryPickCommand())
return cmd
}
func rootCmd(cmd *cobra.Command, args []string) {
log.Debug("Debug log in rootCmd")
}

14
tools/ods/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/onyx-dot-app/onyx/tools/ods
go 1.25.0
require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

24
tools/ods/go.sum Normal file
View File

@@ -0,0 +1,24 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

44
tools/ods/hatch_build.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import os
import subprocess
from typing import Any
import manygo
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
"""Build hook to compile the Go binary and include it in the wheel."""
def initialize(self, version: Any, build_data: Any) -> None: # noqa: ARG002
"""Build the Go binary before packaging."""
build_data["pure_python"] = False
# Set platform tag for cross-compilation
goos = os.getenv("GOOS")
goarch = os.getenv("GOARCH")
if goos and goarch:
build_data["tag"] = "py3-none-" + manygo.get_platform_tag(
goos=goos, goarch=goarch
)
# Get config and environment
binary_name = self.config["binary_name"]
tag = os.getenv("GITHUB_REF_NAME", "dev").lstrip(f"{binary_name}/")
commit = os.getenv("GITHUB_SHA", "none")
# Build the Go binary if it doesn't exist
if not os.path.exists(binary_name):
print(f"Building Go binary '{binary_name}'...")
subprocess.check_call( # noqa: S603
[
"go",
"build",
f"-ldflags=-X main.version={tag} -X main.commit={commit} -s -w",
"-o",
binary_name,
],
)
build_data["shared_scripts"] = {binary_name: binary_name}

26
tools/ods/main.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"fmt"
"os"
"github.com/onyx-dot-app/onyx/tools/ods/cmd"
)
var (
version = "dev"
commit = "none"
)
func main() {
// Set the version in the cmd package
cmd.Version = version
cmd.Commit = commit
rootCmd := cmd.NewRootCommand()
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
}

38
tools/ods/pyproject.toml Normal file
View File

@@ -0,0 +1,38 @@
[build-system]
requires = ["hatchling", "hatch-vcs", "go-bin~=1.25.0", "manygo"]
build-backend = "hatchling.build"
[project]
name = "onyx-devtools"
description = "Developer utilities for working on onyx.app"
license = {file = "../../LICENSE"}
authors = [{ name = "Onyx AI", email = "founders@onyx.app" }]
readme = "README.md"
requires-python = ">=3.9"
keywords = [
"onyx", "cli", "devtools", "tools", "tooling",
]
classifiers = [
"Programming Language :: Go",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dynamic = ["version"]
[project.urls]
Repository = "https://github.com/onyx-dot-app/onyx"
[tool.hatch.build]
include = ["go.mod", "go.sum", "main.go", "**/*.go"]
[tool.hatch.version]
source = "vcs"
[tool.hatch.version.raw-options]
root = "../.."
tag_regex = "^ods/v(?P<version>[vV]?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
[tool.hatch.build.targets.wheel.hooks.custom]
path = "hatch_build.py"
binary_name = "ods"

16
uv.lock generated
View File

@@ -3589,6 +3589,7 @@ dev = [
{ name = "ipykernel" },
{ name = "mypy" },
{ name = "mypy-extensions" },
{ name = "onyx-devtools" },
{ name = "pandas-stubs" },
{ name = "pre-commit" },
{ name = "pytest" },
@@ -3758,6 +3759,7 @@ dev = [
{ name = "ipykernel", specifier = "==6.29.5" },
{ name = "mypy", specifier = "==1.13.0" },
{ name = "mypy-extensions", specifier = "==1.0.0" },
{ name = "onyx-devtools", specifier = "==0.0.6" },
{ name = "pandas-stubs", specifier = "==2.2.3.241009" },
{ name = "pre-commit", specifier = "==3.2.2" },
{ name = "pytest", specifier = "==8.3.5" },
@@ -3797,6 +3799,20 @@ model-server = [
{ name = "transformers", specifier = "==4.53.0" },
]
[[package]]
name = "onyx-devtools"
version = "0.0.6"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/f5/a7fc85e855ef42777775f4f516a0087c7b28400892f0311006026a569eb0/onyx_devtools-0.0.6-py3-none-any.whl", hash = "sha256:eb87fbb2889c96bffe44b93d7dcb06ee25c30f239082339703c079bab6723ecb", size = 1151749, upload-time = "2025-12-03T09:06:14.706Z" },
{ url = "https://files.pythonhosted.org/packages/75/f0/f6b3ba11da9143da08b5600e80d3f80db4075a55a6983bbe8cc726b482b4/onyx_devtools-0.0.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58989dcab7a4066fa93eed0e2f18feeda1ba96c29a79d22f19462e9e647b477f", size = 1140210, upload-time = "2025-12-03T09:06:09.724Z" },
{ url = "https://files.pythonhosted.org/packages/ef/23/f1b68cbc8c3cbe6dbbbd7164a390027384c71ac8b4795f86d6bb82cc0120/onyx_devtools-0.0.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:318b62c626a33c2bded9e3bf65ac4a309574d8748e91e124408d9517d18c399f", size = 1074627, upload-time = "2025-12-03T09:06:12.094Z" },
{ url = "https://files.pythonhosted.org/packages/27/4c/b18901e31b8599f628d71c2469ecb1be164caf55d70f966685b2831b87ca/onyx_devtools-0.0.6-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:71a1b626925b0b034ed01b86a11a27fd6a6e51b3d8a328a194bc92a00218a69f", size = 1053516, upload-time = "2025-12-03T09:06:13.463Z" },
{ url = "https://files.pythonhosted.org/packages/76/e7/64147941b30c22e5e45780eed16869da446464a52c49a1fd07f68d806cef/onyx_devtools-0.0.6-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:351d32cca03c1ff42729133990417ec96e18e74968c50605a863245f657b668a", size = 1151775, upload-time = "2025-12-03T09:06:14.437Z" },
{ url = "https://files.pythonhosted.org/packages/b3/c4/14895e7135c156c71cc4b5acac52321e216789c19ed5281d28e1cd255eac/onyx_devtools-0.0.6-py3-none-win_amd64.whl", hash = "sha256:1076d4b90f247333b5b97d12f7014a8e03e563c181620e0bdb02a510a06ee1e2", size = 1233522, upload-time = "2025-12-03T09:06:12.234Z" },
{ url = "https://files.pythonhosted.org/packages/12/a9/a78a56299b023c24dee4fc679e0ba4d711b4d469c9deece0b63f7630feb2/onyx_devtools-0.0.6-py3-none-win_arm64.whl", hash = "sha256:d1ac4e7f6ba796fd5080dd356379e6150205c4b779257b808c8770ce82228877", size = 1116285, upload-time = "2025-12-03T09:06:12.097Z" },
]
[[package]]
name = "openai"
version = "2.6.1"