ci: migrate from Woodpecker CI to Forgejo Actions #1

Merged
fmartingr merged 17 commits from feat/forgejo-actions into master 2026-09-22 17:33:29 +02:00
23 changed files with 342 additions and 332 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
hako
hako.db
dist/
build/
devdata/
.git/
webapp/node_modules/

110
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,110 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Install make
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: make format
- run: git diff --exit-code
goreleaser-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: goreleaser/goreleaser-action@v6.4.0
with:
args: check
lint:
runs-on: ubuntu-latest
steps:
- name: Install make
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: make ci-lint
test:
runs-on: ubuntu-latest
steps:
- name: Install make
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make build-essential
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: make test
e2e:
runs-on: ubuntu-latest
services:
dind:
image: docker:27-dind
env:
DOCKER_TLS_CERTDIR: ""
# GitHub honours this. The Forgejo runner ignores a workflow's options
# and takes `privileged` from its own config, which is set there.
options: --privileged
# containerd takes a shade over ten seconds to boot on this runner and
# dockerd waits exactly ten, so the first attempt always dies with
# "timeout waiting for containerd to start". The second one wins.
cmd:
- sh
- -c
- >-
for i in 1 2 3 4 5; do
dockerd-entrypoint.sh dockerd --host=tcp://0.0.0.0:2375 --host=unix:///var/run/docker.sock && break;
echo "dockerd attempt $i failed, retrying" >&2;
sleep 2;
done
env:
DOCKER_HOST: tcp://dind:2375
TESTCONTAINERS_RYUK_DISABLED: true
steps:
- name: Install make
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: e2e/go.mod
- uses: oven-sh/setup-bun@v2
- name: Wait for Docker
run: |
for i in $(seq 1 120); do
docker info >/dev/null 2>&1 && exit 0
sleep 1
done
echo "Docker daemon not ready" >&2
exit 1
- name: Install Playwright browsers
run: make e2e-install
- run: make e2e
build:
runs-on: ubuntu-latest
steps:
- name: Install make and UPX
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make upx-ucl
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: oven-sh/setup-bun@v2
- uses: goreleaser/goreleaser-action@v6.4.0
with:
install-only: true
- run: make build

41
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Release
on:
push:
tags: ["v*"]
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Install make and UPX
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends make upx-ucl
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to container registry
run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login git.nakama.town -u ${{ github.actor }} --password-stdin
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6.4.0
with:
args: release --clean
env:
GORELEASER_FORCE_TOKEN: gitea
GITEA_TOKEN: ${{ secrets.FORGEJO_TOKEN }}

View file

@ -57,6 +57,7 @@ dockers_v2:
- "git.nakama.town/fmartingr/hako" - "git.nakama.town/fmartingr/hako"
tags: tags:
- "{{ .Version }}" - "{{ .Version }}"
- latest
dockerfile: Containerfile dockerfile: Containerfile
platforms: platforms:
- linux/amd64 - linux/amd64
@ -65,7 +66,7 @@ dockers_v2:
nfpms: nfpms:
- maintainer: Felipe Martin <me@fmartingr.com> - maintainer: Felipe Martin <me@fmartingr.com>
description: SMTP server to forward messages to shoutrrr endpoints description: Self-hosted link archival service
homepage: https://git.nakama.town/fmartingr/hako homepage: https://git.nakama.town/fmartingr/hako
license: AGPL-3.0 license: AGPL-3.0
formats: formats:
@ -119,4 +120,7 @@ changelog:
- "^chore\\(deps\\):" - "^chore\\(deps\\):"
release: release:
gitea:
owner: fmartingr
name: hako
prerelease: auto prerelease: auto

View file

@ -1,51 +0,0 @@
when:
event:
- push
- pull_request
branch:
- main
steps:
format:
image: golang:1.24
commands:
- make format
- git diff --exit-code # Fail if files were changed
lint-goreleaser:
image: goreleaser/goreleaser:latest
commands:
- goreleaser check
lint:
image: golang:1.24
commands:
- make ci-lint
test:
image: golang:1.24
commands:
- make test
e2e:
image: golang:1.24
environment:
TESTCONTAINERS_RYUK_DISABLED: true
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
# Install system dependencies
- apt-get update && apt-get install -y docker.io curl unzip
# Install Bun for webapp build
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
# Install Playwright browsers
- go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
# Run e2e tests
- make e2e
when:
status: [success, failure]

View file

@ -1,47 +0,0 @@
when:
- event: tag
branch: main
steps:
- name: Release
image: ubuntu:24.04
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
commands:
# Install system dependencies
- apt-get update
- apt-get install -y ca-certificates curl git make wget unzip upx-ucl
# Install Docker from official repository (for buildx support)
- curl -fsSL https://get.docker.com | sh
# Install Go 1.23
- wget -q https://go.dev/dl/go1.23.5.linux-amd64.tar.gz
- tar -C /usr/local -xzf go1.23.5.linux-amd64.tar.gz
- rm go1.23.5.linux-amd64.tar.gz
- export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"
# Install GoReleaser via go install
- go install github.com/goreleaser/goreleaser/v2@latest
# Install Bun (using official installer)
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
# Setup Docker buildx for multi-platform builds
- docker buildx create --use --name multiplatform --driver docker-container
# Verify installations
- docker --version
- docker buildx version
- goreleaser --version
- bun --version
- upx --version
# Authenticate and release
- docker login -u fmartingr -p $GITEA_TOKEN git.nakama.town
- goreleaser release --clean

View file

@ -1,36 +1,20 @@
# This file is used directly by the goreleaser build FROM alpine:3.23
# It is used to build the final container image
# To build the image locally use make build-docker
# Runtime image
FROM alpine:3.20
LABEL maintainer="Felipe Martin <me@fmartingr.com>" LABEL maintainer="Felipe Martin <me@fmartingr.com>"
LABEL org.opencontainers.image.source="https://git.nakama.town/fmartingr/hako" LABEL org.opencontainers.image.source="https://git.nakama.town/fmartingr/hako"
# Install runtime dependencies (yt-dlp and ffmpeg for video archival from Alpine community)
RUN apk add --no-cache \ RUN apk add --no-cache \
poppler-utils \ poppler-utils \
ca-certificates \ ca-certificates \
tzdata \ tzdata \
&& apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/v3.20/community yt-dlp ffmpeg && apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/v3.23/community yt-dlp ffmpeg
# Create non-root user RUN addgroup -g 1000 hako && adduser -u 1000 -G hako -s /bin/sh -D hako
RUN addgroup -g 1000 hako && \
adduser -D -u 1000 -G hako hako
WORKDIR /app WORKDIR /app
COPY hako /usr/local/bin/hako
RUN chown -R hako:hako /app
# Copy binary from goreleaser build context
# With dockers_v2, goreleaser provides the pre-built binary at $TARGETPLATFORM/hako
# The binary already has the webapp embedded via //go:embed
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/hako /usr/local/bin/hako
RUN chmod +x /usr/local/bin/hako && \
chown -R hako:hako /app
# Switch to non-root user
USER hako USER hako
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/hako"] ENTRYPOINT ["/usr/local/bin/hako"]

166
Makefile
View file

@ -1,156 +1,108 @@
PROJECT_NAME := hako PROJECT_NAME := hako
SOURCE_FILES ?=./...
TEST_OPTIONS ?= -v -failfast -race -bench=. -benchtime=100000x -cover -coverprofile=coverage.out
TEST_TIMEOUT ?=1m
GOLANGCI_LINT_VERSION ?= v1.64.5
CLEAN_OPTIONS ?=-modcache -testcache
CGO_ENABLED := 0 CGO_ENABLED := 0
BUILDS_PATH := ./dist DIST_PATH := ./dist
FROM_MAKEFILE := y TEST_OPTIONS := -v -failfast -race -bench=. -benchtime=100000x -cover -coverprofile=coverage.out
TEST_TIMEOUT := 5m
CONTAINERFILE_NAME := Containerfile GOLANGCI_LINT_VERSION := v1.64.5
CONTAINER_ALPINE_VERSION := 3.20
CONTAINER_SOURCE_URL := "https://git.nakama.town/fmartingr/${PROJECT_NAME}"
CONTAINER_MAINTAINER := "Felipe Martin <me@fmartingr.com>"
CONTAINER_BIN_NAME := ${PROJECT_NAME}
BUILDX_PLATFORMS := linux/amd64,linux/arm64,linux/arm/v7 # The Playwright CLI must match the module the e2e tests link against.
PLAYWRIGHT_MODULE := github.com/mxschmitt/playwright-go
PLAYWRIGHT_CMD := $(PLAYWRIGHT_MODULE)/cmd/playwright
export PROJECT_NAME .DEFAULT_GOAL := help
export FROM_MAKEFILE
export CGO_ENABLED ## help: Display this help message
export SOURCE_FILES
export TEST_OPTIONS
export TEST_TIMEOUT
export BUILDS_PATH
export CONTAINERFILE_NAME
export CONTAINER_ALPINE_VERSION
export CONTAINER_SOURCE_URL
export CONTAINER_MAINTAINER
export CONTAINER_BIN_NAME
export BUILDX_PLATFORMS
.PHONY: all
all: help
# this is godly
# https://news.ycombinator.com/item?id=11939200
.PHONY: help .PHONY: help
help: ### this screen. Keep it first target to be default help:
ifeq ($(UNAME), Linux) @echo "Available targets:"
@grep -P '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /' | sort
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
else
@# this is not tested, but prepared in advance for you, Mac drivers
@awk -F ':.*###' '$$0 ~ FS {printf "%15s%s\n", $$1 ":", $$2}' \
$(MAKEFILE_LIST) | grep -v '@awk' | sort
endif
## clean: Clean test cache, build files
.PHONY: clean .PHONY: clean
clean: ### clean test cache, build files clean:
$(info: Make: Clean) @rm -rf $(DIST_PATH)
@rm -rf ${BUILDS_PATH}
@rm -rf webapp/dist @rm -rf webapp/dist
@rm -rf webapp/node_modules @rm -rf webapp/node_modules
@rm -f webapp/bun.lockb @rm -f webapp/bun.lockb
@go clean ${CLEAN_OPTIONS} @go clean -modcache -testcache
@find devdata -mindepth 1 ! -name config.yaml -delete 2>/dev/null || true
@-docker buildx rm ${PROJECT_NAME}_builder
## clean-devdata: Clear devdata database and files (keeps config.yaml)
.PHONY: clean-devdata .PHONY: clean-devdata
clean-devdata: ### Clear devdata database and files (keeps config.yaml) clean-devdata:
$(info: Make: Cleaning devdata)
@rm -rf devdata/data @rm -rf devdata/data
@rm -f devdata/hako.db devdata/hako.db-shm devdata/hako.db-wal @rm -f devdata/hako.db devdata/hako.db-shm devdata/hako.db-wal
## build: Build project using goreleaser (snapshot)
.PHONY: build .PHONY: build
build: build-webapp ### builds the project for the setup os/arch combinations build: build-webapp
$(info: Make: Build) goreleaser build --snapshot --clean
@goreleaser --clean --snapshot
## build-webapp: Build the webapp
.PHONY: build-webapp .PHONY: build-webapp
build-webapp: ### Build the webapp build-webapp:
$(info: Make: Building webapp)
@which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1)
@cd webapp && bun install && bun --bun node_modules/.bin/vite build @cd webapp && bun install && bun --bun node_modules/.bin/vite build
## build-docker: Build Docker image locally via goreleaser
.PHONY: build-docker
build-docker: build-webapp
goreleaser release --snapshot --clean
## run: Build frontend and run the server
.PHONY: run
run: build-webapp run-server
## run-server: Run the API server only (without building webapp)
.PHONY: run-server .PHONY: run-server
run-server: ### Runs the API server only (without building webapp) run-server:
$(info: Make: Running server) CGO_ENABLED=$(CGO_ENABLED) go run ./cmd/$(PROJECT_NAME)/*.go server --config devdata/config.yaml
CGO_ENABLED=${CGO_ENABLED} go run ./cmd/${PROJECT_NAME}/*.go server --config devdata/config.yaml
.PHONY: run-quick
run-quick: build-webapp run-server ### Builds the webapp and runs the server
## run-webapp: Run the webapp in dev mode with hot-reload
.PHONY: run-webapp .PHONY: run-webapp
run-webapp: ### Runs the webapp in dev mode with hot-reload run-webapp:
$(info: Make: Running webapp dev server)
@which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1)
@cd webapp && bun --bun node_modules/.bin/vite --mode development @cd webapp && bun --bun node_modules/.bin/vite --mode development
.PHONY: run ## format: Format code and tidy modules
run: ### Executes the project build locally
@make build
${BUILDS_PATH}/${PROJECT_NAME}
.PHONY: build-docker
build-docker: build-webapp ### Build Docker image locally using goreleaser snapshot
$(info: Make: Building Docker image)
@which goreleaser > /dev/null || (echo "Error: goreleaser is not installed. Install it from https://goreleaser.com" && exit 1)
@which docker > /dev/null || (echo "Error: docker is not installed" && exit 1)
@goreleaser --clean --snapshot --skip=publish
.PHONY: format .PHONY: format
format: ### Executes the formatting pipeline on the project format:
$(info: Make: Format) go fmt ./...
@go fmt ./... go mod tidy
@go mod tidy
## ci-lint: Run golangci-lint (installs if missing)
.PHONY: ci-lint .PHONY: ci-lint
ci-lint: ### Check the project for errors ci-lint:
$(info: Make: Lint) @which golangci-lint > /dev/null 2>&1 || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} golangci-lint run ./...
@golangci-lint run ./...
## lint: Run all linters
.PHONY: lint .PHONY: lint
lint: lint-server lint-webapp ### Check the project for errors lint: lint-server lint-webapp
$(info: Make: Lint)
## lint-server: Check the server for linting errors
.PHONY: lint-server .PHONY: lint-server
lint-server: ### Check the server for linting errors lint-server:
$(info: Make: Lint server)
@golangci-lint run ./... @golangci-lint run ./...
## lint-webapp: Check the webapp for linting errors
.PHONY: lint-webapp .PHONY: lint-webapp
lint-webapp: ### Check the webapp for linting errors lint-webapp:
$(info: Make: Lint webapp)
@which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1) @which bun > /dev/null || (echo "Error: Bun is not installed. Install it from https://bun.sh" && exit 1)
@cd webapp && bun --bun node_modules/.bin/eslint . @cd webapp && bun --bun node_modules/.bin/eslint .
## test: Run tests with coverage
.PHONY: test .PHONY: test
test: ### Runs the test suite test:
$(info: Make: Test) CGO_ENABLED=1 go test $(TEST_OPTIONS) -timeout=$(TEST_TIMEOUT) ./...
CGO_ENABLED=1 go test ${TEST_OPTIONS} -timeout=${TEST_TIMEOUT} ${SOURCE_FILES}
@echo "Generating HTML coverage report..."
@go tool cover -html=coverage.out -o coverage.html
@echo "✅ Coverage report generated: coverage.html"
## e2e: Run end-to-end integration tests
.PHONY: e2e .PHONY: e2e
e2e: ### Run end-to-end integration tests e2e:
$(info: Make: E2E Tests)
@./scripts/e2e.sh @./scripts/e2e.sh
## e2e-install: Install Playwright browsers
.PHONY: e2e-install .PHONY: e2e-install
e2e-install: ### Install Playwright browsers e2e-install:
$(info: Make: Installing E2E dependencies) @cd e2e && go run $(PLAYWRIGHT_CMD)@$$(go list -m -f '{{.Version}}' $(PLAYWRIGHT_MODULE)) install --with-deps chromium
@go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium

View file

@ -1,16 +1,10 @@
services: services:
hako: hako:
# Note: Before running docker-compose, ensure the Linux binary is prepared: # Note: Build the Docker image via goreleaser:
# Run: make prepare-docker-compose # Run: make build-docker
# Or: make build-docker (which builds via goreleaser)
build: build:
context: . context: .
dockerfile: Containerfile dockerfile: Containerfile
platforms:
- linux/amd64
args:
TARGETPLATFORM: dist/hako_linux_amd64_v1
platform: linux/amd64
container_name: hako container_name: hako
restart: unless-stopped restart: unless-stopped
ports: ports:

View file

@ -1,4 +1,4 @@
FROM alpine:3.20 FROM alpine:3.23
RUN apk add --no-cache poppler-utils ca-certificates tzdata && \ RUN apk add --no-cache poppler-utils ca-certificates tzdata && \
addgroup -g 1000 hako && \ addgroup -g 1000 hako && \

View file

@ -53,16 +53,23 @@ func StartHakoContainer(ctx context.Context, t *testing.T) (testcontainers.Conta
os.Remove(binaryPath) os.Remove(binaryPath)
}) })
// Create container request // Ensure shared network exists so Hako can reach the test web server by alias.
nw, err := getSharedNetwork(ctx)
if err != nil {
return nil, "", err
}
// Create container request attached to the shared network.
req := testcontainers.ContainerRequest{ req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{ FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join(projectRoot, "e2e"), Context: filepath.Join(projectRoot, "e2e"),
Dockerfile: "Dockerfile.e2e", Dockerfile: "Dockerfile.e2e",
PrintBuildLog: true, PrintBuildLog: true,
}, },
ExposedPorts: []string{"8080/tcp"}, ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForLog("Starting HTTP server"). WaitingFor: wait.ForLog("Starting HTTP server").
WithStartupTimeout(30 * time.Second), WithStartupTimeout(30 * time.Second),
Networks: []string{nw.Name},
} }
// Start container // Start container

View file

@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/playwright-community/playwright-go" "github.com/mxschmitt/playwright-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -178,10 +178,12 @@ func (h *TestHelper) GetURL() string {
return h.page.URL() return h.page.URL()
} }
// IsVisible checks if an element matching the selector is visible. // IsVisible checks if any element matching the selector is visible.
// Uses .First() to avoid Playwright strict-mode errors when the selector
// resolves to multiple elements.
func (h *TestHelper) IsVisible(selector string) bool { func (h *TestHelper) IsVisible(selector string) bool {
h.t.Helper() h.t.Helper()
visible, err := h.page.Locator(selector).IsVisible() visible, err := h.page.Locator(selector).First().IsVisible()
if err != nil { if err != nil {
h.t.Logf("Error checking visibility of %s: %v", selector, err) h.t.Logf("Error checking visibility of %s: %v", selector, err)
return false return false
@ -305,9 +307,18 @@ func (h *TestHelper) Logout() error {
} }
// CreateLink creates a new link via the Quick Add form. // CreateLink creates a new link via the Quick Add form.
// Navigates to /home first to ensure the form is available even when called
// repeatedly (after the first call the user is redirected to /links).
func (h *TestHelper) CreateLink(url string) error { func (h *TestHelper) CreateLink(url string) error {
h.t.Helper() h.t.Helper()
// Ensure we're on the home page where the Quick Add form lives.
if !strings.HasSuffix(h.GetURL(), "/home") {
if err := h.Navigate("/home"); err != nil {
return fmt.Errorf("failed to navigate to home: %w", err)
}
}
// Wait for URL input on home page // Wait for URL input on home page
if err := h.WaitForSelector("input[name=\"url\"]", 5*time.Second); err != nil { if err := h.WaitForSelector("input[name=\"url\"]", 5*time.Second); err != nil {
return fmt.Errorf("URL input not found: %w", err) return fmt.Errorf("URL input not found: %w", err)

34
e2e/e2eutil/network.go Normal file
View file

@ -0,0 +1,34 @@
package e2eutil
import (
"context"
"fmt"
"sync"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/network"
)
// Shared network used by the test web server and Hako container so that
// Hako can fetch URLs from the test web server via Docker DNS.
var (
sharedNetwork *testcontainers.DockerNetwork
sharedNetworkErr error
sharedNetworkOnce sync.Once
)
// Network alias used by the test web server inside the shared network.
const testServerAlias = "hako-testserver"
// getSharedNetwork lazily creates a shared Docker network for the test process.
// Both StartTestWebServer and StartHakoContainer attach to it so containers
// can communicate by alias.
func getSharedNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error) {
sharedNetworkOnce.Do(func() {
sharedNetwork, sharedNetworkErr = network.New(ctx)
})
if sharedNetworkErr != nil {
return nil, fmt.Errorf("failed to create shared network: %w", sharedNetworkErr)
}
return sharedNetwork, nil
}

View file

@ -3,7 +3,7 @@ package e2eutil
import ( import (
"testing" "testing"
"github.com/playwright-community/playwright-go" "github.com/mxschmitt/playwright-go"
) )
// NewPlaywright initializes Playwright and returns a browser instance. // NewPlaywright initializes Playwright and returns a browser instance.

View file

@ -7,13 +7,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
) )
// StartTestWebServer starts an nginx container with test content for E2E testing. // StartTestWebServer starts an nginx container with test content for E2E testing.
// It builds the test web server image and returns the container instance and base URL. // The container is attached to the shared network with the testServerAlias hostname,
// and the returned URL uses that alias so the Hako container can fetch from it.
func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Container, string, error) { func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Container, string, error) {
t.Helper() t.Helper()
@ -23,7 +23,13 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta
return nil, "", fmt.Errorf("failed to get project root: %w", err) return nil, "", fmt.Errorf("failed to get project root: %w", err)
} }
// Create container request // Ensure shared network exists so Hako can reach this server by alias.
nw, err := getSharedNetwork(ctx)
if err != nil {
return nil, "", err
}
// Create container request attached to the shared network.
req := testcontainers.ContainerRequest{ req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{ FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join(projectRoot, "e2e", "testserver"), Context: filepath.Join(projectRoot, "e2e", "testserver"),
@ -32,6 +38,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta
}, },
ExposedPorts: []string{"80/tcp"}, ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second),
Networks: []string{nw.Name},
NetworkAliases: map[string][]string{
nw.Name: {testServerAlias},
},
} }
// Start container // Start container
@ -51,21 +61,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta
} }
}) })
// Get the mapped port // Use the network alias as the base URL so Hako can reach this server
mappedPort, err := container.MappedPort(ctx, nat.Port("80/tcp")) // from inside its container via Docker DNS.
if err != nil { baseURL := fmt.Sprintf("http://%s", testServerAlias)
return nil, "", fmt.Errorf("failed to get mapped port: %w", err) t.Logf("Test web server started at %s (network alias)", baseURL)
}
// Get the host
host, err := container.Host(ctx)
if err != nil {
return nil, "", fmt.Errorf("failed to get container host: %w", err)
}
// Construct base URL
baseURL := fmt.Sprintf("http://%s:%s", host, mappedPort.Port())
t.Logf("Test web server started at %s", baseURL)
return container, baseURL, nil return container, baseURL, nil
} }

View file

@ -6,7 +6,9 @@ replace github.com/fmartingr/hako => ../
require ( require (
github.com/docker/go-connections v0.6.0 github.com/docker/go-connections v0.6.0
github.com/playwright-community/playwright-go v0.5200.1 github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.41
github.com/mxschmitt/playwright-go v0.6201.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go v0.40.0
) )
@ -23,18 +25,16 @@ require (
github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.7.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.1 // indirect github.com/go-stack/stack v1.8.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect github.com/magiconair/properties v1.8.10 // indirect

View file

@ -25,8 +25,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
@ -39,8 +39,6 @@ github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0o
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -51,7 +49,6 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -68,8 +65,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@ -88,14 +85,14 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mxschmitt/playwright-go v0.6201.1 h1:KBBDopE+IIdjAp2FIk6zhLcKCCSSx/T6rGjKZmfo/Bs=
github.com/mxschmitt/playwright-go v0.6201.1/go.mod h1:vLBP2yVO1gokUlb9ENA+YE3VQaVqf2A9BGxcb7aPjMI=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/playwright-community/playwright-go v0.5200.1 h1:Sm2oOuhqt0M5Y4kUi/Qh9w4cyyi3ZIWTBeGKImc2UVo=
github.com/playwright-community/playwright-go v0.5200.1/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
@ -118,7 +115,6 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@ -139,60 +135,24 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=

View file

@ -265,7 +265,7 @@ func TestRules_RuleApplication(t *testing.T) {
helper.Screenshot("link-created-rule-test") helper.Screenshot("link-created-rule-test")
// Navigate to link details to see which archiver was used // Navigate to link details to see which archiver was used
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
if err == nil { if err == nil {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
helper.Screenshot("link-details-archiver-used") helper.Screenshot("link-details-archiver-used")

View file

@ -53,13 +53,14 @@ func TestArchive_AutomaticCreation(t *testing.T) {
helper.Screenshot("link-created") helper.Screenshot("link-created")
// Navigate to link details to see archive status // Navigate to link details to see archive status
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second)
helper.Screenshot("link-details-archive") helper.Screenshot("link-details-archive")
// Verify archive section exists // Wait for archive processing to surface in the UI
helper.AssertVisible(".archive-status, .archive-history, [class*='archive']", "Archive information should be visible") err = helper.WaitForSelector(".archive-status, [class*='archive']", 30*time.Second)
require.NoError(t, err, "Archive information should appear")
helper.AssertVisible(".archive-status, [class*='archive']", "Archive information should be visible")
t.Log("Archive automatic creation test completed successfully") t.Log("Archive automatic creation test completed successfully")
} }
@ -105,15 +106,15 @@ func TestArchive_ViewHistory(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second)
helper.Screenshot("archive-history") helper.Screenshot("archive-history")
// Verify archive history is visible // Wait for archive to appear in the UI
helper.AssertVisible(".archive-history, .archive-list, [class*='archive']", "Archive history should be visible") err = helper.WaitForSelector(".archive-status, [class*='archive']", 30*time.Second)
require.NoError(t, err, "Archive history should appear")
helper.AssertVisible(".archive-status, [class*='archive']", "Archive history should be visible")
// Wait for archive to process (with timeout)
t.Log("Waiting for archive to complete...") t.Log("Waiting for archive to complete...")
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
helper.Screenshot("archive-after-wait") helper.Screenshot("archive-after-wait")
@ -162,7 +163,7 @@ func TestArchive_StatusDisplay(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
helper.Screenshot("archive-status") helper.Screenshot("archive-status")
@ -222,7 +223,7 @@ func TestArchive_Rearchive(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
helper.Screenshot("before-rearchive") helper.Screenshot("before-rearchive")
@ -295,7 +296,7 @@ func TestArchive_ViewFiles(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
@ -356,7 +357,7 @@ func TestArchive_PreviewHTML(t *testing.T) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
require.NoError(t, err, "Failed to navigate to link details") require.NoError(t, err, "Failed to navigate to link details")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)

View file

@ -144,7 +144,7 @@ func TestError_ArchivalFailure(t *testing.T) {
helper.Screenshot("archival-failure-link-created") helper.Screenshot("archival-failure-link-created")
// Navigate to link details // Navigate to link details
err = helper.Click("a[href*='/links/']") err = helper.Click(".link-item")
if err == nil { if err == nil {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
helper.Screenshot("archival-failure-details") helper.Screenshot("archival-failure-details")

View file

@ -4,19 +4,23 @@ set -e
echo "🧪 Running Hako E2E Tests" echo "🧪 Running Hako E2E Tests"
# Environment setup # Environment setup
export TESTCONTAINERS_RYUK_DISABLED=false export TESTCONTAINERS_RYUK_DISABLED="${TESTCONTAINERS_RYUK_DISABLED:-true}"
export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}" export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}"
# Check Docker # Check Docker
if ! docker info > /dev/null 2>&1; then if ! docker info > /dev/null 2>&1; then
echo "❌ Error: Docker is not running" echo "❌ Error: Docker is not available (check DOCKER_HOST and socket mount)"
exit 1 exit 1
fi fi
# Install Playwright if needed # Install Playwright if needed, pinned to the module the tests link against
if ! go run github.com/playwright-community/playwright-go/cmd/playwright@latest --help > /dev/null 2>&1; then PLAYWRIGHT_MODULE="github.com/mxschmitt/playwright-go"
PLAYWRIGHT_VERSION="$(cd e2e && go list -m -f '{{.Version}}' "$PLAYWRIGHT_MODULE")"
PLAYWRIGHT_CMD="$PLAYWRIGHT_MODULE/cmd/playwright@$PLAYWRIGHT_VERSION"
if ! go run "$PLAYWRIGHT_CMD" --help > /dev/null 2>&1; then
echo "📦 Installing Playwright browsers..." echo "📦 Installing Playwright browsers..."
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium (cd e2e && go run "$PLAYWRIGHT_CMD" install chromium)
fi fi
# Build webapp (required for embed) # Build webapp (required for embed)

0
webapp/dist/.gitkeep vendored Normal file
View file

View file

@ -7,7 +7,7 @@ import (
"os" "os"
) )
//go:embed dist //go:embed all:dist
var embeddedWebappFS embed.FS var embeddedWebappFS embed.FS
// GetFilesystem returns a HTTP filesystem with the embedded webapp files // GetFilesystem returns a HTTP filesystem with the embedded webapp files