Compare commits

..

13 commits

Author SHA1 Message Date
eding 478b2c8c73 Merge pull request 'fix: use Tekton compute resources' (#6) from fix/tekton-compute-resources into main
Reviewed-on: #6
2026-09-16 09:05:51 +02:00
eding db9423f180 fix: use Tekton compute resources 2026-09-16 08:33:13 +02:00
eding 0a765489ac Merge pull request 'feat: align catalog runtime builds' (#5) from feat/align-runtime-catalog into main
Reviewed-on: #5
2026-09-15 20:44:37 +02:00
eding 72700bdb84 feat: align catalog runtime builds 2026-09-15 20:12:55 +02:00
eding 16cf6f66f3 Merge pull request 'fix: bound static build memory' (#3) from fix/static-build-resource-contract into main
Reviewed-on: #3
2026-09-12 23:36:23 +02:00
eding 2e7854f629 fix: bound static build memory 2026-09-12 19:52:36 +02:00
eding fc588d26f8 Merge pull request 'fix: build static sites outside shared workspace' (#2) from fix/local-build-workspace into main
Reviewed-on: #2
2026-09-08 23:45:51 +02:00
eding 4ce86da69f fix: build static sites outside shared workspace 2026-09-08 23:43:39 +02:00
eding 7bfea96fd6 Merge pull request 'feat: isolate delivery task credentials' (#1) from feat/split-delivery-credentials into main 2026-09-08 15:40:46 +02:00
Maidn 484744b07f feat: isolate delivery task credentials 2026-09-08 15:39:31 +02:00
Edin 94cfa67d33
Merge pull request #2 from Pingu-Studio/feat/preview-orphan-reconciler
feat: add preview orphan reconciler
2026-08-22 14:06:27 +02:00
eding b431f5cadb feat: add preview orphan reconciler 2026-08-22 13:58:28 +02:00
Edin 3386982cfc
Merge pull request #1 from Pingu-Studio/feat/tekton-task-catalog
feat: add node static image Task catalog
2026-07-30 00:48:41 +02:00
7 changed files with 679 additions and 50 deletions

View file

@ -2,3 +2,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- maidn-node-static-image.yaml - maidn-node-static-image.yaml
- maidn-node-runtime-image.yaml
- maidn-preview-orphan-reconciler.yaml

View file

@ -0,0 +1,136 @@
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: maidn-node-runtime-image
namespace: tekton-pipelines
spec:
params:
- name: url
type: string
- name: revision
type: string
- name: image
type: string
volumes:
- name: work
emptyDir: {}
- name: git-credentials
secret:
secretName: forgejo-git-credentials
- name: registry-credentials
secret:
secretName: forgejo-registry-credentials
items:
- key: .dockerconfigjson
path: config.json
stepTemplate:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
steps:
- name: clone
image: alpine/git:2.47.2
env:
- name: REPOSITORY_URL
value: $(params.url)
- name: REVISION
value: $(params.revision)
- name: IMAGE
value: $(params.image)
volumeMounts:
- name: work
mountPath: /work
- name: git-credentials
mountPath: /credentials
readOnly: true
script: |
#!/bin/sh
set -eu
case "$REPOSITORY_URL" in https://git.pingu.pw/*) ;; *) exit 1 ;; esac
repository_path=${REPOSITORY_URL#https://git.pingu.pw/}
case "$repository_path" in [A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*.git) ;; *) exit 1 ;; esac
case "$repository_path" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/|*/*/*) exit 1 ;; esac
case "$REVISION" in [A-Za-z0-9]*) ;; *) exit 1 ;; esac
case "$REVISION" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac
case "$IMAGE" in git.pingu.pw/*) ;; *) exit 1 ;; esac
image_path=${IMAGE#git.pingu.pw/}
case "$image_path" in [A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*) ;; *) exit 1 ;; esac
case "$image_path" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/|*/*/*) exit 1 ;; esac
cat >/work/askpass <<'EOF'
#!/bin/sh
case "$1" in *Username*) cat /credentials/username ;; *) cat /credentials/password ;; esac
EOF
chmod 0700 /work/askpass
GIT_ASKPASS=/work/askpass GIT_TERMINAL_PROMPT=0 git clone "$REPOSITORY_URL" /work/source
git -C /work/source checkout "$REVISION"
rm -f /work/askpass
- name: build-layer
image: node:22-alpine
computeResources:
requests:
memory: 256Mi
limits:
memory: 512Mi
volumeMounts:
- name: work
mountPath: /work
script: |
#!/bin/sh
set -eu
cd /work/source
npm install --ignore-scripts --no-audit --no-fund --package-lock=false
npm run build
npm prune --omit=dev --ignore-scripts --no-audit --no-fund
test -f package.json
test -d node_modules
test -d dist
mkdir -p /work/layer/app
cp package.json /work/layer/app/
cp -R node_modules dist /work/layer/app/
tar -C /work/layer -cf /work/layer.tar app
rm -rf /work/source /work/layer
- name: append
image: gcr.io/go-containerregistry/crane:v0.21.7
env:
- name: HOME
value: /tekton/home
- name: DOCKER_CONFIG
value: /tekton/home/.docker
args:
- append
- --base=node:22-alpine
- --new_layer=/work/layer.tar
- --new_tag=$(params.image):$(params.revision)
volumeMounts:
- name: work
mountPath: /work
- name: registry-credentials
mountPath: /tekton/home/.docker
readOnly: true
- name: mutate
image: gcr.io/go-containerregistry/crane:v0.21.7
env:
- name: HOME
value: /tekton/home
- name: DOCKER_CONFIG
value: /tekton/home/.docker
args:
- mutate
- $(params.image):$(params.revision)
- --entrypoint=node
- --cmd=dist/index.js
- --workdir=/app
- --user=node
- --env=PORT=8080
- --exposed-ports=8080/tcp
- --tag=$(params.image):$(params.revision)
volumeMounts:
- name: registry-credentials
mountPath: /tekton/home/.docker
readOnly: true

View file

@ -4,20 +4,6 @@ metadata:
name: maidn-node-static-image name: maidn-node-static-image
namespace: tekton-pipelines namespace: tekton-pipelines
spec: spec:
stepTemplate:
env:
- name: HOME
value: /tekton/home
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
params: params:
- name: url - name: url
type: string type: string
@ -33,18 +19,28 @@ spec:
- name: base-image - name: base-image
type: string type: string
default: nginx:1.27-alpine default: nginx:1.27-alpine
- name: target-directory
type: string
default: /www/target
workspaces:
- name: source
volumes: volumes:
- name: work
emptyDir: {}
- name: git-credentials
secret:
secretName: forgejo-git-credentials
- name: registry-credentials - name: registry-credentials
secret: secret:
secretName: forgejo-registry-credentials secretName: forgejo-registry-credentials
items: items:
- key: .dockerconfigjson - key: .dockerconfigjson
path: config.json path: config.json
stepTemplate:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
steps: steps:
- name: clone - name: clone
image: alpine/git:2.47.2 image: alpine/git:2.47.2
@ -53,62 +49,71 @@ spec:
value: $(params.url) value: $(params.url)
- name: REVISION - name: REVISION
value: $(params.revision) value: $(params.revision)
- name: SOURCE_PATH volumeMounts:
value: $(workspaces.source.path) - name: work
mountPath: /work
- name: git-credentials
mountPath: /credentials
readOnly: true
script: | script: |
#!/bin/sh #!/bin/sh
set -eu set -eu
case "$REPOSITORY_URL" in https://git.pingu.pw/*) ;; *) exit 1 ;; esac case "$REPOSITORY_URL" in https://git.pingu.pw/*.git) ;; *) exit 1 ;; esac
repository_path=${REPOSITORY_URL#https://git.pingu.pw/}
case "$repository_path" in *.git) ;; *) exit 1 ;; esac
case "$repository_path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*) exit 1 ;; esac
case "$REVISION" in [A-Za-z0-9]*) ;; *) exit 1 ;; esac case "$REVISION" in [A-Za-z0-9]*) ;; *) exit 1 ;; esac
case "$REVISION" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac case "$REVISION" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac
git clone "$REPOSITORY_URL" "$SOURCE_PATH" cat >/work/askpass <<'EOF'
git config --global --add safe.directory "$SOURCE_PATH" #!/bin/sh
git -C "$SOURCE_PATH" checkout "$REVISION" case "$1" in *Username*) cat /credentials/username ;; *) cat /credentials/password ;; esac
EOF
chmod 0700 /work/askpass
GIT_ASKPASS=/work/askpass GIT_TERMINAL_PROMPT=0 git clone "$REPOSITORY_URL" /work/source
git -C /work/source checkout "$REVISION"
rm -f /work/askpass
- name: build-layer - name: build-layer
image: node:22-alpine image: node:22-alpine
workingDir: $(workspaces.source.path) computeResources:
requests:
memory: 256Mi
limits:
memory: 512Mi
env: env:
- name: IMAGE - name: NODE_OPTIONS
value: $(params.image) value: --max-old-space-size=384
- name: REVISION
value: $(params.revision)
- name: OUTPUT_DIRECTORY - name: OUTPUT_DIRECTORY
value: $(params.output-directory) value: $(params.output-directory)
- name: TARGET_DIRECTORY
value: $(params.target-directory)
- name: BUILD_CONFIGURATION - name: BUILD_CONFIGURATION
value: $(params.build-configuration) value: $(params.build-configuration)
volumeMounts:
- name: work
mountPath: /work
script: | script: |
#!/bin/sh #!/bin/sh
set -eu set -eu
case "$IMAGE" in git.pingu.pw/*) ;; *) exit 1 ;; esac
image_path=${IMAGE#git.pingu.pw/}
case "$image_path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*|*/) exit 1 ;; esac
case "$REVISION" in [A-Za-z0-9]*) ;; *) exit 1 ;; esac
case "$REVISION" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac
case "$OUTPUT_DIRECTORY" in ''|/*|-*|*[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac case "$OUTPUT_DIRECTORY" in ''|/*|-*|*[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac
case "$TARGET_DIRECTORY" in /*) ;; *) exit 1 ;; esac case "$BUILD_CONFIGURATION" in ''|-*|*[!A-Za-z0-9._-]*) exit 1 ;; esac
target_path=${TARGET_DIRECTORY#/} cd /work/source
case "$target_path" in ''|-*|*[!A-Za-z0-9._/-]*|*..*|*//*|*/) exit 1 ;; esac
# ponytail: no lockfile; skip install scripts and use npm ci when package-lock.json is committed.
npm install --ignore-scripts --no-audit --no-fund --package-lock=false npm install --ignore-scripts --no-audit --no-fund --package-lock=false
npm run build -- --configuration "$BUILD_CONFIGURATION" npm run build -- --configuration "$BUILD_CONFIGURATION"
test -d "$OUTPUT_DIRECTORY" test -d "$OUTPUT_DIRECTORY"
layer_dir=$(mktemp -d) mkdir -p /work/layer/www
trap 'rm -rf "$layer_dir"' EXIT cp -R "$OUTPUT_DIRECTORY"/. /work/layer/www/
mkdir -p "$layer_dir/$target_path" tar -C /work/layer -cf /work/layer.tar www
cp -R "$OUTPUT_DIRECTORY"/. "$layer_dir/$target_path/" rm -rf /work/source /work/layer
tar -C "$layer_dir" -cf layer.tar "$target_path"
- name: push - name: push
image: gcr.io/go-containerregistry/crane:v0.21.7 image: gcr.io/go-containerregistry/crane:v0.21.7
env:
- name: HOME
value: /tekton/home
- name: DOCKER_CONFIG
value: /tekton/home/.docker
args: args:
- append - append
- --base=$(params.base-image) - --base=$(params.base-image)
- --new_layer=$(workspaces.source.path)/layer.tar - --new_layer=/work/layer.tar
- --new_tag=$(params.image):$(params.revision) - --new_tag=$(params.image):$(params.revision)
volumeMounts: volumeMounts:
- name: work
mountPath: /work
- name: registry-credentials - name: registry-credentials
mountPath: /tekton/home/.docker mountPath: /tekton/home/.docker
readOnly: true

View file

@ -0,0 +1,276 @@
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: maidn-preview-orphan-reconciler
namespace: tekton-pipelines
annotations:
# The environment GitOps repo owns this ConfigMap; render it before enabling this Task.
maidn.io/delivery-config: maidn-preview-delivery-config
# The TaskRun service account supplies this annotated Git Secret through Tekton's initializer.
maidn.io/git-credentials: forgejo-git-credentials
spec:
stepTemplate:
env:
- name: HOME
value: /tekton/home
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
params:
- name: app-name
type: string
- name: app-repository
type: string
- name: pr-number
type: string
steps:
- name: verify-closed-pr
image: python:3.13-alpine3.21
env:
- name: TRUSTED_FORGEJO_ORIGIN
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: forgejo-origin
- name: TRUSTED_MANIFESTS_URL
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-url
- name: TRUSTED_MANIFESTS_BRANCH
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-branch
- name: APP_NAME
value: $(params.app-name)
- name: APP_REPOSITORY
value: $(params.app-repository)
- name: PR_NUMBER
value: $(params.pr-number)
script: |
#!/bin/sh
set -eu
fail() { exit 1; }
valid_forgejo_origin() {
case "$1" in https://*) ;; *) fail ;; esac
host=${1#https://}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
}
valid_git_url() {
case "$1" in https://*/*.git) ;; *) fail ;; esac
repository=${1#https://}
host=${repository%%/*}
path=${repository#*/}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
case "$path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*) fail ;; esac
[ "https://$host" = "$TRUSTED_FORGEJO_ORIGIN" ] || fail
}
valid_branch() {
case "$1" in [A-Za-z0-9]*) ;; *) fail ;; esac
case "$1" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) fail ;; esac
}
valid_name() {
case "$1" in ''|*[!a-z0-9-]*|-*|*-) fail ;; esac
[ "${#1}" -le 63 ] || fail
}
valid_repository() {
case "$1" in */*) ;; *) fail ;; esac
owner=${1%%/*}
repository=${1#*/}
[ "$owner/$repository" = "$1" ] || fail
case "$owner" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
case "$repository" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
}
valid_pr_number() {
case "$1" in [1-9]*) ;; *) fail ;; esac
case "$1" in *[!0-9]*) fail ;; esac
[ $((${#APP_NAME} + ${#1} + 4)) -le 63 ] || fail
}
valid_forgejo_origin "$TRUSTED_FORGEJO_ORIGIN"
valid_git_url "$TRUSTED_MANIFESTS_URL"
valid_branch "$TRUSTED_MANIFESTS_BRANCH"
valid_name "$APP_NAME"
valid_repository "$APP_REPOSITORY"
valid_pr_number "$PR_NUMBER"
python3 - <<'PY'
import base64
import json
import os
import sys
from pathlib import Path
from urllib.error import URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
origin = urlsplit(os.environ["TRUSTED_FORGEJO_ORIGIN"])
try:
lines = Path(os.environ["HOME"], ".git-credentials").read_text().splitlines()
matches = []
for line in lines:
credential = urlsplit(line)
if (
credential.scheme == "https"
and credential.hostname == origin.hostname
and credential.path in ("", "/")
and not credential.query
and not credential.fragment
and credential.username
and credential.password
):
matches.append((credential.username, credential.password))
except (OSError, ValueError):
sys.exit(1)
if len(matches) != 1:
sys.exit(1)
repository = os.environ["APP_REPOSITORY"]
number = int(os.environ["PR_NUMBER"])
credentials = "%s:%s" % matches[0]
request = Request("%s/api/v1/repos/%s/pulls/%s" % (os.environ["TRUSTED_FORGEJO_ORIGIN"], repository, number))
request.add_header("Authorization", "Basic " + base64.b64encode(credentials.encode()).decode())
request.add_header("Accept", "application/json")
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, fp, code, msg, headers, url):
return None
try:
with build_opener(NoRedirect).open(request, timeout=20) as response:
pull = json.load(response)
except (URLError, ValueError, json.JSONDecodeError):
sys.exit(1)
if type(pull) is not dict or type(pull.get("number")) is not int or pull["number"] != number or pull.get("state") != "closed":
sys.exit(1)
PY
- name: reconcile
image: alpine/git:2.47.2
env:
- name: TRUSTED_FORGEJO_ORIGIN
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: forgejo-origin
- name: TRUSTED_MANIFESTS_URL
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-url
- name: TRUSTED_MANIFESTS_BRANCH
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-branch
- name: APP_NAME
value: $(params.app-name)
- name: APP_REPOSITORY
value: $(params.app-repository)
- name: PR_NUMBER
value: $(params.pr-number)
script: |
#!/bin/sh
set -eu
fail() { exit 1; }
valid_forgejo_origin() {
case "$1" in https://*) ;; *) fail ;; esac
host=${1#https://}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
}
valid_git_url() {
case "$1" in https://*/*.git) ;; *) fail ;; esac
repository=${1#https://}
host=${repository%%/*}
path=${repository#*/}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
case "$path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*) fail ;; esac
[ "https://$host" = "$TRUSTED_FORGEJO_ORIGIN" ] || fail
}
valid_branch() {
case "$1" in [A-Za-z0-9]*) ;; *) fail ;; esac
case "$1" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) fail ;; esac
}
valid_name() {
case "$1" in ''|*[!a-z0-9-]*|-*|*-) fail ;; esac
[ "${#1}" -le 63 ] || fail
}
valid_repository() {
case "$1" in */*) ;; *) fail ;; esac
owner=${1%%/*}
repository=${1#*/}
[ "$owner/$repository" = "$1" ] || fail
case "$owner" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
case "$repository" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
}
valid_pr_number() {
case "$1" in [1-9]*) ;; *) fail ;; esac
case "$1" in *[!0-9]*) fail ;; esac
[ $((${#APP_NAME} + ${#1} + 4)) -le 63 ] || fail
}
valid_forgejo_origin "$TRUSTED_FORGEJO_ORIGIN"
valid_git_url "$TRUSTED_MANIFESTS_URL"
valid_branch "$TRUSTED_MANIFESTS_BRANCH"
valid_name "$APP_NAME"
valid_repository "$APP_REPOSITORY"
valid_pr_number "$PR_NUMBER"
namespace="$APP_NAME-pr-$PR_NUMBER"
preview_path="apps/previews/$namespace"
umask 077
private_dir=$(mktemp -d)
chmod 700 "$private_dir"
trap 'rm -rf -- "$private_dir"' EXIT HUP INT TERM
repository_path="$private_dir/manifests"
git clone --branch "$TRUSTED_MANIFESTS_BRANCH" "$TRUSTED_MANIFESTS_URL" "$repository_path"
[ "$(git -C "$repository_path" remote get-url origin)" = "$TRUSTED_MANIFESTS_URL" ] || fail
git -C "$repository_path" rev-parse --verify "refs/heads/$TRUSTED_MANIFESTS_BRANCH" >/dev/null
cd "$repository_path"
[ -d apps ] && [ ! -L apps ] || fail
[ -d apps/previews ] && [ ! -L apps/previews ] || fail
[ -d "$preview_path" ] && [ ! -L "$preview_path" ] || fail
marker="$preview_path/ownership.yaml"
[ -f "$marker" ] && [ ! -L "$marker" ] || fail
for existing in "$preview_path"/* "$preview_path"/.[!.]* "$preview_path"/..?*; do
[ -e "$existing" ] || [ -L "$existing" ] || continue
[ -f "$existing" ] && [ ! -L "$existing" ] || fail
case "${existing##*/}" in namespace.yaml|ownership.yaml|values.yaml|kustomization.yaml|release.yaml) ;; *) fail ;; esac
done
expected_marker=$(mktemp "$private_dir/expected-marker.XXXXXX")
cat > "$expected_marker" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: maidn-preview-owner
namespace: $namespace
labels:
maidn.io/preview-owner: "true"
maidn.io/preview-app: "$APP_NAME"
maidn.io/preview-pr: "$PR_NUMBER"
annotations:
maidn.io/preview-repository: "$APP_REPOSITORY"
data:
app: "$APP_NAME"
repository: "$APP_REPOSITORY"
pr-number: "$PR_NUMBER"
EOF
cmp -s "$expected_marker" "$marker" || fail
root="apps/previews/kustomization.yaml"
[ -f "$root" ] && [ ! -L "$root" ] || fail
grep -qxF 'kind: Kustomization' "$root" || fail
grep -qxF 'resources:' "$root" || fail
[ "$(grep -cxF " - $namespace" "$root")" -eq 1 ] || fail
rm -rf -- "$preview_path"
sed -i "\|^ - $namespace$|d" "$root"
git add -u -- "$preview_path" "$root"
git diff --cached --quiet && fail
git config --local user.name Maidn
git config --local user.email maidn@free-maidn.com
git commit -m "chore: remove closed preview $namespace"
# HOME is /tekton/home, where Tekton's annotated-secret initializer configures git.
git push origin "$TRUSTED_MANIFESTS_BRANCH"

View file

@ -0,0 +1,53 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
task="$root/catalog/maidn-node-runtime-image.yaml"
contains() {
grep -qF -- "$1" "$task"
}
for value in \
'apiVersion: tekton.dev/v1' \
'kind: Task' \
'name: maidn-node-runtime-image' \
'emptyDir: {}' \
'forgejo-git-credentials' \
'forgejo-registry-credentials' \
'case "$REPOSITORY_URL" in https://git.pingu.pw/*)' \
'case "$REVISION" in [A-Za-z0-9]*)' \
'case "$IMAGE" in git.pingu.pw/*)' \
'GIT_ASKPASS=/work/askpass' \
'runAsNonRoot: true' \
'allowPrivilegeEscalation: false' \
'drop: [ALL]' \
'type: RuntimeDefault' \
'npm install --ignore-scripts --no-audit --no-fund --package-lock=false' \
'memory: 256Mi' \
'memory: 512Mi' \
'computeResources:' \
'npm run build' \
'npm prune --omit=dev --ignore-scripts --no-audit --no-fund' \
'cp package.json /work/layer/app/' \
'cp -R node_modules dist /work/layer/app/' \
'tar -C /work/layer -cf /work/layer.tar app' \
'--base=node:22-alpine' \
'--new_tag=$(params.image):$(params.revision)' \
'- mutate' \
'--entrypoint=node' \
'--cmd=dist/index.js' \
'--workdir=/app' \
'--user=node' \
'--env=PORT=8080' \
'--exposed-ports=8080/tcp' \
'--tag=$(params.image):$(params.revision)'; do
contains "$value"
done
for param in url revision image; do
contains "name: $param"
done
! grep -qiE 'kaniko|privileged: true' "$task"
! grep -q '^[[:space:]]*resources:' "$task"

View file

@ -0,0 +1,42 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
task="$root/catalog/maidn-node-static-image.yaml"
contains() {
grep -qF -- "$1" "$task"
}
for value in \
'apiVersion: tekton.dev/v1' \
'kind: Task' \
'name: maidn-node-static-image' \
'emptyDir: {}' \
'forgejo-git-credentials' \
'forgejo-registry-credentials' \
'case "$REPOSITORY_URL" in https://git.pingu.pw/*.git)' \
'case "$REVISION" in [A-Za-z0-9]*)' \
'case "$OUTPUT_DIRECTORY" in' \
'case "$BUILD_CONFIGURATION" in' \
'GIT_ASKPASS=/work/askpass' \
'runAsNonRoot: true' \
'allowPrivilegeEscalation: false' \
'drop: [ALL]' \
'type: RuntimeDefault' \
'npm install --ignore-scripts --no-audit --no-fund --package-lock=false' \
'memory: 256Mi' \
'memory: 512Mi' \
'computeResources:' \
'tar -C /work/layer -cf /work/layer.tar www' \
'--base=$(params.base-image)' \
'--new_tag=$(params.image):$(params.revision)'; do
contains "$value"
done
for param in url revision image output-directory build-configuration base-image; do
contains "name: $param"
done
! grep -qiE 'kaniko|privileged: true' "$task"
! grep -q '^[[:space:]]*resources:' "$task"

View file

@ -0,0 +1,115 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
task="$root/catalog/maidn-preview-orphan-reconciler.yaml"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
! grep -q 'secretKeyRef' "$task"
grep -qF 'key: forgejo-origin' "$task"
awk '
/ - name: reconcile/ { found=1 }
found && /^ script: \|/ { script=1; next }
script && /^ - name:/ { exit }
script { sub(/^ /, ""); print }
' "$task" > "$tmp/reconcile.sh"
mkdir "$tmp/bin" "$tmp/fixture"
cat > "$tmp/bin/git" <<'EOF'
#!/bin/sh
set -eu
case "$1" in
clone)
for arg; do target=$arg; done
cp -R "$TEST_FIXTURE" "$target"
;;
-C)
shift 2
case "$1" in
remote)
[ "$2" = get-url ] && [ "$3" = origin ] || exit 1
printf '%s\n' "$TEST_ORIGIN"
;;
rev-parse|config|add|push) exit 0 ;;
diff) exit 1 ;;
esac
;;
diff) exit 1 ;;
commit)
[ ! -e "$PWD/apps/previews/web-ui-pr-42" ]
! grep -qF ' - web-ui-pr-42' "$PWD/apps/previews/kustomization.yaml"
;;
config|add|push) exit 0 ;;
*) exit 1 ;;
esac
EOF
chmod +x "$tmp/bin/git"
marker() {
cat <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: maidn-preview-owner
namespace: web-ui-pr-42
labels:
maidn.io/preview-owner: "true"
maidn.io/preview-app: "web-ui"
maidn.io/preview-pr: "42"
annotations:
maidn.io/preview-repository: "platform/web-ui"
data:
app: "web-ui"
repository: "platform/web-ui"
pr-number: "42"
EOF
}
fixture() {
dir=$1
mkdir -p "$dir/apps/previews/web-ui-pr-42"
cat > "$dir/apps/previews/kustomization.yaml" <<'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- web-ui-pr-42
EOF
marker > "$dir/apps/previews/web-ui-pr-42/ownership.yaml"
: > "$dir/apps/previews/web-ui-pr-42/namespace.yaml"
: > "$dir/apps/previews/web-ui-pr-42/values.yaml"
: > "$dir/apps/previews/web-ui-pr-42/kustomization.yaml"
: > "$dir/apps/previews/web-ui-pr-42/release.yaml"
}
run() {
TRUSTED_FORGEJO_ORIGIN=https://git.example.invalid \
TRUSTED_MANIFESTS_URL=https://git.example.invalid/platform/manifests.git \
TRUSTED_MANIFESTS_BRANCH=main APP_NAME="$1" APP_REPOSITORY=platform/web-ui PR_NUMBER=42 \
TEST_FIXTURE="$tmp/fixture" PATH="$tmp/bin:$PATH" \
TEST_ORIGIN="${TEST_ORIGIN:-https://git.example.invalid/platform/manifests.git}" \
sh "$tmp/reconcile.sh"
}
fixture "$tmp/fixture"
run web-ui
[ -f "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml" ]
grep -qF ' - web-ui-pr-42' "$tmp/fixture/apps/previews/kustomization.yaml"
rm -rf "$tmp/fixture"
fixture "$tmp/fixture"
printf '\n' >> "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
if run web-ui; then exit 1; fi
[ -f "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml" ]
rm -rf "$tmp/fixture"
fixture "$tmp/fixture"
rm "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
ln -s /tmp/not-owned "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
if run web-ui; then exit 1; fi
rm -rf "$tmp/fixture"
fixture "$tmp/fixture"
TEST_ORIGIN=https://git.example.invalid/platform/other.git
if run web-ui; then exit 1; fi
if run 'web-ui/escape'; then exit 1; fi