Skip to content

Commit 0f492c0

Browse files
authored
Merge pull request #270 from SPANDigital/feat/PRSDM-10268-incl-themes-in-binary
feat: PRSDM-10268 incl themes in binary
2 parents a35d16f + 361decb commit 0f492c0

File tree

16 files changed

+784
-1143
lines changed

16 files changed

+784
-1143
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- uses: actions/checkout@v6
19+
with:
20+
submodules: true
1921
- uses: actions/setup-go@v6
2022
with:
2123
go-version: stable
24+
- name: Prepare themes for embedding
25+
run: make prepare-themes
2226
- name: cli-lint
2327
uses: golangci/golangci-lint-action@v9
2428
with:
@@ -29,6 +33,8 @@ jobs:
2933
steps:
3034
- name: Checkout code
3135
uses: actions/checkout@v6
36+
with:
37+
submodules: true
3238
- name: Set up Go
3339
uses: actions/setup-go@v6
3440
with:
@@ -45,6 +51,8 @@ jobs:
4551
steps:
4652
- name: Checkout code
4753
uses: actions/checkout@v6
54+
with:
55+
submodules: true
4856
- name: Set up Go
4957
uses: actions/setup-go@v6
5058
with:

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ jobs:
1414
uses: actions/checkout@v4
1515
with:
1616
fetch-depth: 0
17+
submodules: true
1718
- name: Set up Go
1819
uses: actions/setup-go@v5
1920
with:
2021
go-version: 1.25.x
22+
- name: Prepare themes for embedding
23+
run: make prepare-themes
2124
- name: Run GoReleaser
2225
uses: goreleaser/goreleaser-action@v6
2326
with:

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ presidium
99
dist/
1010
.DS_Store
1111
resources/
12-
1312
dist/
1413
docs/resources
15-
reports/
14+
reports/
15+
reports/tests-cov.out
16+
.claude/

Dockerfile.test

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Multi-stage Dockerfile to test presidium with embedded themes
2+
# This tests that themes work without network access
3+
4+
# Stage 1: Build presidium binary for Linux
5+
FROM golang:1.25-alpine AS builder
6+
7+
# Install build dependencies
8+
RUN apk add --no-cache git gcc g++ musl-dev make
9+
10+
# Copy source code
11+
WORKDIR /build
12+
COPY . .
13+
14+
# Build presidium binary (prepare-themes renames go.mod files so they can be embedded)
15+
RUN make prepare-themes && go build -tags extended -o presidium .
16+
17+
# Stage 2: Test without network access
18+
FROM golang:1.25-alpine AS test
19+
20+
# Install runtime dependencies (C++ libraries required by Hugo/presidium)
21+
RUN apk add --no-cache curl libstdc++ libgcc
22+
23+
# Copy the docs site
24+
COPY ./docs /workspace/docs
25+
26+
# Copy the built presidium binary from builder stage
27+
COPY --from=builder /build/presidium /usr/local/bin/presidium
28+
RUN chmod +x /usr/local/bin/presidium
29+
30+
# Set working directory to the docs site
31+
WORKDIR /workspace/docs
32+
33+
# Verify presidium binary
34+
RUN presidium --version || echo "Presidium version check completed"
35+
36+
# Run hugo build using presidium (this will use embedded themes)
37+
# This should work without network access, proving themes are embedded
38+
RUN echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && \
39+
echo "Building site with embedded themes..." && \
40+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && \
41+
presidium hugo && \
42+
echo "" && \
43+
echo "✓ Build successful! Themes were loaded from embedded binary." && \
44+
echo "" && \
45+
echo "Generated files:" && \
46+
ls -lh public/ | head -20
47+
48+
# Verify the build output exists and contains expected files
49+
RUN test -d public && \
50+
test -f public/index.html && \
51+
test -f public/sitemap.xml && \
52+
echo "" && \
53+
echo "✓ Hugo site built successfully!" && \
54+
echo "✓ All expected files generated" && \
55+
echo "✓ Themes loaded from binary (no GitHub fetch required)"
56+
57+
# Create a test script that will run with --network=none
58+
RUN echo '#!/bin/sh' > /test.sh && \
59+
echo 'echo "╔═══════════════════════════════════════════════════════════╗"' >> /test.sh && \
60+
echo 'echo "║ Network Isolation Test ║"' >> /test.sh && \
61+
echo 'echo "╚═══════════════════════════════════════════════════════════╝"' >> /test.sh && \
62+
echo 'echo ""' >> /test.sh && \
63+
echo 'echo "Testing network isolation..."' >> /test.sh && \
64+
echo 'if curl -s --max-time 2 https://github.com >/dev/null 2>&1; then' >> /test.sh && \
65+
echo ' echo "❌ FAIL: Network access detected!"' >> /test.sh && \
66+
echo ' exit 1' >> /test.sh && \
67+
echo 'else' >> /test.sh && \
68+
echo ' echo "✓ Confirmed: No network access"' >> /test.sh && \
69+
echo 'fi' >> /test.sh && \
70+
echo 'echo ""' >> /test.sh && \
71+
echo 'echo "Build artifacts:"' >> /test.sh && \
72+
echo 'ls -lh public/ | head -15' >> /test.sh && \
73+
echo 'echo ""' >> /test.sh && \
74+
echo 'echo "╔═══════════════════════════════════════════════════════════╗"' >> /test.sh && \
75+
echo 'echo "║ ✅ SUCCESS: Embedded themes work without network! ║"' >> /test.sh && \
76+
echo 'echo "╚═══════════════════════════════════════════════════════════╝"' >> /test.sh && \
77+
chmod +x /test.sh
78+
79+
# Default command runs the test script
80+
CMD ["/test.sh"]

Makefile

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,69 @@
11
FILENAME=presidium
22
DOCSDIR=docs
33
.DEFAULT_GOAL=help
4-
.PHONY: build test dist clean fmt vet tidy coverage_report help
4+
.PHONY: build test dist clean fmt vet tidy coverage_report help update-themes prepare-themes restore-themes lint checks serve-docs
55

66
help: ## Display available targets
77
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}'
88

9-
build: ## Build the presidium binary
10-
go build -tags extended -o $(FILENAME) .
9+
update-themes: ## Update theme submodules to their latest versions
10+
@echo "Updating theme submodules to latest..."
11+
@git submodule update --remote themes/presidium-styling-base themes/presidium-layouts-base themes/presidium-layouts-blog
12+
@echo "Theme submodules updated."
1113

12-
test: ## Run tests with coverage
14+
prepare-themes: ## Prepare themes for embedding (rename go.mod to make embeddable)
15+
@echo "Preparing themes for embedding..."
16+
@for theme in themes/presidium-*; do \
17+
if [ -f "$$theme/go.mod" ]; then \
18+
echo " Renaming $$theme/go.mod -> go.mod.tmpl"; \
19+
mv "$$theme/go.mod" "$$theme/go.mod.tmpl"; \
20+
fi; \
21+
if [ -f "$$theme/go.sum" ]; then \
22+
echo " Renaming $$theme/go.sum -> go.sum.tmpl"; \
23+
mv "$$theme/go.sum" "$$theme/go.sum.tmpl"; \
24+
fi; \
25+
done
26+
27+
restore-themes: ## Restore theme go.mod files to original names
28+
@echo "Restoring theme go.mod files..."
29+
@for theme in themes/presidium-*; do \
30+
if [ -f "$$theme/go.mod.tmpl" ]; then \
31+
echo " Renaming $$theme/go.mod.tmpl -> go.mod"; \
32+
mv "$$theme/go.mod.tmpl" "$$theme/go.mod"; \
33+
fi; \
34+
if [ -f "$$theme/go.sum.tmpl" ]; then \
35+
echo " Renaming $$theme/go.sum.tmpl -> go.sum"; \
36+
mv "$$theme/go.sum.tmpl" "$$theme/go.sum"; \
37+
fi; \
38+
done
39+
40+
build: prepare-themes ## Build the presidium binary
41+
go build -tags extended -o $(FILENAME) . ; status=$$? ; $(MAKE) restore-themes ; rstatus=$$? ; if [ $$status -eq 0 ]; then status=$$rstatus; fi ; exit $$status
42+
43+
test: prepare-themes ## Run tests with coverage
1344
@mkdir -p reports
14-
go test -race -timeout 120s ./... -coverprofile=reports/tests-cov.out
45+
go test -race -timeout 120s ./... -coverprofile=reports/tests-cov.out ; status=$$? ; $(MAKE) restore-themes ; rstatus=$$? ; if [ $$status -eq 0 ]; then status=$$rstatus; fi ; exit $$status
1546

1647
fmt: ## Format Go source files
1748
go fmt ./...
1849

19-
vet: ## Run go vet
20-
go vet ./...
50+
vet: prepare-themes ## Run go vet
51+
go vet ./... ; status=$$? ; $(MAKE) restore-themes ; rstatus=$$? ; if [ $$status -eq 0 ]; then status=$$rstatus; fi ; exit $$status
2152

2253
tidy: ## Tidy and verify module dependencies
2354
go mod tidy && go mod verify
2455

25-
clean: ## Remove build artifacts
26-
rm -fr "dist"
56+
clean: restore-themes ## Remove build artifacts and restore themes
57+
rm -fr "dist" "$(FILENAME)" "presidium-test"
2758

2859
coverage_report: ## Open coverage report in browser
2960
@go tool cover -html=reports/tests-cov.out
3061

31-
dist: ## Build distribution binary
32-
mkdir -p "dist"
33-
go build -trimpath -o "dist/presidium" --tags extended
34-
35-
checks: tidy fmt vet lint test build
62+
dist: prepare-themes ## Build distribution binary
63+
mkdir -p "dist" && go build -trimpath -o "dist/presidium" --tags extended ; status=$$? ; $(MAKE) restore-themes ; rstatus=$$? ; if [ $$status -eq 0 ]; then status=$$rstatus; fi ; exit $$status
3664

3765
serve-docs:
3866
cd $(DOCSDIR) && make serve
3967

40-
lint:
41-
golangci-lint run --timeout 10m
68+
lint: prepare-themes ## Run golangci-lint
69+
golangci-lint run --timeout 10m ; status=$$? ; $(MAKE) restore-themes ; rstatus=$$? ; if [ $$status -eq 0 ]; then status=$$rstatus; fi ; exit $$status

cmd/hugo.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
package cmd
22

33
import (
4+
"os"
5+
46
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/hugo"
7+
"github.com/SPANDigital/presidium-hugo/pkg/log"
58
"github.com/spf13/cobra"
69
)
710

811
var (
912
// hugoCommand wraps hugo into Presidium. This allows you to run hugo
1013
// in Presidium, and makes it easier to debug etc.
14+
// All arguments and flags are passed through to Hugo unchanged.
1115
hugoCommand = &cobra.Command{
12-
Use: "hugo",
13-
Short: "Runs hugo against your presidium site",
16+
Use: "hugo",
17+
Short: "Runs hugo with full access to all Hugo commands and flags",
18+
DisableFlagParsing: true, // Don't parse flags - let Hugo handle them
1419
Run: func(cmd *cobra.Command, args []string) {
15-
hugo := hugo.New()
16-
hugo.Execute(args...)
20+
hugoService := hugo.New()
21+
err := hugoService.Execute(args...)
22+
if err != nil {
23+
log.Error(err)
24+
os.Exit(1)
25+
}
1726
},
1827
}
1928
)

embedded.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ import "embed"
44

55
//go:embed all:templates
66
var templatesFS embed.FS
7+
8+
//go:embed all:themes
9+
var themesFS embed.FS

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package main
33
import (
44
"github.com/SPANDigital/presidium-hugo/cmd"
55
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/template"
6+
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/themes"
67
)
78

89
func main() {
910
template.SetFS(templatesFS)
11+
themes.SetFS(themesFS)
1012
cmd.Execute()
1113
}

pkg/domain/service/conversion/conversion.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ package conversion
33
import (
44
"errors"
55
"fmt"
6-
"github.com/SPANDigital/presidium-hugo/pkg/config"
7-
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/hugo"
8-
"github.com/SPANDigital/presidium-hugo/pkg/utils"
96
"io"
107
"io/fs"
118
"log"
129
"os"
1310
"path/filepath"
1411
"strings"
1512

13+
"github.com/SPANDigital/presidium-hugo/pkg/config"
14+
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/hugo"
15+
"github.com/SPANDigital/presidium-hugo/pkg/utils"
16+
1617
"github.com/SPANDigital/presidium-hugo/pkg/configtranslation"
1718
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/conversion/fileactions"
1819
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/conversion/resources"
@@ -408,10 +409,14 @@ func (c *Converter) generateHugoModule() {
408409
}
409410

410411
c.messageUser(infoMessage("Adding Hugo GO module to site").withContentStyle(colors.Labels.Wanted))
411-
hugo.New().Execute("--source", c.stagingDir, "mod", "init", c.moduleName())
412+
if err := hugo.New().Execute("--source", c.stagingDir, "mod", "init", c.moduleName()); err != nil {
413+
log.Fatalf("failed to initialize Hugo Go module in staging directory %q: %v", c.stagingDir, err)
414+
}
412415
srcModFile := filepath.Join(c.stagingDir, "go.mod")
413416
dstModFile := filepath.Join(c.destinationRepoDir, "go.mod")
414-
_ = c.fs.Copy(srcModFile, dstModFile, fs.ModePerm)
417+
if err := c.fs.Copy(srcModFile, dstModFile, fs.ModePerm); err != nil {
418+
log.Fatalf("failed to copy generated go.mod from %q to %q: %v", srcModFile, dstModFile, err)
419+
}
415420
c.messageUser(infoMessage("Copied over hugo mod file"))
416421

417422
}

pkg/domain/service/hugo/hugo.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package hugo
22

3-
import "github.com/gohugoio/hugo/commands"
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/SPANDigital/presidium-hugo/pkg/domain/service/themes"
8+
"github.com/SPANDigital/presidium-hugo/pkg/filesystem"
9+
"github.com/SPANDigital/presidium-hugo/pkg/log"
10+
"github.com/gohugoio/hugo/commands"
11+
)
412

513
type Service struct {
614
}
@@ -9,6 +17,37 @@ func New() Service {
917
return Service{}
1018
}
1119

12-
func (s Service) Execute(args ...string) {
13-
_ = commands.Execute(args)
20+
func (s Service) Execute(args ...string) error {
21+
// Initialize themes service
22+
themesService, err := themes.New()
23+
if err != nil {
24+
log.Warn(fmt.Sprintf("Failed to initialize themes service, falling back to remote theme fetch: %v", err))
25+
return commands.Execute(args)
26+
}
27+
28+
// Extract embedded themes to temp directory
29+
tmpDir, replacements, err := themesService.Extract()
30+
if err != nil {
31+
log.Warn(fmt.Sprintf("Failed to extract embedded themes, falling back to remote theme fetch: %v", err))
32+
return commands.Execute(args)
33+
}
34+
35+
// Capture the previous value (if any) so we can restore it after execution
36+
prevReplacements, hadPrevReplacements := os.LookupEnv("HUGO_MODULE_REPLACEMENTS")
37+
38+
// Ensure cleanup happens regardless of how Hugo execution ends
39+
defer func() {
40+
_ = filesystem.AFS.RemoveAll(tmpDir)
41+
if hadPrevReplacements {
42+
os.Setenv("HUGO_MODULE_REPLACEMENTS", prevReplacements)
43+
} else {
44+
os.Unsetenv("HUGO_MODULE_REPLACEMENTS")
45+
}
46+
}()
47+
48+
// Set environment variable to point Hugo to local themes
49+
os.Setenv("HUGO_MODULE_REPLACEMENTS", replacements)
50+
51+
// Execute Hugo with local themes - return the error to preserve Hugo's exit behavior
52+
return commands.Execute(args)
1453
}

0 commit comments

Comments
 (0)