Skip to content

Commit 17d8afc

Browse files
committed
1 parent 62aac25 commit 17d8afc

4 files changed

Lines changed: 280 additions & 2 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Publish version
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
create_tag:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
# Step 1: Checkout the repository
14+
- name: Checkout repository
15+
uses: actions/checkout@v3
16+
with:
17+
fetch-depth: 0
18+
19+
# Step 2: Set up Git for tagging
20+
- name: Set up Git for tagging
21+
run: |
22+
git config user.name "${{ github.actor }}"
23+
git config user.email "${{ github.actor }}@users.noreply.github.com"
24+
25+
# Step 3: Get the latest tag and increment the minor version
26+
- name: Get latest tag and increment
27+
id: get_tag
28+
run: |
29+
# Fetch all tags
30+
git fetch --tags
31+
32+
# Get the latest tag version
33+
latest_tag=$(git describe --tags --abbrev=0 || echo "v0.0.0")
34+
echo "Latest tag: $latest_tag"
35+
36+
# Extract the version numbers (major, minor, patch)
37+
major=$(echo $latest_tag | cut -d. -f1 | cut -dv -f2)
38+
minor=$(echo $latest_tag | cut -d. -f2)
39+
patch=$(echo $latest_tag | cut -d. -f3)
40+
41+
# Increment the minor version and reset patch to 0
42+
new_minor=$((minor + 1))
43+
new_version="v${major}.${new_minor}.0"
44+
45+
# Write the new version to the GITHUB_OUTPUT file for later use
46+
echo "tag=$new_version" >> $GITHUB_OUTPUT
47+
48+
# Step 4: Create and push the new tag
49+
- name: Create new tag
50+
run: |
51+
new_tag="${{ steps.get_tag.outputs.tag }}"
52+
git tag $new_tag
53+
git push origin $new_tag

README.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/cloudimpl/poly-watcher
2+
3+
go 1.23.0

main.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"hash/fnv"
7+
"log"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
12+
"sync"
13+
"time"
14+
)
15+
16+
type Watcher struct {
17+
dir string
18+
interval time.Duration
19+
buildCmd string
20+
runCmd string
21+
includes []string
22+
excludes []string
23+
depFile string
24+
depCmd string
25+
prevHash uint64
26+
prevDepMTime time.Time
27+
process *exec.Cmd
28+
processMu sync.Mutex
29+
}
30+
31+
func NewWatcher(dir string, interval time.Duration, buildCmd, runCmd, depFile, depCmd string, includes, excludes []string) *Watcher {
32+
return &Watcher{
33+
dir: dir,
34+
interval: interval,
35+
buildCmd: buildCmd,
36+
runCmd: runCmd,
37+
depFile: depFile,
38+
depCmd: depCmd,
39+
includes: includes,
40+
excludes: excludes,
41+
}
42+
}
43+
44+
func (w *Watcher) shouldProcess(relPath string) bool {
45+
for _, ex := range w.excludes {
46+
if strings.HasPrefix(relPath, ex) || strings.HasSuffix(relPath, ex) {
47+
return false
48+
}
49+
}
50+
if len(w.includes) == 0 {
51+
return true
52+
}
53+
for _, in := range w.includes {
54+
if strings.HasPrefix(relPath, in) || strings.HasSuffix(relPath, in) {
55+
return true
56+
}
57+
}
58+
return false
59+
}
60+
61+
func (w *Watcher) hashDir() (uint64, bool, error) {
62+
h := fnv.New64a()
63+
depChanged := false
64+
65+
err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
66+
if err != nil {
67+
return err
68+
}
69+
relPath, _ := filepath.Rel(w.dir, path)
70+
71+
if info.IsDir() {
72+
if info.Name()[0] == '.' {
73+
return filepath.SkipDir
74+
}
75+
if !w.shouldProcess(relPath) {
76+
return filepath.SkipDir
77+
}
78+
return nil
79+
}
80+
81+
if info.Name()[0] == '.' {
82+
return nil
83+
}
84+
if !w.shouldProcess(relPath) {
85+
return nil
86+
}
87+
88+
h.Write([]byte(relPath))
89+
h.Write([]byte(fmt.Sprintf("%d", info.Size())))
90+
h.Write([]byte(info.ModTime().String()))
91+
92+
if w.depFile != "" && filepath.Base(path) == filepath.Base(w.depFile) {
93+
if info.ModTime() != w.prevDepMTime {
94+
depChanged = true
95+
w.prevDepMTime = info.ModTime()
96+
}
97+
}
98+
return nil
99+
})
100+
101+
if err != nil {
102+
return 0, false, err
103+
}
104+
return h.Sum64(), depChanged, nil
105+
}
106+
107+
func (w *Watcher) runShell(command string) error {
108+
if command == "" {
109+
return nil
110+
}
111+
cmd := exec.Command("/bin/sh", "-c", command)
112+
cmd.Stdout = os.Stdout
113+
cmd.Stderr = os.Stderr
114+
return cmd.Run()
115+
}
116+
117+
func (w *Watcher) runBuild(depChanged bool) error {
118+
if depChanged && w.depCmd != "" {
119+
log.Printf("%s changed: running %s...\n", w.depFile, w.depCmd)
120+
if err := w.runShell(w.depCmd); err != nil {
121+
return err
122+
}
123+
}
124+
125+
log.Println("Running build command...")
126+
return w.runShell(w.buildCmd)
127+
}
128+
129+
func (w *Watcher) startApp() error {
130+
w.processMu.Lock()
131+
defer w.processMu.Unlock()
132+
133+
if w.process != nil && w.process.Process != nil {
134+
log.Println("Stopping previous app process...")
135+
_ = w.process.Process.Kill()
136+
w.process = nil
137+
}
138+
139+
log.Println("Starting app...")
140+
cmd := exec.Command("/bin/sh", "-c", w.runCmd)
141+
cmd.Stdout = os.Stdout
142+
cmd.Stderr = os.Stderr
143+
144+
if err := cmd.Start(); err != nil {
145+
return err
146+
}
147+
148+
w.process = cmd
149+
go func() {
150+
_ = cmd.Wait()
151+
log.Println("App exited")
152+
w.processMu.Lock()
153+
w.process = nil
154+
w.processMu.Unlock()
155+
}()
156+
return nil
157+
}
158+
159+
func (w *Watcher) Run() {
160+
for {
161+
hash, depChanged, err := w.hashDir()
162+
if err != nil {
163+
log.Println("Error hashing dir:", err)
164+
time.Sleep(w.interval)
165+
continue
166+
}
167+
168+
if hash != w.prevHash {
169+
log.Println("Change detected, rebuilding...")
170+
w.prevHash = hash
171+
172+
if err := w.runBuild(depChanged); err != nil {
173+
log.Println("Build failed:", err)
174+
time.Sleep(w.interval)
175+
continue
176+
}
177+
178+
if err := w.startApp(); err != nil {
179+
log.Println("App start failed:", err)
180+
}
181+
}
182+
183+
time.Sleep(w.interval)
184+
}
185+
}
186+
187+
func printBanner() {
188+
fmt.Println("🚀 Poly Watcher — The universal build-run watcher for your projects. Change it. Build it. Run it. Repeat.")
189+
fmt.Println("Example:")
190+
fmt.Println(` poly-watcher --depfile=go.mod --depcommand="go mod tidy && go mod download" --build="go build -o myapp ." --run="./myapp" --include=.go --exclude=.git,.polycode`)
191+
fmt.Println()
192+
}
193+
194+
func main() {
195+
printBanner()
196+
197+
dir := flag.String("dir", ".", "Working directory to watch")
198+
buildCmd := flag.String("build", "echo 'No build command specified'", "Build command to run on change")
199+
runCmd := flag.String("run", "echo 'No run command specified'", "Run command to execute built app")
200+
depFile := flag.String("depfile", "", "Dependency file to monitor for changes (e.g. go.mod, package.json)")
201+
depCmd := flag.String("depcommand", "", "Command to run when dependency file changes (e.g. 'go mod tidy', 'npm install')")
202+
interval := flag.Duration("interval", 1*time.Second, "Polling interval (e.g. 1s, 500ms)")
203+
includeDirs := flag.String("include", "", "Comma-separated list of include rules (prefix or suffix, e.g. '.go,services')")
204+
excludeDirs := flag.String("exclude", "", "Comma-separated list of exclude rules (prefix or suffix, e.g. '.git,tmp')")
205+
206+
flag.Parse()
207+
208+
includes := []string{}
209+
excludes := []string{}
210+
if *includeDirs != "" {
211+
includes = strings.Split(*includeDirs, ",")
212+
}
213+
if *excludeDirs != "" {
214+
excludes = strings.Split(*excludeDirs, ",")
215+
}
216+
217+
if err := os.Chdir(*dir); err != nil {
218+
log.Fatalf("Failed to change directory: %v", err)
219+
}
220+
221+
watcher := NewWatcher(".", *interval, *buildCmd, *runCmd, *depFile, *depCmd, includes, excludes)
222+
log.Println("Starting watcher...")
223+
watcher.Run()
224+
}

0 commit comments

Comments
 (0)