build: add VERSION-based release workflow
Introduce VERSION as the app version source of truth and add make bump-patch to increment VERSION, commit, and create a matching vX.Y.Z tag. Wire VERSION into build outputs by injecting it into webterm.Version for make build-go and Docker image builds, and include VERSION in Docker build context. Also remove the visible dashboard container count subtitle while keeping count updates in browser console logs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -3,3 +3,4 @@
|
||||
!go/**
|
||||
!Dockerfile
|
||||
!.dockerignore
|
||||
!VERSION
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ WORKDIR /src
|
||||
COPY go/go.mod go/go.sum ./go/
|
||||
RUN cd go && go mod download
|
||||
COPY go ./go
|
||||
RUN cd go && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/webterm ./cmd/webterm
|
||||
COPY VERSION ./VERSION
|
||||
RUN cd go && VERSION=$(cat /src/VERSION) && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X github.com/rcarmo/webterm-go-port/webterm.Version=$VERSION" -o /out/webterm ./cmd/webterm
|
||||
|
||||
FROM alpine:3.21 AS runtime
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
.PHONY: help install install-dev lint format test race coverage check fuzz build-go build build-fast bundle bundle-watch bundle-clean clean clean-all build-all typecheck
|
||||
.PHONY: help install install-dev lint format test race coverage check fuzz build-go build build-fast bundle bundle-watch bundle-clean clean clean-all build-all typecheck bump-patch
|
||||
|
||||
GO_DIR = go
|
||||
STATIC_JS_DIR = go/webterm/static/js
|
||||
TERMINAL_TS = $(STATIC_JS_DIR)/terminal.ts
|
||||
TERMINAL_JS = $(STATIC_JS_DIR)/terminal.js
|
||||
GHOSTTY_WASM = $(STATIC_JS_DIR)/ghostty-vt.wasm
|
||||
VERSION_FILE = VERSION
|
||||
VERSION = $(shell test -f $(VERSION_FILE) && cat $(VERSION_FILE) || echo dev)
|
||||
GO_VERSION_LDFLAGS = -X github.com/rcarmo/webterm-go-port/webterm.Version=$(VERSION)
|
||||
|
||||
help: ## Show this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -55,7 +58,7 @@ bundle-watch: node_modules ## Watch mode for frontend development
|
||||
bun run watch
|
||||
|
||||
build-go: ## Build Go CLI binary
|
||||
cd $(GO_DIR) && mkdir -p bin && go build -o ./bin/webterm ./cmd/webterm
|
||||
cd $(GO_DIR) && mkdir -p bin && go build -ldflags "$(GO_VERSION_LDFLAGS)" -o ./bin/webterm ./cmd/webterm
|
||||
|
||||
clean: ## Remove coverage artifacts
|
||||
rm -f $(GO_DIR)/coverage.out
|
||||
@@ -67,3 +70,16 @@ clean-all: clean bundle-clean ## Remove all generated artifacts
|
||||
|
||||
build-all: clean-all install-dev build check build-go ## Full reproducible build from scratch
|
||||
@echo "Build complete!"
|
||||
|
||||
bump-patch: ## Bump patch version in VERSION and create git tag
|
||||
@if [ ! -f $(VERSION_FILE) ]; then echo "VERSION file not found"; exit 1; fi
|
||||
@OLD=$$(cat $(VERSION_FILE)); \
|
||||
MAJOR=$$(echo $$OLD | cut -d. -f1); \
|
||||
MINOR=$$(echo $$OLD | cut -d. -f2); \
|
||||
PATCH=$$(echo $$OLD | cut -d. -f3); \
|
||||
NEW="$$MAJOR.$$MINOR.$$((PATCH + 1))"; \
|
||||
echo $$NEW > $(VERSION_FILE); \
|
||||
git add $(VERSION_FILE); \
|
||||
git commit -m "Bump version to $$NEW"; \
|
||||
git tag "v$$NEW"; \
|
||||
echo "Bumped version: $$OLD -> $$NEW (tagged v$$NEW)"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||

|
||||
|
||||
`webterm` serves terminal sessions over HTTP/WebSocket, with a dashboard mode for multiple sessions and Docker-aware tiles.
|
||||
`webterm` serves terminal sessions over HTTP/WebSocket, with a dashboard mode for multiple sessions and live-updating terminal tiles.
|
||||
|
||||
This repository is the Go port of the original Python implementation, which is preserved in the `python` branch.
|
||||
|
||||
@@ -10,6 +10,7 @@ This repository is the Go port of the original Python implementation, which is p
|
||||
|
||||
## Features
|
||||
|
||||
- Typeahead find for quickly finding and launching sessions with minimal friction
|
||||
- Web terminal with reconnect support
|
||||
- Ghostty WebAssembly terminal engine for fast rendering
|
||||
- Session dashboard with live SVG screenshots
|
||||
@@ -101,6 +102,7 @@ make install-dev
|
||||
make check
|
||||
make race
|
||||
make test
|
||||
make bump-patch
|
||||
```
|
||||
|
||||
Frontend bundle tasks:
|
||||
|
||||
+18
-1
@@ -7,7 +7,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "1.3.0"
|
||||
DefaultHost = "0.0.0.0"
|
||||
DefaultPort = 8080
|
||||
DefaultTheme = "xterm"
|
||||
@@ -23,8 +22,26 @@ const (
|
||||
AutoCommandSentinel = "__docker_exec__"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
var Windows = runtime.GOOS == "windows"
|
||||
|
||||
func init() {
|
||||
if strings.TrimSpace(Version) != "" && Version != "dev" {
|
||||
return
|
||||
}
|
||||
for _, candidate := range []string{"VERSION", "../VERSION", "../../VERSION"} {
|
||||
data, err := os.ReadFile(candidate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v := strings.TrimSpace(string(data)); v != "" {
|
||||
Version = v
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EnvBool(name string) bool {
|
||||
v, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
|
||||
@@ -755,7 +755,7 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
if s.dockerWatch {
|
||||
dockerWatchJS = "true"
|
||||
}
|
||||
html := fmt.Sprintf(`<!DOCTYPE html><html><head><title>Session Dashboard</title><link rel="manifest" href="/static/manifest.json"><meta name="theme-color" content="#0d1117"><link rel="icon" href="/static/icons/webterm-192.png" sizes="192x192"><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;margin:16px;background:#0f172a;color:#e2e8f0}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.tile{background:#1e293b;border:1px solid #334155;border-radius:8px;overflow:hidden;cursor:pointer}.tile-header{padding:10px 12px;font-weight:bold;border-bottom:1px solid #334155;display:flex;justify-content:space-between}.thumb{width:100%%;height:180px;object-fit:contain;background:#0b1220;display:block}.meta{padding:8px 12px;color:#94a3b8;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty{color:#64748b;text-align:center;padding:40px}</style></head><body><h1>Sessions</h1><div id="subtitle"></div><div class="grid" id="grid"></div><script>let tiles=%s;const composeMode=%s;const dockerWatchMode=%s;let cardsBySlug={};const grid=document.getElementById("grid");const subtitle=document.getElementById("subtitle");function openTile(tile){const url='/?route_key='+encodeURIComponent(tile.slug);const target='webterm-'+tile.slug;let win=window.open(url,target);if(!win){window.location.href=url;return}if(typeof win.focus==='function'){win.focus()}}function makeTile(tile){const card=document.createElement('div');card.className='tile';const header=document.createElement('div');header.className='tile-header';header.innerHTML='<span>'+tile.name+'</span>';if(composeMode){const spark=document.createElement('img');spark.width=80;spark.height=16;spark.alt='CPU';header.appendChild(spark);card.sparkline=spark}const img=document.createElement('img');img.className='thumb';img.alt=tile.name;const meta=document.createElement('div');meta.className='meta';meta.textContent=tile.command||'';const body=document.createElement('div');body.appendChild(img);card.appendChild(header);card.appendChild(body);card.appendChild(meta);card.onclick=()=>openTile(tile);card.img=img;return card}function refreshTile(slug){const card=cardsBySlug[slug];if(card){card.img.src='/screenshot.svg?route_key='+encodeURIComponent(slug)+'&_t='+Date.now()}}function refreshSparklines(){if(!composeMode)return;tiles.forEach(tile=>{const card=cardsBySlug[tile.slug];if(card&&card.sparkline){card.sparkline.src='/cpu-sparkline.svg?container='+encodeURIComponent(tile.slug)+'&width=80&height=16&_t='+Date.now()}})}async function refreshTiles(){try{const resp=await fetch('/tiles');const next=await resp.json();const oldSlugs=tiles.map(t=>t.slug).sort().join(',');const newSlugs=next.map(t=>t.slug).sort().join(',');if(oldSlugs!==newSlugs){tiles=next;render()}}catch(_){}}function render(){grid.innerHTML='';cardsBySlug={};if(!tiles.length){grid.innerHTML='<div class="empty">No containers found. Start containers with the webterm-command label.</div>';subtitle.textContent=dockerWatchMode?'Watching for containers with webterm-command label...':'';return}subtitle.textContent=dockerWatchMode?tiles.length+' container(s) found':'';tiles.forEach(tile=>{const card=makeTile(tile);card.img.src='/screenshot.svg?route_key='+encodeURIComponent(tile.slug);grid.appendChild(card);cardsBySlug[tile.slug]=card});refreshSparklines()}let source=null;function startSSE(){if(source)return;source=new EventSource('/events');source.addEventListener('activity',(e)=>{if(e.data==='__dashboard__'){refreshTiles()}else{refreshTile(e.data)}});source.onerror=()=>{source.close();source=null;setTimeout(startSSE,2000)}}render();if(!document.hidden)startSSE();document.addEventListener('visibilitychange',()=>{if(document.hidden){if(source){source.close();source=null}}else startSSE()});if(composeMode){refreshSparklines();setInterval(refreshSparklines,30000)}</script></body></html>`, string(tilesJSON), composeModeJS, dockerWatchJS)
|
||||
html := fmt.Sprintf(`<!DOCTYPE html><html><head><title>Session Dashboard</title><link rel="manifest" href="/static/manifest.json"><meta name="theme-color" content="#0d1117"><link rel="icon" href="/static/icons/webterm-192.png" sizes="192x192"><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;margin:16px;background:#0f172a;color:#e2e8f0}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.tile{background:#1e293b;border:1px solid #334155;border-radius:8px;overflow:hidden;cursor:pointer}.tile-header{padding:10px 12px;font-weight:bold;border-bottom:1px solid #334155;display:flex;justify-content:space-between}.thumb{width:100%%;height:180px;object-fit:contain;background:#0b1220;display:block}.meta{padding:8px 12px;color:#94a3b8;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty{color:#64748b;text-align:center;padding:40px}</style></head><body><h1>Sessions</h1><div id="subtitle"></div><div class="grid" id="grid"></div><script>let tiles=%s;const composeMode=%s;const dockerWatchMode=%s;let cardsBySlug={};const grid=document.getElementById("grid");const subtitle=document.getElementById("subtitle");function openTile(tile){const url='/?route_key='+encodeURIComponent(tile.slug);const target='webterm-'+tile.slug;let win=window.open(url,target);if(!win){window.location.href=url;return}if(typeof win.focus==='function'){win.focus()}}function makeTile(tile){const card=document.createElement('div');card.className='tile';const header=document.createElement('div');header.className='tile-header';header.innerHTML='<span>'+tile.name+'</span>';if(composeMode){const spark=document.createElement('img');spark.width=80;spark.height=16;spark.alt='CPU';header.appendChild(spark);card.sparkline=spark}const img=document.createElement('img');img.className='thumb';img.alt=tile.name;const meta=document.createElement('div');meta.className='meta';meta.textContent=tile.command||'';const body=document.createElement('div');body.appendChild(img);card.appendChild(header);card.appendChild(body);card.appendChild(meta);card.onclick=()=>openTile(tile);card.img=img;return card}function refreshTile(slug){const card=cardsBySlug[slug];if(card){card.img.src='/screenshot.svg?route_key='+encodeURIComponent(slug)+'&_t='+Date.now()}}function refreshSparklines(){if(!composeMode)return;tiles.forEach(tile=>{const card=cardsBySlug[tile.slug];if(card&&card.sparkline){card.sparkline.src='/cpu-sparkline.svg?container='+encodeURIComponent(tile.slug)+'&width=80&height=16&_t='+Date.now()}})}async function refreshTiles(){try{const resp=await fetch('/tiles');const next=await resp.json();const oldSlugs=tiles.map(t=>t.slug).sort().join(',');const newSlugs=next.map(t=>t.slug).sort().join(',');if(oldSlugs!==newSlugs){tiles=next;render()}}catch(_){}}function render(){grid.innerHTML='';cardsBySlug={};if(!tiles.length){grid.innerHTML='<div class="empty">No containers found. Start containers with the webterm-command label.</div>';subtitle.textContent=dockerWatchMode?'Watching for containers with webterm-command label...':'';return}subtitle.textContent='';if(dockerWatchMode){console.log(tiles.length+' container(s) found')};tiles.forEach(tile=>{const card=makeTile(tile);card.img.src='/screenshot.svg?route_key='+encodeURIComponent(tile.slug);grid.appendChild(card);cardsBySlug[tile.slug]=card});refreshSparklines()}let source=null;function startSSE(){if(source)return;source=new EventSource('/events');source.addEventListener('activity',(e)=>{if(e.data==='__dashboard__'){refreshTiles()}else{refreshTile(e.data)}});source.onerror=()=>{source.close();source=null;setTimeout(startSSE,2000)}}render();if(!document.hidden)startSSE();document.addEventListener('visibilitychange',()=>{if(document.hidden){if(source){source.close();source=null}}else startSSE()});if(composeMode){refreshSparklines();setInterval(refreshSparklines,30000)}</script></body></html>`, string(tilesJSON), composeModeJS, dockerWatchJS)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = io.WriteString(w, html)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user