main.go
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"strings"
)
func main() {
var (
tag = flag.String("tag", "latest", "Image tag")
dockerhub = flag.String("dockerhub", "ccutch/readysite", "Docker Hub repository")
skipRegistry = flag.Bool("skip-registry", false, "Skip pushing to private registry")
skipDockerhub = flag.Bool("skip-dockerhub", false, "Skip pushing to Docker Hub")
buildServer = flag.String("build-server", "", "Server to build on (uses launch if set)")
pushServer = flag.String("push-server", "readysite-org-1", "Server to push from (must be logged into Docker Hub)")
skipTests = flag.Bool("skip-tests", false, "Skip running tests before publish")
)
flag.Parse()
// Run tests before publishing
if !*skipTests {
if err := runTests(); err != nil {
log.Fatalf("Tests failed, aborting publish: %v", err)
}
}
imageName := "website"
// Get push server IP and registry from infra.json
pushServerIP := getServerIP(*pushServer)
if pushServerIP == "" {
log.Fatalf("Could not find IP for push server %s in infra.json", *pushServer)
}
registry := pushServerIP + ":5001"
// Check if Docker is available locally
localDocker := exec.Command("docker", "info").Run() == nil
if localDocker {
// Build locally
log.Printf("Building %s locally...", imageName)
if err := run("docker", "build", "-t", imageName+":"+*tag, "./website"); err != nil {
log.Fatalf("Build failed: %v", err)
}
// Push to private registry
if !*skipRegistry {
registryImage := fmt.Sprintf("%s/%s:%s", registry, imageName, *tag)
log.Printf("Pushing to %s...", registryImage)
if err := run("docker", "tag", imageName+":"+*tag, registryImage); err != nil {
log.Fatalf("Tag failed: %v", err)
}
if err := run("docker", "push", registryImage); err != nil {
log.Fatalf("Push to registry failed: %v", err)
}
log.Printf("Pushed to %s ✓", registryImage)
}
// Push to Docker Hub
if !*skipDockerhub {
dockerhubImage := fmt.Sprintf("%s:%s", *dockerhub, *tag)
log.Printf("Pushing to %s...", dockerhubImage)
if err := run("docker", "tag", imageName+":"+*tag, dockerhubImage); err != nil {
log.Fatalf("Tag failed: %v", err)
}
if err := run("docker", "push", dockerhubImage); err != nil {
log.Fatalf("Push to Docker Hub failed: %v", err)
}
log.Printf("Pushed to %s ✓", dockerhubImage)
}
} else {
// Build remotely using launch, then push from server
log.Println("Local Docker unavailable, using remote build...")
if *buildServer != "" {
// Build on specified server using launch
log.Printf("Building on %s using launch...", *buildServer)
if err := run("go", "run", "./cmd/launch", "--server", *buildServer); err != nil {
log.Fatalf("Remote build failed: %v", err)
}
// Push from build server to registry
if !*skipRegistry {
log.Printf("Pushing to registry from %s...", *buildServer)
serverIP := getServerIP(*buildServer)
if serverIP == "" {
log.Fatalf("Could not find IP for server %s", *buildServer)
}
registryImage := fmt.Sprintf("%s/%s:%s", registry, imageName, *tag)
if err := sshRun(serverIP, fmt.Sprintf("docker tag %s:latest %s && docker push %s", imageName, registryImage, registryImage)); err != nil {
log.Fatalf("Push to registry failed: %v", err)
}
log.Printf("Pushed to %s ✓", registryImage)
}
} else {
log.Println("Skipping build (no --build-server specified, assuming image exists in registry)")
}
// Push to Docker Hub from push server
if !*skipDockerhub {
log.Printf("Pushing to Docker Hub from %s...", *pushServer)
dockerhubImage := fmt.Sprintf("%s:%s", *dockerhub, *tag)
registryImage := fmt.Sprintf("localhost:5001/%s:%s", imageName, *tag)
cmds := fmt.Sprintf("docker pull %s && docker tag %s %s && docker push %s",
registryImage, registryImage, dockerhubImage, dockerhubImage)
if err := sshRun(pushServerIP, cmds); err != nil {
log.Fatalf("Push to Docker Hub failed: %v", err)
}
log.Printf("Pushed to %s ✓", dockerhubImage)
}
}
log.Println("Publish complete!")
}
func run(name string, args ...string) error {
log.Printf(" %s %s", name, strings.Join(args, " "))
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// sshRun executes a shell command on a remote host via SSH.
// The command is passed as a single string to be interpreted by the remote shell.
// Only use with trusted, hardcoded command strings — never with user-supplied input.
func sshRun(host, command string) error {
log.Printf(" ssh root@%s %q", host, command)
cmd := exec.Command("ssh", "root@"+host, "--", command)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runTests() error {
log.Println("Running tests...")
cmd := exec.Command("go", "test", "-timeout", "5m", "./...")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func getServerIP(name string) string {
data, err := os.ReadFile("infra.json")
if err != nil {
log.Printf("Warning: could not read infra.json: %v", err)
return ""
}
var config struct {
Instances map[string][]struct {
Name string `json:"name"`
IP string `json:"ip"`
} `json:"instances"`
}
if err := json.Unmarshal(data, &config); err != nil {
log.Printf("Warning: could not parse infra.json: %v", err)
return ""
}
for _, instances := range config.Instances {
for _, inst := range instances {
if inst.Name == name {
return inst.IP
}
}
}
return ""
}