ci: migrate from Woodpecker CI to Forgejo Actions #1
22 changed files with 293 additions and 279 deletions
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
hako
|
||||
hako.db
|
||||
dist/
|
||||
build/
|
||||
devdata/
|
||||
.git/
|
||||
webapp/node_modules/
|
||||
87
.github/workflows/ci.yml
vendored
Normal file
87
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- 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: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/goreleaser-action@v6.4.0
|
||||
with:
|
||||
args: check
|
||||
|
||||
lint:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: make ci-lint
|
||||
|
||||
test:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: make test
|
||||
|
||||
e2e:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
services:
|
||||
dind:
|
||||
image: docker:27-dind
|
||||
env:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
options: --privileged
|
||||
env:
|
||||
DOCKER_HOST: tcp://dind:2375
|
||||
TESTCONTAINERS_RYUK_DISABLED: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: e2e/go.mod
|
||||
- name: Wait for Docker
|
||||
run: |
|
||||
for i in $(seq 1 60); 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: go run github.com/playwright-community/playwright-go/cmd/playwright@v0.5200.1 install chromium
|
||||
- run: make e2e
|
||||
|
||||
build:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: make build
|
||||
33
.github/workflows/release.yml
vendored
Normal file
33
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: docker
|
||||
container: git.nakama.town/fmartingr/ci-images/ci-base:1.1.0
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Setup Docker buildx
|
||||
run: docker buildx create --use --name multiplatform --driver docker-container
|
||||
|
||||
- name: Login to container registry
|
||||
run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login git.nakama.town -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: actions/goreleaser-action@v6.4.0
|
||||
with:
|
||||
args: release --clean
|
||||
env:
|
||||
GORELEASER_FORCE_TOKEN: gitea
|
||||
GITEA_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
|
@ -57,6 +57,7 @@ dockers_v2:
|
|||
- "git.nakama.town/fmartingr/hako"
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
- latest
|
||||
dockerfile: Containerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
|
|
@ -65,7 +66,7 @@ dockers_v2:
|
|||
|
||||
nfpms:
|
||||
- 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
|
||||
license: AGPL-3.0
|
||||
formats:
|
||||
|
|
@ -119,4 +120,7 @@ changelog:
|
|||
- "^chore\\(deps\\):"
|
||||
|
||||
release:
|
||||
gitea:
|
||||
owner: fmartingr
|
||||
name: hako
|
||||
prerelease: auto
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -1,36 +1,20 @@
|
|||
# This file is used directly by the goreleaser build
|
||||
# It is used to build the final container image
|
||||
# To build the image locally use make build-docker
|
||||
|
||||
# Runtime image
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.23
|
||||
LABEL maintainer="Felipe Martin <me@fmartingr.com>"
|
||||
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 \
|
||||
poppler-utils \
|
||||
ca-certificates \
|
||||
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 -D -u 1000 -G hako hako
|
||||
RUN addgroup -g 1000 hako && adduser -u 1000 -G hako -s /bin/sh -D hako
|
||||
|
||||
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
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/hako"]
|
||||
|
|
|
|||
162
Makefile
162
Makefile
|
|
@ -1,156 +1,104 @@
|
|||
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
|
||||
|
||||
BUILDS_PATH := ./dist
|
||||
FROM_MAKEFILE := y
|
||||
DIST_PATH := ./dist
|
||||
TEST_OPTIONS := -v -failfast -race -bench=. -benchtime=100000x -cover -coverprofile=coverage.out
|
||||
TEST_TIMEOUT := 5m
|
||||
|
||||
CONTAINERFILE_NAME := Containerfile
|
||||
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}
|
||||
GOLANGCI_LINT_VERSION := v1.64.5
|
||||
|
||||
BUILDX_PLATFORMS := linux/amd64,linux/arm64,linux/arm/v7
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
export PROJECT_NAME
|
||||
export FROM_MAKEFILE
|
||||
|
||||
export CGO_ENABLED
|
||||
|
||||
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
|
||||
## help: Display this help message
|
||||
.PHONY: help
|
||||
help: ### this screen. Keep it first target to be default
|
||||
ifeq ($(UNAME), Linux)
|
||||
@grep -P '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | 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
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /' | sort
|
||||
|
||||
## clean: Clean test cache, build files
|
||||
.PHONY: clean
|
||||
clean: ### clean test cache, build files
|
||||
$(info: Make: Clean)
|
||||
@rm -rf ${BUILDS_PATH}
|
||||
clean:
|
||||
@rm -rf $(DIST_PATH)
|
||||
@rm -rf webapp/dist
|
||||
@rm -rf webapp/node_modules
|
||||
@rm -f webapp/bun.lockb
|
||||
@go clean ${CLEAN_OPTIONS}
|
||||
@find devdata -mindepth 1 ! -name config.yaml -delete 2>/dev/null || true
|
||||
@-docker buildx rm ${PROJECT_NAME}_builder
|
||||
@go clean -modcache -testcache
|
||||
|
||||
## clean-devdata: Clear devdata database and files (keeps config.yaml)
|
||||
.PHONY: clean-devdata
|
||||
clean-devdata: ### Clear devdata database and files (keeps config.yaml)
|
||||
$(info: Make: Cleaning devdata)
|
||||
clean-devdata:
|
||||
@rm -rf devdata/data
|
||||
@rm -f devdata/hako.db devdata/hako.db-shm devdata/hako.db-wal
|
||||
|
||||
## build: Build project using goreleaser (snapshot)
|
||||
.PHONY: build
|
||||
build: build-webapp ### builds the project for the setup os/arch combinations
|
||||
$(info: Make: Build)
|
||||
@goreleaser --clean --snapshot
|
||||
build: build-webapp
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
## build-webapp: Build the webapp
|
||||
.PHONY: build-webapp
|
||||
build-webapp: ### Build the webapp
|
||||
$(info: Make: Building webapp)
|
||||
build-webapp:
|
||||
@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
|
||||
|
||||
## 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
|
||||
run-server: ### Runs the API server only (without building webapp)
|
||||
$(info: Make: Running server)
|
||||
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-server:
|
||||
CGO_ENABLED=$(CGO_ENABLED) go run ./cmd/$(PROJECT_NAME)/*.go server --config devdata/config.yaml
|
||||
|
||||
## run-webapp: Run the webapp in dev mode with hot-reload
|
||||
.PHONY: run-webapp
|
||||
run-webapp: ### Runs the webapp in dev mode with hot-reload
|
||||
$(info: Make: Running webapp dev server)
|
||||
run-webapp:
|
||||
@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
|
||||
|
||||
.PHONY: run
|
||||
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
|
||||
|
||||
## format: Format code and tidy modules
|
||||
.PHONY: format
|
||||
format: ### Executes the formatting pipeline on the project
|
||||
$(info: Make: Format)
|
||||
@go fmt ./...
|
||||
@go mod tidy
|
||||
format:
|
||||
go fmt ./...
|
||||
go mod tidy
|
||||
|
||||
## ci-lint: Run golangci-lint (installs if missing)
|
||||
.PHONY: ci-lint
|
||||
ci-lint: ### Check the project for errors
|
||||
$(info: Make: Lint)
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}
|
||||
@golangci-lint run ./...
|
||||
ci-lint:
|
||||
@which golangci-lint > /dev/null 2>&1 || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
golangci-lint run ./...
|
||||
|
||||
## lint: Run all linters
|
||||
.PHONY: lint
|
||||
lint: lint-server lint-webapp ### Check the project for errors
|
||||
$(info: Make: Lint)
|
||||
lint: lint-server lint-webapp
|
||||
|
||||
## lint-server: Check the server for linting errors
|
||||
.PHONY: lint-server
|
||||
lint-server: ### Check the server for linting errors
|
||||
$(info: Make: Lint server)
|
||||
lint-server:
|
||||
@golangci-lint run ./...
|
||||
|
||||
## lint-webapp: Check the webapp for linting errors
|
||||
.PHONY: lint-webapp
|
||||
lint-webapp: ### Check the webapp for linting errors
|
||||
$(info: Make: Lint webapp)
|
||||
lint-webapp:
|
||||
@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 .
|
||||
|
||||
## test: Run tests with coverage
|
||||
.PHONY: test
|
||||
test: ### Runs the test suite
|
||||
$(info: Make: Test)
|
||||
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"
|
||||
test:
|
||||
CGO_ENABLED=1 go test $(TEST_OPTIONS) -timeout=$(TEST_TIMEOUT) ./...
|
||||
|
||||
## e2e: Run end-to-end integration tests
|
||||
.PHONY: e2e
|
||||
e2e: ### Run end-to-end integration tests
|
||||
$(info: Make: E2E Tests)
|
||||
e2e:
|
||||
@./scripts/e2e.sh
|
||||
|
||||
## e2e-install: Install Playwright browsers
|
||||
.PHONY: e2e-install
|
||||
e2e-install: ### Install Playwright browsers
|
||||
$(info: Make: Installing E2E dependencies)
|
||||
e2e-install:
|
||||
@go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
services:
|
||||
hako:
|
||||
# Note: Before running docker-compose, ensure the Linux binary is prepared:
|
||||
# Run: make prepare-docker-compose
|
||||
# Or: make build-docker (which builds via goreleaser)
|
||||
# Note: Build the Docker image via goreleaser:
|
||||
# Run: make build-docker
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Containerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
args:
|
||||
TARGETPLATFORM: dist/hako_linux_amd64_v1
|
||||
platform: linux/amd64
|
||||
container_name: hako
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.20
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk add --no-cache poppler-utils ca-certificates tzdata && \
|
||||
addgroup -g 1000 hako && \
|
||||
|
|
|
|||
|
|
@ -53,16 +53,23 @@ func StartHakoContainer(ctx context.Context, t *testing.T) (testcontainers.Conta
|
|||
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{
|
||||
FromDockerfile: testcontainers.FromDockerfile{
|
||||
Context: filepath.Join(projectRoot, "e2e"),
|
||||
Dockerfile: "Dockerfile.e2e",
|
||||
Context: filepath.Join(projectRoot, "e2e"),
|
||||
Dockerfile: "Dockerfile.e2e",
|
||||
PrintBuildLog: true,
|
||||
},
|
||||
ExposedPorts: []string{"8080/tcp"},
|
||||
WaitingFor: wait.ForLog("Starting HTTP server").
|
||||
WithStartupTimeout(30 * time.Second),
|
||||
Networks: []string{nw.Name},
|
||||
}
|
||||
|
||||
// Start container
|
||||
|
|
|
|||
|
|
@ -178,10 +178,12 @@ func (h *TestHelper) GetURL() string {
|
|||
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 {
|
||||
h.t.Helper()
|
||||
visible, err := h.page.Locator(selector).IsVisible()
|
||||
visible, err := h.page.Locator(selector).First().IsVisible()
|
||||
if err != nil {
|
||||
h.t.Logf("Error checking visibility of %s: %v", selector, err)
|
||||
return false
|
||||
|
|
@ -305,9 +307,18 @@ func (h *TestHelper) Logout() error {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
if err := h.WaitForSelector("input[name=\"url\"]", 5*time.Second); err != nil {
|
||||
return fmt.Errorf("URL input not found: %w", err)
|
||||
|
|
|
|||
34
e2e/e2eutil/network.go
Normal file
34
e2e/e2eutil/network.go
Normal 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
|
||||
}
|
||||
|
|
@ -7,13 +7,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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{
|
||||
FromDockerfile: testcontainers.FromDockerfile{
|
||||
Context: filepath.Join(projectRoot, "e2e", "testserver"),
|
||||
|
|
@ -32,6 +38,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta
|
|||
},
|
||||
ExposedPorts: []string{"80/tcp"},
|
||||
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second),
|
||||
Networks: []string{nw.Name},
|
||||
NetworkAliases: map[string][]string{
|
||||
nw.Name: {testServerAlias},
|
||||
},
|
||||
}
|
||||
|
||||
// Start container
|
||||
|
|
@ -51,21 +61,10 @@ func StartTestWebServer(ctx context.Context, t *testing.T) (testcontainers.Conta
|
|||
}
|
||||
})
|
||||
|
||||
// Get the mapped port
|
||||
mappedPort, err := container.MappedPort(ctx, nat.Port("80/tcp"))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get mapped port: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Use the network alias as the base URL so Hako can reach this server
|
||||
// from inside its container via Docker DNS.
|
||||
baseURL := fmt.Sprintf("http://%s", testServerAlias)
|
||||
t.Logf("Test web server started at %s (network alias)", baseURL)
|
||||
|
||||
return container, baseURL, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ replace github.com/fmartingr/hako => ../
|
|||
|
||||
require (
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.41
|
||||
github.com/playwright-community/playwright-go v0.5200.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
|
|
@ -34,7 +36,6 @@ require (
|
|||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // 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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
|
|
|
|||
|
|
@ -68,6 +68,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/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
|
||||
github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ func TestRules_RuleApplication(t *testing.T) {
|
|||
helper.Screenshot("link-created-rule-test")
|
||||
|
||||
// Navigate to link details to see which archiver was used
|
||||
err = helper.Click("a[href*='/links/']")
|
||||
err = helper.Click(".link-item")
|
||||
if err == nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
helper.Screenshot("link-details-archiver-used")
|
||||
|
|
|
|||
|
|
@ -53,13 +53,14 @@ func TestArchive_AutomaticCreation(t *testing.T) {
|
|||
helper.Screenshot("link-created")
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
helper.Screenshot("link-details-archive")
|
||||
|
||||
// Verify archive section exists
|
||||
helper.AssertVisible(".archive-status, .archive-history, [class*='archive']", "Archive information should be visible")
|
||||
// Wait for archive processing to surface in the UI
|
||||
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")
|
||||
}
|
||||
|
|
@ -105,15 +106,15 @@ func TestArchive_ViewHistory(t *testing.T) {
|
|||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
helper.Screenshot("archive-history")
|
||||
|
||||
// Verify archive history is visible
|
||||
helper.AssertVisible(".archive-history, .archive-list, [class*='archive']", "Archive history should be visible")
|
||||
// Wait for archive to appear in the UI
|
||||
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...")
|
||||
time.Sleep(10 * time.Second)
|
||||
helper.Screenshot("archive-after-wait")
|
||||
|
|
@ -162,7 +163,7 @@ func TestArchive_StatusDisplay(t *testing.T) {
|
|||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
helper.Screenshot("archive-status")
|
||||
|
|
@ -222,7 +223,7 @@ func TestArchive_Rearchive(t *testing.T) {
|
|||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
helper.Screenshot("before-rearchive")
|
||||
|
|
@ -295,7 +296,7 @@ func TestArchive_ViewFiles(t *testing.T) {
|
|||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
|
|
@ -356,7 +357,7 @@ func TestArchive_PreviewHTML(t *testing.T) {
|
|||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ func TestError_ArchivalFailure(t *testing.T) {
|
|||
helper.Screenshot("archival-failure-link-created")
|
||||
|
||||
// Navigate to link details
|
||||
err = helper.Click("a[href*='/links/']")
|
||||
err = helper.Click(".link-item")
|
||||
if err == nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
helper.Screenshot("archival-failure-details")
|
||||
|
|
|
|||
|
|
@ -4,19 +4,19 @@ set -e
|
|||
echo "🧪 Running Hako E2E Tests"
|
||||
|
||||
# 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}"
|
||||
|
||||
# Check Docker
|
||||
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
|
||||
fi
|
||||
|
||||
# Install Playwright if needed
|
||||
if ! go run github.com/playwright-community/playwright-go/cmd/playwright@latest --help > /dev/null 2>&1; then
|
||||
echo "📦 Installing Playwright browsers..."
|
||||
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
|
||||
(cd e2e && go run github.com/playwright-community/playwright-go/cmd/playwright@latest install chromium)
|
||||
fi
|
||||
|
||||
# Build webapp (required for embed)
|
||||
|
|
|
|||
0
webapp/dist/.gitkeep
vendored
Normal file
0
webapp/dist/.gitkeep
vendored
Normal file
|
|
@ -7,7 +7,7 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
//go:embed dist
|
||||
//go:embed all:dist
|
||||
var embeddedWebappFS embed.FS
|
||||
|
||||
// GetFilesystem returns a HTTP filesystem with the embedded webapp files
|
||||
|
|
|
|||
Loading…
Reference in a new issue