diff --git a/.tangled/workflow.schema.json b/.tangled/workflow.schema.json new file mode 100644 index 0000000..bf50219 --- /dev/null +++ b/.tangled/workflow.schema.json @@ -0,0 +1,408 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://rssbase.io/schemas/tangled-workflow.schema.json", + "title": "Tangled Spindle workflow", + "description": "Local JSON Schema for Tangled CI / Spindle workflow files under .tangled/workflows/*.yml. Based on https://docs.tangled.org/spindles.", + "type": "object", + "additionalProperties": false, + "required": ["when", "engine"], + "properties": { + "when": { + "title": "Trigger conditions", + "description": "Defines when the workflow runs. Each condition can match events, branches, and/or tags.", + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/trigger" }, + "defaultSnippets": [ + { + "label": "push/manual on main + PR to main", + "body": [ + { "event": ["push", "manual"], "branch": ["main"] }, + { "event": ["pull_request"], "branch": ["main"] } + ] + }, + { + "label": "tag push", + "body": [ + { "event": ["push"], "tag": ["v*"] } + ] + } + ] + }, + "engine": { + "title": "Execution engine", + "description": "The Spindle engine used to run the workflow.", + "type": "string", + "enum": ["microvm", "nixery"] + }, + "clone": { + "title": "Clone options", + "description": "Controls how the repository is cloned before workflow steps run.", + "type": "object", + "additionalProperties": false, + "properties": { + "skip": { + "description": "Skip cloning the repository. Default: false.", + "type": "boolean", + "default": false + }, + "depth": { + "description": "Number of commits to fetch. Default: 1.", + "type": "integer", + "minimum": 0, + "default": 1 + }, + "submodules": { + "description": "Recursively fetch git submodules. Default: false.", + "type": "boolean", + "default": false + } + }, + "defaultSnippets": [ + { + "label": "default clone options", + "body": { "skip": false, "depth": 1, "submodules": false } + } + ] + }, + "environment": { + "title": "Workflow environment variables", + "description": "Public environment variables available throughout the workflow. Do not put secrets here; use Tangled repository secrets.", + "$ref": "#/definitions/environment" + }, + "steps": { + "title": "Workflow steps", + "description": "Commands to run in order. Commands run in a Bash shell.", + "type": "array", + "items": { "$ref": "#/definitions/step" }, + "defaultSnippets": [ + { + "label": "single command step", + "body": [ + { "name": "$1", "command": "$2" } + ] + }, + { + "label": "multi-line Bash step", + "body": [ + { "name": "$1", "command": "set -euo pipefail\n$2" } + ] + } + ] + }, + "dependencies": { + "title": "Dependencies", + "description": "For microvm: a flat list of packages/flakeref#attrs. For nixery: a map of registry refs to package lists.", + "oneOf": [ + { "$ref": "#/definitions/microvmDependencies" }, + { "$ref": "#/definitions/nixeryDependencies" } + ] + }, + "image": { + "title": "microVM image", + "description": "microVM image name. Examples: nixos, alpine. Available images depend on the Spindle operator; if omitted, the operator default is used.", + "type": "string", + "examples": ["nixos", "alpine"], + "defaultSnippets": [ + { "label": "nixos", "body": "nixos" }, + { "label": "alpine", "body": "alpine" } + ] + }, + "registry": { + "title": "microVM flake registry aliases", + "description": "Remaps flake references for microVM NixOS images, similar to `nix registry`.", + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Flake reference, e.g. github:nixos/nixpkgs/nixos-unstable." + }, + "defaultSnippets": [ + { + "label": "pin nixpkgs to unstable", + "body": { "nixpkgs": "github:nixos/nixpkgs/nixos-unstable" } + } + ] + }, + "caches": { + "title": "microVM Nix binary caches", + "description": "Map of Nix binary cache URL to trusted public key. Used by microVM NixOS images.", + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Trusted public key for this binary cache." + }, + "defaultSnippets": [ + { + "label": "nix-community Cachix", + "body": { + "https://nix-community.cachix.org": "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + } + } + ] + }, + "services": { + "title": "microVM NixOS services.* configuration", + "description": "Passed through to NixOS as services.* for microVM NixOS images. `true` is shorthand for `.enable = true` where an enable option exists.", + "type": "object", + "additionalProperties": { "$ref": "#/definitions/nixosValue" }, + "defaultSnippets": [ + { + "label": "PostgreSQL service", + "body": { + "postgresql": { + "enable": true, + "ensureDatabases": ["spindle-workflow"], + "ensureUsers": [ + { "name": "spindle-workflow", "ensureDBOwnership": true } + ] + } + } + } + ] + }, + "virtualisation": { + "title": "microVM NixOS virtualisation.* configuration", + "description": "Passed through to NixOS as virtualisation.* for microVM NixOS images. `true` is shorthand for `.enable = true` where an enable option exists.", + "type": "object", + "additionalProperties": { "$ref": "#/definitions/nixosValue" }, + "defaultSnippets": [ + { + "label": "enable Docker", + "body": { "docker": true } + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { "engine": { "const": "microvm" } }, + "required": ["engine"] + }, + "then": { + "properties": { + "dependencies": { "$ref": "#/definitions/microvmDependencies" } + } + } + }, + { + "if": { + "properties": { "engine": { "const": "nixery" } }, + "required": ["engine"] + }, + "then": { + "properties": { + "dependencies": { "$ref": "#/definitions/nixeryDependencies" } + }, + "not": { + "anyOf": [ + { "required": ["image"] }, + { "required": ["registry"] }, + { "required": ["caches"] }, + { "required": ["services"] }, + { "required": ["virtualisation"] } + ] + } + } + } + ], + "defaultSnippets": [ + { + "label": "microVM NixOS workflow", + "body": { + "when": [ + { "event": ["push", "manual"], "branch": ["main"] }, + { "event": ["pull_request"], "branch": ["main"] } + ], + "engine": "microvm", + "image": "nixos", + "dependencies": ["devenv"], + "steps": [ + { "name": "Run checks", "command": "devenv shell check" } + ] + } + }, + { + "label": "microVM Alpine workflow", + "body": { + "when": [ + { "event": ["push", "pull_request"], "branch": ["main"] } + ], + "engine": "microvm", + "image": "alpine", + "steps": [ + { "name": "Install dependencies", "command": "apk add --no-cache go" }, + { "name": "Check formatting", "command": "test -z $(gofmt -l .)" } + ] + } + }, + { + "label": "nixery workflow", + "body": { + "when": [ + { "event": ["push", "manual"], "branch": ["main", "develop"] }, + { "event": ["pull_request"], "branch": ["main"] } + ], + "engine": "nixery", + "clone": { "skip": false, "depth": 1, "submodules": false }, + "dependencies": { + "nixpkgs": ["nodejs", "go"] + }, + "steps": [ + { "name": "Build backend", "command": "go build" }, + { "name": "Build frontend", "command": "npm run build" } + ] + } + } + ], + "definitions": { + "trigger": { + "type": "object", + "additionalProperties": false, + "required": ["event"], + "properties": { + "event": { + "description": "Events that can trigger this condition.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": ["push", "pull_request", "manual"] + } + }, + "branch": { + "description": "Branch patterns. For push, matches pushed branches. For pull_request, matches target branches. No effect for manual. Supports * and ** globs.", + "$ref": "#/definitions/patternList" + }, + "tag": { + "description": "Tag patterns for push events. No effect for pull_request or manual. Supports * and ** globs.", + "$ref": "#/definitions/patternList" + } + }, + "allOf": [ + { + "if": { + "properties": { + "event": { "contains": { "const": "push" } } + }, + "required": ["event"] + }, + "then": { + "anyOf": [ + { "required": ["branch"] }, + { "required": ["tag"] } + ] + } + } + ], + "defaultSnippets": [ + { + "label": "push to main", + "body": { "event": ["push"], "branch": ["main"] } + }, + { + "label": "pull request to main", + "body": { "event": ["pull_request"], "branch": ["main"] } + }, + { + "label": "manual", + "body": { "event": ["manual"] } + }, + { + "label": "push tag v*", + "body": { "event": ["push"], "tag": ["v*"] } + } + ] + }, + "patternList": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "environment": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "defaultSnippets": [ + { + "label": "common environment", + "body": { + "NODE_ENV": "production" + } + } + ] + }, + "step": { + "type": "object", + "additionalProperties": false, + "required": ["name", "command"], + "properties": { + "name": { + "description": "Human-readable step name shown in workflow runs.", + "type": "string" + }, + "command": { + "description": "Bash command to run for this step.", + "type": "string" + }, + "environment": { + "description": "Public environment variables for this step only. Do not put secrets here; use Tangled repository secrets.", + "$ref": "#/definitions/environment" + } + } + }, + "microvmDependencies": { + "description": "microVM dependencies: a flat list of packages available to every step. Bare names resolve from nixpkgs; flakeref#attr is also supported.", + "type": "array", + "items": { + "type": "string", + "examples": ["go", "pnpm", "pkg-config", "openssl", "github:nixos/nixpkgs#hello"] + } + }, + "nixeryDependencies": { + "description": "Nixery dependencies: map registry references to package lists.", + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + }, + "defaultSnippets": [ + { + "label": "nixpkgs packages", + "body": { "nixpkgs": ["nodejs", "go"] } + }, + { + "label": "unstable package", + "body": { "nixpkgs/nixpkgs-unstable": ["bun"] } + }, + { + "label": "custom registry", + "body": { "git+https://tangled.org/@example.com/my_pkg": ["my_pkg"] } + } + ] + }, + "nixosValue": { + "description": "Arbitrary NixOS option value used under services.* or virtualisation.*.", + "anyOf": [ + { "type": "boolean" }, + { "type": "string" }, + { "type": "number" }, + { "type": "null" }, + { + "type": "array", + "items": { "$ref": "#/definitions/nixosValue" } + }, + { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/nixosValue" } + } + ] + } + } +} diff --git a/.tangled/workflows/build.yml b/.tangled/workflows/build.yml index 7a8700e..8c3d0f4 100644 --- a/.tangled/workflows/build.yml +++ b/.tangled/workflows/build.yml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../workflow.schema.json + when: - event: ["push", "manual"] branch: ["main"] @@ -6,41 +8,37 @@ when: engine: microvm image: nixos +dependencies: + - devenv +virtualisation: + docker: true clone: skip: false depth: 1 -dependencies: - - devenv - steps: - name: "Initialize devenv" command: devenv shell true - - name: "Build production image with Dagger" - command: DAGGER_NO_NAG=1 devenv shell dagger call application build-prod sync - - - name: "Build production image with Docker Compose" - command: | - devenv shell -- bash -c ' - set -euo pipefail - cd application - export APP_SECRET="${APP_SECRET:-ci-app-secret}" - export CADDY_MERCURE_JWT_SECRET="${CADDY_MERCURE_JWT_SECRET:-ci-mercure-secret}" - docker-compose -f compose.yaml -f compose.prod.yaml build --pull - ' + - name: "Build docker image" + command: DAGGER_NO_NAG=1 devenv shell dagger call application build-prod export-image --name app-php-prod:latest - - name: "Start production services and smoke test" + - name: "Start docker container and smoke test" command: | devenv shell -- bash -c ' set -euo pipefail - cd application + export DOCKER_BUILDKIT=1 + export COMPOSE_DOCKER_CLI_BUILD=1 export APP_SECRET="${APP_SECRET:-ci-app-secret}" export CADDY_MERCURE_JWT_SECRET="${CADDY_MERCURE_JWT_SECRET:-ci-mercure-secret}" - trap "docker-compose -f compose.yaml -f compose.prod.yaml down --volumes --remove-orphans" EXIT - docker-compose -f compose.yaml -f compose.prod.yaml down --volumes --remove-orphans - docker-compose -f compose.yaml -f compose.prod.yaml up --wait --wait-timeout 120 --no-build -d - curl -v --fail-with-body http://localhost - curl -vI --insecure --fail-with-body "https://localhost/.well-known/mercure?topic=test" + export HTTP_PORT="${HTTP_PORT:-8080}" + export HTTPS_PORT="${HTTPS_PORT:-8443}" + export HTTP3_PORT="${HTTP3_PORT:-8443}" + export COMPOSE_FILE="application/compose.yaml:application/compose.prod.yaml" + trap "docker-compose down --volumes --remove-orphans" EXIT + docker-compose down --volumes --remove-orphans + docker-compose up --wait --wait-timeout 120 --no-build -d + curl -v --fail-with-body "http://localhost:${HTTP_PORT}" + curl -vI --insecure --fail-with-body "https://localhost:${HTTPS_PORT}/.well-known/mercure?topic=test" ' diff --git a/.tangled/workflows/check.yml b/.tangled/workflows/check.yml index a366d84..f591e9d 100644 --- a/.tangled/workflows/check.yml +++ b/.tangled/workflows/check.yml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=../workflow.schema.json + when: - event: ["push", "manual"] branch: ["main"] @@ -6,17 +8,18 @@ when: engine: microvm image: nixos +dependencies: + - devenv +virtualisation: + docker: true clone: skip: false depth: 1 -dependencies: - - devenv - steps: - name: "Initialize devenv" command: devenv shell true - - name: "Run Dagger checks" + - name: "Run checks" command: DAGGER_NO_NAG=1 devenv shell dagger check diff --git a/devenv.nix b/devenv.nix index 187082b..7297405 100644 --- a/devenv.nix +++ b/devenv.nix @@ -11,6 +11,7 @@ in { # https://devenv.sh/packages/ packages = [ + pkgs.docker-buildx # BuildKit builder for CI image builds pkgs.docker-compose # used by application dagger # dagger CLI dev build (until 1.0) pkgs.skills # used to manage .agents/skills @@ -21,6 +22,7 @@ in enterShell = devenvSummary { title = "RSSBase devenv:"; rows = [ + { pkg = pkgs.docker-buildx; } { pkg = pkgs.docker-compose; } { pkg = dagger; } { pkg = pkgs.skills; }