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:
GitHub Copilot
2026-02-14 18:58:59 +00:00
parent a49c4d8718
commit cb36beaf2e
7 changed files with 44 additions and 6 deletions
+1
View File
@@ -3,3 +3,4 @@
!go/**
!Dockerfile
!.dockerignore
!VERSION
+2 -1
View File
@@ -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
+18 -2
View File
@@ -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)"
+3 -1
View File
@@ -2,7 +2,7 @@
![Icon](docs/icon-256.png)
`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:
+1
View File
@@ -0,0 +1 @@
1.3.0
+18 -1
View File
@@ -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 {
+1 -1
View File
@@ -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