Set up a pipeline
This commit is contained in:
parent
016dbd0814
commit
ed825ce202
8 changed files with 508 additions and 0 deletions
274
.dagger/main.go
Normal file
274
.dagger/main.go
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
// A Dagger module for building and deploying a Zola static website
|
||||
//
|
||||
// This module provides modular functions to:
|
||||
// - Build the Zola website
|
||||
// - Create a Docker image serving the site with nginx
|
||||
// - Deploy to S3 (future)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dagger/blog/internal/dagger"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Zola version to use for building
|
||||
ZolaImage = "ghcr.io/getzola/zola:v0.21.0"
|
||||
// Nginx version to use for serving
|
||||
NginxImage = "nginx:1.27-alpine3.20-slim"
|
||||
// Build output directory
|
||||
BuildOutputDir = "public"
|
||||
// Lychee version for link checking
|
||||
LycheeImage = "lycheeverse/lychee:0.22-alpine"
|
||||
// htmltest version for HTML validation
|
||||
HtmltestImage = "wjdp/htmltest:v0.17.0"
|
||||
)
|
||||
|
||||
type Blog struct{}
|
||||
|
||||
// Build builds the Zola website and returns the public directory
|
||||
//
|
||||
// Args:
|
||||
// - source: The source directory containing the Zola site
|
||||
//
|
||||
// Returns:
|
||||
// - A Directory containing the built website
|
||||
func (m *Blog) Build(
|
||||
ctx context.Context,
|
||||
// The source directory containing the Zola site
|
||||
// +defaultPath="/"
|
||||
// +ignore=[".git", ".dagger", "public"]
|
||||
source *dagger.Directory,
|
||||
) *dagger.Directory {
|
||||
return dag.Container().
|
||||
From(ZolaImage).
|
||||
WithMountedDirectory("/site", source).
|
||||
WithWorkdir("/site").
|
||||
WithExec([]string{"zola", "build", "--output-dir", BuildOutputDir, "--minify"}).
|
||||
Directory(BuildOutputDir)
|
||||
}
|
||||
|
||||
// prepareNginx prepares an nginx container with configuration
|
||||
func (m *Blog) prepareNginx() *dagger.Container {
|
||||
nginxConfig := `server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Enable gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}`
|
||||
|
||||
return dag.Container().
|
||||
From(NginxImage).
|
||||
WithNewFile("/etc/nginx/conf.d/default.conf", nginxConfig).
|
||||
WithExposedPort(80)
|
||||
}
|
||||
|
||||
// BuildContainer creates a Docker container image with nginx serving the built website
|
||||
//
|
||||
// Args:
|
||||
// - source: The source directory containing the Zola site
|
||||
//
|
||||
// Returns:
|
||||
// - A Container ready to serve the website
|
||||
func (m *Blog) BuildContainer(
|
||||
ctx context.Context,
|
||||
// The source directory containing the Zola site
|
||||
// +defaultPath="/"
|
||||
// +ignore=[".git", ".dagger", "public"]
|
||||
source *dagger.Directory,
|
||||
) *dagger.Container {
|
||||
builtSite := m.Build(ctx, source)
|
||||
nginxContainer := m.prepareNginx()
|
||||
|
||||
return nginxContainer.WithDirectory("/usr/share/nginx/html", builtSite)
|
||||
}
|
||||
|
||||
// getDefaultTag returns a tag based on the latest git commit date
|
||||
func (m *Blog) getDefaultTag(
|
||||
ctx context.Context,
|
||||
source *dagger.Directory,
|
||||
) (string, error) {
|
||||
// Get the commit date in YYYYMMDDHHMMSS format
|
||||
output, err := dag.Container().
|
||||
From("alpine/git:latest").
|
||||
WithMountedDirectory("/repo", source).
|
||||
WithWorkdir("/repo").
|
||||
WithExec([]string{"git", "log", "-1", "--format=%cd", "--date=format:%Y%m%d%H%M%S"}).
|
||||
Stdout(ctx)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(output), nil
|
||||
}
|
||||
|
||||
// Publish publishes the container to a registry
|
||||
//
|
||||
// Args:
|
||||
// - source: The source directory containing the Zola site
|
||||
// - registry: The registry address (e.g., "ghcr.io/username/blog")
|
||||
// - tag: The tag to use (defaults to git commit date in YYYYMMDDHHMMSS format)
|
||||
//
|
||||
// Returns:
|
||||
// - The published image reference
|
||||
func (m *Blog) Publish(
|
||||
ctx context.Context,
|
||||
// The source directory containing the Zola site
|
||||
// +defaultPath="/"
|
||||
// +ignore=[".git", ".dagger", "public"]
|
||||
source *dagger.Directory,
|
||||
// The registry address
|
||||
registry string,
|
||||
// +optional
|
||||
tag string,
|
||||
) (string, error) {
|
||||
if tag == "" {
|
||||
defaultTag, err := m.getDefaultTag(ctx, source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tag = defaultTag
|
||||
}
|
||||
|
||||
container := m.BuildContainer(ctx, source)
|
||||
address := registry + ":" + tag
|
||||
|
||||
return container.Publish(ctx, address)
|
||||
}
|
||||
|
||||
// checkLinks validates all links in the built site
|
||||
func (m *Blog) checkLinks(
|
||||
ctx context.Context,
|
||||
builtSite *dagger.Directory,
|
||||
) error {
|
||||
_, err := dag.Container().
|
||||
From(LycheeImage).
|
||||
WithMountedDirectory("/site", builtSite).
|
||||
WithWorkdir("/site").
|
||||
WithExec([]string{
|
||||
"lychee",
|
||||
"--base-url", "https://enoent.fr",
|
||||
"--remap", "https://enoent\\.fr/ file:///site/",
|
||||
"--offline",
|
||||
"--no-progress",
|
||||
"/site",
|
||||
}).
|
||||
Sync(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// checkHTML validates HTML structure
|
||||
func (m *Blog) checkHTML(
|
||||
ctx context.Context,
|
||||
builtSite *dagger.Directory,
|
||||
) error {
|
||||
// htmltest configuration
|
||||
// Enable comprehensive HTML validation
|
||||
htmltestConfig := `DirectoryPath: /site
|
||||
CheckDoctype: true
|
||||
CheckAnchors: true
|
||||
CheckLinks: true
|
||||
CheckImages: true
|
||||
CheckScripts: true
|
||||
CheckFavicon: false
|
||||
CheckMetaRefresh: true
|
||||
CheckMetaDescription: true
|
||||
EnforceHTTPS: false
|
||||
IgnoreInternalEmptyHash: true
|
||||
IgnoreDirectoryMissingTrailingSlash: true
|
||||
CheckExternal: false
|
||||
IgnoreURLs:
|
||||
- "^https://enoent\\.fr/"`
|
||||
|
||||
_, err := dag.Container().
|
||||
From(HtmltestImage).
|
||||
WithMountedDirectory("/site", builtSite).
|
||||
WithWorkdir("/test").
|
||||
WithNewFile(".htmltest.yml", htmltestConfig).
|
||||
WithExec([]string{"htmltest"}).
|
||||
Sync(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Check runs all quality checks on the built site
|
||||
//
|
||||
// Args:
|
||||
// - source: The source directory containing the Zola site
|
||||
//
|
||||
// Returns:
|
||||
// - An error if any check fails
|
||||
func (m *Blog) Check(
|
||||
ctx context.Context,
|
||||
// The source directory containing the Zola site
|
||||
// +defaultPath="/"
|
||||
// +ignore=[".git", ".dagger", "public"]
|
||||
source *dagger.Directory,
|
||||
) error {
|
||||
// Build the site first
|
||||
builtSite := m.Build(ctx, source)
|
||||
|
||||
type checkResult struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
checks := []struct {
|
||||
name string
|
||||
fn func(context.Context, *dagger.Directory) error
|
||||
}{
|
||||
{"Link Check", m.checkLinks},
|
||||
{"HTML Validation", m.checkHTML},
|
||||
}
|
||||
|
||||
results := make(chan checkResult, len(checks))
|
||||
|
||||
for _, check := range checks {
|
||||
check := check
|
||||
go func() {
|
||||
results <- checkResult{
|
||||
name: check.name,
|
||||
err: check.fn(ctx, builtSite),
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Collect results
|
||||
var failures []string
|
||||
for i := 0; i < len(checks); i++ {
|
||||
result := <-results
|
||||
if result.err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s failed: %v", result.name, result.err))
|
||||
}
|
||||
}
|
||||
|
||||
// Return combined error if any checks failed
|
||||
if len(failures) > 0 {
|
||||
return fmt.Errorf("checks failed:\n%s", strings.Join(failures, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue