Add Gitea build, container tests and registry publication
All checks were successful
Build, test and publish / verify (push) Successful in 1m41s
All checks were successful
Build, test and publish / verify (push) Successful in 1m41s
This commit is contained in:
@@ -3,3 +3,16 @@
|
||||
.idea
|
||||
.mvn
|
||||
*.iml
|
||||
|
||||
**/node_modules
|
||||
**/.angular
|
||||
**/dist
|
||||
**/test-results
|
||||
**/playwright-report
|
||||
ci/artifacts
|
||||
# Allow only the verified runtime output for the CI prebuilt target.
|
||||
!agiledancers-be/target
|
||||
agiledancers-be/target/*
|
||||
!agiledancers-be/target/quarkus-app
|
||||
|
||||
**/unit-results
|
||||
|
||||
41
.gitea/workflows/build.yaml
Normal file
41
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Build, test and publish
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: agiledancers-mini-pc
|
||||
env:
|
||||
CI: 'true'
|
||||
TARGET_PLATFORM: ${{ vars.TARGET_PLATFORM }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
CI_REPOSITORY: ${{ gitea.repository }}
|
||||
CI_COMMIT: ${{ gitea.sha }}
|
||||
CI_REF: ${{ gitea.ref }}
|
||||
CI_RUN_ID: ${{ gitea.run_id }}
|
||||
CI_RUN_ATTEMPT: ${{ gitea.run_attempt }}
|
||||
steps:
|
||||
- name: Check out pushed commit
|
||||
uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build and test both images
|
||||
run: timeout --signal=TERM --kill-after=60s 75m bash ci/verify.sh
|
||||
- name: Publish verified main images
|
||||
if: success() && gitea.ref == 'refs/heads/main'
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: timeout --signal=TERM --kill-after=60s 15m bash ci/publish.sh
|
||||
- name: Collect diagnostics and clean up test containers
|
||||
if: always()
|
||||
run: bash ci/cleanup.sh
|
||||
- name: Retain diagnostics for fourteen days
|
||||
if: always()
|
||||
uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
|
||||
with:
|
||||
name: diagnostics-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
||||
path: ci/artifacts/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,3 +1,12 @@
|
||||
*.iml
|
||||
.idea
|
||||
target
|
||||
# Local CI diagnostics and runner credentials/state
|
||||
/ci/artifacts/
|
||||
/ci/runner/.env
|
||||
/ci/runner/.runner
|
||||
/ci/runner/data/
|
||||
/ci/runner/local/
|
||||
|
||||
**/unit-results/
|
||||
**/__pycache__/
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -5,9 +5,16 @@ COPY agiledancers-be/pom.xml agiledancers-be/pom.xml
|
||||
COPY agiledancers-be/src agiledancers-be/src
|
||||
RUN mvn -B -ntp clean verify
|
||||
|
||||
FROM eclipse-temurin:25-jre
|
||||
FROM eclipse-temurin:25-jre AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build --chown=10001:10001 /workspace/agiledancers-be/target/quarkus-app/ ./
|
||||
USER 10001:10001
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "/app/quarkus-run.jar"]
|
||||
|
||||
# CI packages the exact output that already passed mvn clean verify.
|
||||
FROM runtime AS prebuilt
|
||||
COPY --chown=10001:10001 agiledancers-be/target/quarkus-app/ ./
|
||||
|
||||
# Default standalone build still compiles and tests from source.
|
||||
FROM runtime AS standalone
|
||||
COPY --from=build --chown=10001:10001 /workspace/agiledancers-be/target/quarkus-app/ ./
|
||||
|
||||
@@ -168,3 +168,11 @@ Open <http://localhost:4200>. The development proxy forwards API requests to the
|
||||
backend on port 8080. Frontend builds and tests run independently of Maven:
|
||||
`npm run build` and `npm test`. See the frontend README for browser tests and
|
||||
production hosting requirements.
|
||||
|
||||
## Gitea CI
|
||||
|
||||
Every branch push builds and tests both applications and their Docker images.
|
||||
Only successful `main` runs publish images to the Synology registry. See
|
||||
[the CI management guide](docs/gitea-ci.md) for runner setup, registry secrets,
|
||||
image names, diagnostics and recovery, and [validation evidence](docs/s4-validation.md)
|
||||
for the current operational status.
|
||||
|
||||
7
agiledancers-fe/.dockerignore
Normal file
7
agiledancers-fe/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.angular
|
||||
.git
|
||||
test-results
|
||||
playwright-report
|
||||
|
||||
unit-results
|
||||
18
agiledancers-fe/Dockerfile
Normal file
18
agiledancers-fe/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:24.15.0-bookworm AS build
|
||||
WORKDIR /workspace
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm test && npm run build
|
||||
|
||||
FROM nginx:1.28-alpine AS runtime
|
||||
ENV BACKEND_UPSTREAM=backend:8080
|
||||
COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template
|
||||
EXPOSE 8080
|
||||
|
||||
# CI supplies the production output it has already built and tested.
|
||||
FROM runtime AS prebuilt
|
||||
COPY dist/agiledancers-fe/browser/ /usr/share/nginx/html/
|
||||
|
||||
FROM runtime AS standalone
|
||||
COPY --from=build /workspace/dist/agiledancers-fe/browser/ /usr/share/nginx/html/
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const backend = 'http://127.0.0.1:8080';
|
||||
const backend = process.env['PLAYWRIGHT_BASE_URL'] || 'http://127.0.0.1:8080';
|
||||
let schoolToDelete: number | undefined;
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
|
||||
18
agiledancers-fe/nginx/default.conf.template
Normal file
18
agiledancers-fe/nginx/default.conf.template
Normal file
@@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://${BACKEND_UPSTREAM};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const externalURL = process.env['PLAYWRIGHT_BASE_URL'];
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
timeout: 60_000,
|
||||
reporter: 'list',
|
||||
reporter: process.env['CI']
|
||||
? [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/playwright.xml' }],
|
||||
['html', { open: 'never' }],
|
||||
]
|
||||
: 'list',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
baseURL: 'http://127.0.0.1:4200',
|
||||
baseURL: externalURL || 'http://127.0.0.1:4200',
|
||||
trace: 'retain-on-failure',
|
||||
actionTimeout: 15_000,
|
||||
screenshot: 'only-on-failure',
|
||||
@@ -17,10 +25,12 @@ export default defineConfig({
|
||||
? { executablePath: process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] }
|
||||
: {},
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm start -- --host 127.0.0.1 --port 4200',
|
||||
url: 'http://127.0.0.1:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
timeout: 60_000,
|
||||
},
|
||||
webServer: externalURL
|
||||
? undefined
|
||||
: {
|
||||
command: 'npm start -- --host 127.0.0.1 --port 4200',
|
||||
url: 'http://127.0.0.1:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
timeout: 60_000,
|
||||
},
|
||||
});
|
||||
|
||||
29
ci/cleanup.sh
Normal file
29
ci/cleanup.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname -- "$0")/common.sh"
|
||||
status=0
|
||||
# Copy only test diagnostics, never environment dumps, Docker inspect or credentials.
|
||||
for report in agiledancers-be/target/surefire-reports agiledancers-be/target/failsafe-reports \
|
||||
agiledancers-fe/unit-results agiledancers-fe/test-results agiledancers-fe/playwright-report; do
|
||||
if [[ -d "$report" ]]; then
|
||||
destination="$ARTIFACTS/$report"
|
||||
mkdir -p "$destination"
|
||||
cp -R "$report/." "$destination/" || status=1
|
||||
fi
|
||||
done
|
||||
if docker info >/dev/null 2>&1; then
|
||||
for role in backend frontend browser; do
|
||||
container="$RUN_NAME-$role"
|
||||
if docker container inspect "$container" >/dev/null 2>&1; then
|
||||
docker logs "$container" > "$ARTIFACTS/$role.log" 2>&1 || status=1
|
||||
docker rm -f "$container" >/dev/null || status=1
|
||||
fi
|
||||
done
|
||||
if docker network inspect "$RUN_NAME" >/dev/null 2>&1; then
|
||||
docker network rm "$RUN_NAME" >/dev/null || status=1
|
||||
fi
|
||||
rm -f "$ARTIFACTS/containers-started"
|
||||
elif [[ -f "$ARTIFACTS/containers-started" ]]; then
|
||||
echo "Docker is unavailable; cleanup could not be confirmed" >&2
|
||||
status=1
|
||||
fi
|
||||
exit "$status"
|
||||
40
ci/common.sh
Normal file
40
ci/common.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared by host-runner scripts. No credentials are persisted in the workspace.
|
||||
set -Eeuo pipefail
|
||||
CI_ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
cd "$CI_ROOT"
|
||||
ARTIFACTS="$CI_ROOT/ci/artifacts"
|
||||
mkdir -p "$ARTIFACTS"
|
||||
: "${CI_COMMIT:=$(git rev-parse HEAD)}"
|
||||
: "${CI_RUN_ID:=local}"
|
||||
: "${CI_RUN_ATTEMPT:=1}"
|
||||
[[ "$CI_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid commit SHA' >&2; exit 1; }
|
||||
[[ "$CI_RUN_ID-$CI_RUN_ATTEMPT" =~ ^[a-zA-Z0-9-]+$ ]] || { echo 'Invalid run identifier' >&2; exit 1; }
|
||||
RUN_NAME="agiledancers-${CI_RUN_ID}-${CI_RUN_ATTEMPT}"
|
||||
BACKEND_IMAGE="agiledancers-ci-backend:${CI_COMMIT}-${CI_RUN_ID}-${CI_RUN_ATTEMPT}"
|
||||
FRONTEND_IMAGE="agiledancers-ci-frontend:${CI_COMMIT}-${CI_RUN_ID}-${CI_RUN_ATTEMPT}"
|
||||
|
||||
require_platform() {
|
||||
: "${TARGET_PLATFORM:?Set TARGET_PLATFORM to the verified Synology platform (for example linux/amd64)}"
|
||||
local arch native
|
||||
arch=$(docker info --format '{{.Architecture}}')
|
||||
case "$arch" in x86_64|amd64) arch=amd64;; aarch64|arm64) arch=arm64;; esac
|
||||
native="$(docker info --format '{{.OSType}}')/$arch"
|
||||
[[ "$TARGET_PLATFORM" == "$native" ]] || {
|
||||
echo "Target $TARGET_PLATFORM differs from Docker host $native. Resolve and test the architecture before running CI." >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Anonymous build pulls must not invoke the developer's desktop credential helper.
|
||||
# Publication uses the same isolation and logs in only to our registry.
|
||||
isolate_docker_config() {
|
||||
if [[ -z "${DOCKER_HOST:-}" ]]; then
|
||||
DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}')
|
||||
export DOCKER_HOST
|
||||
fi
|
||||
unset DOCKER_CONTEXT
|
||||
CI_DOCKER_CONFIG=$(mktemp -d)
|
||||
DOCKER_CONFIG=$CI_DOCKER_CONFIG
|
||||
export DOCKER_CONFIG
|
||||
}
|
||||
67
ci/configure-gitea.py
Normal file
67
ci/configure-gitea.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configure repository Actions through the Gitea API; read tokens from local files."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
BASE = 'http://192.168.2.28:3000/api/v1/repos/wanjakrah/agiledancers'
|
||||
|
||||
def read_secret(variable):
|
||||
path = Path(os.environ[variable]).expanduser()
|
||||
if path.stat().st_mode & 0o077:
|
||||
raise ValueError(f'{variable} must refer to a file readable only by its owner (chmod 600)')
|
||||
value = path.read_text().strip()
|
||||
if not value:
|
||||
raise ValueError(f'{variable} is empty')
|
||||
return value
|
||||
|
||||
def main():
|
||||
admin = read_secret('GITEA_ADMIN_TOKEN_FILE')
|
||||
registry = read_secret('REGISTRY_TOKEN_FILE')
|
||||
user = os.environ['REGISTRY_USER']
|
||||
platform = os.environ['TARGET_PLATFORM']
|
||||
if platform not in ('linux/amd64', 'linux/arm64'):
|
||||
raise ValueError('Set TARGET_PLATFORM from the verified Synology architecture')
|
||||
|
||||
def api(method, path, body):
|
||||
request = urllib.request.Request(BASE + path, method=method,
|
||||
data=json.dumps(body).encode() if body is not None else None, headers={
|
||||
'Authorization': 'token ' + admin, 'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
data = response.read()
|
||||
return json.loads(data) if data else None
|
||||
|
||||
api('PATCH', '', {'has_actions': True})
|
||||
for name, value in [('REGISTRY_USER', user), ('REGISTRY_TOKEN', registry)]:
|
||||
api('PUT', '/actions/secrets/' + name, {'data': value})
|
||||
try:
|
||||
api('GET', '/actions/variables/TARGET_PLATFORM', None)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code != 404:
|
||||
raise
|
||||
api('POST', '/actions/variables/TARGET_PLATFORM', {'value': platform})
|
||||
else:
|
||||
api('PUT', '/actions/variables/TARGET_PLATFORM', {'value': platform})
|
||||
# Store registration credentials outside Git; never print API responses/tokens.
|
||||
result = api('POST', '/actions/runners/registration-token', None)
|
||||
token_path = Path.home() / '.config/agiledancers-runner/registration-token'
|
||||
token_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
fd = os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, 'w') as output:
|
||||
output.write(result['token'])
|
||||
print('Actions, registry secrets and target platform configured; runner token saved outside Git.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as error:
|
||||
print(f'Gitea configuration failed: HTTP {error.code}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except (OSError, ValueError, KeyError, urllib.error.URLError) as error:
|
||||
# Do not include request bodies, headers, response bodies or token values.
|
||||
print(f'Gitea configuration failed ({type(error).__name__}); check local files and connection.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
16
ci/main-head.py
Normal file
16
ci/main-head.py
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read the current main head without placing an access token in arguments/logs."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
url = os.environ['GITEA_SERVER_URL'].rstrip('/') + '/api/v1/repos/' + os.environ['CI_REPOSITORY'] + '/branches/main'
|
||||
request = urllib.request.Request(url, headers={'Authorization': 'token ' + os.environ['REGISTRY_TOKEN']})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
print(json.load(response)['commit']['id'])
|
||||
except (urllib.error.URLError, KeyError, ValueError) as error:
|
||||
print(f'Cannot verify current main head ({type(error).__name__})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
76
ci/publish.sh
Normal file
76
ci/publish.sh
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname -- "$0")/common.sh"
|
||||
# Also enforce this inside the script, independently of the workflow expression.
|
||||
[[ "${CI_REF:-}" == refs/heads/main ]] || { echo 'Branch is not main; no publication'; exit 0; }
|
||||
[[ -f "$ARTIFACTS/verified-commit" && "$(cat "$ARTIFACTS/verified-commit")" == "$CI_COMMIT" ]] || {
|
||||
echo 'This commit has not passed all verification steps' >&2; exit 1;
|
||||
}
|
||||
: "${REGISTRY_USER:?Missing REGISTRY_USER secret}"
|
||||
: "${REGISTRY_TOKEN:?Missing REGISTRY_TOKEN secret}"
|
||||
: "${GITEA_SERVER_URL:?Missing Gitea server URL}"
|
||||
: "${CI_REPOSITORY:?Missing repository}"
|
||||
require_platform
|
||||
exec > >(tee "$ARTIFACTS/publish.log") 2>&1
|
||||
REGISTRY=${GITEA_SERVER_URL#http://}
|
||||
REGISTRY=${REGISTRY#https://}
|
||||
REGISTRY=${REGISTRY%/}
|
||||
[[ "$REGISTRY" != */* ]] || { echo 'Registry must be hosted at the Gitea server root'; exit 1; }
|
||||
OWNER=${CI_REPOSITORY%%/*}
|
||||
OWNER=${OWNER,,}
|
||||
[[ "$OWNER" =~ ^[a-z0-9][a-z0-9_.-]*$ ]] || { echo 'Invalid registry namespace'; exit 1; }
|
||||
# Gitea 1.25 ignores workflow concurrency. All publications on the sole runner
|
||||
# share this lock, outside checkout directories and across workflow attempts.
|
||||
lock_dir=${CI_PUBLISH_LOCK_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/agiledancers-ci}
|
||||
mkdir -p "$lock_dir"
|
||||
exec 9>"$lock_dir/publish.lock"
|
||||
flock -w 600 9
|
||||
# Preserve the selected daemon and isolate credentials from the developer's config.
|
||||
isolate_docker_config
|
||||
trap 'rm -rf -- "$DOCKER_CONFIG"' EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USER" --password-stdin
|
||||
|
||||
ensure_commit_image() {
|
||||
local role=$1 source=$2 repository="$REGISTRY/$OWNER/agiledancers-$1" ref error
|
||||
ref="$repository:$CI_COMMIT"
|
||||
error=$(mktemp)
|
||||
if docker manifest inspect --insecure "$ref" > /dev/null 2>"$error"; then
|
||||
echo "Preserving existing $ref"
|
||||
elif grep -Eqi 'manifest unknown|no such manifest' "$error"; then
|
||||
docker tag "$source" "$ref"
|
||||
docker push "$ref"
|
||||
else
|
||||
# Authentication/network/server errors are never treated as missing tags.
|
||||
cat "$error" >&2
|
||||
rm -f "$error"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$error"
|
||||
docker pull --platform "$TARGET_PLATFORM" "$ref"
|
||||
[[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$ref")" == "$CI_COMMIT" ]] || {
|
||||
echo "Existing $role image has an unexpected source revision" >&2; return 1;
|
||||
}
|
||||
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$ref" \
|
||||
> "$ARTIFACTS/$role-digests.txt"
|
||||
grep -Fq "$repository@sha256:" "$ARTIFACTS/$role-digests.txt" || { echo 'Missing registry digest'; return 1; }
|
||||
}
|
||||
ensure_commit_image backend "$BACKEND_IMAGE"
|
||||
ensure_commit_image frontend "$FRONTEND_IMAGE"
|
||||
# Both immutable images are now pull-verified. Only current main may move aliases.
|
||||
head=$(python3 ci/main-head.py)
|
||||
if [[ "$head" != "$CI_COMMIT" ]]; then
|
||||
echo 'A newer main commit exists; leaving moving tags unchanged.'
|
||||
exit 0
|
||||
fi
|
||||
for role in backend frontend; do
|
||||
repository="$REGISTRY/$OWNER/agiledancers-$role"
|
||||
# Recheck under the publication lock before each moving-tag write.
|
||||
[[ "$(python3 ci/main-head.py)" == "$CI_COMMIT" ]] || {
|
||||
echo 'Main advanced during publication; leaving remaining moving tags unchanged.'
|
||||
exit 0
|
||||
}
|
||||
docker tag "$repository:$CI_COMMIT" "$repository:main"
|
||||
docker push "$repository:main"
|
||||
docker pull --platform "$TARGET_PLATFORM" "$repository:main"
|
||||
done
|
||||
17
ci/runner/agiledancers-runner.service
Normal file
17
ci/runner/agiledancers-runner.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Agiledancers Gitea Actions runner
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/.local/share/agiledancers-runner
|
||||
EnvironmentFile=%h/.config/agiledancers-runner/environment
|
||||
ExecStart=%h/.local/bin/act_runner daemon --config %h/.local/share/agiledancers-runner/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=150
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
11
ci/runner/config.yaml
Normal file
11
ci/runner/config.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
log:
|
||||
level: info
|
||||
runner:
|
||||
file: .runner
|
||||
capacity: 1
|
||||
timeout: 2h
|
||||
shutdown_timeout: 2m
|
||||
labels:
|
||||
- agiledancers-mini-pc:host
|
||||
cache:
|
||||
enabled: false
|
||||
32
ci/runner/configure-docker.py
Normal file
32
ci/runner/configure-docker.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Merge this project's HTTP registry into a selected daemon JSON file."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit('Usage: python3 ci/runner/configure-docker.py <daemon.json>')
|
||||
path = Path(sys.argv[1]).expanduser()
|
||||
data = json.loads(path.read_text()) if path.exists() else {}
|
||||
registries = data.setdefault('insecure-registries', [])
|
||||
if not isinstance(registries, list):
|
||||
sys.exit('Existing insecure-registries must be an array; configuration left unchanged')
|
||||
if '192.168.2.28:3000' in registries:
|
||||
print('Internal registry already configured; no change')
|
||||
sys.exit(0)
|
||||
registries.append('192.168.2.28:3000')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists():
|
||||
backup = path.with_name(path.name + '.before-agiledancers')
|
||||
if not backup.exists():
|
||||
shutil.copy2(path, backup)
|
||||
# Write in the same directory, then replace atomically.
|
||||
temporary = path.with_name(path.name + '.agiledancers.tmp')
|
||||
with temporary.open('x') as output:
|
||||
json.dump(data, output, indent=2)
|
||||
output.write('\n')
|
||||
if path.exists():
|
||||
shutil.copymode(path, temporary)
|
||||
temporary.replace(path)
|
||||
print('Internal registry added; restart/apply Docker Engine configuration to activate')
|
||||
3
ci/runner/docker-daemon.json
Normal file
3
ci/runner/docker-daemon.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"insecure-registries": ["192.168.2.28:3000"]
|
||||
}
|
||||
52
ci/runner/install.sh
Normal file
52
ci/runner/install.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run as the development user, not root. Configuration stays outside the checkout.
|
||||
set -Eeuo pipefail
|
||||
script_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
runner_dir="$HOME/.local/share/agiledancers-runner"
|
||||
config_dir="$HOME/.config/agiledancers-runner"
|
||||
install -d -m 700 "$runner_dir" "$config_dir" "$HOME/.local/bin" "$HOME/.config/systemd/user"
|
||||
[[ "$(uname -s)/$(uname -m)" == Linux/x86_64 ]] || { echo 'This installer targets the existing Linux x86_64 mini-PC'; exit 1; }
|
||||
for tool in node java mvn docker curl python3 flock timeout; do command -v "$tool" >/dev/null; done
|
||||
: "${JAVA_HOME:?Set JAVA_HOME to your JDK 25 directory}"
|
||||
"$JAVA_HOME/bin/java" -version 2>&1 | grep -Eq 'version "25[.\"]' || { echo 'JDK 25 required'; exit 1; }
|
||||
node -e 'const [a,b,c]=process.versions.node.split(".").map(Number); if (!((a===22&&(b>22||(b===22&&c>=3)))||(a===24&&(b>15||(b===15&&c>=0)))||a>=26)) process.exit(1)'
|
||||
if [[ ! -x "$HOME/.local/bin/act_runner" ]]; then
|
||||
download_dir=$(mktemp -d)
|
||||
trap 'rm -rf -- "$download_dir"' EXIT
|
||||
curl --fail --location --retry 3 \
|
||||
https://dl.gitea.com/act_runner/0.2.13/act_runner-0.2.13-linux-amd64 -o "$download_dir/act_runner"
|
||||
printf '%s %s\n' 3acac8b506ac8cadc88a55155b5d6378f0fab0b8f62d1e0c0450f4ccd69733e2 "$download_dir/act_runner" | sha256sum --check
|
||||
install -m 755 "$download_dir/act_runner" "$HOME/.local/bin/act_runner"
|
||||
fi
|
||||
"$HOME/.local/bin/act_runner" --version | grep -F 'v0.2.13'
|
||||
install -m 600 "$script_dir/config.yaml" "$runner_dir/config.yaml"
|
||||
install -m 644 "$script_dir/agiledancers-runner.service" "$HOME/.config/systemd/user/agiledancers-runner.service"
|
||||
# Resolve paths now: user services do not inherit an interactive shell's PATH/context.
|
||||
node_dir=$(dirname -- "$(command -v node)")
|
||||
maven_dir=$(dirname -- "$(command -v mvn)")
|
||||
docker_host=${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}')}
|
||||
umask 077
|
||||
printf 'JAVA_HOME=%s\nPATH=%s/bin:%s:%s:/usr/local/bin:/usr/bin:/bin\nDOCKER_HOST=%s\n' \
|
||||
"$JAVA_HOME" "$JAVA_HOME" "$node_dir" "$maven_dir" "$docker_host" > "$config_dir/environment"
|
||||
systemctl --user daemon-reload
|
||||
if [[ "${1:-}" == --prepare-only ]]; then
|
||||
echo 'Runner installed; registration and startup still required.'
|
||||
exit 0
|
||||
fi
|
||||
docker info >/dev/null
|
||||
if [[ ! -f "$runner_dir/.runner" ]]; then
|
||||
token_file=${RUNNER_REGISTRATION_TOKEN_FILE:-$config_dir/registration-token}
|
||||
[[ -f "$token_file" ]] || { echo "Place the repository runner registration token in $token_file (mode 600), then rerun."; exit 1; }
|
||||
registration_token=$(cat "$token_file")
|
||||
[[ -n "$registration_token" ]] || { echo 'Empty registration token'; exit 1; }
|
||||
(
|
||||
cd "$runner_dir"
|
||||
"$HOME/.local/bin/act_runner" register --no-interactive \
|
||||
--instance http://192.168.2.28:3000 --name agiledancers-mini-pc \
|
||||
--labels agiledancers-mini-pc:host --token "$registration_token"
|
||||
chmod 600 .runner
|
||||
)
|
||||
unset registration_token
|
||||
fi
|
||||
systemctl --user enable --now agiledancers-runner.service
|
||||
echo 'Runner registered and started. The mini-PC and Docker must remain available.'
|
||||
29
ci/tests/test_docker_configuration.py
Normal file
29
ci/tests/test_docker_configuration.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'runner/configure-docker.py'
|
||||
|
||||
class DockerConfigurationTest(unittest.TestCase):
|
||||
def test_merge_preserves_existing_settings_and_is_idempotent(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'daemon.json'
|
||||
original = {'features': {'buildkit': True}, 'insecure-registries': ['existing:5000']}
|
||||
path.write_text(json.dumps(original))
|
||||
for _ in range(2):
|
||||
result = subprocess.run(['python3', str(SCRIPT), str(path)], capture_output=True, text=True)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
actual = json.loads(path.read_text())
|
||||
self.assertEqual(original['features'], actual['features'])
|
||||
self.assertEqual(['existing:5000', '192.168.2.28:3000'], actual['insecure-registries'])
|
||||
self.assertEqual(original, json.loads(path.with_name('daemon.json.before-agiledancers').read_text()))
|
||||
def test_invalid_configuration_is_left_unchanged(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'daemon.json'
|
||||
original = '{"insecure-registries": "invalid"}'
|
||||
path.write_text(original)
|
||||
result = subprocess.run(['python3', str(SCRIPT), str(path)], capture_output=True, text=True)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertEqual(original, path.read_text())
|
||||
115
ci/tests/test_publication.py
Normal file
115
ci/tests/test_publication.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Exercise real shell control flow against a stateful registry/daemon double."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SHA = 'a' * 40
|
||||
REGISTRY = '192.168.2.28:3000/wanjakrah/agiledancers-'
|
||||
|
||||
DOCKER = r'''#!/usr/bin/env python3
|
||||
import json, os, pathlib, sys
|
||||
p = pathlib.Path(os.environ['FAKE_STATE'])
|
||||
s = json.loads(p.read_text())
|
||||
a = sys.argv[1:]
|
||||
s['calls'].append(a)
|
||||
p.write_text(json.dumps(s))
|
||||
if a[0] == 'info':
|
||||
print('amd64' if 'Architecture' in ' '.join(a) else 'linux')
|
||||
elif a[0] == 'context': print('unix:///tmp/test-docker.sock')
|
||||
elif a[0] == 'login': sys.stdin.read()
|
||||
elif a[:2] == ['manifest', 'inspect']:
|
||||
if os.environ.get('FAKE_MANIFEST_ERROR'):
|
||||
print('unauthorized: authentication required', file=sys.stderr); sys.exit(1)
|
||||
if a[-1] not in s['images']:
|
||||
print('manifest unknown', file=sys.stderr); sys.exit(1)
|
||||
elif a[0] == 'push':
|
||||
if os.environ.get('FAKE_PUSH_FAILURE', '') in a[-1] and os.environ.get('FAKE_PUSH_FAILURE'):
|
||||
sys.exit(1)
|
||||
s['images'][a[-1]] = os.environ['CI_COMMIT']; p.write_text(json.dumps(s))
|
||||
elif a[0] == 'pull':
|
||||
if os.environ.get('FAKE_PULL_FAILURE', '') in a[-1] and os.environ.get('FAKE_PULL_FAILURE'):
|
||||
sys.exit(1)
|
||||
elif a[:2] == ['image', 'inspect']:
|
||||
if 'RepoDigests' in ' '.join(a): print(a[-1].rsplit(':', 1)[0] + '@sha256:' + 'b' * 64)
|
||||
else: print(s['images'].get(a[-1], 'missing'))
|
||||
elif a[:2] == ['container', 'inspect'] or a[:2] == ['network', 'inspect']: sys.exit(1)
|
||||
'''
|
||||
|
||||
class PublicationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
shutil.copytree(ROOT / 'ci', self.root / 'ci', ignore=shutil.ignore_patterns('artifacts', '__pycache__'))
|
||||
(self.root / 'ci/main-head.py').write_text("import os; print(os.environ.get('FAKE_MAIN_HEAD', os.environ['CI_COMMIT']))\n")
|
||||
self.bin = self.root / 'bin'; self.bin.mkdir()
|
||||
(self.bin / 'docker').write_text(DOCKER); (self.bin / 'docker').chmod(0o755)
|
||||
self.state = self.root / 'state.json'
|
||||
self.state.write_text(json.dumps({'images': {}, 'calls': []}))
|
||||
artifacts = self.root / 'ci/artifacts'; artifacts.mkdir()
|
||||
(artifacts / 'verified-commit').write_text(SHA + '\n')
|
||||
self.env = dict(os.environ, PATH=str(self.bin) + ':' + os.environ['PATH'],
|
||||
CI_COMMIT=SHA, CI_REF='refs/heads/main', CI_REPOSITORY='wanjakrah/agiledancers',
|
||||
CI_RUN_ID='test', CI_RUN_ATTEMPT='1', TARGET_PLATFORM='linux/amd64',
|
||||
GITEA_SERVER_URL='http://192.168.2.28:3000', REGISTRY_USER='test', REGISTRY_TOKEN='not-a-secret',
|
||||
CI_PUBLISH_LOCK_DIR=str(self.root / 'lock'), FAKE_STATE=str(self.state))
|
||||
def run_script(self, script='publish.sh', **extra):
|
||||
return subprocess.run(['bash', str(self.root / 'ci' / script)], cwd=self.root,
|
||||
env=dict(self.env, **extra), capture_output=True, text=True, timeout=20)
|
||||
def calls(self, command):
|
||||
return [a for a in json.loads(self.state.read_text())['calls'] if a[0] == command]
|
||||
def test_feature_branch_does_not_contact_registry(self):
|
||||
result = self.run_script(CI_REF='refs/heads/feature/test')
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual([], self.calls('login'))
|
||||
self.assertEqual([], self.calls('push'))
|
||||
def test_unverified_commit_cannot_publish(self):
|
||||
(self.root / 'ci/artifacts/verified-commit').unlink()
|
||||
self.assertNotEqual(0, self.run_script().returncode)
|
||||
self.assertEqual([], self.calls('push'))
|
||||
def test_publish_pull_and_rerun_preserves_commit_tags(self):
|
||||
result = self.run_script()
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
self.assertEqual(4, len(self.calls('push')))
|
||||
result = self.run_script()
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
self.assertEqual(6, len(self.calls('push'))) # only main aliases on rerun
|
||||
self.assertEqual(8, len(self.calls('pull')))
|
||||
def test_partial_publication_is_completed_without_overwriting_backend(self):
|
||||
self.assertNotEqual(0, self.run_script(FAKE_PUSH_FAILURE='frontend:' + SHA).returncode)
|
||||
self.assertFalse(any(a[-1].endswith(':main') for a in self.calls('push')))
|
||||
result = self.run_script()
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
backend_pushes = [a for a in self.calls('push') if a[-1] == REGISTRY + 'backend:' + SHA]
|
||||
self.assertEqual(1, len(backend_pushes))
|
||||
def test_registry_auth_failure_is_not_treated_as_missing_image(self):
|
||||
self.assertNotEqual(0, self.run_script(FAKE_MANIFEST_ERROR='1').returncode)
|
||||
self.assertEqual([], self.calls('push'))
|
||||
def test_pull_failure_blocks_main_aliases(self):
|
||||
self.assertNotEqual(0, self.run_script(FAKE_PULL_FAILURE='backend:' + SHA).returncode)
|
||||
self.assertFalse(any(a[-1].endswith(':main') for a in self.calls('push')))
|
||||
def test_old_commit_does_not_move_main(self):
|
||||
result = self.run_script(FAKE_MAIN_HEAD='c' * 40)
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
self.assertEqual(2, len(self.calls('push')))
|
||||
self.assertTrue(all(a[-1].endswith(':' + SHA) for a in self.calls('push')))
|
||||
def test_architecture_mismatch_fails_before_publication(self):
|
||||
self.assertNotEqual(0, self.run_script(TARGET_PLATFORM='linux/arm64').returncode)
|
||||
self.assertEqual([], self.calls('push'))
|
||||
def test_failed_verification_removes_stale_success_marker_and_never_builds_images(self):
|
||||
for command, content in [('java', 'exit 0'), ('node', 'exit 0'), ('mvn', 'exit 42'),
|
||||
('git', 'echo ' + SHA)]:
|
||||
path = self.bin / command; path.write_text('#!/bin/sh\n' + content + '\n'); path.chmod(0o755)
|
||||
result = self.run_script('verify.sh')
|
||||
self.assertEqual(42, result.returncode, result.stdout + result.stderr)
|
||||
self.assertFalse((self.root / 'ci/artifacts/verified-commit').exists())
|
||||
self.assertEqual([], self.calls('build'))
|
||||
self.assertEqual([], self.calls('push'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
58
ci/verify.sh
Normal file
58
ci/verify.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname -- "$0")/common.sh"
|
||||
export CI=true
|
||||
# Start fresh: an earlier attempt's success marker must never authorize publishing.
|
||||
rm -f "$ARTIFACTS/verified-commit"
|
||||
exec > >(tee "$ARTIFACTS/verify.log") 2>&1
|
||||
finish() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
bash "$CI_ROOT/ci/cleanup.sh" || { (( status != 0 )) || status=1; }
|
||||
if [[ -n "${CI_DOCKER_CONFIG:-}" ]]; then rm -rf -- "$CI_DOCKER_CONFIG"; fi
|
||||
exit "$status"
|
||||
}
|
||||
trap finish EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
require_platform
|
||||
isolate_docker_config
|
||||
[[ "$(git rev-parse HEAD)" == "$CI_COMMIT" ]] || { echo 'Checkout does not match pushed commit'; exit 1; }
|
||||
java -version
|
||||
node --version
|
||||
mvn -B -ntp clean verify
|
||||
python3 -m unittest discover -s ci/tests -v
|
||||
(
|
||||
cd agiledancers-fe
|
||||
npm ci
|
||||
npm test -- --reporters=default --reporters=junit --output-file=unit-results/junit.xml
|
||||
npm run build
|
||||
)
|
||||
docker build --platform "$TARGET_PLATFORM" --target prebuilt \
|
||||
--label "org.opencontainers.image.revision=$CI_COMMIT" -t "$BACKEND_IMAGE" .
|
||||
docker build --platform "$TARGET_PLATFORM" --target prebuilt \
|
||||
--label "org.opencontainers.image.revision=$CI_COMMIT" -t "$FRONTEND_IMAGE" agiledancers-fe
|
||||
|
||||
touch "$ARTIFACTS/containers-started"
|
||||
docker network create --label "agiledancers.ci.run=$RUN_NAME" "$RUN_NAME"
|
||||
docker run -d --name "$RUN_NAME-backend" --label "agiledancers.ci.run=$RUN_NAME" \
|
||||
--platform "$TARGET_PLATFORM" --network "$RUN_NAME" --network-alias backend \
|
||||
--memory 1g --cpus 2 "$BACKEND_IMAGE"
|
||||
docker run -d --name "$RUN_NAME-frontend" --label "agiledancers.ci.run=$RUN_NAME" \
|
||||
--platform "$TARGET_PLATFORM" --network "$RUN_NAME" --network-alias frontend \
|
||||
--memory 256m --cpus 1 "$FRONTEND_IMAGE"
|
||||
# The browser, tests, readiness checks and API cleanup all use the frontend proxy.
|
||||
# Install this test container's browser dependencies for the exact lockfile version.
|
||||
docker run --name "$RUN_NAME-browser" --label "agiledancers.ci.run=$RUN_NAME" \
|
||||
--platform "$TARGET_PLATFORM" --network "$RUN_NAME" --memory 2g --cpus 2 --shm-size=1g \
|
||||
--user "$(id -u):$(id -g)" --env HOME=/tmp --env CI=true \
|
||||
--env PLAYWRIGHT_BASE_URL=http://frontend:8080 \
|
||||
--volume "$CI_ROOT/agiledancers-fe:/workspace" --workdir /workspace \
|
||||
mcr.microsoft.com/playwright:v1.64.1-noble bash -euc '
|
||||
for attempt in $(seq 1 60); do
|
||||
if curl --fail --silent http://frontend:8080/api/dance-schools >/dev/null; then break; fi
|
||||
if [ "$attempt" = 60 ]; then echo "Application readiness timed out" >&2; exit 1; fi
|
||||
sleep 2
|
||||
done
|
||||
npm run e2e
|
||||
'
|
||||
printf '%s\n' "$CI_COMMIT" > "$ARTIFACTS/verified-commit"
|
||||
236
docs/gitea-ci.md
Normal file
236
docs/gitea-ci.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Gitea build pipeline beheren
|
||||
|
||||
De workflow staat in `.gitea/workflows/build.yaml`. Elke branchpush bouwt en test
|
||||
backend en frontend, bouwt twee images en draait Playwright tegen die containers.
|
||||
Alleen `main` mag daarna images publiceren. Er vindt geen deployment plaats.
|
||||
Tagpushes en aparte pull-requestevents starten geen extra workflow.
|
||||
|
||||
## Machines en voorwaarden
|
||||
|
||||
Gitea 1.25.4 en de registry staan op de bestaande Synology,
|
||||
`http://192.168.2.28:3000`. De runner draait als gebruikersservice op de bestaande
|
||||
Linux x86_64 mini-pc. Er is geen extra hardware nodig. De mini-pc moet aanstaan en
|
||||
wakker blijven; Docker en de runner moeten draaien. Anders blijven jobs wachten.
|
||||
Een host-runner voert repositorycode uit als de ontwikkelgebruiker: gebruik deze
|
||||
runner alleen voor deze vertrouwde repository.
|
||||
|
||||
De runner heeft Git, Bash, JDK 25, Maven 3.9+, Node 22.22.3+/24.15.0+/26+, npm,
|
||||
Python 3, curl, flock (util-linux), timeout (coreutils) en een werkende Docker-daemon
|
||||
nodig. De huidige mini-pc heeft de ontwikkeltools en Docker Desktop al geïnstalleerd.
|
||||
De workflow gebruikt één job tegelijk (`capacity: 1`), één Playwright-worker en
|
||||
CPU/geheugenlimieten voor de tijdelijke applicatie- en browsercontainers.
|
||||
|
||||
### Doelarchitectuur eerst vaststellen
|
||||
|
||||
Controleer op de Synology via een bestaande beheerverbinding:
|
||||
|
||||
```sh
|
||||
uname -m
|
||||
docker info --format '{{.OSType}}/{{.Architecture}}'
|
||||
```
|
||||
|
||||
`x86_64`/`amd64` betekent `linux/amd64`; `aarch64`/`arm64` betekent `linux/arm64`.
|
||||
Leg de werkelijke uitvoer, datum en het Synology-model vast bij de oplevering en
|
||||
stel repositoryvariabele `TARGET_PLATFORM` daarop in. De workflow weigert een
|
||||
ontbrekende waarde en een verschil met de Docker-daemon van de runner; hij doet
|
||||
dus geen stilzwijgende aanname over de NAS en gebruikt geen ongeteste emulatie.
|
||||
Een mismatch vereist een geteste oplossing op de bestaande machines voordat S4
|
||||
afgerond kan worden; er worden geen multi-architecture images gebouwd.
|
||||
|
||||
### Docker op de mini-pc
|
||||
|
||||
Start de al aanwezige Docker Desktop via de desktop of:
|
||||
|
||||
```sh
|
||||
systemctl --user start docker-desktop
|
||||
docker --context desktop-linux info
|
||||
```
|
||||
|
||||
Bij Docker Desktop configureer je onder **Settings → Docker Engine** de inhoud
|
||||
van `ci/runner/docker-daemon.json`, samengevoegd met bestaande instellingen.
|
||||
Je kunt de instelling ook samenvoegen met behoud van bestaande instellingen en
|
||||
backup via:
|
||||
|
||||
```sh
|
||||
python3 ci/runner/configure-docker.py "$HOME/.docker/daemon.json"
|
||||
```
|
||||
|
||||
Voor Docker Engine op Linux is dat dezelfde toevoeging aan
|
||||
`/etc/docker/daemon.json` (met de daarvoor benodigde beheerrechten). Overschrijf geen andere daemoninstellingen. Pas de
|
||||
wijziging toe/herstart de daemon en controleer met `docker info` dat
|
||||
`192.168.2.28:3000` onder de insecure registries staat. Configureer ook andere
|
||||
Docker-clients die images van deze interne HTTP-registry moeten ophalen.
|
||||
|
||||
Gebruik de Docker Desktop-context of zet `DOCKER_HOST` expliciet. De installer
|
||||
neemt de geselecteerde socket over in de gebruikersservice. Als de socket later
|
||||
wijzigt, pas dan `~/.config/agiledancers-runner/environment` aan en herstart de
|
||||
runner. Bij Docker Desktop blijft de Desktop-VM eveneens nodig voor builds.
|
||||
Runner en containers moeten de Synology en internetdownloads kunnen bereiken.
|
||||
|
||||
## Gitea-instellingen en credentials
|
||||
|
||||
Repository: `wanjakrah/agiledancers`, hoofdbranch: `main`.
|
||||
Actions staat voor deze repository al aan. Controleer zo nodig op de Synology
|
||||
`app.ini` onder `[actions]` de instelling `ENABLED = true` en de Actions-feature
|
||||
onder repositoryinstellingen. Stel `SKIP_WORKFLOW_STRINGS` leeg in als ook commits met teksten als `[skip ci]`
|
||||
verplicht moeten draaien, zoals de afspraak elke branchpush vereist.
|
||||
Wijzig serverconfiguratie alleen indien nodig.
|
||||
|
||||
Stel bij repository **Settings → Actions** in:
|
||||
|
||||
| Soort | Naam | Waarde/doel |
|
||||
| --- | --- | --- |
|
||||
| Variabele | `TARGET_PLATFORM` | Vastgestelde Synology-platformwaarde |
|
||||
| Secret | `REGISTRY_USER` | Gitea-gebruiker die in namespace `wanjakrah` mag publiceren |
|
||||
| Secret | `REGISTRY_TOKEN` | PAT met `write:package` en `read:repository` voor imagepublicatie en controle van de huidige hoofdbranch |
|
||||
|
||||
Het gewone jobtoken van Gitea 1.25 biedt geen packagepublicatie; gebruik een PAT.
|
||||
Gebruik een repository-runnerregistratietoken, geen instancebrede registratie.
|
||||
Sla tokenwaarden buiten Git op en plaats ze niet in opdrachten, screenshots of logs.
|
||||
|
||||
Voor reproduceerbare configuratie bestaat `ci/configure-gitea.py`. Maak twee
|
||||
bestanden buiten de repository met mode `600`: één voor een PAT met voldoende
|
||||
repositorybeheerrechten (repository Actions, secrets, variabelen en runnerregistratie),
|
||||
en één voor de publicatie-PAT. De beheer-PAT hoeft niet als workflowsecret te worden
|
||||
bewaard. Vul die bestanden via een lokale editor of verborgen terminalinvoer.
|
||||
Voer daarna uit met de werkelijke platformwaarde:
|
||||
|
||||
```sh
|
||||
export GITEA_ADMIN_TOKEN_FILE="$HOME/.config/agiledancers-runner/admin-token"
|
||||
export REGISTRY_TOKEN_FILE="$HOME/.config/agiledancers-runner/registry-token"
|
||||
export REGISTRY_USER=wanjakrah
|
||||
export TARGET_PLATFORM=linux/amd64 # alleen als dit op de Synology is vastgesteld
|
||||
python3 ci/configure-gitea.py
|
||||
```
|
||||
|
||||
Het script stelt de repository-instellingen in via de API en schrijft het
|
||||
runnerregistratietoken naar
|
||||
`~/.config/agiledancers-runner/registration-token` met mode `600`. Het toont geen
|
||||
tokenwaarden. Voor handmatige inrichting kun je dat registratietoken uit de
|
||||
repository-instellingen halen en in hetzelfde bestand plaatsen.
|
||||
|
||||
Controleer de artifactretentie op de server: een workflow vraagt veertien dagen
|
||||
via `retention-days: 14`. Een kortere serverlimiet of het vroegtijdig verwijderen
|
||||
van workflowruns mag die periode niet inkorten. Artifactretentie geldt voor
|
||||
testdiagnostiek; gepubliceerde containerimages worden niet automatisch verwijderd.
|
||||
|
||||
## Runner installeren en starten
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64
|
||||
bash ci/runner/install.sh --prepare-only
|
||||
# Na Dockerconfiguratie en het opslaan van het registratietoken:
|
||||
bash ci/runner/install.sh
|
||||
systemctl --user status agiledancers-runner
|
||||
journalctl --user -u agiledancers-runner -f
|
||||
```
|
||||
|
||||
De installer controleert tools, installeert checksum-gecontroleerde runner
|
||||
`v0.2.13`, neemt label `agiledancers-mini-pc:host` over en registreert de runner.
|
||||
Configuratie en `.runner`-credentials staan in
|
||||
`~/.local/share/agiledancers-runner`, de service in
|
||||
`~/.config/systemd/user/agiledancers-runner.service` en de expliciete Java-, PATH-
|
||||
en Docker-socketinstellingen in `~/.config/agiledancers-runner/environment`.
|
||||
Een bestaande registratie wordt hergebruikt. Er worden geen secrets in de
|
||||
repository geschreven. Bewaar het tokenbestand alleen zolang registratie/herstel
|
||||
dat nodig heeft en behandel `.runner` als credential.
|
||||
|
||||
De gebruikersservice start bij een gebruikerssessie. Automatisch doorlopen zonder
|
||||
inloggen vereist daarnaast een bewuste keuze voor user lingering en een Docker-
|
||||
daemon die zonder desktopsessie beschikbaar is. S4 belooft geen beschikbaarheid
|
||||
wanneer de mini-pc uitstaat. Herstarten/stoppen:
|
||||
|
||||
```sh
|
||||
systemctl --user restart agiledancers-runner
|
||||
systemctl --user stop agiledancers-runner
|
||||
```
|
||||
|
||||
## Images en publicatie
|
||||
|
||||
| Component | Image |
|
||||
| --- | --- |
|
||||
| Backend | `192.168.2.28:3000/wanjakrah/agiledancers-backend` |
|
||||
| Frontend | `192.168.2.28:3000/wanjakrah/agiledancers-frontend` |
|
||||
|
||||
De volledige commit-SHA is de versie van beide images. Een `main`-tag wijst naar
|
||||
de nieuwste met succes gepubliceerde actuele hoofdbranchbuild. Gebruik voor een
|
||||
exact passend paar op beide images dezelfde SHA:
|
||||
|
||||
```sh
|
||||
# Gebruik bij private packages eerst docker login met interactieve invoer.
|
||||
commit_sha=<volledige-commit-SHA>
|
||||
docker pull "192.168.2.28:3000/wanjakrah/agiledancers-backend:$commit_sha"
|
||||
docker pull "192.168.2.28:3000/wanjakrah/agiledancers-frontend:$commit_sha"
|
||||
```
|
||||
|
||||
De frontend luistert op containerpoort 8080, ondersteunt SPA-routes en proxy't
|
||||
`/api/` naar `BACKEND_UPSTREAM` (standaard `backend:8080`). De backend luistert op
|
||||
8080. Voorbeelden voor eventueel handmatig starten staan los van de pipeline;
|
||||
de pipeline start geen blijvende applicatie op de NAS.
|
||||
|
||||
`ci/verify.sh` maakt een succesmarker pas na alle builds en tests.
|
||||
`ci/publish.sh` vereist die marker en hoofdbranchref. Onder een gedeeld `flock`
|
||||
buiten de checkout worden bestaande commitimages hergebruikt en ontbrekende
|
||||
images geüpload. Iedere image wordt teruggehaald, op revisielabel gecontroleerd en
|
||||
met digest vastgelegd in de diagnostiek. Authenticatie- of netwerkfouten tellen
|
||||
niet als een ontbrekende tag.
|
||||
|
||||
Pas na beide images worden `main`-tags bijgewerkt, met een actuele hoofdbranch-
|
||||
controle voor iedere wijziging. De gedeelde lock voorkomt dat een oudere pipeline
|
||||
een door een nieuwere pipeline bijgewerkte tag terugzet. `capacity: 1` ondersteunt
|
||||
dat op de enige runner. Vertrouw niet op workflow-`concurrency`: Gitea 1.25 negeert
|
||||
die instelling. De twee bewegende tags zijn geen atomisch paar.
|
||||
|
||||
## Diagnostiek en herstellen
|
||||
|
||||
Iedere workflowpoging bewaart beschikbare diagnostiek als
|
||||
`diagnostics-<run-id>-<attempt>` gedurende veertien dagen: Maven-rapporten, JUnit-
|
||||
rapporten van frontend/Playwright, het Playwright HTML-rapport, traces/screenshots
|
||||
en containerlogs. Open de artifactdownload op de Gitea Actions-runpagina.
|
||||
Build- en publicatielogs staan er eveneens in. Geen environmentdump of Docker
|
||||
inspect met mogelijke credentials wordt opgenomen.
|
||||
|
||||
Bij mislukking blijven de oorspronkelijke exitstatus en rapporten behouden.
|
||||
Een trap én een `always()`-stap verzamelen diagnostiek en verwijderen de tijdelijke
|
||||
containers en het netwerk met hun unieke runnaam. Een harde machine-uitval kan
|
||||
opruimen onderbreken: controleer daarna uitsluitend achtergebleven resources met
|
||||
label `agiledancers.ci.run`; gebruik geen algemene Docker-prune op de ontwikkel-PC.
|
||||
|
||||
Herstart een mislukte run vanuit Gitea. Als slechts één commitimage gepubliceerd
|
||||
was, bewaart de herhaling die en vult de ontbrekende image aan. Een fout bij
|
||||
publiceren, terughalen of bijwerken van `main` laat de pipeline falen. Bestaande
|
||||
commitimages worden niet overschreven. Bij een inmiddels nieuwere hoofdbranch
|
||||
blijven de committags bruikbaar en worden de oude `main`-updates overgeslagen.
|
||||
|
||||
Lokale controle van publicatie-/foutgedrag zonder registrywijzigingen:
|
||||
|
||||
```sh
|
||||
python3 -m unittest discover -s ci/tests -v
|
||||
```
|
||||
|
||||
Volledige lokale verificatie met Docker en de vastgestelde platformwaarde:
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64
|
||||
export TARGET_PLATFORM=linux/amd64 # eerst de Synology controleren
|
||||
bash ci/verify.sh
|
||||
```
|
||||
|
||||
## Operationele oplevering
|
||||
|
||||
Leg links/run-ID's en beide image-digests vast in `docs/s4-validation.md` voor:
|
||||
|
||||
1. Een geslaagde feature-branchpush, inclusief twee builds en container-E2E,
|
||||
zonder imagepublicatie.
|
||||
2. Een expres mislukte build/test op een tijdelijke branch die publicatie blokkeert,
|
||||
met downloadbare foutdiagnostiek. Herstel die testwijziging daarna.
|
||||
3. Een geslaagde hoofdbranchrun met beide teruggehaalde commitimages.
|
||||
4. Een herhaling/partiële-publicatieherstel en bewijs dat bestaande committags
|
||||
behouden blijven en oudere builds `main` niet terugzetten.
|
||||
|
||||
Zonder werkende runner, vastgestelde NAS-architectuur, registrytoegang en echte
|
||||
pipelinebewijzen is S4 niet volledig opgeleverd.
|
||||
|
||||
Bronnen: [runner](https://docs.gitea.com/1.25/usage/actions/act-runner/),
|
||||
[Gitea-verschillen](https://docs.gitea.com/1.25/usage/actions/comparison/),
|
||||
[containerregistry](https://docs.gitea.com/1.25/usage/packages/container/).
|
||||
75
docs/s4-validation.md
Normal file
75
docs/s4-validation.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# S4 verification evidence
|
||||
|
||||
Date: 2026-10-28. Status: repository implementation and local container verification
|
||||
passed; operational Gitea acceptance remains pending. S4 is **not complete**.
|
||||
|
||||
## Verified locally
|
||||
|
||||
- Gitea reports version 1.25.4; repository `wanjakrah/agiledancers` has Actions
|
||||
enabled and uses `main` as its default branch.
|
||||
- Docker Desktop was started on the existing Linux x86_64 mini-PC. Its Docker
|
||||
daemon reports `linux/x86_64` (`linux/amd64`). This is not evidence of the
|
||||
Synology's architecture.
|
||||
- `mvn clean verify` passed, including all 20 backend tests and architecture tests.
|
||||
- `npm ci`, all 14 frontend unit tests with JUnit output, and the production build
|
||||
passed in the full verification script.
|
||||
- Both runtime images built successfully with the `prebuilt` targets. The images
|
||||
package the same outputs that passed verification.
|
||||
- Both existing Playwright tests passed (2/2) using the built frontend and backend
|
||||
containers, the frontend `/api/` proxy and Chromium in the version-matched
|
||||
Playwright container. No Angular development server was used.
|
||||
- The full `ci/verify.sh` execution exited successfully; its trap removed all three
|
||||
temporary test containers and the run network.
|
||||
- Eleven isolated CI tests pass: failed verification removes its success marker,
|
||||
feature branches do not publish, unverified commits cannot publish, reruns
|
||||
preserve commit tags, partial publication is recoverable, authentication and
|
||||
pull failures block publication, older commits do not move `main`, platform
|
||||
mismatches fail, and daemon configuration preserves existing settings.
|
||||
- Shell syntax, Python compilation, workflow YAML parsing and the changed files'
|
||||
whitespace checks passed.
|
||||
- Runner v0.2.13 was downloaded from the official source, SHA-256 checked, and
|
||||
installed with its user-service configuration using `--prepare-only`.
|
||||
The runner was subsequently registered and its user service started using the
|
||||
supplied repository administration credentials.
|
||||
- The internal registry was merged into Docker Desktop's daemon configuration,
|
||||
preserving existing settings and a backup. Docker Desktop was restarted and
|
||||
the running daemon confirmed that `192.168.2.28:3000` is an active HTTP registry.
|
||||
|
||||
Local outputs are in `ci/artifacts/` (ignored by Git): verify log, backend test
|
||||
reports, frontend unit JUnit report, Playwright JUnit/HTML report and container
|
||||
logs. These are local files, not yet artifacts uploaded by a real Gitea run.
|
||||
The unit JUnit report uses a separate directory so Playwright output cleanup
|
||||
cannot delete it.
|
||||
|
||||
The local test used working-tree changes on base commit
|
||||
`e3e362c2ac30d6b365f752f3da6c8b0250c019db`, with run ID `s4local`. These local
|
||||
images are test outputs, not published release images for that commit.
|
||||
|
||||
Local image IDs (not registry digests):
|
||||
|
||||
- Backend: `sha256:bd77a46e3dbdcaea9662d21c608098999fe17dfdf241cd28a24a935f47401f61`.
|
||||
- Frontend: `sha256:ce8e6ab21fb8f9cbee280ac2c4d7b4ca24a30e899a457382f1e45c6baf99dfb6`.
|
||||
|
||||
## Remaining operational acceptance
|
||||
|
||||
- [x] Synology architecture confirmed by the maintainer: `x86_64`,
|
||||
`linux/amd64`; repository variable `TARGET_PLATFORM=linux/amd64` configured.
|
||||
- [x] Activate and verify the Docker Desktop HTTP-registry setting.
|
||||
- [x] Repository `REGISTRY_USER`/`REGISTRY_TOKEN` secrets configured using the
|
||||
supplied token files. Registry access is verified by actual publication below.
|
||||
- [ ] Obtain a repository runner registration token and register/start the
|
||||
installed mini-PC runner; verify it is online in Gitea.
|
||||
- [ ] Confirm server settings permit every branchpush (including messages with
|
||||
skip strings) and retain uploaded diagnostic artifacts for fourteen days.
|
||||
- [ ] Real feature-branch run: record URL/run ID, successful container-E2E and
|
||||
absence of publication.
|
||||
- [ ] Real intentionally failing test/build run: record URL/run ID, blocked
|
||||
publication and downloadable failure artifacts.
|
||||
- [ ] Real main run: record URL/run ID, both full-SHA images, successful pullback,
|
||||
registry digests and the two `main` aliases.
|
||||
- [ ] Real rerun/partial-publication recovery and stale-main evidence: record
|
||||
run IDs and unchanged existing commit digests.
|
||||
|
||||
The isolated publication tests provide automated control-flow coverage; they do
|
||||
not substitute for real authenticated registry/Gitea acceptance runs. No application
|
||||
images have been published and no application has been deployed by this work.
|
||||
181
specs/in_progress/S4_create-build-pipeline-at-gitea
Normal file
181
specs/in_progress/S4_create-build-pipeline-at-gitea
Normal file
@@ -0,0 +1,181 @@
|
||||
# S4 — Build and publish pipelines in Gitea
|
||||
|
||||
Status: IN_PROGRESS
|
||||
|
||||
Agreed after the grill-me review; shared understanding confirmed.
|
||||
|
||||
## User story
|
||||
|
||||
As a maintainer, I want every branch push to build and test the application,
|
||||
so that broken changes are detected and successful main-branch builds provide
|
||||
identifiable Docker images in our own registry.
|
||||
|
||||
## Agreed environment and feasibility
|
||||
|
||||
- Gitea and its container registry run on the existing Synology.
|
||||
- The existing development mini-PC runs the Gitea Actions runner, separately from
|
||||
Gitea. No additional hardware is available or required.
|
||||
- Gitea supports push-triggered Actions workflows and a runner on a separate
|
||||
machine. Codex can implement the repository configuration and assist with the
|
||||
installation and configuration required on the existing machines.
|
||||
- Gitea was inspected as version 1.25.4. Match the runner and workflow setup to
|
||||
the installed version rather than assuming newer documentation applies.
|
||||
- The mini-PC was inspected as Linux x86_64 with a Docker client installed, but
|
||||
without a reachable Docker daemon. Configuring a working daemon is part of S4.
|
||||
- The current internal Gitea/registry endpoint is `http://192.168.2.28:3000`.
|
||||
Use that endpoint over the trusted internal network and explicitly configure
|
||||
Docker clients that need registry access to trust this particular HTTP registry.
|
||||
Introducing HTTPS is outside S4.
|
||||
- The runner and job containers need access to Gitea, the registry and dependency
|
||||
download sources. The runner connects to Gitea; it needs no public incoming port.
|
||||
- The mini-PC must be on, awake and running the runner. Jobs wait while it is
|
||||
unavailable; S4 does not promise unattended availability when the PC is off.
|
||||
|
||||
References:
|
||||
|
||||
- [Gitea 1.25 Actions quickstart](https://docs.gitea.com/1.25/usage/actions/quickstart/)
|
||||
- [Gitea 1.25 Actions design](https://docs.gitea.com/1.25/usage/actions/design/)
|
||||
- [Gitea container registry](https://docs.gitea.com/usage/packages/container/)
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Version-controlled Gitea Actions workflows and supporting build configuration.
|
||||
- Enable Actions as needed at instance and repository level, configure Docker on
|
||||
the mini-PC, and install, register and configure its runner.
|
||||
- Build and test both backend and frontend for every push to every branch,
|
||||
including newly created branches. Separate tag and pull-request event workflows
|
||||
are not required; branch pushes also check changes used in pull requests.
|
||||
- Build two runnable Docker images: backend and frontend.
|
||||
- Run end-to-end tests against those built containers before publication.
|
||||
- Publish both images only from successful builds of the `main` branch to the
|
||||
container registry on the Synology.
|
||||
- Configure registry authentication and the necessary secrets outside Git.
|
||||
- Preserve failure diagnostics and document operation and recovery.
|
||||
- Demonstrate a real successful pipeline and images that can be pulled back.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Automatic deployment, starting application containers on the Synology, and OTAP.
|
||||
Starting temporary containers for pipeline tests is in scope.
|
||||
- New linting or static-analysis gates, such as ESLint or Sonar. Existing tests,
|
||||
including architecture tests, remain mandatory.
|
||||
- Merge or release policies, separate tag/release workflows, and automatic image
|
||||
deletion or retention cleanup. Published images remain available for now.
|
||||
- Multi-architecture images, HTTPS setup, new hardware or provisioning additional
|
||||
infrastructure.
|
||||
|
||||
## Build and verification behavior
|
||||
|
||||
1. Every branch push runs backend and frontend builds and their existing tests.
|
||||
2. Backend verification includes the existing unit, integration/system and
|
||||
architecture tests. Current entry point: `mvn clean verify` using Java 25.
|
||||
3. Frontend verification uses the lockfile (`npm ci`), unit tests (`npm test`) and
|
||||
production build (`npm run build`) with a supported Node version.
|
||||
4. Build both Docker images on every branch, including branches that cannot
|
||||
publish. The current Dockerfile only covers the backend; frontend packaging
|
||||
must be added as part of implementation.
|
||||
5. The frontend image serves the production Angular build, supports direct SPA
|
||||
route navigation, and forwards `/api` requests to the backend container.
|
||||
Container addressing/configuration is an implementation choice.
|
||||
6. Run the existing Playwright end-to-end suite with Chromium against the built
|
||||
frontend/backend containers, through the frontend's API forwarding. Adapt the
|
||||
existing development-server test configuration as necessary: the acceptance
|
||||
run must exercise the actual images, not development servers.
|
||||
7. Determine the intended server's CPU architecture during implementation. Build
|
||||
for that single target architecture and run the end-to-end tests on that same
|
||||
architecture. Do not infer the Synology's architecture from the mini-PC.
|
||||
Any architecture mismatch must be resolved and tested on the existing hardware
|
||||
before claiming acceptance.
|
||||
8. A failed build or test fails the pipeline and prevents publication. Test
|
||||
containers are temporary and are cleaned up after the run, including failure.
|
||||
|
||||
## Publication and image identity
|
||||
|
||||
- Publish separate backend and frontend images under the repository owner's
|
||||
namespace in the existing Gitea container registry. Document the exact names.
|
||||
- Feature branches must not publish application images.
|
||||
- Both images from a main-branch commit use the full Git commit SHA as their tag.
|
||||
This identifies the matching pair independently of moving tags.
|
||||
- Once both commit images have been successfully published, reruns verify/reuse
|
||||
them without overwriting either commit tag.
|
||||
- Publication succeeds only when both images are published and can be pulled
|
||||
back from the registry. A partial publication fails the pipeline; one image may
|
||||
already exist. A rerun preserves that image and may publish the missing image.
|
||||
- After both commit images are available, update the moving `main` tags only if
|
||||
the commit is still the current main-branch head. Serialize/coordinate that
|
||||
operation so an older, slower pipeline cannot move `main` backwards.
|
||||
- The two moving tags are not an atomic pair. Use the same commit SHA on both
|
||||
images when an exact matching pair is needed. A failure updating a moving tag
|
||||
must be visible as a pipeline failure and recoverable through a rerun.
|
||||
- No automatic deletion of published images is introduced in S4.
|
||||
|
||||
## Failure diagnostics and operation
|
||||
|
||||
- Failed pipelines retain available test reports, container logs, and Playwright
|
||||
traces and screenshots for fourteen days. Collect available diagnostics even
|
||||
when a preceding step fails; do not hide the original failure.
|
||||
- Provide a short management guide covering:
|
||||
- Docker/runner installation, registration, startup and required access.
|
||||
- Gitea instance/repository Actions settings and registry configuration.
|
||||
- Required credentials, permissions and secret names, without secret values.
|
||||
- Which settings exist outside Git and how to reproduce them.
|
||||
- Trigger behavior, mini-PC availability and queued jobs.
|
||||
- Image names, tags and commands to pull a matching image pair.
|
||||
- Finding diagnostics, rerunning failed jobs and completing partial publication.
|
||||
- The target architecture and how it was established.
|
||||
- Keep registry credentials and registration tokens out of repository files and
|
||||
logs; use the relevant secret settings.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A push to any branch, including a new branch, starts a pipeline.
|
||||
- [ ] Backend and frontend build and all existing tests, including architecture
|
||||
and Playwright end-to-end tests, pass before publication is permitted.
|
||||
- [ ] Every branch builds both Docker images; end-to-end tests exercise those
|
||||
images through the frontend and its `/api` forwarding.
|
||||
- [ ] A failing build or test fails the pipeline without publishing images.
|
||||
- [ ] A successful feature-branch pipeline publishes no application images.
|
||||
- [ ] A successful `main` pipeline publishes both images to the Synology registry
|
||||
with the full commit SHA; both images can be pulled back.
|
||||
- [ ] Published commit images are preserved on rerun; partial publication can be
|
||||
completed without overwriting an existing commit image.
|
||||
- [ ] Moving `main` tags are updated only after both commit images are available,
|
||||
and an older pipeline cannot move them backwards.
|
||||
- [ ] Publication or pull-verification failure makes the pipeline fail, even if
|
||||
some images have already been uploaded.
|
||||
- [ ] Available failure diagnostics are retained for fourteen days, and temporary
|
||||
test containers are cleaned up on success and failure.
|
||||
- [ ] The runner operates on the existing mini-PC, with Gitea and the registry on
|
||||
the existing Synology; no additional hardware is needed.
|
||||
- [ ] The single target architecture is established and the images are tested on
|
||||
that architecture using the existing hardware.
|
||||
- [ ] A management guide records setup, external settings, secrets, image usage
|
||||
and recovery, including the need to keep the mini-PC/runner available.
|
||||
- [ ] Actual pipeline runs demonstrate successful main-branch publication,
|
||||
feature-branch checks without publication, and failure blocking publication.
|
||||
|
||||
## Implementation handoff
|
||||
|
||||
This specification captures the agreed design. It does not install the runner,
|
||||
change server settings or implement the pipeline. S4 is complete only when the
|
||||
configuration, documentation and operational acceptance evidence are delivered;
|
||||
workflow files alone are insufficient.
|
||||
|
||||
|
||||
## Implementation progress
|
||||
|
||||
Repository implementation and local Docker verification are available. See
|
||||
[management instructions](../../docs/gitea-ci.md) and
|
||||
[validation evidence](../../docs/s4-validation.md).
|
||||
|
||||
Backend verification (20 tests), frontend verification (14 tests), both runtime
|
||||
image builds and both Playwright tests against the containers passed. Eleven
|
||||
isolated CI publication/configuration tests pass. The runner binary and user-service
|
||||
configuration are installed on the mini-PC.
|
||||
|
||||
Operational acceptance is still pending: authenticated Synology architecture
|
||||
verification, Gitea credentials/secrets, runner registration/startup and real
|
||||
feature/failure/main pipeline runs with registry pullback evidence. The spec stays
|
||||
IN_PROGRESS until those requirements are met.
|
||||
Reference in New Issue
Block a user