This commit is contained in:
commit
947b247ce0
19
.gitea/workflows/test.yaml
Normal file
19
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: Go Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: github.com/actions/checkout@v4
|
||||||
|
- name: Set up Go
|
||||||
|
uses: github.com/actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24.3'
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
25
action.yaml
Normal file
25
action.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: 'Build docker image'
|
||||||
|
description: 'Docker builder for images + pushing, secret management, automatic versioning, and more'
|
||||||
|
inputs:
|
||||||
|
path:
|
||||||
|
description: 'Path to image registry (e.g., ghcr.io/owner/repo)'
|
||||||
|
required: true
|
||||||
|
image_name:
|
||||||
|
description: 'The name of the image to build'
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
desction: 'Secrets lists to pass to the build'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
docker_file:
|
||||||
|
description: 'Path to the Dockerfile (default: Dockerfile)'
|
||||||
|
required: false
|
||||||
|
default: 'Dockerfile'
|
||||||
|
outputs:
|
||||||
|
full_image_name:
|
||||||
|
description: 'Full image name'
|
||||||
|
image_tag:
|
||||||
|
description: 'Version of image'
|
||||||
|
runs:
|
||||||
|
using: 'go'
|
||||||
|
main: 'main.go'
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module git.nanobyte.cz/actions/go-dockerbuild
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
require github.com/sethvargo/go-githubactions v1.3.1
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/sethvargo/go-githubactions v1.3.1 h1:rlwwLRUaunWLQ1aN2o5Y+3s0xhaTC30YObCnilRx448=
|
||||||
|
github.com/sethvargo/go-githubactions v1.3.1/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80=
|
144
main.go
Normal file
144
main.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gha "github.com/sethvargo/go-githubactions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getShortHash() string {
|
||||||
|
// Get short git commit hash
|
||||||
|
gitCmd := exec.Command("git", "rev-parse", "--short", "HEAD")
|
||||||
|
gitCmd.Dir = "."
|
||||||
|
gitCmd.Stderr = os.Stderr
|
||||||
|
hashBytes, err := gitCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get git hash: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
shortHash := string(hashBytes)
|
||||||
|
shortHash = shortHash[:len(shortHash)-1] // remove trailing newline
|
||||||
|
return shortHash
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSecrets(secretsInput string) map[string]string {
|
||||||
|
secrets := make(map[string]string)
|
||||||
|
if secretsInput != "" {
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(secretsInput))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
val := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||||
|
secrets[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDockerCommand(context string, dockerFile string, secrets map[string]string, tagLatest string, tagNightly string) *exec.Cmd {
|
||||||
|
// Parse secrets and write to .secrets file
|
||||||
|
buildArgs := []string{}
|
||||||
|
if len(secrets) > 0 {
|
||||||
|
f, err := os.Create(".secrets")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create .secrets file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.Remove(".secrets") // Clean up after build
|
||||||
|
for secretKey, secretVal := range secrets {
|
||||||
|
_, err := fmt.Fprintf(f, "%s=%s\n", secretKey, secretVal)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write secret: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildArgs = append(buildArgs, "--secret", "id=build_secrets,src=./.secrets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build command
|
||||||
|
buildArgsExpanded := append([]string{"build", context, "-f", dockerFile, "-t", tagLatest, "-t", tagNightly}, buildArgs...)
|
||||||
|
buildCmd := exec.Command("docker", buildArgsExpanded...)
|
||||||
|
buildCmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=1")
|
||||||
|
buildCmd.Stdout = os.Stdout
|
||||||
|
buildCmd.Stderr = os.Stderr
|
||||||
|
return buildCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushDockerCommand(tag string) *exec.Cmd {
|
||||||
|
// Push nightly tag
|
||||||
|
pushNightlyCmd := exec.Command("docker", "push", tag)
|
||||||
|
pushNightlyCmd.Stdout = os.Stdout
|
||||||
|
pushNightlyCmd.Stderr = os.Stderr
|
||||||
|
return pushNightlyCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
basePath := gha.GetInput("path")
|
||||||
|
imageName := gha.GetInput("image-name")
|
||||||
|
dockerFile := gha.GetInput("dockerfile")
|
||||||
|
context := gha.GetInput("context")
|
||||||
|
secrets := parseSecrets(gha.GetInput("secrets"))
|
||||||
|
date := time.Now().Format("2006-01-02")
|
||||||
|
shortHash := getShortHash()
|
||||||
|
|
||||||
|
if basePath == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: 'path' input is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageName == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: 'image-name' input is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if basePath[len(basePath)-1] != '/' {
|
||||||
|
basePath += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct image names
|
||||||
|
imageBase := fmt.Sprintf("%s/%s", basePath, imageName)
|
||||||
|
tagLatest := fmt.Sprintf("%s:latest", imageBase)
|
||||||
|
tagNightly := fmt.Sprintf("%s:nightly-%s.%s", imageBase, date, shortHash)
|
||||||
|
|
||||||
|
// Build Docker command
|
||||||
|
buildCmd := buildDockerCommand(context, dockerFile, secrets, tagLatest, tagNightly)
|
||||||
|
fmt.Println("Running:", buildCmd.String())
|
||||||
|
if err := buildCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Build failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push nightly tag
|
||||||
|
pushNightlyCmd := pushDockerCommand(tagNightly)
|
||||||
|
fmt.Println("Running:", pushNightlyCmd.String())
|
||||||
|
if err := pushNightlyCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Push nightly failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push latest tag
|
||||||
|
pushLatestCmd := pushDockerCommand(tagLatest)
|
||||||
|
fmt.Println("Running:", pushLatestCmd.String())
|
||||||
|
if err := pushLatestCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Push latest failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gha.SetOutput("image_tag", fmt.Sprintf("%s:%s", imageBase, shortHash))
|
||||||
|
gha.SetOutput("full_image_name", tagLatest)
|
||||||
|
}
|
136
main_test.go
Normal file
136
main_test.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test parseSecrets with various input formats
|
||||||
|
func TestParseSecrets(t *testing.T) {
|
||||||
|
input := `
|
||||||
|
SECRET1=foo
|
||||||
|
SECRET2="bar"
|
||||||
|
# This is a comment
|
||||||
|
SECRET3= baz
|
||||||
|
`
|
||||||
|
expected := map[string]string{
|
||||||
|
"SECRET1": "foo",
|
||||||
|
"SECRET2": "bar",
|
||||||
|
"SECRET3": "baz",
|
||||||
|
}
|
||||||
|
result := parseSecrets(input)
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Fatalf("expected %d secrets, got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
for k, v := range expected {
|
||||||
|
if result[k] != v {
|
||||||
|
t.Errorf("expected %s=%s, got %s", k, v, result[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration-like test for writing secrets to file
|
||||||
|
func TestWriteSecretsFile(t *testing.T) {
|
||||||
|
secrets := map[string]string{
|
||||||
|
"FOO": "bar",
|
||||||
|
"BAR": "baz",
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp("", "secrets-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
for k, v := range secrets {
|
||||||
|
_, err := f.WriteString(k + "=" + v + "\n")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write secret: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Sync()
|
||||||
|
|
||||||
|
// Read back and check
|
||||||
|
content, err := os.ReadFile(f.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read secrets file: %v", err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||||
|
if len(lines) != len(secrets) {
|
||||||
|
t.Fatalf("expected %d lines, got %d", len(secrets), len(lines))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getShortHash (requires git repo)
|
||||||
|
func TestGetShortHash(t *testing.T) {
|
||||||
|
cmd := exec.Command("git", "rev-parse", "--short", "HEAD")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Skipping getShortHash test: not a git repo", err)
|
||||||
|
}
|
||||||
|
expected := strings.TrimSpace(string(out))
|
||||||
|
got := getShortHash()
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("expected hash %s, got %s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test buildDockerCommand returns a valid *exec.Cmd with context and dockerfile
|
||||||
|
func TestBuildDockerCommandWithContextAndDockerfile(t *testing.T) {
|
||||||
|
secrets := map[string]string{
|
||||||
|
"FOO": "bar",
|
||||||
|
}
|
||||||
|
context := "./foo"
|
||||||
|
dockerfile := "Dockerfile"
|
||||||
|
tagLatest := "test:latest"
|
||||||
|
tagNightly := "test:nightly"
|
||||||
|
cmd := buildDockerCommand(context, dockerfile, secrets, tagLatest, tagNightly)
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("expected non-nil *exec.Cmd")
|
||||||
|
}
|
||||||
|
args := strings.Join(cmd.Args, " ")
|
||||||
|
if !strings.Contains(args, context) {
|
||||||
|
t.Errorf("docker build args missing context: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
if !strings.Contains(args, "-f") || !strings.Contains(args, dockerfile) {
|
||||||
|
t.Errorf("docker build args missing dockerfile: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
if !strings.Contains(args, tagLatest) || !strings.Contains(args, tagNightly) {
|
||||||
|
t.Errorf("docker build args missing tags: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
if !strings.Contains(args, "--secret") {
|
||||||
|
t.Errorf("docker build args missing --secret: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test buildDockerCommand returns a valid *exec.Cmd without secrets
|
||||||
|
func TestBuildDockerCommandNoSecrets(t *testing.T) {
|
||||||
|
secrets := map[string]string{}
|
||||||
|
context := "."
|
||||||
|
dockerfile := "Dockerfile"
|
||||||
|
tagLatest := "test:latest"
|
||||||
|
tagNightly := "test:nightly"
|
||||||
|
cmd := buildDockerCommand(context, dockerfile, secrets, tagLatest, tagNightly)
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("expected non-nil *exec.Cmd")
|
||||||
|
}
|
||||||
|
args := strings.Join(cmd.Args, " ")
|
||||||
|
if strings.Contains(args, "--secret") {
|
||||||
|
t.Errorf("docker build args should not contain --secret when no secrets are provided: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test pushDockerCommand returns a valid *exec.Cmd
|
||||||
|
func TestPushDockerCommand(t *testing.T) {
|
||||||
|
tag := "test:latest"
|
||||||
|
cmd := pushDockerCommand(tag)
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("expected non-nil *exec.Cmd")
|
||||||
|
}
|
||||||
|
args := strings.Join(cmd.Args, " ")
|
||||||
|
if !strings.Contains(args, "push") || !strings.Contains(args, tag) {
|
||||||
|
t.Errorf("docker push args missing: %v", cmd.Args)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user