Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
41c48af
Add handling for Vercel apps to exclude entire branches from deployme…
ashtom Apr 9, 2026
77a12f2
Update cmd/entire/cli/setup.go
ashtom Apr 9, 2026
fa28def
Update cmd/entire/cli/setup.go
ashtom Apr 9, 2026
fcf3459
Handle non-NotExist os.Stat errors in Vercel detection and fix ignore…
Copilot Apr 9, 2026
570726a
Change behavior to create vercel.json in Entire metadata branch.
ashtom Apr 12, 2026
d25d4da
Merge branch 'main' into ashtom/vercel-config
ashtom Apr 13, 2026
8569a40
Fix linter error.
ashtom Apr 14, 2026
e1cefae
Fix more linter errors.
ashtom Apr 14, 2026
62631bf
Address Copilot feedback to consider --local for vercel config.
ashtom Apr 14, 2026
9932710
Potential fix for pull request finding
ashtom Apr 14, 2026
c95946e
Merge branch 'main' into ashtom/vercel-config
ashtom Apr 14, 2026
23f9111
Deduplicate metadata branch vercel config merge
ashtom Apr 14, 2026
a791683
Optimize config loading for Vercel.
ashtom Apr 14, 2026
ca39df9
Clean up settings target file parameter.
ashtom Apr 14, 2026
eb4b5f4
Clean up test seam.
ashtom Apr 14, 2026
11c911e
Clean up load method.
ashtom Apr 14, 2026
b11dab3
Move Vercel helper methods into one file.
ashtom Apr 14, 2026
3f5893f
Fix missing target file.
ashtom Apr 14, 2026
60d2bb7
Merge remote-tracking branch 'origin/main' into ashtom/vercel-config
Copilot Apr 14, 2026
79cdfba
Improve enable/config messages for Vercel setting
ashtom Apr 15, 2026
5bf04bc
Fix vercel config lint issues
ashtom Apr 15, 2026
42f2803
Merge branch 'main' into ashtom/vercel-config
ashtom Apr 15, 2026
5c0a6ff
Merge branch 'main' into ashtom/vercel-config
gtrrz-victor Apr 16, 2026
2ed62c2
Merge branch 'main' into ashtom/vercel-config
gtrrz-victor Apr 16, 2026
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
115 changes: 115 additions & 0 deletions .opencode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions cmd/entire/cli/checkpoint/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import (
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/cmd/entire/cli/vercelconfig"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/entireio/cli/redact"
"github.com/stretchr/testify/require"

"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/config"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/filemode"
"github.com/go-git/go-git/v6/plumbing/object"
)

Expand Down Expand Up @@ -448,6 +450,166 @@ func setupBranchTestRepo(t *testing.T) (*git.Repository, plumbing.Hash) {
return repo, commitHash
}

func TestEnsureSessionsBranch_WritesVercelConfigWhenEnabled(t *testing.T) {
vercelconfig.ResetSettingsCache()
t.Cleanup(vercelconfig.ResetSettingsCache)

repo, _ := setupBranchTestRepo(t)
worktree, err := repo.Worktree()
if err != nil {
t.Fatalf("repo.Worktree() error = %v", err)
}
t.Chdir(worktree.Filesystem.Root())

entireDir := filepath.Join(worktree.Filesystem.Root(), ".entire")
if err := os.MkdirAll(entireDir, 0o755); err != nil {
t.Fatalf("mkdir .entire: %v", err)
}
if err := os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{"enabled":true,"vercel":true}`), 0o644); err != nil {
t.Fatalf("write settings.json: %v", err)
}

store := NewGitStore(repo)
if err := store.ensureSessionsBranch(context.Background()); err != nil {
t.Fatalf("ensureSessionsBranch() error = %v", err)
}

ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true)
if err != nil {
t.Fatalf("metadata branch ref: %v", err)
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
t.Fatalf("metadata commit: %v", err)
}
tree, err := commit.Tree()
if err != nil {
t.Fatalf("metadata tree: %v", err)
}
file, err := tree.File(vercelconfig.FileName)
if err != nil {
t.Fatalf("expected %s on metadata branch: %v", vercelconfig.FileName, err)
}
content, err := file.Contents()
if err != nil {
t.Fatalf("read %s: %v", vercelconfig.FileName, err)
}

var config map[string]any
if err := json.Unmarshal([]byte(content), &config); err != nil {
t.Fatalf("parse %s: %v", vercelconfig.FileName, err)
}
if !vercelconfig.DeploymentDisabled(config) {
t.Fatalf("expected %s to disable %s, got %s", vercelconfig.FileName, vercelconfig.BranchPattern, content)
}
}

func TestWriteCommitted_MergesVercelConfigOnMetadataBranch(t *testing.T) {
vercelconfig.ResetSettingsCache()
t.Cleanup(vercelconfig.ResetSettingsCache)

repo, _ := setupBranchTestRepo(t)
worktree, err := repo.Worktree()
if err != nil {
t.Fatalf("repo.Worktree() error = %v", err)
}
repoRoot := worktree.Filesystem.Root()
t.Chdir(repoRoot)

entireDir := filepath.Join(repoRoot, ".entire")
if err := os.MkdirAll(entireDir, 0o755); err != nil {
t.Fatalf("mkdir .entire: %v", err)
}
if err := os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{"enabled":true,"vercel":true}`), 0o644); err != nil {
t.Fatalf("write settings.json: %v", err)
}

initialConfig := []byte(`{
"cleanUrls": true,
"git": {
"deploymentEnabled": {
"main": true
}
}
}
`)
blobHash, err := CreateBlobFromContent(repo, initialConfig)
if err != nil {
t.Fatalf("CreateBlobFromContent() error = %v", err)
}
treeHash, err := BuildTreeFromEntries(context.Background(), repo, map[string]object.TreeEntry{
vercelconfig.FileName: {Name: vercelconfig.FileName, Mode: filemode.Regular, Hash: blobHash},
})
if err != nil {
t.Fatalf("BuildTreeFromEntries() error = %v", err)
}

store := NewGitStore(repo)
commitHash, err := store.createCommit(treeHash, plumbing.ZeroHash, "Initialize metadata branch", "Test", "test@test.com")
if err != nil {
t.Fatalf("createCommit() error = %v", err)
}
if err := repo.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), commitHash)); err != nil {
t.Fatalf("set metadata branch ref: %v", err)
}

cpID := id.MustCheckpointID("abcdef123456")
err = store.WriteCommitted(context.Background(), WriteCommittedOptions{
CheckpointID: cpID,
SessionID: "test-session-id",
Strategy: "manual-commit",
Transcript: redact.AlreadyRedacted([]byte(`{"test": true}`)),
AuthorName: "Test",
AuthorEmail: "test@test.com",
})
if err != nil {
t.Fatalf("WriteCommitted() error = %v", err)
}

ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true)
if err != nil {
t.Fatalf("metadata branch ref: %v", err)
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
t.Fatalf("metadata commit: %v", err)
}
tree, err := commit.Tree()
if err != nil {
t.Fatalf("metadata tree: %v", err)
}
file, err := tree.File(vercelconfig.FileName)
if err != nil {
t.Fatalf("expected %s on metadata branch: %v", vercelconfig.FileName, err)
}
content, err := file.Contents()
if err != nil {
t.Fatalf("read %s: %v", vercelconfig.FileName, err)
}

var config map[string]any
if err := json.Unmarshal([]byte(content), &config); err != nil {
t.Fatalf("parse %s: %v", vercelconfig.FileName, err)
}
if config["cleanUrls"] != true {
t.Fatalf("expected cleanUrls to be preserved, got %#v", config["cleanUrls"])
}
gitConfig, ok := config["git"].(map[string]any)
if !ok {
t.Fatalf("expected git object, got %#v", config["git"])
}
deploymentEnabled, ok := gitConfig["deploymentEnabled"].(map[string]any)
if !ok {
t.Fatalf("expected deploymentEnabled object, got %#v", gitConfig["deploymentEnabled"])
}
if deploymentEnabled["main"] != true {
t.Fatalf("expected main rule to be preserved, got %#v", deploymentEnabled["main"])
}
if deploymentEnabled[vercelconfig.BranchPattern] != false {
t.Fatalf("expected %s to be disabled, got %#v", vercelconfig.BranchPattern, deploymentEnabled[vercelconfig.BranchPattern])
}
}

// verifyBranchInMetadata reads and verifies the branch field in metadata.json.
func verifyBranchInMetadata(t *testing.T, repo *git.Repository, checkpointID id.CheckpointID, expectedBranch string, shouldOmit bool) {
t.Helper()
Expand Down
24 changes: 24 additions & 0 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/cmd/entire/cli/validation"
"github.com/entireio/cli/cmd/entire/cli/vercelconfig"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/entireio/cli/perf"
"github.com/entireio/cli/redact"
Expand Down Expand Up @@ -102,6 +103,10 @@ func (s *GitStore) WriteCommitted(ctx context.Context, opts WriteCommittedOption
if err != nil {
return err
}
newTreeHash, err = s.maybeMergeVercelConfig(ctx, newTreeHash)
if err != nil {
return err
}

commitMsg := s.buildCommitMessage(opts, taskMetadataPath)
newCommitHash, err := s.createCommit(newTreeHash, parentHash, commitMsg, opts.AuthorName, opts.AuthorEmail)
Expand Down Expand Up @@ -1337,6 +1342,10 @@ func (s *GitStore) UpdateCommitted(ctx context.Context, opts UpdateCommittedOpti
if err != nil {
return err
}
newTreeHash, err = s.maybeMergeVercelConfig(ctx, newTreeHash)
if err != nil {
return err
}

authorName, authorEmail := GetGitAuthorFromRepo(s.repo)
commitMsg := fmt.Sprintf("Finalize transcript for Checkpoint: %s", opts.CheckpointID)
Expand Down Expand Up @@ -1414,6 +1423,10 @@ func (s *GitStore) ensureSessionsBranch(ctx context.Context) error {
if err != nil {
return err
}
emptyTreeHash, err = s.maybeMergeVercelConfig(ctx, emptyTreeHash)
if err != nil {
return err
}

authorName, authorEmail := GetGitAuthorFromRepo(s.repo)
commitHash, err := s.createCommit(emptyTreeHash, plumbing.ZeroHash, "Initialize sessions branch", authorName, authorEmail)
Expand All @@ -1428,6 +1441,17 @@ func (s *GitStore) ensureSessionsBranch(ctx context.Context) error {
return nil
}

func (s *GitStore) maybeMergeVercelConfig(ctx context.Context, rootTreeHash plumbing.Hash) (plumbing.Hash, error) {
if err := vercelconfig.InitSettings(ctx); err != nil {
return plumbing.ZeroHash, fmt.Errorf("initialize vercel settings: %w", err)
}
mergedTreeHash, err := vercelconfig.MaybeMergeMetadataBranchConfig(s.repo, rootTreeHash)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("merge vercel metadata branch config: %w", err)
}
return mergedTreeHash, nil
}

// getFetchingTree returns a FetchingTree for the metadata branch.
// If a blob fetcher is configured on the store, File() calls on the returned
// tree will automatically fetch missing blobs from the remote.
Expand Down
17 changes: 17 additions & 0 deletions cmd/entire/cli/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ type EntireSettings struct {
// plugins (entire-agent-* binaries on $PATH). Defaults to false.
ExternalAgents bool `json:"external_agents,omitempty"`

// Vercel indicates that the repository uses Vercel and the metadata branch
// should include a vercel.json that disables deployments for Entire branches.
Vercel bool `json:"vercel,omitempty"`

// Deprecated: no longer used. Exists to tolerate old settings files
// that still contain "strategy": "auto-commit" or similar.
Strategy string `json:"strategy,omitempty"`
Expand Down Expand Up @@ -116,6 +120,10 @@ func Load(ctx context.Context) (*EntireSettings, error) {
localSettingsFileAbs = EntireSettingsLocalFile // Fallback to relative
}

return loadMergedSettings(settingsFileAbs, localSettingsFileAbs)
}

func loadMergedSettings(settingsFileAbs, localSettingsFileAbs string) (*EntireSettings, error) {
// Load base settings
settings, err := loadFromFile(settingsFileAbs)
if err != nil {
Expand Down Expand Up @@ -300,6 +308,15 @@ func mergeJSON(settings *EntireSettings, data []byte) error {
settings.ExternalAgents = ea
}

// Override vercel if present
if vercelRaw, ok := raw["vercel"]; ok {
var vercel bool
if err := json.Unmarshal(vercelRaw, &vercel); err != nil {
return fmt.Errorf("parsing vercel field: %w", err)
}
settings.Vercel = vercel
}

return nil
}

Expand Down
Loading
Loading